/** * Routing matrix for `edge/rpkgs-router.ts`. * * The script is exercised through the SDK's local server rather than by * importing its internals, so what is tested is the artifact that gets * deployed. Requests that the script passes through are proxied to the real * origin, which keeps the "no redirect" cases honest: they assert that the * client reached the flat slot, not merely that no `Location` was set. * * Run with `just edge-test`. */ import { assertEquals } from 'jsr:@std/assert@1'; const SCRIPT = new URL('./rpkgs-router.ts', import.meta.url).pathname; const BASE = 'http://127.0.0.1:8080'; const UNION_SLOTS = 'amd64/alpine324'; const UA_R45_MUSL = 'R (4.5.3 x86_64-pc-linux-musl x86_64 linux-musl)'; const UA_R46_MUSL = 'R (4.6.0 x86_64-pc-linux-musl x86_64 linux-musl)'; const UA_R45_ALPINE = 'R/4.5.3 R (4.5.3 x86_64-pc-linux-musl x86_64 linux-musl) Alpine Linux 3.24'; const UA_R45_DARWIN = 'R (4.5.1 aarch64-apple-darwin20 aarch64 darwin20)'; const UA_CURL = 'curl/8.0.1'; const SLOT = '/amd64/alpine324/latest/src/contrib'; const OTHER_SLOT = '/amd64/noble/latest/src/contrib'; interface Probe { status: number; location: string | null; cacheControl: string | null; } async function probe(path: string, userAgent: string): Promise { const res = await fetch(BASE + path, { headers: { 'User-Agent': userAgent }, redirect: 'manual', }); await res.body?.cancel(); return { status: res.status, location: res.headers.get('location'), cacheControl: res.headers.get('cache-control'), }; } /** Kill tolerantly: the child has already exited if the script failed to load. */ async function stopServer(child: Deno.ChildProcess): Promise { try { child.kill(); } catch { // already gone } await child.status; } async function startServer(): Promise { const child = new Deno.Command(Deno.execPath(), { args: ['run', '-A', SCRIPT], env: { UNION_SLOTS }, stdout: 'null', stderr: 'inherit', }).spawn(); for (let attempt = 0; attempt < 150; attempt++) { try { const res = await fetch(`${BASE}/`, { headers: { 'User-Agent': UA_CURL }, redirect: 'manual', }); await res.body?.cancel(); return child; } catch { await new Promise((resolve) => setTimeout(resolve, 200)); } } await stopServer(child); throw new Error('edge script did not start listening on ' + BASE); } Deno.test('rpkgs-router', async (t) => { const server = await startServer(); try { await t.step("routes an index request to the client's R minor", async () => { const res = await probe(`${SLOT}/PACKAGES.gz`, UA_R45_MUSL); assertEquals(res.status, 302); assertEquals(res.location, `https://cran.rpkgs.com${SLOT}/4.5/PACKAGES.gz`); }); await t.step('routes R 4.6 to its own slot', async () => { const res = await probe(`${SLOT}/PACKAGES.gz`, UA_R46_MUSL); assertEquals(res.location, `https://cran.rpkgs.com${SLOT}/4.6/PACKAGES.gz`); }); await t.step('routes PACKAGES and PACKAGES.rds too', async () => { for (const file of ['PACKAGES', 'PACKAGES.rds']) { const res = await probe(`${SLOT}/${file}`, UA_R45_MUSL); assertEquals(res.location, `https://cran.rpkgs.com${SLOT}/4.5/${file}`, `expected ${file} to be routed`); } }); await t.step('marks the redirect uncacheable', async () => { const res = await probe(`${SLOT}/PACKAGES.gz`, UA_R45_MUSL); assertEquals(res.cacheControl, 'no-store'); }); await t.step('leaves a slot outside UNION_SLOTS alone', async () => { const res = await probe(`${OTHER_SLOT}/PACKAGES.gz`, UA_R45_MUSL); assertEquals(res.location, null); assertEquals(res.status, 200); }); await t.step('never routes a tarball', async () => { const res = await probe(`${SLOT}/jsonlite_2.0.0.tar.gz`, UA_R45_MUSL); assertEquals(res.location, null); assertEquals(res.status, 200); }); await t.step('does not redirect a path already under a minor', async () => { const res = await probe(`${SLOT}/4.5/PACKAGES.gz`, UA_R45_MUSL); assertEquals(res.location, null); assertEquals(res.status, 200); }); await t.step('leaves a client without an R version alone', async () => { const res = await probe(`${SLOT}/PACKAGES.gz`, UA_CURL); assertEquals(res.location, null); assertEquals(res.status, 200); }); await t.step('resolves the bare root to slot and minor', async () => { const res = await probe('/src/contrib/PACKAGES.gz', UA_R45_ALPINE); assertEquals(res.location, `https://cran.rpkgs.com${SLOT}/4.5/PACKAGES.gz`); }); await t.step('sends an unidentifiable distro to CRAN', async () => { const res = await probe('/src/contrib/PACKAGES.gz', UA_R45_MUSL); assertEquals(res.location, 'https://cran.r-project.org/src/contrib/PACKAGES.gz'); }); await t.step('keeps the macOS rewrite', async () => { const res = await probe('/src/contrib/foo_1.0.tar.gz', UA_R45_DARWIN); assertEquals(res.location, 'https://cran.rpkgs.com/bin/macosx/big-sur-arm64/contrib/4.5/foo_1.0.tar.gz'); }); await t.step('keeps the macOS binary passthrough to CRAN', async () => { const path = '/bin/macosx/big-sur-arm64/contrib/4.5/foo_1.0.tar.gz'; const res = await probe(path, UA_R45_DARWIN); assertEquals(res.location, `https://cran.r-project.org${path}`); }); await t.step('collapses duplicate slashes before matching', async () => { const res = await probe(`/amd64/alpine324//latest/src/contrib//PACKAGES.gz`, UA_R45_MUSL); assertEquals(res.location, `https://cran.rpkgs.com${SLOT}/4.5/PACKAGES.gz`); }); } finally { await stopServer(server); } });