Files
festipod/backend/services/base-path.test.js
Sylvain Duchesne 738d59596a Serve the backend under a base path: one URL per environment, the backend under /api
The test deployment keeps one base URL per environment, as the sibling
ActivityPods apps do, so a pull-request preview lives on a single host: the
app at the root, the backend under /api on the same host, its /.well-known
routes at the origin root, the proxy passing paths through unchanged.

SemApps already works that way — it mounts every route it adds under the base
URL's path and keeps /.well-known at the root — but Festipod's own pieces were
written for a backend at the root of its own origin: its routes (/app,
/as/actor/app, /catalog) were mounted at fixed paths, the public-surface
allow-list matched fixed paths (so under /api every request would have been
refused), and the Solid-OIDC client_id joined the raw request path to the base
URL, which would have repeated the base path (/api/api/...).

base-path.js derives the base path from SEMAPPS_HOME_URL; the routes are
mounted under it; the allow-list matches paths relative to it, the two
/.well-known endpoints against the full path; the client_id is the requested
URI rebuilt from the base URL as written, equal byte for byte to the actor
URI and to what the front end derives. At the root every gate decision is
identical to before (compared over ~8M method/path cases). The rights matrix
probes /.well-known at the backend's origin root.

Verified: bun test (424 pass); the full gate at the root; and the built images
behind a local proxy routing /api and /.well-known unstripped with the Host
header kept — login flow, rights matrix 98/98, revocation.
2026-10-19 16:44:09 +02:00

56 lines
2.5 KiB
JavaScript

const { test, expect, describe } = require('bun:test');
const { basePathOf, underBasePath, requestUri } = require('./base-path');
describe('basePathOf — the path the backend is served under', () => {
test('a backend owning its origin is served at the root, with or without a trailing slash', () => {
expect(basePathOf('http://localhost:3003')).toBe('/');
expect(basePathOf('http://localhost:3003/')).toBe('/');
});
test('a backend sharing the front end origin is served under its path, always ending with a slash', () => {
expect(basePathOf('https://festipod.dev.reconnexion.coop/api/')).toBe('/api/');
expect(basePathOf('https://festipod.dev.reconnexion.coop/api')).toBe('/api/');
});
test('an unset base URL reads as the root', () => {
expect(basePathOf(undefined)).toBe('/');
expect(basePathOf('')).toBe('/');
});
});
describe('underBasePath — where a route is mounted', () => {
test('at the root, a route keeps its path', () => {
expect(underBasePath('/', '/catalog')).toBe('/catalog');
expect(underBasePath('/', '/as/actor/app')).toBe('/as/actor/app');
});
test('under a base path, a route is prefixed by it, without a doubled slash', () => {
expect(underBasePath('/api/', '/catalog')).toBe('/api/catalog');
expect(underBasePath('/api/', 'as/actor/app')).toBe('/api/as/actor/app');
});
});
describe('requestUri — the URI a request was made to', () => {
test('at the root, the request path is appended to the origin', () => {
expect(requestUri('http://localhost:3003', '/as/actor/app')).toBe('http://localhost:3003/as/actor/app');
expect(requestUri('http://localhost:3003/', '/app')).toBe('http://localhost:3003/app');
});
test('under a base path, the raw request path already carries it and is not prefixed twice', () => {
expect(requestUri('https://festipod.dev.reconnexion.coop/api/', '/api/as/actor/app')).toBe(
'https://festipod.dev.reconnexion.coop/api/as/actor/app'
);
expect(requestUri('https://festipod.dev.reconnexion.coop/api', '/api/app')).toBe(
'https://festipod.dev.reconnexion.coop/api/app'
);
});
test('the base URL is kept exactly as written — the same URI the backend mints its actor under', () => {
const urlJoin = require('url-join');
for (const baseUrl of ['http://localhost:80', 'https://Festipod.Example/api/', 'https://host:443/api']) {
const path = `${basePathOf(baseUrl)}as/actor/app`;
expect(requestUri(baseUrl, path)).toBe(urlJoin(baseUrl, 'as/actor/app'));
}
});
});