Files
ihasmail-inbuxa/server/src/staticcache.test.ts
T
jcoffey-dev bb25355c23 AGPL source offer and name cleanup
Every build writes the exact source it was built from, uncommitted work and
new files included, as source.tar.gz next to the app, named after that tree.
Docker builds, which have no git, pack the build context and name it by a
hash of its files. The sign-in page and Settings > About link to it instead of
a repository that can drift.

What users, operators and packagers see no longer names the upstream server:
- interface text, in all nine catalogues, with a token-session line for
  Security;
- server messages;
- the settings, now MAIL_SERVER_URL, MAIL_SERVERS_FILE, ADMIN_URL and
  MAIL_SERVER_FOLLOW_ADVERTISED_URLS, and mail-servers.example.json;
- the Tenants notice, which is gone;
- the README, CONTRIBUTING and SECURITY.

ihasmail's own FEATURES, KNOWN-ISSUES and ROADMAP stay with public ihasmail,
and INBUXA.md is folded into the README.
2026-09-19 00:13:12 -07:00

82 lines
3.6 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, writeFileSync, mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
/*
* What may be served stale, and what may not.
*
* This is not a preference about freshness. The service worker is the app's
* whole update mechanism: a browser holding an old one goes on being served
* the shell that worker knows and never finds out a deploy happened. On
* 2026-09-08 the origin had the new build while Cloudflare handed out the
* previous `sw.js` for hours, because it was neither a hashed asset nor HTML
* and so went out with an hour's max-age that the CDN then extended.
*
* A static root of our own, since CI runs the tests before the build and
* `web/dist` does not exist yet.
*/
const root = mkdtempSync(join(tmpdir(), "ihasmail-cache-"));
mkdirSync(join(root, "assets"));
writeFileSync(join(root, "assets", "app-a1b2c3.js"), "console.log(1)\n");
writeFileSync(join(root, "sw.js"), "/* worker */\n");
writeFileSync(join(root, "manifest.webmanifest"), `{"name":"ihasmail"}`);
writeFileSync(join(root, "index.html"), "<!doctype html><title>t</title>");
writeFileSync(join(root, "img.png"), "not really a png");
process.env.STATIC_DIR = root;
process.env.MAIL_SERVER_URL = "http://127.0.0.1:1";
const { createApp } = await import("./app.js");
const cacheControl = async (path: string) => {
const res = await createApp().request(path);
assert.equal(res.status, 200, `${path} should be served`);
return res.headers.get("cache-control") ?? "";
};
test("the service worker is never served from a cache without asking", async () => {
// `no-cache` permits storing it and requires revalidating it, which is a 304
// and costs nothing. What it forbids is a browser or a CDN answering with
// its own copy, which is the whole failure.
assert.match(await cacheControl("/sw.js"), /no-cache/);
});
test("nor is the manifest, which the worker has to agree with", async () => {
// A fresh manifest advertising a share target, answered by a worker that has
// never heard of one, sends the share to the server for a 405. Either being
// old is survivable; the two disagreeing is not.
assert.match(await cacheControl("/manifest.webmanifest"), /no-cache/);
});
test("the manifest is still served as a manifest", async () => {
const res = await createApp().request("/manifest.webmanifest");
assert.match(res.headers.get("content-type") ?? "", /application\/manifest\+json/);
});
test("index.html was already revalidated, and still is", async () => {
assert.match(await cacheControl("/"), /no-cache/);
});
test("hashed assets are still immutable for a year", async () => {
// The name changes when the bytes do, so there is nothing to go stale --
// and this is the caching that makes the app load quickly at all.
const cc = await cacheControl("/assets/app-a1b2c3.js");
assert.match(cc, /immutable/);
assert.match(cc, /max-age=31536000/);
});
test("everything else keeps its ordinary hour", async () => {
// The rule is narrow on purpose: two files, named, rather than a policy that
// quietly stops the icons and fonts being cached too.
assert.match(await cacheControl("/img.png"), /max-age=3600/);
});
test("under a prefix, the worker is still the worker", async () => {
// The mount comes off before the path is matched, so this has to hold for a
// subpath deployment as well -- where a stale worker is exactly as bad.
const res = await createApp("/mail").request("/mail/sw.js");
assert.equal(res.status, 200);
assert.match(res.headers.get("cache-control") ?? "", /no-cache/);
});