/* Unit tests for r2 adapter covering createR2StorageAdapter. It also guards the documented public contract and regression-sensitive edge cases. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-1311: Compass header sweep — declares the r2 suite's contract for test-file scaffold coverage. */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { createR2StorageAdapter } from "../r2-adapter.ts"; import type { Sha256Digest } from "@warpgogol/werkstatt-shared/fingerprint"; const mockConfig = { accountId: "test-account-id", accessKeyId: "test-access-key-id", secretAccessKey: "test-secret-access-key", bucketName: "test-bucket", apiToken: "test-api-token", }; const testDigest = ("sha256:" + "a".repeat(64)) as Sha256Digest; const testBytes = new Uint8Array([1, 2, 3, 4, 5]); function createMockResponse( status: number, headers: Record = {}, body: ArrayBuffer | string = new ArrayBuffer(0), ): Response { return new Response(body, { status, headers, }); } describe("createR2StorageAdapter", () => { let fetchSpy: ReturnType; beforeEach(() => { fetchSpy = vi.spyOn(globalThis, "fetch"); }); afterEach(() => { vi.restoreAllMocks(); }); it("returns adapter with correct id", () => { const adapter = createR2StorageAdapter(mockConfig); expect(adapter.id).toBe("r2:test-bucket"); }); it("putObject sends PUT request and returns locator + size", async () => { fetchSpy.mockResolvedValueOnce(createMockResponse(200, { etag: '"abc123"' })); const adapter = createR2StorageAdapter(mockConfig); const result = await adapter.putObject({ digest: testDigest, bytes: testBytes, mediaType: "application/json", }); expect(result.sizeBytes).toBe(5); expect(result.locator).toContain("r2://test-bucket/"); expect(result.locator).toContain("abc123"); expect(fetchSpy).toHaveBeenCalledOnce(); const call = fetchSpy.mock.calls[0]; const url = call[0] as string; expect(url).toContain("api.cloudflare.com/client/v4/accounts/test-account-id/r2"); expect(url).toContain("buckets/test-bucket/objects/"); expect(url).toContain(encodeURIComponent("sha256:" + "a".repeat(64))); const init = call[1] as RequestInit; expect(init.method).toBe("PUT"); const headers = init.headers as Record; expect(headers["Authorization"]).toBe("Bearer test-api-token"); expect(headers["Content-Type"]).toBe("application/json"); }); it("putObject throws on non-ok response", async () => { fetchSpy.mockResolvedValueOnce(createMockResponse(403, {}, "Forbidden")); const adapter = createR2StorageAdapter(mockConfig); await expect( adapter.putObject({ digest: testDigest, bytes: testBytes, mediaType: "application/json", }), ).rejects.toThrow("CERT-R2-01"); }); it("headObject returns exists:false on 404", async () => { fetchSpy.mockResolvedValueOnce(createMockResponse(404)); const adapter = createR2StorageAdapter(mockConfig); const result = await adapter.headObject(testDigest); expect(result.exists).toBe(false); }); it("headObject returns exists:true with size from content-range on 206", async () => { fetchSpy.mockResolvedValueOnce( createMockResponse(206, { "content-range": "bytes 0-0/42", etag: '"def456"', }), ); const adapter = createR2StorageAdapter(mockConfig); const result = await adapter.headObject(testDigest); expect(result.exists).toBe(true); expect(result.sizeBytes).toBe(42); expect(result.digest).toBe(testDigest); expect(result.locator).toContain("def456"); }); it("headObject throws on non-404 error", async () => { fetchSpy.mockResolvedValueOnce(createMockResponse(405, {}, "Method Not Allowed")); const adapter = createR2StorageAdapter(mockConfig); await expect(adapter.headObject(testDigest)).rejects.toThrow("CERT-R2-02"); }); it("getObject returns bytes on 200", async () => { const data = new Uint8Array([10, 20, 30]); fetchSpy.mockResolvedValueOnce(createMockResponse(200, {}, data.buffer.slice(0))); const adapter = createR2StorageAdapter(mockConfig); const result = await adapter.getObject(testDigest); expect(result).toBeInstanceOf(Uint8Array); expect(Array.from(result)).toEqual([10, 20, 30]); }); it("getObject throws CERT-STORAGE-01 on 404", async () => { fetchSpy.mockResolvedValueOnce(createMockResponse(404)); const adapter = createR2StorageAdapter(mockConfig); await expect(adapter.getObject(testDigest)).rejects.toThrow("CERT-STORAGE-01"); }); it("getObject throws CERT-R2-03 on non-404 error", async () => { fetchSpy.mockResolvedValueOnce(createMockResponse(500, {}, "Internal Server Error")); const adapter = createR2StorageAdapter(mockConfig); await expect(adapter.getObject(testDigest)).rejects.toThrow("CERT-R2-03"); }); it("appendAuditRecord sends PUT and returns locator", async () => { fetchSpy.mockResolvedValueOnce(createMockResponse(200, { etag: '"audit-etag"' })); const adapter = createR2StorageAdapter(mockConfig); const result = await adapter.appendAuditRecord(new Uint8Array([99, 98])); expect(result.locator).toContain("r2://test-bucket/audit/"); expect(result.locator).toContain("audit-etag"); const call = fetchSpy.mock.calls[0]; const url = call[0] as string; expect(url).toContain("buckets/test-bucket/objects/"); expect(url).toContain("audit%2F"); }); it("appendAuditRecord throws on non-ok response", async () => { fetchSpy.mockResolvedValueOnce(createMockResponse(500, {}, "Internal Server Error")); const adapter = createR2StorageAdapter(mockConfig); await expect(adapter.appendAuditRecord(new Uint8Array([1]))).rejects.toThrow("CERT-R2-04"); }); it("Authorization header uses Bearer token", async () => { fetchSpy.mockResolvedValueOnce(createMockResponse(200, { etag: '"abc"' })); const adapter = createR2StorageAdapter(mockConfig); await adapter.putObject({ digest: testDigest, bytes: testBytes, mediaType: "application/json", }); const init = fetchSpy.mock.calls[0][1] as RequestInit; const headers = init.headers as Record; expect(headers["Authorization"]).toBe("Bearer test-api-token"); }); });