1import assert from "node:assert/strict";2import test from "node:test";34import { sha256hex } from "../../shared/hash.ts";5import { MAX_BLOB_BYTES } from "../../shared/limits.ts";6import { api } from "../src/api.ts";78function envForBlob() {9 const puts = [];10 return {11 puts,12 env: {13 CONTENT: {14 put: async (key, bytes) => puts.push([key, bytes]),15 },16 },17 };18}1920async function put(request, env, hash = "a".repeat(64)) {21 const url = new URL(`https://n.zip/api/blob/${hash}`);22 return await api(request, env, url, {});23}2425test("blob upload requires content-length before reading the body", async () => {26 const { env, puts } = envForBlob();27 const response = await put(28 new Request(`https://n.zip/api/blob/${"a".repeat(64)}`, {29 method: "PUT",30 body: new Uint8Array([1]),31 }),32 env,33 );3435 assert.equal(response.status, 411);36 assert.equal(puts.length, 0);37});3839test("blob upload rejects an oversized declared body before reading it", async () => {40 const { env, puts } = envForBlob();41 const response = await put(42 new Request(`https://n.zip/api/blob/${"a".repeat(64)}`, {43 method: "PUT",44 headers: { "content-length": String(MAX_BLOB_BYTES + 1) },45 body: new Uint8Array([1]),46 }),47 env,48 );4950 assert.equal(response.status, 413);51 assert.equal(puts.length, 0);52});5354test("blob upload rejects a body that disagrees with its declared length", async () => {55 const { env, puts } = envForBlob();56 const response = await put(57 new Request(`https://n.zip/api/blob/${"a".repeat(64)}`, {58 method: "PUT",59 headers: { "content-length": "1" },60 body: new Uint8Array([1, 2]),61 }),62 env,63 );6465 assert.equal(response.status, 400);66 assert.equal(puts.length, 0);67});6869test("blob upload accepts a bounded body with a matching hash", async () => {70 const { env, puts } = envForBlob();71 const bytes = new Uint8Array([1, 2, 3]);72 const hash = await sha256hex(bytes);73 const response = await put(74 new Request(`https://n.zip/api/blob/${hash}`, {75 method: "PUT",76 headers: { "content-length": String(bytes.length) },77 body: bytes,78 }),79 env,80 hash,81 );8283 assert.equal(response.status, 200);84 assert.equal(puts.length, 1);85 assert.equal(puts[0][0], `blob/${hash}`);86});8788test("prepare performs no more than six concurrent R2 HEAD requests", async () => {89 const files = {};90 for (let i = 0; i < 13; i++) {91 files[`file-${i}.txt`] = {92 h: i.toString(16).padStart(64, "0"),93 s: 1,94 ct: "text/plain",95 };96 }97 let active = 0;98 let maximumActive = 0;99 const env = {100 CONTENT: {101 head: async () => {102 active++;103 maximumActive = Math.max(maximumActive, active);104 await new Promise((resolve) => setImmediate(resolve));105 active--;106 return null;107 },108 },109 };110 const url = new URL("https://n.zip/api/push/prepare");111 const response = await api(112 new Request(url, {113 method: "POST",114 headers: { "content-type": "application/json" },115 body: JSON.stringify({ manifest: { v: 1, files } }),116 }),117 env,118 url,119 {},120 );121122 assert.equal(response.status, 200);123 assert.equal(maximumActive, 6);124 assert.equal((await response.json()).missing.length, 13);125});126127test("shared validation errors are typed as bad requests", async () => {128 const url = new URL("https://n.zip/api/push/prepare");129 const response = await api(130 new Request(url, {131 method: "POST",132 headers: { "content-type": "application/json" },133 body: JSON.stringify({ manifest: { v: 2, files: {} } }),134 }),135 {},136 url,137 {},138 );139140 assert.equal(response.status, 400);141 assert.deepEqual(await response.json(), { error: "unsupported manifest version" });142});143144test("unexpected storage errors are logged but not exposed", async () => {145 const url = new URL("https://n.zip/api/push/prepare");146 const response = await api(147 new Request(url, {148 method: "POST",149 headers: { "content-type": "application/json" },150 body: JSON.stringify({151 manifest: {152 v: 1,153 files: { "index.html": { h: "a".repeat(64), s: 1, ct: "text/html" } },154 },155 }),156 }),157 { CONTENT: { head: async () => Promise.reject(new Error("private storage detail")) } },158 url,159 {},160 );161162 assert.equal(response.status, 500);163 assert.deepEqual(await response.json(), { error: "internal error" });164});