Files
puter/src/backend/testSharedWorkerSetup.ts
T
Daniel Salazar cc8b9858a2 feat: let extensions claim a subdomain (PUT-2100) (#4155)
`extension.claimSubdomain(name, { authorize, checkRoute? })` lets an
extension serve `<name>.<domain>` without a hard-coded reserved name.

- A claimed host is dispatched where the unknown-subdomain 404 used to be,
  before CORS, the OPTIONS responder, body parsing, auth and every route
  (including `subdomain: '*'` ones).
- `authorize` returning anything but true, or throwing a non-HttpError,
  yields exactly the unknown-subdomain 404. A thrown HttpError goes to the
  error handler.
- Routes whose `subdomain` is the claimed name (decorated controllers and
  extension routes alike) are materialized onto the claim's own router, so
  they only run after `authorize` and never on the main app. Their options
  are limited to ones that don't rely on the main pipeline's actor; an
  optional `checkRoute` lets the claim refuse routes at boot. Case-variant
  labels and claimed names mixed into subdomain arrays fail the boot.
- The unknown-subdomain check now compares the port-stripped hostname and
  requires a dot boundary, so a host with an explicit port behaves like one
  without it. Hosts served by the local worker proxy still pass through.
2026-10-09 18:14:04 -07:00

97 lines
3.5 KiB
TypeScript

/*
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Setup for the `shared-worker` vitest project, whose files run with
// `isolate: false` and so share one module graph per worker. Resets the
// process-wide state that a fresh worker would otherwise have provided.
import MockRedis from 'ioredis-mock';
import { afterAll, beforeAll, vi } from 'vitest';
import { configContainer } from './exports';
import { extensionStore } from './extensions';
// Loaded before the snapshot so anything registered at import time is part
// of the pristine state.
import './server';
type Container = unknown[] | Map<unknown, unknown> | Record<string, unknown>;
// Copies one level deeper than the container: event listener lists are
// arrays inside `events`, and registering pushes into them.
const copyContainer = (value: Container): Container => {
if (Array.isArray(value)) return [...value];
if (value instanceof Map) return new Map(value);
return Object.fromEntries(
Object.entries(value).map(([k, v]) => [
k,
Array.isArray(v) ? [...v] : v,
]),
);
};
// This file is re-evaluated for every test file, so the pristine snapshot
// lives on globalThis, taken before the first file in the worker runs.
const SNAPSHOT_KEY = Symbol.for('puter.test.extensionStoreSnapshot');
const globals = globalThis as {
[SNAPSHOT_KEY]?: Record<string, Container>;
};
globals[SNAPSHOT_KEY] ??= Object.fromEntries(
Object.entries(extensionStore).map(([k, v]) => [
k,
copyContainer(v as Container),
]),
);
const restoreExtensionStore = () => {
for (const [key, pristine] of Object.entries(globals[SNAPSHOT_KEY]!)) {
// The server reads these containers by reference, so refill in place.
const live = (extensionStore as unknown as Record<string, Container>)[
key
];
const fresh = copyContainer(pristine);
if (Array.isArray(live)) {
live.splice(0, live.length, ...(fresh as unknown[]));
} else if (live instanceof Map) {
live.clear();
for (const [k, v] of fresh as Map<unknown, unknown>) live.set(k, v);
} else {
for (const k of Object.keys(live)) delete live[k];
Object.assign(live, fresh);
}
}
};
beforeAll(async () => {
// Every mock client shares the default host/port store, so cached rows
// from a previous file's server would otherwise survive.
const redis = new MockRedis();
await redis.flushall();
redis.disconnect();
for (const key of Object.keys(configContainer)) {
delete (configContainer as unknown as Record<string, unknown>)[key];
}
restoreExtensionStore();
});
afterAll(() => {
vi.unstubAllEnvs();
vi.unstubAllGlobals();
vi.restoreAllMocks();
vi.useRealTimers();
});