mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-03-11 17:44:26 -05: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
667 lines
19 KiB
TypeScript
667 lines
19 KiB
TypeScript
/**
|
||
* Shared helpers for E2E tests.
|
||
*
|
||
* Exports constants, the GiteaAPI wrapper, auth helpers (sign-up / sign-in),
|
||
* the saveConfig helper, and a generic waitFor polling utility.
|
||
*/
|
||
|
||
import {
|
||
expect,
|
||
request as playwrightRequest,
|
||
type Page,
|
||
type APIRequestContext,
|
||
} from "@playwright/test";
|
||
|
||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||
|
||
export const APP_URL = process.env.APP_URL || "http://localhost:4321";
|
||
export const GITEA_URL = process.env.GITEA_URL || "http://localhost:3333";
|
||
export const FAKE_GITHUB_URL =
|
||
process.env.FAKE_GITHUB_URL || "http://localhost:4580";
|
||
export const GIT_SERVER_URL =
|
||
process.env.GIT_SERVER_URL || "http://localhost:4590";
|
||
|
||
export const GITEA_ADMIN_USER = "e2e_admin";
|
||
export const GITEA_ADMIN_PASS = "e2eAdminPass123!";
|
||
export const GITEA_ADMIN_EMAIL = "admin@e2e-test.local";
|
||
|
||
export const APP_USER_EMAIL = "e2e@test.local";
|
||
export const APP_USER_PASS = "E2eTestPass123!";
|
||
export const APP_USER_NAME = "e2e-tester";
|
||
|
||
export const GITEA_MIRROR_ORG = "github-mirrors";
|
||
|
||
// ─── waitFor ─────────────────────────────────────────────────────────────────
|
||
|
||
/** Retry a function until it returns truthy or timeout is reached. */
|
||
export async function waitFor(
|
||
fn: () => Promise<boolean>,
|
||
{
|
||
timeout = 60_000,
|
||
interval = 2_000,
|
||
label = "condition",
|
||
}: { timeout?: number; interval?: number; label?: string } = {},
|
||
): Promise<void> {
|
||
const deadline = Date.now() + timeout;
|
||
let lastErr: Error | undefined;
|
||
while (Date.now() < deadline) {
|
||
try {
|
||
if (await fn()) return;
|
||
} catch (e) {
|
||
lastErr = e instanceof Error ? e : new Error(String(e));
|
||
}
|
||
await new Promise((r) => setTimeout(r, interval));
|
||
}
|
||
throw new Error(
|
||
`waitFor("${label}") timed out after ${timeout}ms` +
|
||
(lastErr ? `: ${lastErr.message}` : ""),
|
||
);
|
||
}
|
||
|
||
// ─── GiteaAPI ────────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Direct HTTP helper for talking to Gitea's API.
|
||
*
|
||
* Uses a manually-created APIRequestContext so it can be shared across
|
||
* beforeAll / afterAll / individual tests without hitting Playwright's
|
||
* "fixture from beforeAll cannot be reused" restriction.
|
||
*/
|
||
export class GiteaAPI {
|
||
private token = "";
|
||
private ctx: APIRequestContext | null = null;
|
||
|
||
constructor(private baseUrl: string) {}
|
||
|
||
/** Lazily create (and cache) a Playwright APIRequestContext. */
|
||
private async getCtx(): Promise<APIRequestContext> {
|
||
if (!this.ctx) {
|
||
this.ctx = await playwrightRequest.newContext({
|
||
baseURL: this.baseUrl,
|
||
});
|
||
}
|
||
return this.ctx;
|
||
}
|
||
|
||
/** Dispose of the underlying context – call in afterAll. */
|
||
async dispose(): Promise<void> {
|
||
if (this.ctx) {
|
||
await this.ctx.dispose();
|
||
this.ctx = null;
|
||
}
|
||
}
|
||
|
||
/** Create the admin user via Gitea's sign-up form (first user becomes admin). */
|
||
async ensureAdminUser(): Promise<void> {
|
||
const ctx = await this.getCtx();
|
||
|
||
// Check if admin already exists by trying basic-auth
|
||
try {
|
||
const resp = await ctx.get(`/api/v1/user`, {
|
||
headers: {
|
||
Authorization: `Basic ${btoa(`${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASS}`)}`,
|
||
},
|
||
failOnStatusCode: false,
|
||
});
|
||
if (resp.ok()) {
|
||
console.log("[GiteaAPI] Admin user already exists");
|
||
return;
|
||
}
|
||
} catch {
|
||
// Expected on first run
|
||
}
|
||
|
||
// Register through the form – first user auto-becomes admin
|
||
console.log("[GiteaAPI] Creating admin via sign-up form...");
|
||
const signUpResp = await ctx.post(`/user/sign_up`, {
|
||
form: {
|
||
user_name: GITEA_ADMIN_USER,
|
||
password: GITEA_ADMIN_PASS,
|
||
retype: GITEA_ADMIN_PASS,
|
||
email: GITEA_ADMIN_EMAIL,
|
||
},
|
||
failOnStatusCode: false,
|
||
maxRedirects: 5,
|
||
});
|
||
console.log(`[GiteaAPI] Sign-up response status: ${signUpResp.status()}`);
|
||
|
||
// Verify
|
||
const check = await ctx.get(`/api/v1/user`, {
|
||
headers: {
|
||
Authorization: `Basic ${btoa(`${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASS}`)}`,
|
||
},
|
||
failOnStatusCode: false,
|
||
});
|
||
if (!check.ok()) {
|
||
throw new Error(
|
||
`Failed to verify admin user after creation (status ${check.status()})`,
|
||
);
|
||
}
|
||
console.log("[GiteaAPI] Admin user verified");
|
||
}
|
||
|
||
/** Generate a Gitea API token for the admin user. */
|
||
async createToken(): Promise<string> {
|
||
if (this.token) return this.token;
|
||
const ctx = await this.getCtx();
|
||
|
||
const tokenName = `e2e-token-${Date.now()}`;
|
||
const resp = await ctx.post(`/api/v1/users/${GITEA_ADMIN_USER}/tokens`, {
|
||
headers: {
|
||
Authorization: `Basic ${btoa(`${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASS}`)}`,
|
||
"Content-Type": "application/json",
|
||
},
|
||
data: {
|
||
name: tokenName,
|
||
scopes: [
|
||
"read:user",
|
||
"write:user",
|
||
"read:organization",
|
||
"write:organization",
|
||
"read:repository",
|
||
"write:repository",
|
||
"read:issue",
|
||
"write:issue",
|
||
"read:misc",
|
||
"write:misc",
|
||
"read:admin",
|
||
"write:admin",
|
||
],
|
||
},
|
||
});
|
||
expect(
|
||
resp.ok(),
|
||
`Failed to create Gitea token: ${resp.status()}`,
|
||
).toBeTruthy();
|
||
const data = await resp.json();
|
||
this.token = data.sha1 || data.token;
|
||
console.log(`[GiteaAPI] Created token: ${tokenName}`);
|
||
return this.token;
|
||
}
|
||
|
||
/** Create an organization in Gitea. */
|
||
async ensureOrg(orgName: string): Promise<void> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
|
||
// Check if org exists
|
||
const check = await ctx.get(`/api/v1/orgs/${orgName}`, {
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
});
|
||
if (check.ok()) {
|
||
console.log(`[GiteaAPI] Org ${orgName} already exists`);
|
||
return;
|
||
}
|
||
|
||
const resp = await ctx.post(`/api/v1/orgs`, {
|
||
headers: {
|
||
Authorization: `token ${token}`,
|
||
"Content-Type": "application/json",
|
||
},
|
||
data: {
|
||
username: orgName,
|
||
full_name: orgName,
|
||
description: "E2E test mirror organization",
|
||
visibility: "public",
|
||
},
|
||
});
|
||
expect(resp.ok(), `Failed to create org: ${resp.status()}`).toBeTruthy();
|
||
console.log(`[GiteaAPI] Created org: ${orgName}`);
|
||
}
|
||
|
||
/** List repos in a Gitea org. */
|
||
async listOrgRepos(orgName: string): Promise<any[]> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const resp = await ctx.get(`/api/v1/orgs/${orgName}/repos`, {
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
});
|
||
if (!resp.ok()) return [];
|
||
return resp.json();
|
||
}
|
||
|
||
/** List repos for the admin user. */
|
||
async listUserRepos(): Promise<any[]> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const resp = await ctx.get(`/api/v1/users/${GITEA_ADMIN_USER}/repos`, {
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
});
|
||
if (!resp.ok()) return [];
|
||
return resp.json();
|
||
}
|
||
|
||
/** Get a specific repo. */
|
||
async getRepo(owner: string, name: string): Promise<any | null> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const resp = await ctx.get(`/api/v1/repos/${owner}/${name}`, {
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
});
|
||
if (!resp.ok()) return null;
|
||
return resp.json();
|
||
}
|
||
|
||
/** List branches for a repo. */
|
||
async listBranches(owner: string, name: string): Promise<any[]> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const resp = await ctx.get(`/api/v1/repos/${owner}/${name}/branches`, {
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
});
|
||
if (!resp.ok()) return [];
|
||
return resp.json();
|
||
}
|
||
|
||
/** List tags for a repo. */
|
||
async listTags(owner: string, name: string): Promise<any[]> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const resp = await ctx.get(`/api/v1/repos/${owner}/${name}/tags`, {
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
});
|
||
if (!resp.ok()) return [];
|
||
return resp.json();
|
||
}
|
||
|
||
/** List commits for a repo (on default branch). */
|
||
async listCommits(
|
||
owner: string,
|
||
name: string,
|
||
opts?: { sha?: string; limit?: number },
|
||
): Promise<any[]> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const params = new URLSearchParams();
|
||
if (opts?.sha) params.set("sha", opts.sha);
|
||
if (opts?.limit) params.set("limit", String(opts.limit));
|
||
const qs = params.toString() ? `?${params.toString()}` : "";
|
||
const resp = await ctx.get(
|
||
`/api/v1/repos/${owner}/${name}/commits${qs}`,
|
||
{
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
if (!resp.ok()) return [];
|
||
return resp.json();
|
||
}
|
||
|
||
/** Get a single branch (includes the commit SHA). */
|
||
async getBranch(
|
||
owner: string,
|
||
name: string,
|
||
branch: string,
|
||
): Promise<any | null> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const resp = await ctx.get(
|
||
`/api/v1/repos/${owner}/${name}/branches/${branch}`,
|
||
{
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
if (!resp.ok()) return null;
|
||
return resp.json();
|
||
}
|
||
|
||
/** Get file content from a repo. */
|
||
async getFileContent(
|
||
owner: string,
|
||
name: string,
|
||
filePath: string,
|
||
ref?: string,
|
||
): Promise<string | null> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const refQuery = ref ? `?ref=${encodeURIComponent(ref)}` : "";
|
||
const resp = await ctx.get(
|
||
`/api/v1/repos/${owner}/${name}/raw/${filePath}${refQuery}`,
|
||
{
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
if (!resp.ok()) return null;
|
||
return resp.text();
|
||
}
|
||
|
||
/** Get a commit by SHA. */
|
||
async getCommit(
|
||
owner: string,
|
||
name: string,
|
||
sha: string,
|
||
): Promise<any | null> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const resp = await ctx.get(
|
||
`/api/v1/repos/${owner}/${name}/git/commits/${sha}`,
|
||
{
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
if (!resp.ok()) return null;
|
||
return resp.json();
|
||
}
|
||
|
||
/** Trigger mirror sync for a repo via the Gitea API directly. */
|
||
async triggerMirrorSync(owner: string, name: string): Promise<boolean> {
|
||
const ctx = await this.getCtx();
|
||
const token = await this.createToken();
|
||
const resp = await ctx.post(
|
||
`/api/v1/repos/${owner}/${name}/mirror-sync`,
|
||
{
|
||
headers: { Authorization: `token ${token}` },
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
return resp.ok() || resp.status() === 200;
|
||
}
|
||
|
||
getTokenValue(): string {
|
||
return this.token;
|
||
}
|
||
}
|
||
|
||
// ─── App auth helpers ────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Sign up + sign in to the gitea-mirror app using the Better Auth REST API
|
||
* and return the session cookie string.
|
||
*/
|
||
export async function getAppSessionCookies(
|
||
request: APIRequestContext,
|
||
): Promise<string> {
|
||
// 1. Try sign-in first (user may already exist from a previous test / run)
|
||
const signInResp = await request.post(`${APP_URL}/api/auth/sign-in/email`, {
|
||
data: { email: APP_USER_EMAIL, password: APP_USER_PASS },
|
||
failOnStatusCode: false,
|
||
});
|
||
|
||
if (signInResp.ok()) {
|
||
const cookies = extractSetCookies(signInResp);
|
||
if (cookies) {
|
||
console.log("[App] Signed in (existing user)");
|
||
return cookies;
|
||
}
|
||
}
|
||
|
||
// 2. Register
|
||
const signUpResp = await request.post(`${APP_URL}/api/auth/sign-up/email`, {
|
||
data: {
|
||
name: APP_USER_NAME,
|
||
email: APP_USER_EMAIL,
|
||
password: APP_USER_PASS,
|
||
},
|
||
failOnStatusCode: false,
|
||
});
|
||
const signUpStatus = signUpResp.status();
|
||
console.log(`[App] Sign-up response: ${signUpStatus}`);
|
||
|
||
// After sign-up Better Auth may already set a session cookie
|
||
const signUpCookies = extractSetCookies(signUpResp);
|
||
if (signUpCookies) {
|
||
console.log("[App] Got session from sign-up response");
|
||
return signUpCookies;
|
||
}
|
||
|
||
// 3. Sign in after registration
|
||
const postRegSignIn = await request.post(
|
||
`${APP_URL}/api/auth/sign-in/email`,
|
||
{
|
||
data: { email: APP_USER_EMAIL, password: APP_USER_PASS },
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
if (!postRegSignIn.ok()) {
|
||
const body = await postRegSignIn.text();
|
||
throw new Error(
|
||
`Sign-in after registration failed (${postRegSignIn.status()}): ${body}`,
|
||
);
|
||
}
|
||
const cookies = extractSetCookies(postRegSignIn);
|
||
if (!cookies) {
|
||
throw new Error("Sign-in succeeded but no session cookie was returned");
|
||
}
|
||
console.log("[App] Signed in (after registration)");
|
||
return cookies;
|
||
}
|
||
|
||
/**
|
||
* Extract session cookies from a response's `set-cookie` headers.
|
||
*/
|
||
export function extractSetCookies(
|
||
resp: Awaited<ReturnType<APIRequestContext["post"]>>,
|
||
): string {
|
||
const raw = resp
|
||
.headersArray()
|
||
.filter((h) => h.name.toLowerCase() === "set-cookie");
|
||
if (raw.length === 0) return "";
|
||
|
||
const pairs: string[] = [];
|
||
for (const header of raw) {
|
||
const nv = header.value.split(";")[0].trim();
|
||
if (nv) pairs.push(nv);
|
||
}
|
||
|
||
return pairs.join("; ");
|
||
}
|
||
|
||
/**
|
||
* Sign in via the browser UI so the browser context gets session cookies.
|
||
*/
|
||
export async function signInViaBrowser(page: Page): Promise<string> {
|
||
const signInResp = await page.request.post(
|
||
`${APP_URL}/api/auth/sign-in/email`,
|
||
{
|
||
data: { email: APP_USER_EMAIL, password: APP_USER_PASS },
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
|
||
if (!signInResp.ok()) {
|
||
const signUpResp = await page.request.post(
|
||
`${APP_URL}/api/auth/sign-up/email`,
|
||
{
|
||
data: {
|
||
name: APP_USER_NAME,
|
||
email: APP_USER_EMAIL,
|
||
password: APP_USER_PASS,
|
||
},
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
console.log(`[Browser] Sign-up status: ${signUpResp.status()}`);
|
||
|
||
const retryResp = await page.request.post(
|
||
`${APP_URL}/api/auth/sign-in/email`,
|
||
{
|
||
data: { email: APP_USER_EMAIL, password: APP_USER_PASS },
|
||
failOnStatusCode: false,
|
||
},
|
||
);
|
||
if (!retryResp.ok()) {
|
||
console.log(`[Browser] Sign-in retry failed: ${retryResp.status()}`);
|
||
}
|
||
}
|
||
|
||
await page.goto(`${APP_URL}/`);
|
||
await page.waitForLoadState("networkidle");
|
||
const url = page.url();
|
||
console.log(`[Browser] After sign-in, URL: ${url}`);
|
||
|
||
const cookies = await page.context().cookies();
|
||
return cookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
||
}
|
||
|
||
// ─── Config helper ───────────────────────────────────────────────────────────
|
||
|
||
/** Save app config via the API. */
|
||
export async function saveConfig(
|
||
request: APIRequestContext,
|
||
giteaToken: string,
|
||
cookies: string,
|
||
overrides: Record<string, any> = {},
|
||
): Promise<void> {
|
||
const giteaConfigDefaults = {
|
||
url: GITEA_URL,
|
||
username: GITEA_ADMIN_USER,
|
||
token: giteaToken,
|
||
organization: GITEA_MIRROR_ORG,
|
||
visibility: "public",
|
||
starredReposOrg: "github-stars",
|
||
preserveOrgStructure: false,
|
||
mirrorStrategy: "single-org",
|
||
backupBeforeSync: false,
|
||
blockSyncOnBackupFailure: false,
|
||
};
|
||
|
||
const configPayload = {
|
||
githubConfig: {
|
||
username: "e2e-test-user",
|
||
token: "fake-github-token-for-e2e",
|
||
privateRepositories: false,
|
||
mirrorStarred: true,
|
||
},
|
||
giteaConfig: { ...giteaConfigDefaults, ...(overrides.giteaConfig || {}) },
|
||
scheduleConfig: {
|
||
enabled: false,
|
||
interval: 3600,
|
||
},
|
||
cleanupConfig: {
|
||
enabled: false,
|
||
retentionDays: 86400,
|
||
deleteIfNotInGitHub: false,
|
||
orphanedRepoAction: "skip",
|
||
dryRun: true,
|
||
},
|
||
mirrorOptions: {
|
||
mirrorReleases: false,
|
||
mirrorLFS: false,
|
||
mirrorMetadata: false,
|
||
metadataComponents: {
|
||
issues: false,
|
||
pullRequests: false,
|
||
labels: false,
|
||
milestones: false,
|
||
wiki: false,
|
||
},
|
||
},
|
||
advancedOptions: {
|
||
skipForks: false,
|
||
starredCodeOnly: false,
|
||
},
|
||
};
|
||
|
||
const resp = await request.post(`${APP_URL}/api/config`, {
|
||
data: configPayload,
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Cookie: cookies,
|
||
},
|
||
failOnStatusCode: false,
|
||
});
|
||
|
||
const status = resp.status();
|
||
console.log(`[App] Save config response: ${status}`);
|
||
|
||
if (status >= 400) {
|
||
const body = await resp.text();
|
||
console.log(`[App] Config error body: ${body}`);
|
||
}
|
||
|
||
expect(status, "Config save should not return server error").toBeLessThan(
|
||
500,
|
||
);
|
||
}
|
||
|
||
// ─── Dashboard / repo helpers ────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Fetch the list of repository IDs from the app's dashboard API.
|
||
* Optionally filter to repos with a given status.
|
||
*/
|
||
export async function getRepositoryIds(
|
||
request: APIRequestContext,
|
||
cookies: string,
|
||
opts?: { status?: string },
|
||
): Promise<{ ids: string[]; repos: any[] }> {
|
||
const dashResp = await request.get(`${APP_URL}/api/dashboard`, {
|
||
headers: { Cookie: cookies },
|
||
failOnStatusCode: false,
|
||
});
|
||
if (!dashResp.ok()) return { ids: [], repos: [] };
|
||
|
||
const dashData = await dashResp.json();
|
||
const repos: any[] = dashData.repositories ?? dashData.repos ?? [];
|
||
|
||
const filtered = opts?.status
|
||
? repos.filter((r: any) => r.status === opts.status)
|
||
: repos;
|
||
|
||
return {
|
||
ids: filtered.map((r: any) => r.id),
|
||
repos: filtered,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Trigger mirror jobs for the given repository IDs via the app API,
|
||
* then wait for a specified delay for async processing.
|
||
*/
|
||
export async function triggerMirrorJobs(
|
||
request: APIRequestContext,
|
||
cookies: string,
|
||
repositoryIds: string[],
|
||
waitMs = 30_000,
|
||
): Promise<number> {
|
||
const mirrorResp = await request.post(`${APP_URL}/api/job/mirror-repo`, {
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Cookie: cookies,
|
||
},
|
||
data: { repositoryIds },
|
||
failOnStatusCode: false,
|
||
});
|
||
|
||
const status = mirrorResp.status();
|
||
if (waitMs > 0) {
|
||
await new Promise((r) => setTimeout(r, waitMs));
|
||
}
|
||
return status;
|
||
}
|
||
|
||
/**
|
||
* Trigger sync-repo (re-sync already-mirrored repos) for the given
|
||
* repository IDs, then wait for processing.
|
||
*/
|
||
export async function triggerSyncRepo(
|
||
request: APIRequestContext,
|
||
cookies: string,
|
||
repositoryIds: string[],
|
||
waitMs = 25_000,
|
||
): Promise<number> {
|
||
const syncResp = await request.post(`${APP_URL}/api/job/sync-repo`, {
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Cookie: cookies,
|
||
},
|
||
data: { repositoryIds },
|
||
failOnStatusCode: false,
|
||
});
|
||
|
||
const status = syncResp.status();
|
||
if (waitMs > 0) {
|
||
await new Promise((r) => setTimeout(r, waitMs));
|
||
}
|
||
return status;
|
||
}
|