Files
buzz-sheet/tests/deployment-contract.test.ts
gunshiz 691a14ca09
CI / Verify (push) Successful in 1m9s
CI / Build immutable images and deploy (push) Successful in 1m41s
fix : add base url
2026-09-13 16:10:15 +07:00

195 lines
7.8 KiB
TypeScript

import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
async function repositoryFile(path: string): Promise<string> {
return readFile(new URL(`../${path}`, import.meta.url), "utf8");
}
describe("production deployment contract", () => {
it("defines a resilient two-replica web workload", async () => {
const deployment = await repositoryFile("k8s/base/deployment.yaml");
expect(deployment).toContain("replicas: 2");
expect(deployment).toContain("maxSurge: 1");
expect(deployment).toContain("maxUnavailable: 0");
expect(deployment).toContain("path: /api/health?ready=1");
expect(deployment).toContain("path: /api/health");
expect(deployment).toContain("kubernetes.io/arch: arm64");
expect(deployment).toMatch(
/requests:\s+cpu: 500m\s+memory: 1Gi\s+limits:\s+cpu: "1"\s+memory: 2Gi/u,
);
expect(deployment).toContain("runAsNonRoot: true");
expect(deployment).toContain("runAsUser: 1000");
expect(deployment).toContain("runAsGroup: 1000");
expect(deployment).toContain("readOnlyRootFilesystem: true");
expect(deployment).toContain('drop: ["ALL"]');
});
it("exposes only the app through the requested edge-TLS host", async () => {
const [service, ingress] = await Promise.all([
repositoryFile("k8s/base/service.yaml"),
repositoryFile("k8s/base/ingress.yaml"),
]);
expect(service).toContain("type: ClusterIP");
expect(service).toContain("port: 3000");
expect(service).toContain("targetPort: http");
expect(ingress).toContain("ingressClassName: traefik");
expect(ingress).toContain("host: guide.sudloh.com");
expect(ingress).toContain(
'traefik.ingress.kubernetes.io/read-timeout: "200"',
);
expect(ingress).not.toContain("secretName:");
expect(ingress).not.toContain("router.tls");
});
it("keeps availability and scaling bounds explicit", async () => {
const [hpa, pdb] = await Promise.all([
repositoryFile("k8s/base/hpa.yaml"),
repositoryFile("k8s/base/pdb.yaml"),
]);
expect(hpa).toContain("minReplicas: 2");
expect(hpa).toContain("maxReplicas: 6");
expect(hpa).toContain("averageUtilization: 70");
expect(hpa).toContain("name: memory");
expect(hpa).toContain("averageUtilization: 75");
expect(pdb).toContain("minAvailable: 1");
});
it("uses externally supplied secrets and does not provision data stores", async () => {
const manifestPaths = [
"k8s/base/namespace.yaml",
"k8s/base/configmap.yaml",
"k8s/base/deployment.yaml",
"k8s/base/worker-deployment.yaml",
"k8s/base/discord-worker-deployment.yaml",
"k8s/base/service.yaml",
"k8s/base/ingress.yaml",
"k8s/base/hpa.yaml",
"k8s/base/pdb.yaml",
"k8s/base/ci-rbac.yaml",
"k8s/migration/job.yaml",
];
const manifests = (
await Promise.all(manifestPaths.map(repositoryFile))
).join("\n---\n");
expect(manifests).toContain("name: buzz-sheet-env");
expect(manifests).not.toMatch(/kind: (Secret|StatefulSet|PersistentVolumeClaim)/u);
expect(manifests).not.toContain("resources: [\"secrets\"]");
expect(manifests).not.toContain("kind: ClusterRole");
expect(manifests).not.toContain("kind: ClusterRoleBinding");
});
it("packages non-root Bun application and migration targets", async () => {
const dockerfile = await repositoryFile("Dockerfile");
expect(dockerfile).toContain("FROM dependencies AS migration");
expect(dockerfile).toContain("FROM oven/bun:${BUN_VERSION} AS app");
expect(dockerfile.match(/^USER 1000:1000$/gmu)).toHaveLength(2);
expect(dockerfile).toContain('ENTRYPOINT ["bun", "scripts/migrate.ts"]');
expect(dockerfile).toContain('CMD ["bun", "server.js"]');
expect(dockerfile).toContain("ARG BASE_URL\nENV BASE_URL=${BASE_URL}");
expect(dockerfile).toContain("backend/discord.ts");
expect(dockerfile).toContain("./worker/discord.js");
});
it("verifies first, publishes immutable images, and migrates before rollout", async () => {
const workflow = await repositoryFile(".gitea/workflows/ci.yml");
expect(workflow).toContain("bun install --frozen-lockfile");
expect(workflow).toContain("bun run test");
expect(workflow).toContain("bun run typecheck");
expect(workflow).toContain("bun run lint");
expect(workflow).toContain("kubectl kustomize k8s/");
expect(workflow).toContain("--target app");
expect(workflow).toContain('--build-arg BASE_URL="$BASE_URL"');
expect(workflow).toContain('\\"BASE_URL\\":\\"$BASE_URL\\"');
expect(workflow).toContain("--target migration");
expect(workflow).not.toContain("platforms: linux/arm64");
expect(workflow).not.toMatch(/docker\/setup-qemu-action|docker\/setup-buildx-action|docker\/login-action/iu);
expect(workflow).toContain(
"registry.neko-piranha.ts.net/astral/buzz-sheet",
);
expect(workflow).toContain("migrate-${{ gitea.sha }}");
expect(workflow).toContain("deployment/buzz-sheet-discord-worker");
const deleteMigrationJob = workflow.indexOf(
'delete job buzz-sheet-migrate --ignore-not-found',
);
const createMigrationJob = workflow.indexOf(
'create --validate=false -f "$migration_manifest"',
);
expect(deleteMigrationJob).toBeGreaterThan(-1);
expect(deleteMigrationJob).toBeLessThan(createMigrationJob);
expect(workflow.indexOf("condition=complete")).toBeLessThan(
workflow.indexOf("set image"),
);
expect(workflow).not.toMatch(/playwright|chromium/iu);
});
it("mounts the deployment stream and offers a full reload for new releases", async () => {
const [layout, notifier, route] = await Promise.all([
repositoryFile("app/layout.tsx"),
repositoryFile("components/deployment-update-notifier.tsx"),
repositoryFile("app/api/active/route.ts"),
]);
expect(layout).toContain("<DeploymentUpdateNotifier");
expect(notifier).toContain('new EventSource("/api/active")');
expect(notifier).toContain("window.location.reload()");
expect(notifier).toContain('label: "รีโหลด"');
expect(route).toContain('channel.sendNamed("deployment", deploymentId)');
});
});
describe("environment template contract", () => {
it("documents every externally supplied production secret", async () => {
const environmentExample = await repositoryFile(".env.example");
const requiredKeys = [
"DATABASE_URL",
"BETTER_AUTH_URL",
"BETTER_AUTH_SECRET",
"REDIS_URL",
"S3_ENDPOINT",
"S3_BUCKET",
"S3_ACCESS_KEY_ID",
"S3_SECRET_ACCESS_KEY",
"NEXT_SERVER_ACTIONS_ENCRYPTION_KEY",
"NEXT_DEPLOYMENT_ID",
"DISCORD_BOT_TOKEN",
"DISCORD_CHANNEL_ID",
"DISCORD_LOG_CHANNEL_ID",
];
for (const key of requiredKeys) {
expect(environmentExample).toMatch(new RegExp(`^${key}=`, "mu"));
}
expect(environmentExample).toContain("BUZZ_DEMO_MODE=false");
});
it("runs Discord catalog sync as an isolated worker", async () => {
const [deployment, rbac] = await Promise.all([
repositoryFile("k8s/base/discord-worker-deployment.yaml"),
repositoryFile("k8s/base/ci-rbac.yaml"),
]);
expect(deployment).toContain("name: buzz-sheet-discord-worker");
expect(deployment).toContain("type: Recreate");
expect(deployment).toContain('command: ["bun", "worker/discord.js"]');
expect(deployment).toContain("replicas: 1");
expect(deployment).toContain("readOnlyRootFilesystem: true");
expect(deployment).toContain('drop: ["ALL"]');
expect(rbac).toContain("buzz-sheet-discord-worker");
});
it("syncs only for new Discord version announcements", async () => {
const worker = await repositoryFile("backend/discord.ts");
expect(worker).toContain("client.on(Events.MessageCreate, handleMessage)");
expect(worker).not.toContain("channel.messages.fetch");
expect(worker).not.toContain('source: "history"');
});
});