Files
gitea-mirror/tests/e2e/03-backup.spec.ts
Xyndra 2e00a610cb Add E2E testing (#201)
* 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
2026-03-01 07:35:13 +05:30

306 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 03 Backup configuration tests.
*
* Exercises the pre-sync backup system by toggling config flags through
* the app API and triggering re-syncs on repos that were already mirrored
* by the 02-mirror-workflow suite.
*
* What is tested:
* B1. Enable backupBeforeSync in config
* B2. Confirm mirrored repos exist in Gitea (precondition)
* B3. Trigger a re-sync with backup enabled — verify the backup code path
* runs (snapshot activity entries appear in the activity log)
* B4. Inspect activity log for snapshot-related entries
* B5. Enable blockSyncOnBackupFailure and verify the flag is persisted
* B6. Disable backup and verify config resets cleanly
*/
import { test, expect } from "@playwright/test";
import {
APP_URL,
GITEA_URL,
GITEA_MIRROR_ORG,
GiteaAPI,
getAppSessionCookies,
saveConfig,
getRepositoryIds,
triggerSyncRepo,
} from "./helpers";
test.describe("E2E: Backup configuration", () => {
let giteaApi: GiteaAPI;
let appCookies = "";
test.beforeAll(async () => {
giteaApi = new GiteaAPI(GITEA_URL);
try {
await giteaApi.createToken();
} catch {
console.log(
"[Backup] Could not create Gitea token; tests may be limited",
);
}
});
test.afterAll(async () => {
await giteaApi.dispose();
});
// ── B1 ─────────────────────────────────────────────────────────────────────
test("Step B1: Enable backup in config", async ({ request }) => {
appCookies = await getAppSessionCookies(request);
const giteaToken = giteaApi.getTokenValue();
expect(giteaToken, "Gitea token required").toBeTruthy();
// Save config with backup enabled
await saveConfig(request, giteaToken, appCookies, {
giteaConfig: {
backupBeforeSync: true,
blockSyncOnBackupFailure: false,
backupRetentionCount: 5,
backupDirectory: "data/repo-backups",
},
});
// Verify config was saved
const configResp = await request.get(`${APP_URL}/api/config`, {
headers: { Cookie: appCookies },
failOnStatusCode: false,
});
expect(configResp.status()).toBeLessThan(500);
if (configResp.ok()) {
const configData = await configResp.json();
const giteaCfg = configData.giteaConfig ?? configData.gitea ?? {};
console.log(
`[Backup] Config saved: backupBeforeSync=${giteaCfg.backupBeforeSync}, blockOnFailure=${giteaCfg.blockSyncOnBackupFailure}`,
);
}
});
// ── B2 ─────────────────────────────────────────────────────────────────────
test("Step B2: Verify mirrored repos exist in Gitea before backup test", async () => {
// We need repos to already be mirrored from the 02-mirror-workflow suite
const orgRepos = await giteaApi.listOrgRepos(GITEA_MIRROR_ORG);
console.log(
`[Backup] Repos in ${GITEA_MIRROR_ORG}: ${orgRepos.length} (${orgRepos.map((r: any) => r.name).join(", ")})`,
);
if (orgRepos.length === 0) {
console.log(
"[Backup] WARNING: No repos in Gitea yet. Backup test will verify " +
"job creation but not bundle creation.",
);
}
});
// ── B3 ─────────────────────────────────────────────────────────────────────
test("Step B3: Trigger re-sync with backup enabled", async ({ request }) => {
if (!appCookies) {
appCookies = await getAppSessionCookies(request);
}
// Fetch mirrored repository IDs (sync-repo requires them)
const { ids: repositoryIds, repos } = await getRepositoryIds(
request,
appCookies,
{ status: "mirrored" },
);
// Also include repos with "success" status
if (repositoryIds.length === 0) {
const { ids: successIds } = await getRepositoryIds(
request,
appCookies,
{ status: "success" },
);
repositoryIds.push(...successIds);
}
// Fall back to all repos if no mirrored/success repos
if (repositoryIds.length === 0) {
const { ids: allIds } = await getRepositoryIds(request, appCookies);
repositoryIds.push(...allIds);
}
console.log(
`[Backup] Found ${repositoryIds.length} repos to re-sync: ` +
repos.map((r: any) => r.name).join(", "),
);
expect(
repositoryIds.length,
"Need at least one repo to test backup",
).toBeGreaterThan(0);
// Trigger sync-repo — this calls syncGiteaRepoEnhanced which checks
// shouldCreatePreSyncBackup and creates bundles before syncing
const status = await triggerSyncRepo(
request,
appCookies,
repositoryIds,
25_000,
);
console.log(`[Backup] Sync-repo response: ${status}`);
expect(status, "Sync-repo should accept request").toBeLessThan(500);
});
// ── B4 ─────────────────────────────────────────────────────────────────────
test("Step B4: Verify backup-related activity in logs", async ({
request,
}) => {
if (!appCookies) {
appCookies = await getAppSessionCookies(request);
}
const activitiesResp = await request.get(`${APP_URL}/api/activities`, {
headers: { Cookie: appCookies },
failOnStatusCode: false,
});
if (!activitiesResp.ok()) {
console.log(
`[Backup] Could not fetch activities: ${activitiesResp.status()}`,
);
return;
}
const activities = await activitiesResp.json();
const jobs: any[] = Array.isArray(activities)
? activities
: (activities.jobs ?? activities.activities ?? []);
// Look for backup / snapshot related messages
const backupJobs = jobs.filter(
(j: any) =>
j.message?.toLowerCase().includes("snapshot") ||
j.message?.toLowerCase().includes("backup") ||
j.details?.toLowerCase().includes("snapshot") ||
j.details?.toLowerCase().includes("backup") ||
j.details?.toLowerCase().includes("bundle"),
);
console.log(
`[Backup] Backup-related activity entries: ${backupJobs.length}`,
);
for (const j of backupJobs.slice(0, 10)) {
console.log(
`[Backup] • ${j.repositoryName ?? "?"}: ${j.status}${j.message ?? ""} | ${(j.details ?? "").substring(0, 120)}`,
);
}
// We expect at least some backup-related entries if repos were mirrored
const orgRepos = await giteaApi.listOrgRepos(GITEA_MIRROR_ORG);
if (orgRepos.length > 0) {
// With repos in Gitea, the backup system should have tried to create
// snapshots. All snapshots should succeed.
expect(
backupJobs.length,
"Expected at least one backup/snapshot activity entry when " +
"backupBeforeSync is enabled and repos exist in Gitea",
).toBeGreaterThan(0);
// Check for any failed backups
const failedBackups = backupJobs.filter(
(j: any) =>
j.status === "failed" &&
(j.message?.toLowerCase().includes("snapshot") ||
j.details?.toLowerCase().includes("snapshot")),
);
expect(
failedBackups.length,
`Expected all backups to succeed, but ${failedBackups.length} backup(s) failed. ` +
`Failed: ${failedBackups.map((j: any) => `${j.repositoryName}: ${j.details?.substring(0, 100)}`).join("; ")}`,
).toBe(0);
console.log(
`[Backup] Confirmed: backup system was invoked for ${backupJobs.length} repos`,
);
}
// Dump all recent jobs for debugging visibility
console.log(`[Backup] All recent jobs (last 20):`);
for (const j of jobs.slice(0, 20)) {
console.log(
`[Backup] - [${j.status}] ${j.repositoryName ?? "?"}: ${j.message ?? ""} ` +
`${j.details ? `(${j.details.substring(0, 80)})` : ""}`,
);
}
});
// ── B5 ─────────────────────────────────────────────────────────────────────
test("Step B5: Enable blockSyncOnBackupFailure and verify behavior", async ({
request,
}) => {
if (!appCookies) {
appCookies = await getAppSessionCookies(request);
}
const giteaToken = giteaApi.getTokenValue();
// Update config to block sync on backup failure
await saveConfig(request, giteaToken, appCookies, {
giteaConfig: {
backupBeforeSync: true,
blockSyncOnBackupFailure: true,
backupRetentionCount: 5,
backupDirectory: "data/repo-backups",
},
});
console.log("[Backup] Config updated: blockSyncOnBackupFailure=true");
// Verify the flag persisted
const configResp = await request.get(`${APP_URL}/api/config`, {
headers: { Cookie: appCookies },
failOnStatusCode: false,
});
if (configResp.ok()) {
const configData = await configResp.json();
const giteaCfg = configData.giteaConfig ?? configData.gitea ?? {};
expect(giteaCfg.blockSyncOnBackupFailure).toBe(true);
console.log(
`[Backup] Verified: blockSyncOnBackupFailure=${giteaCfg.blockSyncOnBackupFailure}`,
);
}
});
// ── B6 ─────────────────────────────────────────────────────────────────────
test("Step B6: Disable backup and verify config resets", async ({
request,
}) => {
if (!appCookies) {
appCookies = await getAppSessionCookies(request);
}
const giteaToken = giteaApi.getTokenValue();
// Disable backup
await saveConfig(request, giteaToken, appCookies, {
giteaConfig: {
backupBeforeSync: false,
blockSyncOnBackupFailure: false,
},
});
const configResp = await request.get(`${APP_URL}/api/config`, {
headers: { Cookie: appCookies },
failOnStatusCode: false,
});
if (configResp.ok()) {
const configData = await configResp.json();
const giteaCfg = configData.giteaConfig ?? configData.gitea ?? {};
console.log(
`[Backup] After disable: backupBeforeSync=${giteaCfg.backupBeforeSync}`,
);
}
console.log("[Backup] Backup configuration test complete");
});
});