mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-06 19:46:44 +03:00
586 lines
16 KiB
TypeScript
586 lines
16 KiB
TypeScript
import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test";
|
|
import { createMockResponse, mockFetch } from "@/tests/mock-fetch";
|
|
|
|
// Mock the helpers module before importing gitea-enhanced
|
|
const mockCreateMirrorJob = mock(() => Promise.resolve("mock-job-id"));
|
|
mock.module("@/lib/helpers", () => ({
|
|
createMirrorJob: mockCreateMirrorJob
|
|
}));
|
|
|
|
const mockMirrorGitHubReleasesToGitea = mock(() => Promise.resolve());
|
|
const mockGetGiteaRepoOwnerAsync = mock(() => Promise.resolve("starred"));
|
|
|
|
// Mock the database module
|
|
const mockDb = {
|
|
insert: mock((table: any) => ({
|
|
values: mock((data: any) => Promise.resolve({ insertedId: "mock-id" }))
|
|
})),
|
|
update: mock(() => ({
|
|
set: mock(() => ({
|
|
where: mock(() => Promise.resolve())
|
|
}))
|
|
}))
|
|
};
|
|
|
|
mock.module("@/lib/db", () => ({
|
|
db: mockDb,
|
|
mirrorJobs: {},
|
|
repositories: {}
|
|
}));
|
|
|
|
// Mock config encryption
|
|
mock.module("@/lib/utils/config-encryption", () => ({
|
|
decryptConfigTokens: (config: any) => config,
|
|
encryptConfigTokens: (config: any) => config,
|
|
getDecryptedGitHubToken: (config: any) => config.githubConfig?.token || "",
|
|
getDecryptedGiteaToken: (config: any) => config.giteaConfig?.token || ""
|
|
}));
|
|
|
|
// Mock http-client
|
|
class MockHttpError extends Error {
|
|
constructor(message: string, public status: number, public statusText: string, public response?: string) {
|
|
super(message);
|
|
this.name = 'HttpError';
|
|
}
|
|
}
|
|
|
|
// Track call counts for org tests
|
|
let orgCheckCount = 0;
|
|
let orgTestContext = "";
|
|
let getOrgCalled = false;
|
|
let createOrgCalled = false;
|
|
|
|
const mockHttpGet = mock(async (url: string, headers?: any) => {
|
|
// Return different responses based on URL patterns
|
|
|
|
// Handle user authentication endpoint
|
|
if (url.includes("/api/v1/user")) {
|
|
return {
|
|
data: {
|
|
id: 1,
|
|
login: "testuser",
|
|
username: "testuser",
|
|
email: "test@example.com",
|
|
is_admin: false,
|
|
full_name: "Test User"
|
|
},
|
|
status: 200,
|
|
statusText: "OK",
|
|
headers: new Headers()
|
|
};
|
|
}
|
|
|
|
if (url.includes("/api/v1/repos/starred/test-repo")) {
|
|
return {
|
|
data: {
|
|
id: 123,
|
|
name: "test-repo",
|
|
mirror: true,
|
|
owner: { login: "starred" },
|
|
mirror_interval: "8h",
|
|
clone_url: "https://github.com/user/test-repo.git",
|
|
private: false
|
|
},
|
|
status: 200,
|
|
statusText: "OK",
|
|
headers: new Headers()
|
|
};
|
|
}
|
|
if (url.includes("/api/v1/repos/starred/regular-repo")) {
|
|
return {
|
|
data: {
|
|
id: 124,
|
|
name: "regular-repo",
|
|
mirror: false,
|
|
owner: { login: "starred" }
|
|
},
|
|
status: 200,
|
|
statusText: "OK",
|
|
headers: new Headers()
|
|
};
|
|
}
|
|
if (url.includes("/api/v1/repos/starred/non-mirror-repo")) {
|
|
return {
|
|
data: {
|
|
id: 456,
|
|
name: "non-mirror-repo",
|
|
mirror: false,
|
|
owner: { login: "starred" },
|
|
private: false
|
|
},
|
|
status: 200,
|
|
statusText: "OK",
|
|
headers: new Headers()
|
|
};
|
|
}
|
|
if (url.includes("/api/v1/repos/starred/mirror-repo")) {
|
|
return {
|
|
data: {
|
|
id: 789,
|
|
name: "mirror-repo",
|
|
mirror: true,
|
|
owner: { login: "starred" },
|
|
mirror_interval: "8h",
|
|
private: false
|
|
},
|
|
status: 200,
|
|
statusText: "OK",
|
|
headers: new Headers()
|
|
};
|
|
}
|
|
if (url.includes("/api/v1/repos/")) {
|
|
throw new MockHttpError("Not Found", 404, "Not Found");
|
|
}
|
|
|
|
// Handle org GET requests based on test context
|
|
if (url.includes("/api/v1/orgs/starred")) {
|
|
orgCheckCount++;
|
|
if (orgTestContext === "duplicate-retry" && orgCheckCount > 2) {
|
|
// After retries, org exists
|
|
return {
|
|
data: { id: 999, username: "starred" },
|
|
status: 200,
|
|
statusText: "OK",
|
|
headers: new Headers()
|
|
};
|
|
}
|
|
// Otherwise, org doesn't exist
|
|
throw new MockHttpError("Not Found", 404, "Not Found");
|
|
}
|
|
|
|
if (url.includes("/api/v1/orgs/neworg")) {
|
|
getOrgCalled = true;
|
|
// Org doesn't exist
|
|
throw new MockHttpError("Not Found", 404, "Not Found");
|
|
}
|
|
|
|
return { data: {}, status: 200, statusText: "OK", headers: new Headers() };
|
|
});
|
|
|
|
const mockHttpPost = mock(async (url: string, body?: any, headers?: any) => {
|
|
if (url.includes("/api/v1/orgs") && body?.username === "starred") {
|
|
// Simulate duplicate org error
|
|
throw new MockHttpError(
|
|
'insert organization: pq: duplicate key value violates unique constraint "UQE_user_lower_name"',
|
|
400,
|
|
"Bad Request",
|
|
JSON.stringify({ message: 'insert organization: pq: duplicate key value violates unique constraint "UQE_user_lower_name"', url: "https://gitea.example.com/api/swagger" })
|
|
);
|
|
}
|
|
if (url.includes("/api/v1/orgs") && body?.username === "neworg") {
|
|
createOrgCalled = true;
|
|
return {
|
|
data: { id: 777, username: "neworg" },
|
|
status: 201,
|
|
statusText: "Created",
|
|
headers: new Headers()
|
|
};
|
|
}
|
|
if (url.includes("/mirror-sync")) {
|
|
return {
|
|
data: { success: true },
|
|
status: 200,
|
|
statusText: "OK",
|
|
headers: new Headers()
|
|
};
|
|
}
|
|
return { data: {}, status: 200, statusText: "OK", headers: new Headers() };
|
|
});
|
|
|
|
const mockHttpDelete = mock(async (url: string, headers?: any) => {
|
|
if (url.includes("/api/v1/repos/starred/test-repo")) {
|
|
return { data: {}, status: 204, statusText: "No Content", headers: new Headers() };
|
|
}
|
|
return { data: {}, status: 200, statusText: "OK", headers: new Headers() };
|
|
});
|
|
|
|
mock.module("@/lib/http-client", () => ({
|
|
httpGet: mockHttpGet,
|
|
httpPost: mockHttpPost,
|
|
httpDelete: mockHttpDelete,
|
|
HttpError: MockHttpError
|
|
}));
|
|
|
|
// Now import the modules we're testing
|
|
import {
|
|
getGiteaRepoInfo,
|
|
getOrCreateGiteaOrgEnhanced,
|
|
syncGiteaRepoEnhanced,
|
|
handleExistingNonMirrorRepo
|
|
} from "./gitea-enhanced";
|
|
import type { Config, Repository } from "./db/schema";
|
|
import { repoStatusEnum } from "@/types/Repository";
|
|
|
|
// Get HttpError from the mocked module
|
|
const { HttpError } = await import("@/lib/http-client");
|
|
|
|
describe("Enhanced Gitea Operations", () => {
|
|
let originalFetch: typeof global.fetch;
|
|
|
|
beforeEach(() => {
|
|
originalFetch = global.fetch;
|
|
// Clear mocks
|
|
mockCreateMirrorJob.mockClear();
|
|
mockDb.insert.mockClear();
|
|
mockDb.update.mockClear();
|
|
mockMirrorGitHubReleasesToGitea.mockClear();
|
|
mockGetGiteaRepoOwnerAsync.mockClear();
|
|
mockGetGiteaRepoOwnerAsync.mockImplementation(() => Promise.resolve("starred"));
|
|
// Reset tracking variables
|
|
orgCheckCount = 0;
|
|
orgTestContext = "";
|
|
getOrgCalled = false;
|
|
createOrgCalled = false;
|
|
});
|
|
|
|
afterEach(() => {
|
|
global.fetch = originalFetch;
|
|
});
|
|
|
|
describe("getGiteaRepoInfo", () => {
|
|
test("should return repo info for existing mirror repository", async () => {
|
|
global.fetch = mockFetch(() =>
|
|
createMockResponse({
|
|
id: 123,
|
|
name: "test-repo",
|
|
owner: "starred",
|
|
mirror: true,
|
|
mirror_interval: "8h",
|
|
clone_url: "https://github.com/user/test-repo.git",
|
|
private: false,
|
|
})
|
|
);
|
|
|
|
const config: Partial<Config> = {
|
|
giteaConfig: {
|
|
url: "https://gitea.example.com",
|
|
token: "encrypted-token",
|
|
defaultOwner: "testuser",
|
|
mirrorReleases: true,
|
|
},
|
|
};
|
|
|
|
const repoInfo = await getGiteaRepoInfo({
|
|
config,
|
|
owner: "starred",
|
|
repoName: "test-repo",
|
|
});
|
|
|
|
expect(repoInfo).toBeTruthy();
|
|
expect(repoInfo?.mirror).toBe(true);
|
|
expect(repoInfo?.name).toBe("test-repo");
|
|
});
|
|
|
|
test("should return repo info for existing non-mirror repository", async () => {
|
|
global.fetch = mockFetch(() =>
|
|
createMockResponse({
|
|
id: 124,
|
|
name: "regular-repo",
|
|
owner: "starred",
|
|
mirror: false,
|
|
private: false,
|
|
})
|
|
);
|
|
|
|
const config: Partial<Config> = {
|
|
giteaConfig: {
|
|
url: "https://gitea.example.com",
|
|
token: "encrypted-token",
|
|
defaultOwner: "testuser",
|
|
mirrorReleases: true,
|
|
},
|
|
};
|
|
|
|
const repoInfo = await getGiteaRepoInfo({
|
|
config,
|
|
owner: "starred",
|
|
repoName: "regular-repo",
|
|
});
|
|
|
|
expect(repoInfo).toBeTruthy();
|
|
expect(repoInfo?.mirror).toBe(false);
|
|
});
|
|
|
|
test("should return null for non-existent repository", async () => {
|
|
global.fetch = mockFetch(() =>
|
|
createMockResponse(
|
|
"Not Found",
|
|
{ ok: false, status: 404, statusText: "Not Found" }
|
|
)
|
|
);
|
|
|
|
const config: Partial<Config> = {
|
|
giteaConfig: {
|
|
url: "https://gitea.example.com",
|
|
token: "encrypted-token",
|
|
defaultOwner: "testuser",
|
|
mirrorReleases: true,
|
|
},
|
|
};
|
|
|
|
const repoInfo = await getGiteaRepoInfo({
|
|
config,
|
|
owner: "starred",
|
|
repoName: "non-existent",
|
|
});
|
|
|
|
expect(repoInfo).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("getOrCreateGiteaOrgEnhanced", () => {
|
|
test("should handle duplicate organization constraint error with retry", async () => {
|
|
orgTestContext = "duplicate-retry";
|
|
orgCheckCount = 0; // Reset the count
|
|
|
|
const config: Partial<Config> = {
|
|
userId: "user123",
|
|
giteaConfig: {
|
|
url: "https://gitea.example.com",
|
|
token: "encrypted-token",
|
|
defaultOwner: "testuser",
|
|
visibility: "public",
|
|
},
|
|
};
|
|
|
|
const orgId = await getOrCreateGiteaOrgEnhanced({
|
|
orgName: "starred",
|
|
config,
|
|
maxRetries: 3,
|
|
retryDelay: 0, // No delay in tests
|
|
});
|
|
|
|
expect(orgId).toBe(999);
|
|
expect(orgCheckCount).toBeGreaterThanOrEqual(3);
|
|
});
|
|
|
|
test("should create organization on first attempt", async () => {
|
|
// Reset tracking variables
|
|
getOrgCalled = false;
|
|
createOrgCalled = false;
|
|
|
|
const config: Partial<Config> = {
|
|
userId: "user123",
|
|
githubConfig: {
|
|
username: "testuser",
|
|
token: "github-token",
|
|
privateRepositories: false,
|
|
mirrorStarred: true,
|
|
},
|
|
giteaConfig: {
|
|
url: "https://gitea.example.com",
|
|
token: "encrypted-token",
|
|
defaultOwner: "testuser",
|
|
mirrorReleases: true,
|
|
},
|
|
};
|
|
|
|
const orgId = await getOrCreateGiteaOrgEnhanced({
|
|
orgName: "neworg",
|
|
config,
|
|
retryDelay: 0, // No delay in tests
|
|
});
|
|
|
|
expect(orgId).toBe(777);
|
|
expect(getOrgCalled).toBe(true);
|
|
expect(createOrgCalled).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("syncGiteaRepoEnhanced", () => {
|
|
test("should fail gracefully when repository is not a mirror", async () => {
|
|
const config: Partial<Config> = {
|
|
userId: "user123",
|
|
githubConfig: {
|
|
username: "testuser",
|
|
token: "github-token",
|
|
privateRepositories: false,
|
|
mirrorStarred: true,
|
|
},
|
|
giteaConfig: {
|
|
url: "https://gitea.example.com",
|
|
token: "encrypted-token",
|
|
defaultOwner: "testuser",
|
|
mirrorReleases: true,
|
|
},
|
|
};
|
|
|
|
const repository: Repository = {
|
|
id: "repo123",
|
|
name: "non-mirror-repo",
|
|
fullName: "user/non-mirror-repo",
|
|
owner: "user",
|
|
cloneUrl: "https://github.com/user/non-mirror-repo.git",
|
|
isPrivate: false,
|
|
isStarred: true,
|
|
status: repoStatusEnum.parse("mirrored"),
|
|
visibility: "public",
|
|
userId: "user123",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
await expect(
|
|
syncGiteaRepoEnhanced(
|
|
{ config, repository },
|
|
{
|
|
getGiteaRepoOwnerAsync: mockGetGiteaRepoOwnerAsync,
|
|
mirrorGitHubReleasesToGitea: mockMirrorGitHubReleasesToGitea,
|
|
}
|
|
)
|
|
).rejects.toThrow("Repository non-mirror-repo is not a mirror. Cannot sync.");
|
|
|
|
expect(mockMirrorGitHubReleasesToGitea).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("should successfully sync a mirror repository", async () => {
|
|
const config: Partial<Config> = {
|
|
userId: "user123",
|
|
githubConfig: {
|
|
username: "testuser",
|
|
token: "github-token",
|
|
privateRepositories: false,
|
|
mirrorStarred: true,
|
|
},
|
|
giteaConfig: {
|
|
url: "https://gitea.example.com",
|
|
token: "encrypted-token",
|
|
defaultOwner: "testuser",
|
|
mirrorReleases: true,
|
|
},
|
|
};
|
|
|
|
const repository: Repository = {
|
|
id: "repo456",
|
|
name: "mirror-repo",
|
|
fullName: "user/mirror-repo",
|
|
owner: "user",
|
|
cloneUrl: "https://github.com/user/mirror-repo.git",
|
|
isPrivate: false,
|
|
isStarred: true,
|
|
status: repoStatusEnum.parse("mirrored"),
|
|
visibility: "public",
|
|
userId: "user123",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
const result = await syncGiteaRepoEnhanced(
|
|
{ config, repository },
|
|
{
|
|
getGiteaRepoOwnerAsync: mockGetGiteaRepoOwnerAsync,
|
|
mirrorGitHubReleasesToGitea: mockMirrorGitHubReleasesToGitea,
|
|
}
|
|
);
|
|
|
|
expect(result).toEqual({ success: true });
|
|
expect(mockGetGiteaRepoOwnerAsync).toHaveBeenCalled();
|
|
expect(mockMirrorGitHubReleasesToGitea).toHaveBeenCalledTimes(1);
|
|
const releaseCall = mockMirrorGitHubReleasesToGitea.mock.calls[0][0];
|
|
expect(releaseCall.giteaOwner).toBe("starred");
|
|
expect(releaseCall.giteaRepoName).toBe("mirror-repo");
|
|
expect(releaseCall.config.githubConfig?.token).toBe("github-token");
|
|
expect(releaseCall.octokit).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("handleExistingNonMirrorRepo", () => {
|
|
test("should skip non-mirror repository with skip strategy", async () => {
|
|
const repoInfo = {
|
|
id: 123,
|
|
name: "test-repo",
|
|
owner: "starred",
|
|
mirror: false,
|
|
private: false,
|
|
};
|
|
|
|
const repository: Repository = {
|
|
id: "repo123",
|
|
name: "test-repo",
|
|
fullName: "user/test-repo",
|
|
owner: "user",
|
|
cloneUrl: "https://github.com/user/test-repo.git",
|
|
isPrivate: false,
|
|
isStarred: true,
|
|
status: repoStatusEnum.parse("imported"),
|
|
visibility: "public",
|
|
userId: "user123",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
const config: Partial<Config> = {
|
|
giteaConfig: {
|
|
url: "https://gitea.example.com",
|
|
token: "encrypted-token",
|
|
defaultOwner: "testuser",
|
|
},
|
|
};
|
|
|
|
await handleExistingNonMirrorRepo({
|
|
config,
|
|
repository,
|
|
repoInfo,
|
|
strategy: "skip",
|
|
});
|
|
|
|
// Test passes if no error is thrown
|
|
expect(true).toBe(true);
|
|
});
|
|
|
|
test("should delete non-mirror repository with delete strategy", async () => {
|
|
// Mock deleteGiteaRepo which uses httpDelete via the http-client mock
|
|
const repoInfo = {
|
|
id: 124,
|
|
name: "test-repo",
|
|
owner: "starred",
|
|
mirror: false,
|
|
private: false,
|
|
};
|
|
|
|
const repository: Repository = {
|
|
id: "repo124",
|
|
name: "test-repo",
|
|
fullName: "user/test-repo",
|
|
owner: "user",
|
|
cloneUrl: "https://github.com/user/test-repo.git",
|
|
isPrivate: false,
|
|
isStarred: true,
|
|
status: repoStatusEnum.parse("imported"),
|
|
visibility: "public",
|
|
userId: "user123",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
const config: Partial<Config> = {
|
|
giteaConfig: {
|
|
url: "https://gitea.example.com",
|
|
token: "encrypted-token",
|
|
defaultOwner: "testuser",
|
|
},
|
|
};
|
|
|
|
// deleteGiteaRepo in the actual code uses fetch directly, not httpDelete
|
|
// We need to mock fetch for this test
|
|
let deleteCalled = false;
|
|
global.fetch = mockFetch(async (url: string, options?: RequestInit) => {
|
|
if (url.includes("/api/v1/repos/starred/test-repo") && options?.method === "DELETE") {
|
|
deleteCalled = true;
|
|
return createMockResponse(null, { ok: true, status: 204 });
|
|
}
|
|
return createMockResponse(null, { ok: false, status: 404 });
|
|
});
|
|
|
|
await handleExistingNonMirrorRepo({
|
|
config,
|
|
repository,
|
|
repoInfo,
|
|
strategy: "delete",
|
|
});
|
|
|
|
expect(deleteCalled).toBe(true);
|
|
});
|
|
});
|
|
});
|