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.
64 lines
2.6 KiB
TypeScript
64 lines
2.6 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
process.env.MAIL_SERVER_URL = "http://127.0.0.1:1";
|
|
const { createApp } = await import("./app.js");
|
|
|
|
/**
|
|
* `BASE_PATH` is read once, into `config`, so these mount the app by argument
|
|
* instead of re-importing the module with a different environment. The
|
|
* root-mounted half is the one that matters most: every instance in existence
|
|
* is at `/`, and this feature has to be invisible to them.
|
|
*/
|
|
|
|
test("at the root, the API is exactly where it was", async () => {
|
|
const app = createApp("");
|
|
const res = await app.request("/api/health");
|
|
assert.equal(res.status, 200);
|
|
});
|
|
|
|
test("under a prefix, the API moves with it", async () => {
|
|
const app = createApp("/mail");
|
|
const res = await app.request("/mail/api/health");
|
|
assert.equal(res.status, 200);
|
|
const body = (await res.json()) as { ok?: boolean };
|
|
assert.equal(body.ok, true);
|
|
});
|
|
|
|
test("under a prefix, the unprefixed API is gone", async () => {
|
|
// Not merely unrouted: a proxy that forwards without the prefix, against a
|
|
// server told to expect one, would otherwise appear to half-work -- the API
|
|
// answering while the app shell it belongs to 404s.
|
|
const app = createApp("/mail");
|
|
const res = await app.request("/api/health");
|
|
assert.equal(res.status, 404);
|
|
});
|
|
|
|
/*
|
|
* Whether a route reached the static handler, without depending on there being
|
|
* a web build in the tree. With one it serves the index; without one it says
|
|
* the build is missing. Either is proof the request got that far -- a routing
|
|
* mistake is the 404, and asserting on 200 or 503 would make these tests pass
|
|
* or fail on whether somebody had run `npm run build` first.
|
|
*/
|
|
const reachedTheApp = (status: number) => status === 200 || status === 503;
|
|
|
|
test("a deep SPA route under the prefix reaches the static handler", async () => {
|
|
const app = createApp("/mail");
|
|
const res = await app.request("/mail/calendar/week/2026-09-01");
|
|
assert.ok(reachedTheApp(res.status), `expected the app shell, got ${res.status}`);
|
|
});
|
|
|
|
test("a path that only shares the prefix's letters is not the app", async () => {
|
|
// `/mailbox` under a `/mail` mount belongs to whatever else the proxy
|
|
// serves on this host; answering it with our shell would shadow it.
|
|
const app = createApp("/mail");
|
|
assert.equal((await app.request("/mailbox")).status, 404);
|
|
assert.equal((await app.request("/")).status, 404);
|
|
});
|
|
|
|
test("the root mount still serves the SPA from the root", async () => {
|
|
const app = createApp("");
|
|
assert.ok(reachedTheApp((await app.request("/calendar/week/2026-09-01")).status));
|
|
assert.ok(reachedTheApp((await app.request("/")).status));
|
|
});
|