import { randomUUID } from "node:crypto"; import { join } from "node:path"; import { Pool, type PoolClient } from "pg"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { runMigrations } from "../db/migrate.js"; import { sha256, keyForHash, type StorageService, directStorageDisabled } from "../storage/storage.js"; import { VideoService, type VideoServiceDependencies } from "./video.service.js"; const dbAvailable = process.env.DATABASE_URL !== undefined; const ownerPool = new Pool({ connectionString: process.env.SUPER_DATABASE_URL ?? process.env.DATABASE_URL }); const appPool = new Pool({ connectionString: process.env.DATABASE_URL }); async function asTenant(workspaceId: string, fn: (client: PoolClient) => Promise): Promise { const client = await appPool.connect(); try { await client.query("BEGIN"); await client.query("SELECT set_config('app.workspace_id', $1, true)", [workspaceId]); const value = await fn(client); await client.query("COMMIT"); return value; } catch (error) { await client.query("ROLLBACK").catch(() => undefined); throw error; } finally { client.release(); } } describe.skipIf(!dbAvailable)("video library (live DB)", () => { let workspaceId: string; let service: VideoService; const objects = new Map(); const storage: StorageService = { putContent: async (bytes, contentType) => { const hash = sha256(bytes); const key = keyForHash(hash, contentType); const deduplicated = objects.has(key); objects.set(key, Buffer.from(bytes)); return { key, hash, sizeBytes: bytes.length, contentType, deduplicated }; }, getObject: async (key) => { const bytes = objects.get(key); if (bytes === undefined) throw new Error("missing stored object"); return Buffer.from(bytes); }, hasContent: async (hash, type) => objects.has(keyForHash(hash, type)), presign: async () => "unused", ...directStorageDisabled(), }; beforeAll(async () => { await runMigrations(process.env.MIGRATIONS_DIR ?? join(process.cwd(), "migrations")); const workspace = await ownerPool.query( "INSERT INTO workspaces (name, slug) VALUES ('Video integration', $1) RETURNING id", [`video-${randomUUID()}`], ); workspaceId = String(workspace.rows[0]?.id); const dependencies: VideoServiceDependencies = { tenantTransaction: asTenant, appendAudit: async (tenantId, event) => asTenant(tenantId, async (client) => { const audit = await client.query( "SELECT audit_append($1, $2, $3, $4, $5, $6, $7) AS id", [ tenantId, event.actorUserId, event.actorRole, event.action, event.targetType, event.targetId, event.diff ?? null, ], ); return String(audit.rows[0]?.id); }), probe: async () => ({ width: 640, height: 360, durationMs: 1000, frameRate: 30, codec: "h264" }), storage, }; service = new VideoService(dependencies); }); afterAll(async () => { if (workspaceId !== undefined) await ownerPool.query("DELETE FROM workspaces WHERE id = $1", [workspaceId]); await appPool.end(); await ownerPool.end(); }); it("uploads, pages, reads status, and deletes a tenant video", async () => { const uploaded = await service.uploadVideoBytes(workspaceId, Buffer.from("integration video bytes"), "video/mp4"); const page = await service.listVideos(workspaceId, undefined, 20); expect(page.items).toHaveLength(1); expect(page.items[0]?.videoAsset.id).toBe(uploaded.videoAsset.id); expect(page.items[0]?.processingState).toBe("complete"); const status = await service.getVideoStatus(uploaded.videoAsset.id, workspaceId); expect(status.processingJob!.state).toBe("complete"); expect(status.processingJob!.processingNote).toContain("Transcode skipped"); expect(status.videoAsset).toMatchObject({ width: 640, height: 360, durationMs: 1000, codec: "h264" }); await expect(service.getVideoStatus(uploaded.videoAsset.id, randomUUID())).rejects.toThrow(/not found/); await service.deleteVideo(uploaded.videoAsset.id, workspaceId); await expect(service.getVideoStatus(uploaded.videoAsset.id, workspaceId)).rejects.toThrow(/not found/); expect(objects.has(uploaded.videoAsset.storageKey)).toBe(true); const jobs = await asTenant(workspaceId, (client) => client.query("SELECT id FROM video_processing_jobs WHERE video_asset_id = $1", [uploaded.videoAsset.id])); expect(jobs.rowCount).toBe(0); }); it("persists probe failure and exposes the error through status", async () => { const failedService = new VideoService({ tenantTransaction: asTenant, appendAudit: async () => "test-audit", storage, probe: async () => { throw new Error("ffprobe: invalid media"); }, }); const uploaded = await failedService.uploadVideoBytes(workspaceId, Buffer.from("invalid video"), "video/mp4"); expect((await failedService.getVideoStatus(uploaded.videoAsset.id, workspaceId)).processingJob).toMatchObject({ state: "failed", error: "ffprobe: invalid media" }); await failedService.deleteVideo(uploaded.videoAsset.id, workspaceId); }); it("shares content-addressed bytes between assets and retains them when one asset is deleted", async () => { const bytes = Buffer.from("same video twice"); const first = await service.uploadVideoBytes(workspaceId, bytes, "video/mp4"); const second = await service.uploadVideoBytes(workspaceId, bytes, "video/mp4"); expect(second.videoAsset.id).not.toBe(first.videoAsset.id); expect(second.videoAsset.storageKey).toBe(first.videoAsset.storageKey); await service.deleteVideo(first.videoAsset.id, workspaceId); expect((await service.getVideoStatus(second.videoAsset.id, workspaceId)).processingJob!.state).toBe("complete"); expect(await storage.getObject(second.videoAsset.storageKey)).toEqual(bytes); await service.deleteVideo(second.videoAsset.id, workspaceId); }); });