| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import { fileStore, s3FileStore } from "./files.ts"; |
| 5 | |
| 6 | type Call = { url: string; method: string; headers: Record<string, string>; body: Uint8Array | null }; |
| 7 | |
| 8 | function fake(responses: Response[] = []) { |
| 9 | const calls: Call[] = []; |
| 10 | const fetcher = (async (url: string, init: RequestInit) => { |
| 11 | calls.push({ url, method: String(init.method), headers: init.headers as Record<string, string>, body: (init.body as Uint8Array | null) ?? null }); |
| 12 | return responses.shift() ?? new Response(null, { status: 200 }); |
| 13 | }) as unknown as typeof fetch; |
| 14 | return { calls, fetcher }; |
| 15 | } |
| 16 | |
| 17 | const config = { endpoint: "https://s3.example.com", bucket: "g1t-docs", region: "us-east-1", access_key_id: "AKID", secret_access_key: "secret" }; |
| 18 | |
| 19 | test("puts, gets and deletes a file by its key, path style, signed", async () => { |
| 20 | const { calls, fetcher } = fake([new Response(null, { status: 200 }), new Response("hello", { status: 200, headers: { "content-type": "image/png", "content-length": "5", etag: '"e"' } }), new Response(null, { status: 204 })]); |
| 21 | const store = s3FileStore(config, fetcher); |
| 22 | await store.put("docs/ab cd", new TextEncoder().encode("hello"), "image/png"); |
| 23 | const got = await store.get("docs/ab cd"); |
| 24 | await store.delete("docs/ab cd"); |
| 25 | assert.deepEqual( |
| 26 | calls.map((c) => [c.method, c.url]), |
| 27 | [ |
| 28 | ["PUT", "https://s3.example.com/g1t-docs/docs/ab%20cd"], |
| 29 | ["GET", "https://s3.example.com/g1t-docs/docs/ab%20cd"], |
| 30 | ["DELETE", "https://s3.example.com/g1t-docs/docs/ab%20cd"], |
| 31 | ], |
| 32 | ); |
| 33 | assert.match(calls[0]!.headers.authorization!, /^AWS4-HMAC-SHA256 Credential=AKID\/\d{8}\/us-east-1\/s3\/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=[0-9a-f]{64}$/); |
| 34 | assert.equal(calls[0]!.headers["x-amz-content-sha256"], "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"); |
| 35 | assert.equal(calls[0]!.headers.host, undefined); |
| 36 | assert.equal(got?.content_type, "image/png"); |
| 37 | assert.equal(got?.bytes, 5); |
| 38 | assert.equal(await new Response(got!.body).text(), "hello"); |
| 39 | }); |
| 40 | |
| 41 | test("a missing file is null", async () => { |
| 42 | const { fetcher } = fake([new Response("", { status: 404 })]); |
| 43 | assert.equal(await s3FileStore(config, fetcher).get("docs/x"), null); |
| 44 | }); |
| 45 | |
| 46 | test("virtual-hosted addresses put the bucket in the host", async () => { |
| 47 | const { calls, fetcher } = fake(); |
| 48 | await s3FileStore({ ...config, virtual_hosted: true }, fetcher).delete("docs/x"); |
| 49 | assert.equal(calls[0]!.url, "https://g1t-docs.s3.example.com/docs/x"); |
| 50 | }); |
| 51 | |
| 52 | test("the store follows the settings, and says what is missing", () => { |
| 53 | assert.throws(() => fileStore({ DOCS_FILES: "s3", DOCS_S3_BUCKET: "b" }), /DOCS_S3_ENDPOINT, DOCS_S3_ACCESS_KEY_ID, DOCS_S3_SECRET_ACCESS_KEY/); |
| 54 | assert.throws(() => fileStore({}), /No file store/); |
| 55 | assert.ok(fileStore({ DOCS_FILES: "s3", DOCS_S3_ENDPOINT: "http://minio:9000", DOCS_S3_BUCKET: "b", DOCS_S3_ACCESS_KEY_ID: "k", DOCS_S3_SECRET_ACCESS_KEY: "s" })); |
| 56 | }); |