···11+/**
22+ * Cleanup Job Integration Tests
33+ *
44+ * Inserts real rows into the test database, calls handleCleanupJob(),
55+ * and verifies the PostgreSQL cleanup_transient_data() function removed
66+ * exactly the expired rows (and kept the fresh ones).
77+ *
88+ * Column names match the actual schema in scripts/init-db.sql:
99+ * oauth_states: state (PK), data (JSONB), created_at
1010+ * user_sessions: session_id (PK), did, fingerprint, expires_at
1111+ */
1212+1313+import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
1414+import type { Job } from "bullmq";
1515+import { sql } from "kysely";
1616+import { db, closeConnection } from "../src/db/client";
1717+import { handleCleanupJob } from "../src/jobs/cleanupJob";
1818+import type { CleanupJobData } from "../src/jobs/cleanupJob";
1919+2020+/** Minimal mock of a BullMQ Job — only the fields our handler accesses */
2121+function mockJob(id: string): Job<CleanupJobData> {
2222+ return { id, attemptsMade: 0 } as Job<CleanupJobData>;
2323+}
2424+2525+describe("Cleanup Job", () => {
2626+ beforeAll(async () => {
2727+ // Smoke-test the DB connection before running tests
2828+ await sql`SELECT 1`.execute(db);
2929+ });
3030+3131+ afterAll(async () => {
3232+ await closeConnection();
3333+ });
3434+3535+ beforeEach(async () => {
3636+ // Remove test rows left from a previous run
3737+ await sql`DELETE FROM oauth_states WHERE state LIKE 'test-%'`.execute(db);
3838+ await sql`DELETE FROM user_sessions WHERE session_id LIKE 'test-%'`.execute(db);
3939+ });
4040+4141+ it("removes expired OAuth states and keeps fresh ones", async () => {
4242+ // Expired: created 2 hours ago (threshold is 1 hour)
4343+ await sql`
4444+ INSERT INTO oauth_states (state, data, created_at)
4545+ VALUES ('test-expired-state', '{}', NOW() - INTERVAL '2 hours')
4646+ `.execute(db);
4747+4848+ // Fresh: created 30 seconds ago — must survive cleanup
4949+ await sql`
5050+ INSERT INTO oauth_states (state, data, created_at)
5151+ VALUES ('test-fresh-state', '{}', NOW() - INTERVAL '30 seconds')
5252+ `.execute(db);
5353+5454+ const result = await handleCleanupJob(mockJob("test-job-1"));
5555+5656+ expect(result.cleaned).toBe(true);
5757+ expect(result.timestamp).toBeDefined();
5858+5959+ const rows = await sql<{ state: string }>`
6060+ SELECT state FROM oauth_states WHERE state LIKE 'test-%'
6161+ `.execute(db);
6262+6363+ expect(rows.rows).toHaveLength(1);
6464+ expect(rows.rows[0].state).toBe("test-fresh-state");
6565+ });
6666+6767+ it("removes expired user sessions and keeps valid ones", async () => {
6868+ // Expired: expires_at in the past
6969+ await sql`
7070+ INSERT INTO user_sessions (session_id, did, fingerprint, expires_at)
7171+ VALUES ('test-expired-session', 'did:plc:test-cleanup-1', 'fp', NOW() - INTERVAL '1 day')
7272+ `.execute(db);
7373+7474+ // Valid: expires_at in the future
7575+ await sql`
7676+ INSERT INTO user_sessions (session_id, did, fingerprint, expires_at)
7777+ VALUES ('test-valid-session', 'did:plc:test-cleanup-2', 'fp', NOW() + INTERVAL '7 days')
7878+ `.execute(db);
7979+8080+ await handleCleanupJob(mockJob("test-job-2"));
8181+8282+ const rows = await sql<{ session_id: string }>`
8383+ SELECT session_id FROM user_sessions WHERE session_id LIKE 'test-%'
8484+ `.execute(db);
8585+8686+ expect(rows.rows).toHaveLength(1);
8787+ expect(rows.rows[0].session_id).toBe("test-valid-session");
8888+ });
8989+9090+ it("completes successfully when there is nothing to clean", async () => {
9191+ const result = await handleCleanupJob(mockJob("test-job-3"));
9292+9393+ expect(result.cleaned).toBe(true);
9494+ expect(result.timestamp).toBeDefined();
9595+ });
9696+9797+ it("returns a valid ISO timestamp", async () => {
9898+ const result = await handleCleanupJob(mockJob("test-job-4"));
9999+100100+ expect(() => new Date(result.timestamp)).not.toThrow();
101101+ expect(new Date(result.timestamp).toISOString()).toBe(result.timestamp);
102102+ });
103103+});
+19
packages/worker/__tests__/setup.ts
···11+/**
22+ * Worker Test Setup
33+ * Runs before all tests to configure the environment.
44+ */
55+66+import { config } from "dotenv";
77+88+// Load .env.test if present (overrides are fine — test env wins)
99+config({ path: ".env.test" });
1010+1111+if (!process.env.DATABASE_URL) {
1212+ process.env.DATABASE_URL =
1313+ "postgresql://atlast:localdev@localhost:5432/atlast";
1414+}
1515+1616+if (!process.env.REDIS_URL) {
1717+ // Use Redis DB index 1 for tests to avoid colliding with dev data
1818+ process.env.REDIS_URL = "redis://localhost:6379/1";
1919+}