Files
gitea-mirror/src/lib/mirror-sync-errors.test.ts
Arunavo Ray 3a9b8380d4 fix: resolve CI test failures and timeouts
- Update Bun version in CI to match local version (1.2.16)
- Add bunfig.toml with 5s test timeout to prevent hanging tests
- Mock setTimeout globally in test setup to avoid timing issues
- Add NODE_ENV check to skip delays during tests
- Fix missing exports in config-encryption mock
- Remove retryDelay in tests to ensure immediate execution

These changes ensure tests run consistently between local and CI environments
2025-07-27 20:27:33 +05:30

382 lines
11 KiB
TypeScript

import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test";
import { db, repositories } from "./db";
import { eq } from "drizzle-orm";
import { repoStatusEnum } from "@/types/Repository";
import type { Config, Repository } from "./db/schema";
describe("Mirror Sync Error Handling", () => {
let originalFetch: typeof global.fetch;
let originalSetTimeout: typeof global.setTimeout;
let mockDbUpdate: any;
beforeEach(() => {
originalFetch = global.fetch;
originalSetTimeout = global.setTimeout;
// Mock setTimeout to avoid delays in tests
global.setTimeout = ((fn: Function) => {
Promise.resolve().then(() => fn());
return 0;
}) as any;
// Mock database update operations
mockDbUpdate = mock(() => ({
set: mock(() => ({
where: mock(() => Promise.resolve())
}))
}));
// Override the db.update method
(db as any).update = mockDbUpdate;
});
afterEach(() => {
global.fetch = originalFetch;
global.setTimeout = originalSetTimeout;
});
describe("Mirror sync API errors", () => {
test("should handle mirror-sync endpoint not available for non-mirror repos", async () => {
const errorResponse = {
ok: false,
status: 400,
statusText: "Bad Request",
headers: new Headers({ "content-type": "application/json" }),
json: async () => ({
message: "Repository is not a mirror",
url: "https://gitea.ui.com/api/swagger"
})
};
global.fetch = mock(async (url: string) => {
if (url.includes("/api/v1/repos/") && url.includes("/mirror-sync")) {
return errorResponse as Response;
}
return originalFetch(url);
});
const config: Partial<Config> = {
giteaConfig: {
url: "https://gitea.ui.com",
token: "gitea-token"
}
};
// Simulate attempting to sync a non-mirror repository
const response = await fetch(
`${config.giteaConfig!.url}/api/v1/repos/starred/test-repo/mirror-sync`,
{
method: "POST",
headers: {
Authorization: `token ${config.giteaConfig!.token}`,
"Content-Type": "application/json"
}
}
);
expect(response.ok).toBe(false);
expect(response.status).toBe(400);
const error = await response.json();
expect(error.message).toBe("Repository is not a mirror");
});
test("should update repository status to 'failed' when sync fails", async () => {
const repository: Repository = {
id: "repo-123",
userId: "user-123",
configId: "config-123",
name: "test-repo",
fullName: "owner/test-repo",
url: "https://github.com/owner/test-repo",
cloneUrl: "https://github.com/owner/test-repo.git",
owner: "owner",
isPrivate: false,
isForked: false,
hasIssues: true,
isStarred: true,
isArchived: false,
size: 1000,
hasLFS: false,
hasSubmodules: false,
defaultBranch: "main",
visibility: "public",
status: "mirroring",
mirroredLocation: "starred/test-repo",
createdAt: new Date(),
updatedAt: new Date()
};
// Simulate error handling in mirror process
const errorMessage = "Repository is not a mirror";
// This simulates what should happen when mirror sync fails
await db
.update(repositories)
.set({
status: repoStatusEnum.parse("failed"),
errorMessage: errorMessage,
updatedAt: new Date()
})
.where(eq(repositories.id, repository.id));
// Verify the update was called with correct parameters
expect(mockDbUpdate).toHaveBeenCalledWith(repositories);
const setCalls = mockDbUpdate.mock.results[0].value.set.mock.calls;
expect(setCalls[0][0]).toMatchObject({
status: "failed",
errorMessage: errorMessage
});
});
});
describe("Repository state detection", () => {
test("should detect when a repository exists but is not configured as mirror", async () => {
// Mock Gitea API response for repo info
global.fetch = mock(async (url: string) => {
if (url.includes("/api/v1/repos/starred/test-repo") && !url.includes("mirror-sync")) {
return {
ok: true,
status: 200,
headers: new Headers({ "content-type": "application/json" }),
json: async () => ({
id: 123,
name: "test-repo",
owner: { login: "starred" },
mirror: false, // This is the issue - should be true
fork: false,
private: false,
clone_url: "https://gitea.ui.com/starred/test-repo.git"
})
} as Response;
}
return originalFetch(url);
});
const config: Partial<Config> = {
giteaConfig: {
url: "https://gitea.ui.com",
token: "gitea-token"
}
};
// Check repository details
const response = await fetch(
`${config.giteaConfig!.url}/api/v1/repos/starred/test-repo`,
{
headers: {
Authorization: `token ${config.giteaConfig!.token}`
}
}
);
const repoInfo = await response.json();
// Verify the repository exists but is not a mirror
expect(repoInfo.mirror).toBe(false);
expect(repoInfo.owner.login).toBe("starred");
// This state causes the "Repository is not a mirror" error
});
test("should identify repositories that need to be recreated as mirrors", async () => {
const problematicRepos = [
{
name: "awesome-project",
owner: "starred",
currentState: "regular",
requiredState: "mirror",
action: "delete and recreate"
},
{
name: "cool-library",
owner: "starred",
currentState: "fork",
requiredState: "mirror",
action: "delete and recreate"
}
];
// This test documents repos that need intervention
expect(problematicRepos).toHaveLength(2);
expect(problematicRepos[0].action).toBe("delete and recreate");
});
});
describe("Organization permission errors", () => {
test("should handle insufficient permissions for organization operations", async () => {
global.fetch = mock(async (url: string, options?: RequestInit) => {
if (url.includes("/api/v1/orgs") && options?.method === "POST") {
return {
ok: false,
status: 403,
statusText: "Forbidden",
headers: new Headers({ "content-type": "application/json" }),
json: async () => ({
message: "You do not have permission to create organizations",
url: "https://gitea.ui.com/api/swagger"
})
} as Response;
}
return originalFetch(url, options);
});
const config: Partial<Config> = {
giteaConfig: {
url: "https://gitea.ui.com",
token: "gitea-token"
}
};
const response = await fetch(
`${config.giteaConfig!.url}/api/v1/orgs`,
{
method: "POST",
headers: {
Authorization: `token ${config.giteaConfig!.token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
username: "starred",
full_name: "Starred Repositories"
})
}
);
expect(response.ok).toBe(false);
expect(response.status).toBe(403);
const error = await response.json();
expect(error.message).toContain("permission");
});
});
describe("Sync operation retry logic", () => {
test("should implement exponential backoff for transient errors", async () => {
let attemptCount = 0;
const maxRetries = 3;
const baseDelay = 1000;
const mockSyncWithRetry = async (url: string, config: any) => {
for (let i = 0; i < maxRetries; i++) {
attemptCount++;
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `token ${config.token}`
}
});
if (response.ok) {
return response;
}
if (response.status === 400) {
// Non-retryable error
throw new Error("Repository is not a mirror");
}
// Retryable error (5xx, network issues)
if (i < maxRetries - 1) {
const delay = baseDelay * Math.pow(2, i);
await new Promise(resolve => setTimeout(resolve, delay));
}
} catch (error) {
if (i === maxRetries - 1) {
throw error;
}
}
}
};
// Mock a server error that resolves after 2 retries
let callCount = 0;
global.fetch = mock(async () => {
callCount++;
if (callCount < 3) {
return {
ok: false,
status: 503,
statusText: "Service Unavailable"
} as Response;
}
return {
ok: true,
status: 200
} as Response;
});
const response = await mockSyncWithRetry(
"https://gitea.ui.com/api/v1/repos/starred/test-repo/mirror-sync",
{ token: "test-token" }
);
expect(response.ok).toBe(true);
expect(attemptCount).toBe(3);
});
});
describe("Bulk operation error handling", () => {
test("should continue processing other repos when one fails", async () => {
const repositories = [
{ name: "repo1", owner: "starred", shouldFail: false },
{ name: "repo2", owner: "starred", shouldFail: true }, // This one will fail
{ name: "repo3", owner: "starred", shouldFail: false }
];
const results: { name: string; success: boolean; error?: string }[] = [];
// Mock fetch to fail for repo2
global.fetch = mock(async (url: string) => {
if (url.includes("repo2")) {
return {
ok: false,
status: 400,
statusText: "Bad Request",
headers: new Headers({ "content-type": "application/json" }),
json: async () => ({
message: "Repository is not a mirror"
})
} as Response;
}
return {
ok: true,
status: 200
} as Response;
});
// Process repositories
for (const repo of repositories) {
try {
const response = await fetch(
`https://gitea.ui.com/api/v1/repos/${repo.owner}/${repo.name}/mirror-sync`,
{ method: "POST" }
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
results.push({ name: repo.name, success: true });
} catch (error) {
results.push({
name: repo.name,
success: false,
error: (error as Error).message
});
}
}
// Verify results
expect(results).toHaveLength(3);
expect(results[0].success).toBe(true);
expect(results[1].success).toBe(false);
expect(results[1].error).toBe("Repository is not a mirror");
expect(results[2].success).toBe(true);
});
});
});