clone

push a directory of html from the terminal and get a four-character url back. a deno cli on cloudflare

feat/source-stable-push
nzip / worker / test / serve.test.mjs
serve.test.mjs 9.7K
1import assert from "node:assert/strict";2import test from "node:test";34import { serve } from "../src/serve.ts";5import { hashPassword } from "../src/password.ts";67const HASH = "a".repeat(64);89function envFor(files, siteOverrides = {}) {10 const manifest = new TextEncoder().encode(JSON.stringify({ v: 1, files }));11 const site = {12 address: 0x2f9b,13 vault_slot: 2,14 alias: null,15 current_manifest: "manifest-hash",16 created_at: 0,17 updated_at: 0,18 expires_at: null,19 password_hash: null,20 auth_version: 1,21 ...siteOverrides,22 };2324 return {25 PUBLIC_BASE: "https://n.zip",26 SITE_DOMAIN: "n.zip",27 DB: {28 prepare() {29 return {30 bind() {31 return { first: async () => site };32 },33 };34 },35 },36 CONTENT: {37 get: async (key) => {38 if (key === "manifest/manifest-hash") {39 return { arrayBuffer: async () => manifest.buffer };40 }41 if (key === `blob/${HASH}`) return { body: new Uint8Array() };42 return null;43 },44 },45 };46}4748const html = { h: HASH, s: 0, ct: "text/html; charset=utf-8" };49const css = { h: HASH, s: 0, ct: "text/css; charset=utf-8" };5051function serveSite(request, env, url) {52 return serve(request, env, url, "2f9b");53}5455test("legacy address paths permanently redirect to the isolated site origin", async () => {56 const response = await serve(57 new Request("https://n.zip/2f9b/index.html?view=full"),58 envFor({ "index.html": html }),59 new URL("https://n.zip/2f9b/index.html?view=full"),60 );6162 assert.equal(response.status, 308);63 assert.equal(response.headers.get("cache-control"), "public, max-age=60");64 assert.equal(response.headers.get("cache-tag"), "nzip-site-2f9b");65 assert.equal(66 response.headers.get("location"),67 "https://2f9b.n.zip/index.html?view=full",68 );69});7071test("root index redirects to the isolated site root", async () => {72 const url = new URL("https://2f9b.n.zip/index.html");73 const response = await serveSite(74 new Request(url),75 envFor({ "index.html": html, "style.css": css }),76 url,77 );7879 assert.equal(response.status, 302);80 assert.equal(response.headers.get("location"), "https://2f9b.n.zip/");81});8283test("nested index redirects to its directory", async () => {84 const url = new URL("https://2f9b.n.zip/docs/index.html");85 const response = await serveSite(86 new Request(url),87 envFor({ "index.html": html, "docs/index.html": html }),88 url,89 );9091 assert.equal(response.status, 302);92 assert.equal(response.headers.get("location"), "https://2f9b.n.zip/docs/");93});9495test("canonical redirects use the HTTPS public base instead of the request origin", async (t) => {96 const cases = [97 {98 name: "explicit index",99 requestUrl: "http://internal/index.html?mode=pwa",100 files: { "index.html": html, "style.css": css },101 location: "https://2f9b.n.zip/?mode=pwa",102 },103 {104 name: "directory without trailing slash",105 requestUrl: "http://internal/docs?mode=pwa",106 files: { "index.html": html, "docs/index.html": html },107 location: "https://2f9b.n.zip/docs/?mode=pwa",108 },109 ];110111 for (const { name, requestUrl, files, location } of cases) {112 await t.test(name, async () => {113 const url = new URL(requestUrl);114 const response = await serveSite(new Request(url), envFor(files), url);115116 assert.equal(response.status, 302);117 assert.equal(response.headers.get("location"), location);118 });119 }120});121122test("canonical directory URL serves its index without redirecting", async () => {123 const url = new URL("https://2f9b.n.zip/");124 const response = await serveSite(125 new Request(url),126 envFor({ "index.html": html, "style.css": css }),127 url,128 );129130 assert.equal(response.status, 200);131 assert.equal(response.headers.get("location"), null);132 assert.equal(response.headers.get("cache-control"), "public, max-age=60");133 assert.equal(response.headers.get("cache-tag"), "nzip-site-2f9b");134 assert.equal(response.headers.get("permissions-policy"), null);135 assert.equal(response.headers.get("origin-agent-cluster"), "?1");136});137138test("missing index path still returns not found", async () => {139 const url = new URL("https://2f9b.n.zip/index.html");140 const response = await serveSite(141 new Request(url),142 envFor({ "download.txt": { ...css, ct: "text/plain; charset=utf-8" } }),143 url,144 );145146 assert.equal(response.status, 404);147});148149test("old address-prefixed asset bases remain usable on the site origin", async () => {150 const assetUrl = new URL("https://2f9b.n.zip/2f9b/style.css");151 const asset = await serveSite(152 new Request(assetUrl),153 envFor({ "index.html": html, "style.css": css }),154 assetUrl,155 );156 const indexUrl = new URL("https://2f9b.n.zip/2f9b/index.html");157 const index = await serveSite(158 new Request(indexUrl),159 envFor({ "index.html": html, "style.css": css }),160 indexUrl,161 );162163 assert.equal(asset.status, 200);164 assert.equal(asset.headers.get("content-type"), "text/css; charset=utf-8");165 assert.equal(index.status, 302);166 assert.equal(index.headers.get("location"), "https://2f9b.n.zip/2f9b/");167});168169test("favicon serves the wordmark PNG with a cacheable response", async () => {170 const url = new URL("https://n.zip/favicon.ico");171 const response = await serve(new Request(url), envFor({}), url);172173 assert.equal(response.status, 200);174 assert.equal(response.headers.get("content-type"), "image/png");175 assert.equal(response.headers.get("cache-control"), "public, max-age=86400");176 const bytes = new Uint8Array(await response.arrayBuffer());177 // PNG signature: the embedded icon decoded from base64 intact.178 assert.deepEqual([...bytes.slice(0, 8)], [179 0x89,180 0x50,181 0x4e,182 0x47,183 0x0d,184 0x0a,185 0x1a,186 0x0a,187 ]);188 assert.ok(bytes.length > 500);189});190191test("unlock requires a declared request size", async () => {192 const url = new URL("https://2f9b.n.zip/__unlock");193 const response = await serveSite(194 new Request(url, {195 method: "POST",196 headers: { "content-type": "application/x-www-form-urlencoded" },197 body: "password=test",198 }),199 envFor({ "index.html": html }, { password_hash: "unused" }),200 url,201 );202203 assert.equal(response.status, 411);204});205206test("unlock page disables mobile zoom", async () => {207 const url = new URL("https://2f9b.n.zip/");208 const response = await serveSite(209 new Request(url),210 envFor({ "index.html": html }, { password_hash: "hash" }),211 url,212 );213214 assert.equal(response.status, 401);215 assert.match(await response.text(), /maximum-scale=1, user-scalable=no/);216});217218test("unlock returns to the requested nested page", async () => {219 const passwordHash = await hashPassword("test");220 const pageUrl = new URL("https://2f9b.n.zip/docs/guide.html?view=full");221 const locked = await serveSite(222 new Request(pageUrl),223 envFor({ "docs/guide.html": html }, { password_hash: passwordHash }),224 pageUrl,225 );226 const returnTo = /name="return_to" value="([^"]+)"/.exec(227 await locked.text(),228 )?.[1].replaceAll("&", "&");229 assert.equal(returnTo, "/docs/guide.html?view=full");230231 const body = new URLSearchParams({ password: "test", return_to: returnTo });232 const unlockUrl = new URL("https://2f9b.n.zip/__unlock");233 const unlocked = await serveSite(234 new Request(unlockUrl, {235 method: "POST",236 headers: {237 "content-type": "application/x-www-form-urlencoded",238 "content-length": String(239 new TextEncoder().encode(body.toString()).length,240 ),241 },242 body,243 }),244 envFor({ "docs/guide.html": html }, { password_hash: passwordHash }),245 unlockUrl,246 );247248 assert.equal(unlocked.status, 303);249 assert.equal(250 unlocked.headers.get("location"),251 "/docs/guide.html?view=full",252 );253 assert.match(254 unlocked.headers.get("set-cookie") ?? "",255 /^__Host-nzip-unlock=/,256 );257 assert.match(unlocked.headers.get("set-cookie") ?? "", /; Path=\//);258 assert.doesNotMatch(unlocked.headers.get("set-cookie") ?? "", /Domain=/i);259});260261test("unlock rejects a return target outside the locked site", async () => {262 const passwordHash = await hashPassword("test");263 const body = new URLSearchParams({264 password: "test",265 return_to: "https://example.com/phishing",266 });267 const unlockUrl = new URL("https://2f9b.n.zip/__unlock");268 const response = await serveSite(269 new Request(unlockUrl, {270 method: "POST",271 headers: {272 "content-type": "application/x-www-form-urlencoded",273 "content-length": String(274 new TextEncoder().encode(body.toString()).length,275 ),276 },277 body,278 }),279 envFor({ "index.html": html }, { password_hash: passwordHash }),280 unlockUrl,281 );282283 assert.equal(response.status, 303);284 assert.equal(response.headers.get("location"), "/");285});286287test("unlock rejects an oversized declared body before password verification", async () => {288 const url = new URL("https://2f9b.n.zip/__unlock");289 const response = await serveSite(290 new Request(url, {291 method: "POST",292 headers: {293 "content-type": "application/x-www-form-urlencoded",294 "content-length": "4097",295 },296 body: "password=test",297 }),298 envFor({ "index.html": html }, { password_hash: "unused" }),299 url,300 );301302 assert.equal(response.status, 413);303});304305test("unlock rejects passwords above 256 characters before hashing", async () => {306 const url = new URL("https://2f9b.n.zip/__unlock");307 const body = new URLSearchParams({ password: "x".repeat(257) }).toString();308 const response = await serveSite(309 new Request(url, {310 method: "POST",311 headers: {312 "content-type": "application/x-www-form-urlencoded",313 "content-length": String(new TextEncoder().encode(body).length),314 },315 body,316 }),317 envFor({ "index.html": html }, { password_hash: "unused" }),318 url,319 );320321 assert.equal(response.status, 400);322});