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 = { 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 = { 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 = { 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); }); }); });