mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-05-04 04:22:32 +02:00
- 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
382 lines
11 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
}); |