Files
wehub-resource-sync b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00

278 lines
7.6 KiB
TypeScript

/**
* Tests for the cleanup subsystems.
*
* Note: runSystemCleanup() is not tested directly here because it imports
* from @emdash-cms/auth/adapters/kysely, which requires the auth package to
* be built. Instead, we test each subsystem independently:
* - cleanupExpiredChallenges: tested in auth/challenge-store.test.ts
* - deleteExpiredTokens: tested below using direct DB operations
* - cleanupPendingUploads: tested below via MediaRepository
* - pruneOldRevisions: tested below via RevisionRepository
*/
import type { Kysely } from "kysely";
import { ulid } from "ulidx";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { MediaRepository } from "../../src/database/repositories/media.js";
import { RevisionRepository } from "../../src/database/repositories/revision.js";
import type { Database } from "../../src/database/types.js";
import { setupTestDatabase, setupTestDatabaseWithCollections } from "../utils/test-db.js";
describe("Revision Pruning", () => {
let db: Kysely<Database>;
let revisionRepo: RevisionRepository;
beforeEach(async () => {
db = await setupTestDatabaseWithCollections();
revisionRepo = new RevisionRepository(db);
});
afterEach(async () => {
await db.destroy();
});
it("prunes old revisions keeping the most recent N", async () => {
const entryId = ulid();
// Create a content entry
const { sql } = await import("kysely");
await sql`
INSERT INTO ec_post (id, slug, status, created_at, updated_at, version)
VALUES (${entryId}, ${"test-post"}, ${"draft"}, ${new Date().toISOString()}, ${new Date().toISOString()}, ${1})
`.execute(db);
// Create 200 revisions
for (let i = 0; i < 200; i++) {
await revisionRepo.create({
collection: "post",
entryId,
data: { title: `Version ${i + 1}` },
});
}
const countBefore = await revisionRepo.countByEntry("post", entryId);
expect(countBefore).toBe(200);
// Prune to keep 50
const pruned = await revisionRepo.pruneOldRevisions("post", entryId, 50);
expect(pruned).toBe(150);
const countAfter = await revisionRepo.countByEntry("post", entryId);
expect(countAfter).toBe(50);
// Verify the remaining 50 are the newest
const remaining = await revisionRepo.findByEntry("post", entryId);
expect(remaining[0]?.data.title).toBe("Version 200");
expect(remaining[49]?.data.title).toBe("Version 151");
});
it("is a no-op when revision count is at or below keepCount", async () => {
const entryId = ulid();
const { sql } = await import("kysely");
await sql`
INSERT INTO ec_post (id, slug, status, created_at, updated_at, version)
VALUES (${entryId}, ${"test-post-2"}, ${"draft"}, ${new Date().toISOString()}, ${new Date().toISOString()}, ${1})
`.execute(db);
// Create 10 revisions
for (let i = 0; i < 10; i++) {
await revisionRepo.create({
collection: "post",
entryId,
data: { title: `Version ${i + 1}` },
});
}
const pruned = await revisionRepo.pruneOldRevisions("post", entryId, 50);
expect(pruned).toBe(0);
const countAfter = await revisionRepo.countByEntry("post", entryId);
expect(countAfter).toBe(10);
});
});
describe("MediaRepository.cleanupPendingUploads", () => {
let db: Kysely<Database>;
let mediaRepo: MediaRepository;
beforeEach(async () => {
db = await setupTestDatabase();
mediaRepo = new MediaRepository(db);
});
afterEach(async () => {
await db.destroy();
});
it("deletes pending uploads older than the default 1 hour", async () => {
vi.useFakeTimers();
// Create pending uploads
for (let i = 0; i < 10; i++) {
await mediaRepo.createPending({
filename: `pending-${i}.jpg`,
mimeType: "image/jpeg",
storageKey: `uploads/pending-${i}.jpg`,
});
}
// Advance past 1 hour
vi.advanceTimersByTime(61 * 60 * 1000);
const deletedKeys = await mediaRepo.cleanupPendingUploads();
expect(deletedKeys).toHaveLength(10);
// Verify actual storage keys are returned
for (let i = 0; i < 10; i++) {
expect(deletedKeys).toContain(`uploads/pending-${i}.jpg`);
}
vi.useRealTimers();
});
it("does not delete recent pending uploads", async () => {
// Create pending uploads (current time -- not yet expired)
for (let i = 0; i < 5; i++) {
await mediaRepo.createPending({
filename: `recent-${i}.jpg`,
mimeType: "image/jpeg",
storageKey: `uploads/recent-${i}.jpg`,
});
}
const deletedKeys = await mediaRepo.cleanupPendingUploads();
expect(deletedKeys).toHaveLength(0);
});
it("does not delete ready or failed items", async () => {
vi.useFakeTimers();
// Create items with different statuses
await mediaRepo.create({
filename: "ready.jpg",
mimeType: "image/jpeg",
storageKey: "uploads/ready.jpg",
status: "ready",
});
const pending = await mediaRepo.createPending({
filename: "pending.jpg",
mimeType: "image/jpeg",
storageKey: "uploads/pending.jpg",
});
await mediaRepo.markFailed(pending.id);
// Advance past 1 hour
vi.advanceTimersByTime(61 * 60 * 1000);
const deletedKeys = await mediaRepo.cleanupPendingUploads();
expect(deletedKeys).toHaveLength(0); // failed + ready should not be deleted
vi.useRealTimers();
const remaining = await db.selectFrom("media").select("id").execute();
expect(remaining).toHaveLength(2);
});
it("respects custom maxAgeMs parameter", async () => {
vi.useFakeTimers();
await mediaRepo.createPending({
filename: "short-lived.jpg",
mimeType: "image/jpeg",
storageKey: "uploads/short-lived.jpg",
});
// Advance 10 minutes
vi.advanceTimersByTime(10 * 60 * 1000);
// Cleanup with 5 min max age
const deletedKeys = await mediaRepo.cleanupPendingUploads(5 * 60 * 1000);
expect(deletedKeys).toHaveLength(1);
expect(deletedKeys[0]).toBe("uploads/short-lived.jpg");
vi.useRealTimers();
});
});
describe("Expired token cleanup", () => {
let db: Kysely<Database>;
beforeEach(async () => {
db = await setupTestDatabase();
});
afterEach(async () => {
await db.destroy();
});
it("deletes expired tokens while keeping valid ones", async () => {
const now = new Date();
const expired = new Date(now.getTime() - 60 * 1000).toISOString(); // 1 min ago
// Create a test user first (tokens reference users)
const userId = ulid();
await db
.insertInto("users")
.values({
id: userId,
email: "test@example.com",
name: "Test",
avatar_url: null,
role: 50,
email_verified: 1,
disabled: 0,
data: null,
created_at: now.toISOString(),
updated_at: now.toISOString(),
})
.execute();
// Create 100 expired tokens
for (let i = 0; i < 100; i++) {
await db
.insertInto("auth_tokens")
.values({
hash: `expired-hash-${i}`,
user_id: userId,
email: "test@example.com",
type: "magic_link",
role: null,
invited_by: null,
expires_at: expired,
created_at: now.toISOString(),
})
.execute();
}
// Create 5 valid tokens
const validExpiry = new Date(now.getTime() + 15 * 60 * 1000).toISOString();
for (let i = 0; i < 5; i++) {
await db
.insertInto("auth_tokens")
.values({
hash: `valid-hash-${i}`,
user_id: userId,
email: "test@example.com",
type: "magic_link",
role: null,
invited_by: null,
expires_at: validExpiry,
created_at: now.toISOString(),
})
.execute();
}
// Use the DB directly to simulate what deleteExpiredTokens does
await db.deleteFrom("auth_tokens").where("expires_at", "<", new Date().toISOString()).execute();
// Verify only valid ones remain
const remaining = await db.selectFrom("auth_tokens").select("hash").execute();
expect(remaining).toHaveLength(5);
expect(remaining.every((r) => r.hash.startsWith("valid-"))).toBe(true);
});
});