mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-03-13 22:12:54 +03:00
* feat: add E2E testing infrastructure with fake GitHub, Playwright, and CI workflow - Add fake GitHub API server (tests/e2e/fake-github-server.ts) with management API for seeding test data - Add Playwright E2E test suite covering full mirror workflow: service health checks, user registration, config, sync, verify - Add Docker Compose for E2E Gitea instance - Add orchestrator script (run-e2e.sh) with cleanup - Add GitHub Actions workflow (e2e-tests.yml) with Gitea service container - Make GITHUB_API_URL configurable via env var for testing - Add npm scripts: test:e2e, test:e2e:ci, test:e2e:keep, test:e2e:cleanup * feat: add real git repos + backup config testing to E2E suite - Create programmatic test git repos (create-test-repos.ts) with real commits, branches (main, develop, feature/*), and tags (v1.0.0, v1.1.0) - Add git-server container to docker-compose serving bare repos via dumb HTTP protocol so Gitea can actually clone them - Update fake GitHub server to emit reachable clone_url fields pointing to the git-server container (configurable via GIT_SERVER_URL env var) - Add management endpoint POST /___mgmt/set-clone-url for runtime config - Update E2E spec with real mirroring verification: * Verify repos appear in Gitea with actual content * Check branches, tags, commits, file content * Verify 4/4 repos mirrored successfully - Add backup configuration test suite: * Enable/disable backupBeforeSync config * Toggle blockSyncOnBackupFailure * Trigger re-sync with backup enabled and verify activities * Verify config persistence across changes - Update CI workflow to use docker compose (not service containers) matching the local run-e2e.sh approach - Update cleanup.sh for git-repos directory and git-server port - All 22 tests passing with real git content verification * refactor: split E2E tests into focused files + add force-push tests Split the monolithic e2e.spec.ts (1335 lines) into 5 focused spec files and a shared helpers module: helpers.ts — constants, GiteaAPI, auth, saveConfig, utilities 01-health.spec.ts — service health checks (4 tests) 02-mirror-workflow.spec.ts — full first-mirror journey (8 tests) 03-backup.spec.ts — backup config toggling (6 tests) 04-force-push.spec.ts — force-push simulation & backup verification (9 tests) 05-sync-verification.spec.ts — dynamic repos, content integrity, reset (5 tests) The force-push tests are the critical addition: F0: Record original state (commit SHAs, file content) F1: Rewrite source repo history (simulate force-push) F2: Sync to Gitea WITHOUT backup F3: Verify data loss — LICENSE file gone, README overwritten F4: Restore source, re-mirror to clean state F5: Enable backup, force-push again, sync through app F6: Verify Gitea reflects the force-push F7: Verify backup system was invoked (snapshot activities logged) F8: Restore source repo for subsequent tests Also added to helpers.ts: - GiteaAPI.getBranch(), .getCommit(), .triggerMirrorSync() - getRepositoryIds(), triggerMirrorJobs(), triggerSyncRepo() All 32 tests passing. * Try to fix actions * Try to fix the other action * Add debug info to check why e2e action is failing * More debug info * Even more debug info * E2E fix attempt #1 * E2E fix attempt #2 * more debug again * E2E fix attempt #3 * E2E fix attempt #4 * Remove a bunch of debug info * Hopefully fix backup bug * Force backups to succeed
345 lines
10 KiB
TypeScript
345 lines
10 KiB
TypeScript
/**
|
||
* 02 – Main mirror workflow.
|
||
*
|
||
* Walks through the full first-time user journey:
|
||
* 1. Create Gitea admin user + API token
|
||
* 2. Create the mirror target organization
|
||
* 3. Register / sign-in to the gitea-mirror app
|
||
* 4. Save GitHub + Gitea configuration
|
||
* 5. Trigger a GitHub data sync (pull repo list from fake GitHub)
|
||
* 6. Trigger mirror jobs (push repos into Gitea)
|
||
* 7. Verify repos actually appeared in Gitea with real content
|
||
* 8. Verify mirror job activity and app state
|
||
*/
|
||
|
||
import { test, expect } from "@playwright/test";
|
||
import {
|
||
APP_URL,
|
||
GITEA_URL,
|
||
GITEA_MIRROR_ORG,
|
||
GiteaAPI,
|
||
getAppSessionCookies,
|
||
saveConfig,
|
||
waitFor,
|
||
getRepositoryIds,
|
||
triggerMirrorJobs,
|
||
} from "./helpers";
|
||
|
||
test.describe("E2E: Mirror workflow", () => {
|
||
let giteaApi: GiteaAPI;
|
||
let appCookies = "";
|
||
|
||
test.beforeAll(async () => {
|
||
giteaApi = new GiteaAPI(GITEA_URL);
|
||
});
|
||
|
||
test.afterAll(async () => {
|
||
await giteaApi.dispose();
|
||
});
|
||
|
||
test("Step 1: Setup Gitea admin user and token", async () => {
|
||
await giteaApi.ensureAdminUser();
|
||
const token = await giteaApi.createToken();
|
||
expect(token).toBeTruthy();
|
||
expect(token.length).toBeGreaterThan(10);
|
||
console.log(`[Setup] Gitea token acquired (length: ${token.length})`);
|
||
});
|
||
|
||
test("Step 2: Create mirror organization in Gitea", async () => {
|
||
await giteaApi.ensureOrg(GITEA_MIRROR_ORG);
|
||
|
||
const repos = await giteaApi.listOrgRepos(GITEA_MIRROR_ORG);
|
||
expect(Array.isArray(repos)).toBeTruthy();
|
||
console.log(
|
||
`[Setup] Org ${GITEA_MIRROR_ORG} exists with ${repos.length} repos`,
|
||
);
|
||
});
|
||
|
||
test("Step 3: Register and sign in to gitea-mirror app", async ({
|
||
request,
|
||
}) => {
|
||
appCookies = await getAppSessionCookies(request);
|
||
expect(appCookies).toBeTruthy();
|
||
console.log(
|
||
`[Auth] Session cookies acquired (length: ${appCookies.length})`,
|
||
);
|
||
|
||
const whoami = await request.get(`${APP_URL}/api/config`, {
|
||
headers: { Cookie: appCookies },
|
||
failOnStatusCode: false,
|
||
});
|
||
expect(
|
||
whoami.status(),
|
||
`Auth check returned ${whoami.status()} – cookies may be invalid`,
|
||
).not.toBe(401);
|
||
console.log(`[Auth] Auth check status: ${whoami.status()}`);
|
||
});
|
||
|
||
test("Step 4: Configure mirrors via API (backup disabled)", async ({
|
||
request,
|
||
}) => {
|
||
if (!appCookies) {
|
||
appCookies = await getAppSessionCookies(request);
|
||
}
|
||
|
||
const giteaToken = giteaApi.getTokenValue();
|
||
expect(giteaToken, "Gitea token should be set from Step 1").toBeTruthy();
|
||
|
||
await saveConfig(request, giteaToken, appCookies, {
|
||
giteaConfig: {
|
||
backupBeforeSync: false,
|
||
blockSyncOnBackupFailure: false,
|
||
},
|
||
});
|
||
console.log("[Config] Configuration saved (backup disabled)");
|
||
});
|
||
|
||
test("Step 5: Trigger GitHub data sync (fetch repos from fake GitHub)", async ({
|
||
request,
|
||
}) => {
|
||
if (!appCookies) {
|
||
appCookies = await getAppSessionCookies(request);
|
||
}
|
||
|
||
const syncResp = await request.post(`${APP_URL}/api/sync`, {
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Cookie: appCookies,
|
||
},
|
||
failOnStatusCode: false,
|
||
});
|
||
|
||
const status = syncResp.status();
|
||
console.log(`[Sync] GitHub sync response: ${status}`);
|
||
|
||
if (status >= 400) {
|
||
const body = await syncResp.text();
|
||
console.log(`[Sync] Error body: ${body}`);
|
||
}
|
||
|
||
expect(status, "Sync should not be unauthorized").not.toBe(401);
|
||
expect(status, "Sync should not return server error").toBeLessThan(500);
|
||
|
||
if (syncResp.ok()) {
|
||
const data = await syncResp.json();
|
||
console.log(
|
||
`[Sync] New repos: ${data.newRepositories ?? "?"}, new orgs: ${data.newOrganizations ?? "?"}`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test("Step 6: Trigger mirror jobs (push repos to Gitea)", async ({
|
||
request,
|
||
}) => {
|
||
if (!appCookies) {
|
||
appCookies = await getAppSessionCookies(request);
|
||
}
|
||
|
||
// Fetch repository IDs from the dashboard API
|
||
const { ids: repositoryIds, repos } = await getRepositoryIds(
|
||
request,
|
||
appCookies,
|
||
);
|
||
console.log(
|
||
`[Mirror] Found ${repositoryIds.length} repos to mirror: ${repos.map((r: any) => r.name).join(", ")}`,
|
||
);
|
||
|
||
if (repositoryIds.length === 0) {
|
||
// Fallback: try the github/repositories endpoint
|
||
const repoResp = await request.get(
|
||
`${APP_URL}/api/github/repositories`,
|
||
{
|
||
headers: { Cookie: appCookies },
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
if (repoResp.ok()) {
|
||
const repoData = await repoResp.json();
|
||
const fallbackRepos: any[] = Array.isArray(repoData)
|
||
? repoData
|
||
: (repoData.repositories ?? []);
|
||
repositoryIds.push(...fallbackRepos.map((r: any) => r.id));
|
||
console.log(
|
||
`[Mirror] Fallback: found ${repositoryIds.length} repos`,
|
||
);
|
||
}
|
||
}
|
||
|
||
expect(
|
||
repositoryIds.length,
|
||
"Should have at least one repository to mirror",
|
||
).toBeGreaterThan(0);
|
||
|
||
const status = await triggerMirrorJobs(
|
||
request,
|
||
appCookies,
|
||
repositoryIds,
|
||
30_000,
|
||
);
|
||
console.log(`[Mirror] Mirror job response: ${status}`);
|
||
|
||
expect(status, "Mirror job should not be unauthorized").not.toBe(401);
|
||
expect(status, "Mirror job should not return server error").toBeLessThan(
|
||
500,
|
||
);
|
||
});
|
||
|
||
test("Step 7: Verify repos were actually mirrored to Gitea", async ({
|
||
request,
|
||
}) => {
|
||
if (!appCookies) {
|
||
appCookies = await getAppSessionCookies(request);
|
||
}
|
||
|
||
// Wait for mirror jobs to finish processing
|
||
await waitFor(
|
||
async () => {
|
||
const orgRepos = await giteaApi.listOrgRepos(GITEA_MIRROR_ORG);
|
||
console.log(
|
||
`[Verify] Gitea org repos so far: ${orgRepos.length} (${orgRepos.map((r: any) => r.name).join(", ")})`,
|
||
);
|
||
// We expect at least 3 repos (my-project, dotfiles, notes)
|
||
return orgRepos.length >= 3;
|
||
},
|
||
{
|
||
timeout: 90_000,
|
||
interval: 5_000,
|
||
label: "repos appear in Gitea",
|
||
},
|
||
);
|
||
|
||
const orgRepos = await giteaApi.listOrgRepos(GITEA_MIRROR_ORG);
|
||
const orgRepoNames = orgRepos.map((r: any) => r.name);
|
||
console.log(
|
||
`[Verify] Gitea org repos: ${orgRepoNames.join(", ")} (total: ${orgRepos.length})`,
|
||
);
|
||
|
||
// Check that at least the 3 personal repos are mirrored
|
||
for (const repoName of ["my-project", "dotfiles", "notes"]) {
|
||
expect(
|
||
orgRepoNames,
|
||
`Expected repo "${repoName}" to be mirrored into org ${GITEA_MIRROR_ORG}`,
|
||
).toContain(repoName);
|
||
}
|
||
|
||
// Verify my-project has actual content (branches, commits)
|
||
const myProjectBranches = await giteaApi.listBranches(
|
||
GITEA_MIRROR_ORG,
|
||
"my-project",
|
||
);
|
||
const branchNames = myProjectBranches.map((b: any) => b.name);
|
||
console.log(`[Verify] my-project branches: ${branchNames.join(", ")}`);
|
||
expect(branchNames, "main branch should exist").toContain("main");
|
||
|
||
// Verify we can read actual file content
|
||
const readmeContent = await giteaApi.getFileContent(
|
||
GITEA_MIRROR_ORG,
|
||
"my-project",
|
||
"README.md",
|
||
);
|
||
expect(readmeContent, "README.md should have content").toBeTruthy();
|
||
expect(readmeContent).toContain("My Project");
|
||
console.log(
|
||
`[Verify] my-project README.md starts with: ${readmeContent?.substring(0, 50)}...`,
|
||
);
|
||
|
||
// Verify tags were mirrored
|
||
const tags = await giteaApi.listTags(GITEA_MIRROR_ORG, "my-project");
|
||
const tagNames = tags.map((t: any) => t.name);
|
||
console.log(`[Verify] my-project tags: ${tagNames.join(", ")}`);
|
||
if (tagNames.length > 0) {
|
||
expect(tagNames).toContain("v1.0.0");
|
||
}
|
||
|
||
// Verify commits exist
|
||
const commits = await giteaApi.listCommits(
|
||
GITEA_MIRROR_ORG,
|
||
"my-project",
|
||
);
|
||
console.log(`[Verify] my-project commits: ${commits.length}`);
|
||
expect(commits.length, "Should have multiple commits").toBeGreaterThan(0);
|
||
|
||
// Verify dotfiles repo has content
|
||
const bashrc = await giteaApi.getFileContent(
|
||
GITEA_MIRROR_ORG,
|
||
"dotfiles",
|
||
".bashrc",
|
||
);
|
||
expect(bashrc, "dotfiles should contain .bashrc").toBeTruthy();
|
||
console.log("[Verify] dotfiles .bashrc verified");
|
||
});
|
||
|
||
test("Step 8: Verify mirror jobs and app state", async ({ request }) => {
|
||
if (!appCookies) {
|
||
appCookies = await getAppSessionCookies(request);
|
||
}
|
||
|
||
// Check activity log
|
||
const activitiesResp = await request.get(`${APP_URL}/api/activities`, {
|
||
headers: { Cookie: appCookies },
|
||
failOnStatusCode: false,
|
||
});
|
||
|
||
if (activitiesResp.ok()) {
|
||
const activities = await activitiesResp.json();
|
||
const jobs: any[] = Array.isArray(activities)
|
||
? activities
|
||
: (activities.jobs ?? activities.activities ?? []);
|
||
console.log(`[State] Activity/job records: ${jobs.length}`);
|
||
|
||
const mirrorJobs = jobs.filter(
|
||
(j: any) =>
|
||
j.status === "mirroring" ||
|
||
j.status === "failed" ||
|
||
j.status === "success" ||
|
||
j.status === "mirrored" ||
|
||
j.message?.includes("mirror") ||
|
||
j.message?.includes("Mirror"),
|
||
);
|
||
console.log(`[State] Mirror-related jobs: ${mirrorJobs.length}`);
|
||
for (const j of mirrorJobs.slice(0, 5)) {
|
||
console.log(
|
||
`[State] • ${j.repositoryName ?? "?"}: ${j.status} — ${j.message ?? ""}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
// Check dashboard repos
|
||
const dashResp = await request.get(`${APP_URL}/api/dashboard`, {
|
||
headers: { Cookie: appCookies },
|
||
failOnStatusCode: false,
|
||
});
|
||
|
||
if (dashResp.ok()) {
|
||
const dashData = await dashResp.json();
|
||
const repos: any[] = dashData.repositories ?? [];
|
||
console.log(`[State] Dashboard repos: ${repos.length}`);
|
||
|
||
for (const r of repos) {
|
||
console.log(
|
||
`[State] • ${r.name}: status=${r.status}, mirrored=${r.mirroredLocation ?? "none"}`,
|
||
);
|
||
}
|
||
|
||
expect(repos.length, "Repos should exist in DB").toBeGreaterThan(0);
|
||
|
||
const succeeded = repos.filter(
|
||
(r: any) => r.status === "mirrored" || r.status === "success",
|
||
);
|
||
console.log(
|
||
`[State] Successfully mirrored repos: ${succeeded.length}/${repos.length}`,
|
||
);
|
||
}
|
||
|
||
// App should still be running
|
||
const healthResp = await request.get(`${APP_URL}/`, {
|
||
failOnStatusCode: false,
|
||
});
|
||
expect(
|
||
healthResp.status(),
|
||
"App should still be running after mirror attempts",
|
||
).toBeLessThan(500);
|
||
console.log(`[State] App health: ${healthResp.status()}`);
|
||
});
|
||
});
|