diff --git a/src/backend/controllers/broadcast/BroadcastController.test.ts b/src/backend/controllers/broadcast/BroadcastController.test.ts
new file mode 100644
index 000000000..c9db6e878
--- /dev/null
+++ b/src/backend/controllers/broadcast/BroadcastController.test.ts
@@ -0,0 +1,199 @@
+/**
+ * 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 .
+ */
+
+import type { Request, Response } from 'express';
+import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
+import { PuterServer } from '../../server.js';
+import { setupTestServer } from '../../testUtil.js';
+import type { BroadcastController } from './BroadcastController.js';
+
+// ── Test harness ────────────────────────────────────────────────────
+//
+// Boots one PuterServer and exercises the live BroadcastController
+// against the wired BroadcastService. The default test config has no
+// configured peers, so every signed-incoming path falls through to the
+// service's "unknown peer" gate (403). That's the right blast radius
+// for these tests — we cover header validation + error→HTTP wiring,
+// and trust BroadcastService's own tests for the crypto path.
+
+let server: PuterServer;
+let controller: BroadcastController;
+
+beforeAll(async () => {
+ server = await setupTestServer();
+ controller = server.controllers.broadcast as unknown as BroadcastController;
+});
+
+afterAll(async () => {
+ await server?.shutdown();
+});
+
+interface CapturedResponse {
+ statusCode: number;
+ body: unknown;
+}
+
+const makeReq = (init: {
+ body?: unknown;
+ rawBody?: Buffer;
+ headers?: Record;
+}): Request => {
+ return {
+ body: init.body ?? {},
+ rawBody: init.rawBody,
+ query: {},
+ headers: init.headers ?? {},
+ } as unknown as Request;
+};
+
+const makeRes = () => {
+ const captured: CapturedResponse = { statusCode: 200, body: undefined };
+ const res = {
+ json: vi.fn((value: unknown) => {
+ captured.body = value;
+ return res;
+ }),
+ status: vi.fn((code: number) => {
+ captured.statusCode = code;
+ return res;
+ }),
+ setHeader: vi.fn(() => res),
+ };
+ return { res: res as unknown as Response, captured };
+};
+
+// Standard signed-payload headers used across cases. Real verification
+// fails because no peer is configured — the test config doesn't set up
+// `broadcast_peers` — so we land on the service's "Unknown peer" gate
+// rather than an HMAC mismatch.
+const signedHeaders = (): Record => ({
+ 'x-broadcast-peer-id': 'unknown-peer',
+ 'x-broadcast-timestamp': String(Math.floor(Date.now() / 1000)),
+ 'x-broadcast-nonce': '1',
+ 'x-broadcast-signature': 'a'.repeat(64),
+});
+
+// ── /broadcast/webhook ──────────────────────────────────────────────
+
+describe('BroadcastController.webhook', () => {
+ it('returns 400 when rawBody is missing', async () => {
+ const { res, captured } = makeRes();
+ await controller.webhook(
+ makeReq({
+ body: { events: [] },
+ headers: signedHeaders(),
+ }),
+ res,
+ );
+ expect(captured.statusCode).toBe(400);
+ expect(captured.body).toMatchObject({
+ error: { message: expect.stringContaining('body') },
+ });
+ });
+
+ it('returns 400 when the JSON body is not an object', async () => {
+ const { res, captured } = makeRes();
+ await controller.webhook(
+ makeReq({
+ body: 'not an object',
+ rawBody: Buffer.from('"not an object"'),
+ headers: signedHeaders(),
+ }),
+ res,
+ );
+ expect(captured.statusCode).toBe(400);
+ });
+
+ it('returns 400 when the body has neither `events` nor a single-event shape', async () => {
+ const raw = Buffer.from('{}');
+ const { res, captured } = makeRes();
+ await controller.webhook(
+ makeReq({
+ body: {},
+ rawBody: raw,
+ headers: signedHeaders(),
+ }),
+ res,
+ );
+ expect(captured.statusCode).toBe(400);
+ expect(captured.body).toMatchObject({
+ error: { message: expect.stringContaining('payload') },
+ });
+ });
+
+ it('returns 403 when the peer-id header is missing', async () => {
+ const raw = Buffer.from('{"events":[]}');
+ const headers = signedHeaders();
+ delete headers['x-broadcast-peer-id'];
+ const { res, captured } = makeRes();
+ await controller.webhook(
+ makeReq({
+ body: { events: [] },
+ rawBody: raw,
+ headers,
+ }),
+ res,
+ );
+ expect(captured.statusCode).toBe(403);
+ expect(captured.body).toMatchObject({
+ error: { message: expect.stringContaining('Peer-Id') },
+ });
+ });
+
+ it('returns 403 for an unknown peer-id (no configured webhook secret)', async () => {
+ const raw = Buffer.from('{"events":[{"key":"x","data":{},"meta":{}}]}');
+ const { res, captured } = makeRes();
+ await controller.webhook(
+ makeReq({
+ body: { events: [{ key: 'x', data: {}, meta: {} }] },
+ rawBody: raw,
+ headers: signedHeaders(),
+ }),
+ res,
+ );
+ expect(captured.statusCode).toBe(403);
+ expect(captured.body).toMatchObject({
+ error: { message: expect.stringContaining('Unknown peer') },
+ });
+ });
+
+ it('reads only the first value when a header is repeated as an array', async () => {
+ // Express normally collapses duplicates to a string, but tests
+ // can supply arrays — the controller's `headerOnce` helper picks
+ // index 0. Verify by sending an unknown peer-id as `[id, junk]`
+ // and confirming we still hit the 403 path (not a 400 from the
+ // body parsing path that runs before peer lookup).
+ const raw = Buffer.from('{"events":[]}');
+ const headers = signedHeaders() as unknown as Record<
+ string,
+ string | string[]
+ >;
+ headers['x-broadcast-peer-id'] = ['unknown-peer', 'second-value'];
+ const { res, captured } = makeRes();
+ await controller.webhook(
+ makeReq({
+ body: { events: [] },
+ rawBody: raw,
+ headers: headers as Record,
+ }),
+ res,
+ );
+ expect(captured.statusCode).toBe(403);
+ });
+});
diff --git a/src/backend/controllers/drivers/DriverController.test.ts b/src/backend/controllers/drivers/DriverController.test.ts
new file mode 100644
index 000000000..d228829f5
--- /dev/null
+++ b/src/backend/controllers/drivers/DriverController.test.ts
@@ -0,0 +1,112 @@
+/**
+ * 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 .
+ */
+
+import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+import { PuterServer } from '../../server.js';
+import { setupTestServer } from '../../testUtil.js';
+import type { DriverController } from './DriverController.js';
+
+// ── Test harness ────────────────────────────────────────────────────
+//
+// Boots one PuterServer (in-memory sqlite + dynamo + s3 + mock redis).
+// The DriverController under test is the same instance the live request
+// pipeline uses, so its iface→driver registry is populated from real
+// drivers (puter-kvstore, puter-apps, puter-subdomains, …). The HTTP
+// handlers (`#handleCall`, `#handleListInterfaces`, `#handleXd`) are
+// private — we exercise the public lookup API (`resolve` / list / get
+// default) which the handlers themselves delegate to.
+
+let server: PuterServer;
+let controller: DriverController;
+
+beforeAll(async () => {
+ server = await setupTestServer();
+ controller = server.controllers.drivers as unknown as DriverController;
+});
+
+afterAll(async () => {
+ await server?.shutdown();
+});
+
+// ── Lookup API ──────────────────────────────────────────────────────
+
+describe('DriverController.listInterfaces', () => {
+ it('exposes built-in interfaces', () => {
+ const interfaces = controller.listInterfaces();
+ // Several drivers ship by default — assert known ones rather
+ // than the exact set so adding a driver doesn't break this.
+ expect(interfaces).toEqual(
+ expect.arrayContaining([
+ 'puter-kvstore',
+ 'puter-apps',
+ 'puter-subdomains',
+ 'puter-notifications',
+ ]),
+ );
+ });
+});
+
+describe('DriverController.listDrivers', () => {
+ it('returns every name registered for an interface', () => {
+ const drivers = controller.listDrivers('puter-kvstore');
+ expect(drivers).toContain('puter-kvstore');
+ });
+
+ it('returns [] for an unknown interface', () => {
+ expect(controller.listDrivers('nonexistent')).toEqual([]);
+ });
+});
+
+describe('DriverController.getDefault', () => {
+ it('returns the registered default driver name', () => {
+ // KVStoreDriver declares `isDefault = true`.
+ expect(controller.getDefault('puter-kvstore')).toBe('puter-kvstore');
+ });
+
+ it('returns undefined for an unknown interface', () => {
+ expect(controller.getDefault('nonexistent')).toBeUndefined();
+ });
+});
+
+describe('DriverController.resolve', () => {
+ it('returns the default-driver instance when no name is given', () => {
+ const driver = controller.resolve('puter-kvstore');
+ expect(driver).not.toBeNull();
+ // The KV driver exposes a `set` method per its interface.
+ expect(typeof (driver as Record)?.set).toBe(
+ 'function',
+ );
+ });
+
+ it('finds the same instance by explicit driver name', () => {
+ const byDefault = controller.resolve('puter-kvstore');
+ const byName = controller.resolve('puter-kvstore', 'puter-kvstore');
+ expect(byName).toBe(byDefault);
+ });
+
+ it('returns null for an unknown interface', () => {
+ expect(controller.resolve('nope')).toBeNull();
+ });
+
+ it('returns null for a known interface but unknown driver name', () => {
+ expect(
+ controller.resolve('puter-kvstore', 'no-such-driver'),
+ ).toBeNull();
+ });
+});
diff --git a/src/backend/controllers/notification/NotificationController.test.ts b/src/backend/controllers/notification/NotificationController.test.ts
new file mode 100644
index 000000000..6367c3a52
--- /dev/null
+++ b/src/backend/controllers/notification/NotificationController.test.ts
@@ -0,0 +1,220 @@
+/**
+ * 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 .
+ */
+
+import type { Request, Response } from 'express';
+import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
+import { v4 as uuidv4 } from 'uuid';
+import type { Actor } from '../../core/actor.js';
+import { PuterServer } from '../../server.js';
+import { setupTestServer } from '../../testUtil.js';
+import type { NotificationController } from './NotificationController.js';
+
+// ── Test harness ────────────────────────────────────────────────────
+//
+// Boots one PuterServer with the live wired NotificationController.
+// Tests seed real notification rows via the store, then drive the
+// controller's `markAck` / `markRead` handlers with stub req/res
+// objects. The controller's path through NotificationService updates
+// the underlying row, so we verify behaviour by reading the store
+// state back.
+
+let server: PuterServer;
+let controller: NotificationController;
+
+beforeAll(async () => {
+ server = await setupTestServer();
+ controller =
+ server.controllers.notification as unknown as NotificationController;
+});
+
+afterAll(async () => {
+ await server?.shutdown();
+});
+
+interface CapturedResponse {
+ statusCode: number;
+ body: unknown;
+}
+
+const makeReq = (init: {
+ body?: unknown;
+ actor?: Actor;
+}): Request => {
+ return {
+ body: init.body ?? {},
+ query: {},
+ headers: {},
+ actor: init.actor,
+ } as unknown as Request;
+};
+
+const makeRes = () => {
+ const captured: CapturedResponse = { statusCode: 200, body: undefined };
+ const res = {
+ json: vi.fn((value: unknown) => {
+ captured.body = value;
+ return res;
+ }),
+ status: vi.fn((code: number) => {
+ captured.statusCode = code;
+ return res;
+ }),
+ setHeader: vi.fn(() => res),
+ };
+ return { res: res as unknown as Response, captured };
+};
+
+const makeUser = async (): Promise<{ actor: Actor; userId: number }> => {
+ const username = `nc-${Math.random().toString(36).slice(2, 10)}`;
+ const created = await server.stores.user.create({
+ username,
+ uuid: uuidv4(),
+ password: null,
+ email: `${username}@test.local`,
+ free_storage: 100 * 1024 * 1024,
+ requires_email_confirmation: false,
+ });
+ const refreshed = (await server.stores.user.getById(created.id))!;
+ return {
+ userId: refreshed.id,
+ actor: {
+ user: {
+ id: refreshed.id,
+ uuid: refreshed.uuid,
+ username: refreshed.username,
+ email: refreshed.email ?? null,
+ email_confirmed: true,
+ } as Actor['user'],
+ },
+ };
+};
+
+// ── /notif/mark-ack ─────────────────────────────────────────────────
+
+describe('NotificationController.markAck', () => {
+ it('sets `acknowledged` on the underlying notification row', async () => {
+ const { actor, userId } = await makeUser();
+ const created = await server.stores.notification.create({
+ userId,
+ value: { title: 't' },
+ });
+
+ const { res, captured } = makeRes();
+ await controller.markAck(
+ makeReq({ body: { uid: created.uid }, actor }),
+ res,
+ );
+
+ // Empty `{}` is the conventional success body for these routes.
+ expect(captured.body).toEqual({});
+ const after = await server.stores.notification.getByUid(
+ created.uid as string,
+ { userId },
+ );
+ expect(after?.acknowledged).not.toBeNull();
+ });
+
+ it('rejects a missing uid with 400', async () => {
+ const { actor } = await makeUser();
+ const { res } = makeRes();
+ await expect(
+ controller.markAck(makeReq({ body: {}, actor }), res),
+ ).rejects.toMatchObject({ statusCode: 400 });
+ });
+
+ it('rejects a non-string uid with 400', async () => {
+ const { actor } = await makeUser();
+ const { res } = makeRes();
+ await expect(
+ controller.markAck(makeReq({ body: { uid: 123 }, actor }), res),
+ ).rejects.toMatchObject({ statusCode: 400 });
+ });
+
+ it('throws 401 when there is no actor on the request', async () => {
+ const { res } = makeRes();
+ await expect(
+ controller.markAck(makeReq({ body: { uid: 'whatever' } }), res),
+ ).rejects.toMatchObject({ statusCode: 401 });
+ });
+
+ it("does not flip another user's notification", async () => {
+ const a = await makeUser();
+ const b = await makeUser();
+ const created = await server.stores.notification.create({
+ userId: a.userId,
+ value: {},
+ });
+
+ const { res } = makeRes();
+ await controller.markAck(
+ makeReq({ body: { uid: created.uid }, actor: b.actor }),
+ res,
+ );
+
+ const after = await server.stores.notification.getByUid(
+ created.uid as string,
+ { userId: a.userId },
+ );
+ // Store update is scoped by user_id — cross-user mutation is a
+ // silent no-op rather than an error from the controller.
+ expect(after?.acknowledged).toBeFalsy();
+ });
+});
+
+// ── /notif/mark-read ────────────────────────────────────────────────
+
+describe('NotificationController.markRead', () => {
+ it('sets `shown` on the underlying notification row', async () => {
+ const { actor, userId } = await makeUser();
+ const created = await server.stores.notification.create({
+ userId,
+ value: {},
+ });
+
+ const { res, captured } = makeRes();
+ await controller.markRead(
+ makeReq({ body: { uid: created.uid }, actor }),
+ res,
+ );
+
+ expect(captured.body).toEqual({});
+ const after = await server.stores.notification.getByUid(
+ created.uid as string,
+ { userId },
+ );
+ expect(after?.shown).not.toBeNull();
+ // Marking read should NOT also set acknowledged.
+ expect(after?.acknowledged).toBeFalsy();
+ });
+
+ it('rejects an empty uid string with 400', async () => {
+ const { actor } = await makeUser();
+ const { res } = makeRes();
+ await expect(
+ controller.markRead(makeReq({ body: { uid: '' }, actor }), res),
+ ).rejects.toMatchObject({ statusCode: 400 });
+ });
+
+ it('throws 401 when there is no actor on the request', async () => {
+ const { res } = makeRes();
+ await expect(
+ controller.markRead(makeReq({ body: { uid: 'x' } }), res),
+ ).rejects.toMatchObject({ statusCode: 401 });
+ });
+});
diff --git a/src/backend/drivers/apps/AppDriver.test.ts b/src/backend/drivers/apps/AppDriver.test.ts
new file mode 100644
index 000000000..4f87179a0
--- /dev/null
+++ b/src/backend/drivers/apps/AppDriver.test.ts
@@ -0,0 +1,496 @@
+/**
+ * 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 .
+ */
+
+import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+import { v4 as uuidv4 } from 'uuid';
+import type { Actor } from '../../core/actor.js';
+import { runWithContext } from '../../core/context.js';
+import { PuterServer } from '../../server.js';
+import { setupTestServer } from '../../testUtil.js';
+
+// ── Test harness ────────────────────────────────────────────────────
+//
+// Boots one PuterServer (in-memory sqlite + dynamo + s3 + mock redis)
+// and exercises the live AppDriver (`puter-apps`) against the real
+// AppStore. Each test makes its own user via `makeUser` so app rows
+// from one test don't pollute another's `select` results.
+
+let server: PuterServer;
+// AppDriver is a JS module without an exported class type; treat as a
+// generic CRUD-Q surface so we don't fight TS over private internals.
+type CrudQDriver = {
+ create: (args: Record) => Promise>;
+ read: (args: Record) => Promise>;
+ select: (args: Record) => Promise;
+ update: (args: Record) => Promise>;
+ upsert: (args: Record) => Promise>;
+ delete: (args: Record) => Promise<{ success: boolean; uid: string }>;
+ isNameAvailable: (name: string) => Promise;
+};
+let driver: CrudQDriver;
+
+beforeAll(async () => {
+ server = await setupTestServer();
+ driver = server.drivers.apps as unknown as CrudQDriver;
+});
+
+afterAll(async () => {
+ await server?.shutdown();
+});
+
+const makeUser = async (): Promise<{ actor: Actor; userId: number }> => {
+ const username = `ad-${Math.random().toString(36).slice(2, 10)}`;
+ const created = await server.stores.user.create({
+ username,
+ uuid: uuidv4(),
+ password: null,
+ email: `${username}@test.local`,
+ free_storage: 100 * 1024 * 1024,
+ requires_email_confirmation: false,
+ });
+ const refreshed = (await server.stores.user.getById(created.id))!;
+ return {
+ userId: refreshed.id,
+ actor: {
+ user: {
+ id: refreshed.id,
+ uuid: refreshed.uuid,
+ username: refreshed.username,
+ email: refreshed.email ?? null,
+ email_confirmed: true,
+ } as Actor['user'],
+ },
+ };
+};
+
+const withActor = async (actor: Actor, fn: () => Promise): Promise =>
+ runWithContext({ actor }, fn);
+
+const uniqueName = (prefix: string) =>
+ `${prefix}-${Math.random().toString(36).slice(2, 10)}`;
+
+const uniqueIndexUrl = () =>
+ `https://example-${Math.random().toString(36).slice(2, 10)}.test/`;
+
+// ── create ──────────────────────────────────────────────────────────
+
+describe('AppDriver.create', () => {
+ it('creates an app and stamps the actor as owner', async () => {
+ const { actor, userId } = await makeUser();
+ const name = uniqueName('app');
+
+ const result = await withActor(actor, () =>
+ driver.create({
+ object: {
+ name,
+ title: 'My App',
+ description: 'desc',
+ index_url: uniqueIndexUrl(),
+ },
+ }),
+ );
+
+ expect(result.uid).toEqual(expect.any(String));
+ expect(result.name).toBe(name);
+ expect(result.title).toBe('My App');
+ // `owner` is only attached when the actor is the owner.
+ expect(result.owner).toMatchObject({ username: actor.user!.username });
+
+ // Confirm DB-level ownership.
+ const stored = await server.stores.app.getByUid(result.uid as string);
+ expect(stored?.owner_user_id).toBe(userId);
+ });
+
+ it('rejects an invalid app name with 400', async () => {
+ const { actor } = await makeUser();
+ await expect(
+ withActor(actor, () =>
+ driver.create({
+ object: {
+ name: 'has spaces',
+ title: 'x',
+ index_url: uniqueIndexUrl(),
+ },
+ }),
+ ),
+ ).rejects.toMatchObject({ statusCode: 400 });
+ });
+
+ it('rejects a missing index_url with 400', async () => {
+ const { actor } = await makeUser();
+ await expect(
+ withActor(actor, () =>
+ driver.create({
+ object: { name: uniqueName('no-url'), title: 't' },
+ }),
+ ),
+ ).rejects.toMatchObject({ statusCode: 400 });
+ });
+
+ it('rejects a duplicate app name with 400', async () => {
+ const a = await makeUser();
+ const b = await makeUser();
+ const name = uniqueName('dup');
+
+ await withActor(a.actor, () =>
+ driver.create({
+ object: { name, title: 'a', index_url: uniqueIndexUrl() },
+ }),
+ );
+ await expect(
+ withActor(b.actor, () =>
+ driver.create({
+ object: {
+ name,
+ title: 'b',
+ index_url: uniqueIndexUrl(),
+ },
+ }),
+ ),
+ ).rejects.toMatchObject({ statusCode: 400 });
+ });
+
+ it('dedupes a colliding name when `dedupe_name` is true', async () => {
+ const { actor } = await makeUser();
+ const name = uniqueName('dedup');
+
+ await withActor(actor, () =>
+ driver.create({
+ object: { name, title: 't', index_url: uniqueIndexUrl() },
+ }),
+ );
+ const second = await withActor(actor, () =>
+ driver.create({
+ object: { name, title: 't', index_url: uniqueIndexUrl() },
+ options: { dedupe_name: true },
+ }),
+ );
+
+ expect(second.name).not.toBe(name);
+ expect(String(second.name).startsWith(name)).toBe(true);
+ });
+
+ it('rejects a non-image data: icon with 400', async () => {
+ const { actor } = await makeUser();
+ await expect(
+ withActor(actor, () =>
+ driver.create({
+ object: {
+ name: uniqueName('bad-icon'),
+ title: 't',
+ index_url: uniqueIndexUrl(),
+ icon: 'data:text/plain;base64,AAAA',
+ },
+ }),
+ ),
+ ).rejects.toMatchObject({ statusCode: 400 });
+ });
+
+ it('throws 401 with no actor in context', async () => {
+ await expect(
+ driver.create({
+ object: {
+ name: uniqueName('noctx'),
+ title: 't',
+ index_url: uniqueIndexUrl(),
+ },
+ }),
+ ).rejects.toMatchObject({ statusCode: 401 });
+ });
+});
+
+// ── read ────────────────────────────────────────────────────────────
+
+describe('AppDriver.read', () => {
+ it('reads a public app for any actor', async () => {
+ const a = await makeUser();
+ const b = await makeUser();
+ const created = await withActor(a.actor, () =>
+ driver.create({
+ object: {
+ name: uniqueName('public'),
+ title: 't',
+ index_url: uniqueIndexUrl(),
+ },
+ }),
+ );
+
+ const fetched = await withActor(b.actor, () =>
+ driver.read({ uid: created.uid }),
+ );
+ expect(fetched.uid).toBe(created.uid);
+ // Owner block is NOT exposed to non-owners.
+ expect(fetched.owner).toBeUndefined();
+ });
+
+ it('reads via id object with `{ name }`', async () => {
+ const { actor } = await makeUser();
+ const name = uniqueName('by-name');
+ await withActor(actor, () =>
+ driver.create({
+ object: { name, title: 't', index_url: uniqueIndexUrl() },
+ }),
+ );
+ const fetched = await withActor(actor, () =>
+ driver.read({ id: { name } }),
+ );
+ expect(fetched.name).toBe(name);
+ });
+
+ it('returns 404 for a missing app', async () => {
+ const { actor } = await makeUser();
+ await expect(
+ withActor(actor, () => driver.read({ uid: 'app-nonexistent' })),
+ ).rejects.toMatchObject({ statusCode: 404 });
+ });
+});
+
+// ── select ──────────────────────────────────────────────────────────
+
+describe('AppDriver.select', () => {
+ it('returns visible apps including those owned by other users', async () => {
+ const a = await makeUser();
+ const b = await makeUser();
+ const aName = uniqueName('a');
+ const bName = uniqueName('b');
+ await withActor(a.actor, () =>
+ driver.create({
+ object: {
+ name: aName,
+ title: 't',
+ index_url: uniqueIndexUrl(),
+ },
+ }),
+ );
+ await withActor(b.actor, () =>
+ driver.create({
+ object: {
+ name: bName,
+ title: 't',
+ index_url: uniqueIndexUrl(),
+ },
+ }),
+ );
+
+ const result = (await withActor(a.actor, () =>
+ driver.select({}),
+ )) as Array>;
+ const names = result.map((r) => r.name);
+ expect(names).toContain(aName);
+ expect(names).toContain(bName);
+ });
+
+ it('predicate `user-can-edit` filters to actor-owned apps only', async () => {
+ const a = await makeUser();
+ const b = await makeUser();
+ const mine = uniqueName('mine');
+ await withActor(a.actor, () =>
+ driver.create({
+ object: {
+ name: mine,
+ title: 't',
+ index_url: uniqueIndexUrl(),
+ },
+ }),
+ );
+ await withActor(b.actor, () =>
+ driver.create({
+ object: {
+ name: uniqueName('theirs'),
+ title: 't',
+ index_url: uniqueIndexUrl(),
+ },
+ }),
+ );
+
+ const result = (await withActor(a.actor, () =>
+ driver.select({ predicate: ['user-can-edit'] }),
+ )) as Array>;
+
+ // `select` only returns one row in this slice — the actor-owned
+ // one. Caller filters server-side via `owner_user_id`.
+ const names = result.map((r) => r.name);
+ expect(names).toContain(mine);
+ for (const row of result) {
+ expect(row.owner).toMatchObject({
+ username: a.actor.user!.username,
+ });
+ }
+ });
+});
+
+// ── update / delete ─────────────────────────────────────────────────
+
+describe('AppDriver.update', () => {
+ it('updates editable fields on an owned app', async () => {
+ const { actor } = await makeUser();
+ const created = await withActor(actor, () =>
+ driver.create({
+ object: {
+ name: uniqueName('upd'),
+ title: 'Old',
+ index_url: uniqueIndexUrl(),
+ },
+ }),
+ );
+ const updated = await withActor(actor, () =>
+ driver.update({
+ uid: created.uid,
+ object: { title: 'New', description: 'now with desc' },
+ }),
+ );
+ expect(updated.title).toBe('New');
+ expect(updated.description).toBe('now with desc');
+ });
+
+ it("rejects updating another user's app with 403", async () => {
+ const a = await makeUser();
+ const b = await makeUser();
+ const created = await withActor(a.actor, () =>
+ driver.create({
+ object: {
+ name: uniqueName('cross'),
+ title: 't',
+ index_url: uniqueIndexUrl(),
+ },
+ }),
+ );
+
+ await expect(
+ withActor(b.actor, () =>
+ driver.update({
+ uid: created.uid,
+ object: { title: 'hacked' },
+ }),
+ ),
+ ).rejects.toMatchObject({ statusCode: 403 });
+ });
+});
+
+describe('AppDriver.delete', () => {
+ it('deletes an owned app and reports `{ success, uid }`', async () => {
+ const { actor } = await makeUser();
+ const created = await withActor(actor, () =>
+ driver.create({
+ object: {
+ name: uniqueName('del'),
+ title: 't',
+ index_url: uniqueIndexUrl(),
+ },
+ }),
+ );
+ const result = await withActor(actor, () =>
+ driver.delete({ uid: created.uid }),
+ );
+ expect(result).toEqual({ success: true, uid: created.uid });
+ expect(
+ await server.stores.app.getByUid(created.uid as string),
+ ).toBeNull();
+ });
+
+ it("refuses to delete another user's app with 403", async () => {
+ const a = await makeUser();
+ const b = await makeUser();
+ const created = await withActor(a.actor, () =>
+ driver.create({
+ object: {
+ name: uniqueName('cross-del'),
+ title: 't',
+ index_url: uniqueIndexUrl(),
+ },
+ }),
+ );
+ await expect(
+ withActor(b.actor, () => driver.delete({ uid: created.uid })),
+ ).rejects.toMatchObject({ statusCode: 403 });
+ });
+
+ it('returns 404 for a non-existent uid', async () => {
+ const { actor } = await makeUser();
+ await expect(
+ withActor(actor, () => driver.delete({ uid: 'app-nonexistent' })),
+ ).rejects.toMatchObject({ statusCode: 404 });
+ });
+});
+
+// ── upsert ──────────────────────────────────────────────────────────
+
+describe('AppDriver.upsert', () => {
+ it('creates when no row matches', async () => {
+ const { actor } = await makeUser();
+ const result = await withActor(actor, () =>
+ driver.upsert({
+ object: {
+ name: uniqueName('ups'),
+ title: 't',
+ index_url: uniqueIndexUrl(),
+ },
+ }),
+ );
+ expect(result.uid).toEqual(expect.any(String));
+ });
+
+ it('updates when a row already exists at the resolved uid', async () => {
+ const { actor } = await makeUser();
+ const created = await withActor(actor, () =>
+ driver.create({
+ object: {
+ name: uniqueName('ups-existing'),
+ title: 'first',
+ index_url: uniqueIndexUrl(),
+ },
+ }),
+ );
+
+ const result = await withActor(actor, () =>
+ driver.upsert({
+ uid: created.uid,
+ object: { title: 'second' },
+ }),
+ );
+ expect(result.title).toBe('second');
+ });
+});
+
+// ── isNameAvailable ────────────────────────────────────────────────
+
+describe('AppDriver.isNameAvailable', () => {
+ it('returns true for an unused name', async () => {
+ const result = await driver.isNameAvailable(uniqueName('avail'));
+ expect(result).toBe(true);
+ });
+
+ it('returns false once an app has claimed the name', async () => {
+ const { actor } = await makeUser();
+ const name = uniqueName('claimed');
+ await withActor(actor, () =>
+ driver.create({
+ object: { name, title: 't', index_url: uniqueIndexUrl() },
+ }),
+ );
+ const result = await driver.isNameAvailable(name);
+ expect(result).toBe(false);
+ });
+
+ it('rejects an invalid name format with 400', async () => {
+ await expect(driver.isNameAvailable('has spaces')).rejects.toMatchObject(
+ { statusCode: 400 },
+ );
+ });
+});
diff --git a/src/backend/drivers/notification/NotificationDriver.test.ts b/src/backend/drivers/notification/NotificationDriver.test.ts
new file mode 100644
index 000000000..0b65f4a3a
--- /dev/null
+++ b/src/backend/drivers/notification/NotificationDriver.test.ts
@@ -0,0 +1,335 @@
+/**
+ * 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 .
+ */
+
+import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+import { v4 as uuidv4 } from 'uuid';
+import type { Actor } from '../../core/actor.js';
+import { runWithContext } from '../../core/context.js';
+import { PuterServer } from '../../server.js';
+import { setupTestServer } from '../../testUtil.js';
+import type { NotificationDriver } from './NotificationDriver.js';
+
+// ── Test harness ────────────────────────────────────────────────────
+//
+// Boots one PuterServer (in-memory sqlite + dynamo + s3 + mock redis)
+// and exercises the live NotificationDriver against the wired stores.
+// Each test allocates its own user via `makeUser` so notification rows
+// from one test don't leak into another's `select` results.
+
+let server: PuterServer;
+let driver: NotificationDriver;
+
+beforeAll(async () => {
+ server = await setupTestServer();
+ driver = server.drivers.notifications as unknown as NotificationDriver;
+});
+
+afterAll(async () => {
+ await server?.shutdown();
+});
+
+const makeUser = async (): Promise<{ actor: Actor; userId: number }> => {
+ const username = `nd-${Math.random().toString(36).slice(2, 10)}`;
+ const created = await server.stores.user.create({
+ username,
+ uuid: uuidv4(),
+ password: null,
+ email: `${username}@test.local`,
+ free_storage: 100 * 1024 * 1024,
+ requires_email_confirmation: false,
+ });
+ const refreshed = (await server.stores.user.getById(created.id))!;
+ return {
+ userId: refreshed.id,
+ actor: {
+ user: {
+ id: refreshed.id,
+ uuid: refreshed.uuid,
+ username: refreshed.username,
+ email: refreshed.email ?? null,
+ email_confirmed: true,
+ } as Actor['user'],
+ },
+ };
+};
+
+const withActor = async (actor: Actor, fn: () => Promise): Promise =>
+ runWithContext({ actor }, fn);
+
+// ── create ──────────────────────────────────────────────────────────
+
+describe('NotificationDriver.create', () => {
+ it('creates a notification row scoped to the actor', async () => {
+ const { actor, userId } = await makeUser();
+ const result = (await withActor(actor, () =>
+ driver.create({
+ object: { value: { title: 'hi' } },
+ }),
+ )) as Record | null;
+
+ expect(result?.uid).toEqual(expect.any(String));
+ expect(result?.value).toEqual({ title: 'hi' });
+ // shown / acknowledged are unset on creation.
+ expect(result?.shown).toBeNull();
+ expect(result?.acknowledged).toBeNull();
+
+ const row = await server.stores.notification.getByUid(
+ result!.uid as string,
+ { userId },
+ );
+ expect(row).not.toBeNull();
+ });
+
+ it('defaults `value` to {} when omitted', async () => {
+ const { actor } = await makeUser();
+ const result = (await withActor(actor, () =>
+ driver.create({ object: {} }),
+ )) as Record | null;
+ expect(result?.value).toEqual({});
+ });
+
+ it('rejects a missing object body with 400', async () => {
+ const { actor } = await makeUser();
+ await expect(
+ withActor(actor, () =>
+ driver.create({} as Record),
+ ),
+ ).rejects.toMatchObject({ statusCode: 400 });
+ });
+
+ it('rejects an app-actor with 403', async () => {
+ const { actor } = await makeUser();
+ const appActor: Actor = {
+ ...actor,
+ app: { uid: 'some-app', id: 1 },
+ };
+ await expect(
+ withActor(appActor, () =>
+ driver.create({ object: { value: { title: 'app' } } }),
+ ),
+ ).rejects.toMatchObject({ statusCode: 403 });
+ });
+
+ it('throws 401 with no actor in context', async () => {
+ await expect(
+ driver.create({ object: { value: { title: 'noctx' } } }),
+ ).rejects.toMatchObject({ statusCode: 401 });
+ });
+});
+
+// ── read ────────────────────────────────────────────────────────────
+
+describe('NotificationDriver.read', () => {
+ it('reads a notification by uid for its owner', async () => {
+ const { actor } = await makeUser();
+ const created = (await withActor(actor, () =>
+ driver.create({ object: { value: { title: 'a' } } }),
+ )) as Record;
+
+ const fetched = (await withActor(actor, () =>
+ driver.read({ uid: created.uid }),
+ )) as Record | null;
+
+ expect(fetched?.uid).toBe(created.uid);
+ expect(fetched?.value).toEqual({ title: 'a' });
+ });
+
+ it('accepts `id` as an alias for `uid`', async () => {
+ const { actor } = await makeUser();
+ const created = (await withActor(actor, () =>
+ driver.create({ object: { value: {} } }),
+ )) as Record;
+ const fetched = (await withActor(actor, () =>
+ driver.read({ id: created.uid }),
+ )) as Record | null;
+ expect(fetched?.uid).toBe(created.uid);
+ });
+
+ it("returns 404 for another user's notification uid", async () => {
+ const a = await makeUser();
+ const b = await makeUser();
+ const created = (await withActor(a.actor, () =>
+ driver.create({ object: { value: { hidden: true } } }),
+ )) as Record;
+
+ await expect(
+ withActor(b.actor, () => driver.read({ uid: created.uid })),
+ ).rejects.toMatchObject({ statusCode: 404 });
+ });
+
+ it('rejects a missing uid with 400', async () => {
+ const { actor } = await makeUser();
+ await expect(
+ withActor(actor, () => driver.read({})),
+ ).rejects.toMatchObject({ statusCode: 400 });
+ });
+});
+
+// ── select / predicates ─────────────────────────────────────────────
+
+describe('NotificationDriver.select', () => {
+ it('returns the actor-owned notifications', async () => {
+ const { actor } = await makeUser();
+ await withActor(actor, () =>
+ driver.create({ object: { value: { i: 1 } } }),
+ );
+ await withActor(actor, () =>
+ driver.create({ object: { value: { i: 2 } } }),
+ );
+
+ const result = (await withActor(actor, () =>
+ driver.select({}),
+ )) as Array>;
+
+ // SQLite's `created_at` is second-precision so two rapid inserts
+ // can tie on the ORDER BY column — assert membership, not order.
+ expect(result.length).toBe(2);
+ const values = result.map(
+ (r) => (r.value as { i: number }).i,
+ );
+ expect(values.sort()).toEqual([1, 2]);
+ });
+
+ it('does not leak other users\' notifications', async () => {
+ const a = await makeUser();
+ const b = await makeUser();
+ await withActor(a.actor, () =>
+ driver.create({ object: { value: { who: 'a' } } }),
+ );
+ const result = (await withActor(b.actor, () =>
+ driver.select({}),
+ )) as Array>;
+ expect(result).toEqual([]);
+ });
+
+ it('predicate `unseen` filters out shown notifications', async () => {
+ const { actor, userId } = await makeUser();
+ const seen = (await withActor(actor, () =>
+ driver.create({ object: { value: { i: 'seen' } } }),
+ )) as Record;
+ const unseen = (await withActor(actor, () =>
+ driver.create({ object: { value: { i: 'unseen' } } }),
+ )) as Record;
+
+ await server.stores.notification.markShown(
+ seen.uid as string,
+ userId,
+ );
+
+ const result = (await withActor(actor, () =>
+ driver.select({ predicate: 'unseen' }),
+ )) as Array>;
+
+ const uids = result.map((r) => r.uid);
+ expect(uids).toContain(unseen.uid);
+ expect(uids).not.toContain(seen.uid);
+ });
+
+ it('predicate `acknowledged` returns only acked rows', async () => {
+ const { actor, userId } = await makeUser();
+ const ack = (await withActor(actor, () =>
+ driver.create({ object: { value: { i: 'ack' } } }),
+ )) as Record;
+ await withActor(actor, () =>
+ driver.create({ object: { value: { i: 'pending' } } }),
+ );
+ await server.stores.notification.markAcknowledged(
+ ack.uid as string,
+ userId,
+ );
+
+ const result = (await withActor(actor, () =>
+ driver.select({ predicate: 'acknowledged' }),
+ )) as Array>;
+
+ expect(result.map((r) => r.uid)).toEqual([ack.uid]);
+ });
+
+ it('caps `limit` at the driver max even when overridden by the caller', async () => {
+ const { actor } = await makeUser();
+ // Verify shape, not exact upper bound — keep test fast.
+ const result = (await withActor(actor, () =>
+ driver.select({ limit: 100_000 }),
+ )) as unknown[];
+ expect(Array.isArray(result)).toBe(true);
+ });
+});
+
+// ── mark_shown / mark_acknowledged ─────────────────────────────────
+
+describe('NotificationDriver.mark_shown / mark_acknowledged', () => {
+ it('mark_shown sets `shown` and reports success', async () => {
+ const { actor, userId } = await makeUser();
+ const created = (await withActor(actor, () =>
+ driver.create({ object: { value: {} } }),
+ )) as Record;
+
+ const result = (await withActor(actor, () =>
+ driver.mark_shown({ uid: created.uid }),
+ )) as { success: boolean };
+ expect(result.success).toBe(true);
+
+ const row = await server.stores.notification.getByUid(
+ created.uid as string,
+ { userId },
+ );
+ expect(row?.shown).not.toBeNull();
+ });
+
+ it('mark_acknowledged sets `acknowledged` and reports success', async () => {
+ const { actor, userId } = await makeUser();
+ const created = (await withActor(actor, () =>
+ driver.create({ object: { value: {} } }),
+ )) as Record;
+
+ const result = (await withActor(actor, () =>
+ driver.mark_acknowledged({ uid: created.uid }),
+ )) as { success: boolean };
+ expect(result.success).toBe(true);
+
+ const row = await server.stores.notification.getByUid(
+ created.uid as string,
+ { userId },
+ );
+ expect(row?.acknowledged).not.toBeNull();
+ });
+
+ it('mark_shown rejects missing uid with 400', async () => {
+ const { actor } = await makeUser();
+ await expect(
+ withActor(actor, () => driver.mark_shown({})),
+ ).rejects.toMatchObject({ statusCode: 400 });
+ });
+
+ it("mark_shown returns success=false for another user's uid", async () => {
+ const a = await makeUser();
+ const b = await makeUser();
+ const created = (await withActor(a.actor, () =>
+ driver.create({ object: { value: {} } }),
+ )) as Record;
+
+ const result = (await withActor(b.actor, () =>
+ driver.mark_shown({ uid: created.uid }),
+ )) as { success: boolean };
+ // Store update is scoped by user_id, so cross-user mutation is a
+ // silent no-op. The driver reports the store's `affected = 0`
+ // verbatim as `success: false`.
+ expect(result.success).toBe(false);
+ });
+});
diff --git a/src/backend/drivers/subdomain/SubdomainDriver.test.ts b/src/backend/drivers/subdomain/SubdomainDriver.test.ts
new file mode 100644
index 000000000..e9c2c8912
--- /dev/null
+++ b/src/backend/drivers/subdomain/SubdomainDriver.test.ts
@@ -0,0 +1,493 @@
+/**
+ * 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 .
+ */
+
+import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+import { v4 as uuidv4 } from 'uuid';
+import type { Actor } from '../../core/actor.js';
+import { runWithContext } from '../../core/context.js';
+import { PuterServer } from '../../server.js';
+import { setupTestServer } from '../../testUtil.js';
+import { generateDefaultFsentries } from '../../util/userProvisioning.js';
+import type { SubdomainDriver } from './SubdomainDriver.js';
+
+// ── Test harness ────────────────────────────────────────────────────
+//
+// Boots one PuterServer (in-memory sqlite + dynamo + s3 + mock redis)
+// and exercises the live SubdomainDriver against the real wired stores.
+// Each test makes its own user via `makeUser` so subdomain rows / quota
+// counts don't leak across cases.
+
+let server: PuterServer;
+let driver: SubdomainDriver;
+
+beforeAll(async () => {
+ server = await setupTestServer();
+ driver = server.drivers.subdomains as unknown as SubdomainDriver;
+});
+
+afterAll(async () => {
+ await server?.shutdown();
+});
+
+const makeUser = async (): Promise<{ actor: Actor; userId: number }> => {
+ const username = `sd-${Math.random().toString(36).slice(2, 10)}`;
+ const created = await server.stores.user.create({
+ username,
+ uuid: uuidv4(),
+ password: null,
+ email: `${username}@test.local`,
+ free_storage: 100 * 1024 * 1024,
+ requires_email_confirmation: false,
+ });
+ // Driver checks ACL on `root_dir`, which requires the home tree to
+ // exist — without provisioning, every create call would 400.
+ await generateDefaultFsentries(
+ server.clients.db,
+ server.stores.user,
+ created,
+ );
+ const refreshed = (await server.stores.user.getById(created.id))!;
+ return {
+ userId: refreshed.id,
+ actor: {
+ user: {
+ id: refreshed.id,
+ uuid: refreshed.uuid,
+ username: refreshed.username,
+ email: refreshed.email ?? null,
+ email_confirmed: true,
+ } as Actor['user'],
+ },
+ };
+};
+
+const withActor = async (actor: Actor, fn: () => Promise): Promise =>
+ runWithContext({ actor }, fn);
+
+const uniqueSubdomain = (prefix: string) =>
+ `${prefix}-${Math.random().toString(36).slice(2, 10)}`;
+
+// ── create ──────────────────────────────────────────────────────────
+
+describe('SubdomainDriver.create', () => {
+ it('creates a subdomain pointing at an owned fs path', async () => {
+ const { actor, userId } = await makeUser();
+ const username = actor.user!.username!;
+ const sub = uniqueSubdomain('site');
+
+ const result = (await withActor(actor, () =>
+ driver.create({
+ object: {
+ subdomain: sub,
+ root_dir: `/${username}/Public`,
+ },
+ }),
+ )) as Record | null;
+
+ expect(result).not.toBeNull();
+ expect(result?.subdomain).toBe(sub);
+ // Owner is hydrated as `{ username, uuid }`, not a numeric id.
+ expect(result?.owner).toMatchObject({ username });
+
+ const row =
+ await server.stores.subdomain.getBySubdomain(sub);
+ expect(row?.user_id).toBe(userId);
+ });
+
+ it('expands `~/Public` against the actor home before resolving root_dir', async () => {
+ const { actor } = await makeUser();
+ const sub = uniqueSubdomain('tilde');
+
+ await withActor(actor, () =>
+ driver.create({
+ object: { subdomain: sub, root_dir: '~/Public' },
+ }),
+ );
+
+ const row =
+ await server.stores.subdomain.getBySubdomain(sub);
+ expect(row).not.toBeNull();
+ });
+
+ it('rejects an invalid subdomain format with 400', async () => {
+ const { actor } = await makeUser();
+ await expect(
+ withActor(actor, () =>
+ driver.create({
+ object: {
+ subdomain: 'NOT_VALID!',
+ root_dir: `/${actor.user!.username}/Public`,
+ },
+ }),
+ ),
+ ).rejects.toMatchObject({ statusCode: 400 });
+ });
+
+ it('rejects a reserved subdomain word with 400', async () => {
+ const { actor } = await makeUser();
+ await expect(
+ withActor(actor, () =>
+ driver.create({
+ object: {
+ subdomain: 'admin',
+ root_dir: `/${actor.user!.username}/Public`,
+ },
+ }),
+ ),
+ ).rejects.toMatchObject({ statusCode: 400 });
+ });
+
+ it('rejects a duplicate subdomain with 409', async () => {
+ const a = await makeUser();
+ const b = await makeUser();
+ const sub = uniqueSubdomain('dup');
+
+ await withActor(a.actor, () =>
+ driver.create({
+ object: {
+ subdomain: sub,
+ root_dir: `/${a.actor.user!.username}/Public`,
+ },
+ }),
+ );
+
+ await expect(
+ withActor(b.actor, () =>
+ driver.create({
+ object: {
+ subdomain: sub,
+ root_dir: `/${b.actor.user!.username}/Public`,
+ },
+ }),
+ ),
+ ).rejects.toMatchObject({ statusCode: 409 });
+ });
+
+ it('rejects when root_dir does not exist', async () => {
+ const { actor } = await makeUser();
+ await expect(
+ withActor(actor, () =>
+ driver.create({
+ object: {
+ subdomain: uniqueSubdomain('missing'),
+ root_dir: `/${actor.user!.username}/does-not-exist`,
+ },
+ }),
+ ),
+ ).rejects.toMatchObject({ statusCode: 400 });
+ });
+
+ it("rejects pointing root_dir at another user's tree", async () => {
+ const a = await makeUser();
+ const b = await makeUser();
+ await expect(
+ withActor(a.actor, () =>
+ driver.create({
+ object: {
+ subdomain: uniqueSubdomain('intruder'),
+ root_dir: `/${b.actor.user!.username}/Public`,
+ },
+ }),
+ ),
+ ).rejects.toMatchObject({
+ statusCode: expect.any(Number),
+ });
+ });
+
+ it('rejects a missing object body with 400', async () => {
+ const { actor } = await makeUser();
+ await expect(
+ withActor(actor, () =>
+ driver.create({} as Record),
+ ),
+ ).rejects.toMatchObject({ statusCode: 400 });
+ });
+
+ it('throws 401 with no actor in context', async () => {
+ await expect(
+ driver.create({
+ object: {
+ subdomain: uniqueSubdomain('noctx'),
+ root_dir: '/x',
+ },
+ }),
+ ).rejects.toMatchObject({ statusCode: 401 });
+ });
+});
+
+// ── read / select ───────────────────────────────────────────────────
+
+describe('SubdomainDriver.read / select', () => {
+ it('reads a subdomain by uid for its owner', async () => {
+ const { actor } = await makeUser();
+ const username = actor.user!.username!;
+ const sub = uniqueSubdomain('read');
+
+ const created = (await withActor(actor, () =>
+ driver.create({
+ object: { subdomain: sub, root_dir: `/${username}/Public` },
+ }),
+ )) as Record;
+
+ const fetched = (await withActor(actor, () =>
+ driver.read({ uid: created.uid }),
+ )) as Record | null;
+
+ expect(fetched?.subdomain).toBe(sub);
+ });
+
+ it('reads via id object with `{ subdomain }`', async () => {
+ const { actor } = await makeUser();
+ const sub = uniqueSubdomain('read-by-name');
+ await withActor(actor, () =>
+ driver.create({
+ object: {
+ subdomain: sub,
+ root_dir: `/${actor.user!.username}/Public`,
+ },
+ }),
+ );
+
+ const fetched = (await withActor(actor, () =>
+ driver.read({ id: { subdomain: sub } }),
+ )) as Record | null;
+
+ expect(fetched?.subdomain).toBe(sub);
+ });
+
+ it("rejects reading another user's subdomain with 403", async () => {
+ const a = await makeUser();
+ const b = await makeUser();
+ const sub = uniqueSubdomain('private');
+
+ const created = (await withActor(a.actor, () =>
+ driver.create({
+ object: {
+ subdomain: sub,
+ root_dir: `/${a.actor.user!.username}/Public`,
+ },
+ }),
+ )) as Record;
+
+ await expect(
+ withActor(b.actor, () => driver.read({ uid: created.uid })),
+ ).rejects.toMatchObject({ statusCode: 403 });
+ });
+
+ it('returns 404 when reading a missing subdomain', async () => {
+ const { actor } = await makeUser();
+ await expect(
+ withActor(actor, () =>
+ driver.read({ uid: 'nonexistent-uuid' }),
+ ),
+ ).rejects.toMatchObject({ statusCode: 404 });
+ });
+
+ it('select returns only the actor-owned subdomains', async () => {
+ const a = await makeUser();
+ const b = await makeUser();
+ await withActor(a.actor, () =>
+ driver.create({
+ object: {
+ subdomain: uniqueSubdomain('mine'),
+ root_dir: `/${a.actor.user!.username}/Public`,
+ },
+ }),
+ );
+ await withActor(b.actor, () =>
+ driver.create({
+ object: {
+ subdomain: uniqueSubdomain('theirs'),
+ root_dir: `/${b.actor.user!.username}/Public`,
+ },
+ }),
+ );
+
+ const result = (await withActor(a.actor, () =>
+ driver.select({}),
+ )) as Array>;
+
+ // Owners surface as `{ username, uuid }`; assert we only see a's.
+ for (const row of result) {
+ expect((row.owner as { username: string }).username).toBe(
+ a.actor.user!.username,
+ );
+ }
+ });
+});
+
+// ── update ──────────────────────────────────────────────────────────
+
+describe('SubdomainDriver.update', () => {
+ it('updates root_dir to another owned path', async () => {
+ const { actor } = await makeUser();
+ const username = actor.user!.username!;
+ const sub = uniqueSubdomain('upd');
+
+ const created = (await withActor(actor, () =>
+ driver.create({
+ object: { subdomain: sub, root_dir: `/${username}/Public` },
+ }),
+ )) as Record;
+
+ const updated = (await withActor(actor, () =>
+ driver.update({
+ uid: created.uid,
+ object: { root_dir: `/${username}/Documents` },
+ }),
+ )) as Record | null;
+
+ expect(updated).not.toBeNull();
+ const rootDir = updated!.root_dir as Record | null;
+ expect(rootDir?.path).toBe(`/${username}/Documents`);
+ });
+
+ it('refuses to update a subdomain owned by another user with 403', async () => {
+ const a = await makeUser();
+ const b = await makeUser();
+ const sub = uniqueSubdomain('cross-upd');
+
+ const created = (await withActor(a.actor, () =>
+ driver.create({
+ object: {
+ subdomain: sub,
+ root_dir: `/${a.actor.user!.username}/Public`,
+ },
+ }),
+ )) as Record;
+
+ await expect(
+ withActor(b.actor, () =>
+ driver.update({
+ uid: created.uid,
+ object: {
+ root_dir: `/${b.actor.user!.username}/Public`,
+ },
+ }),
+ ),
+ ).rejects.toMatchObject({ statusCode: 403 });
+ });
+
+ it('returns 404 for a missing object body', async () => {
+ const { actor } = await makeUser();
+ await expect(
+ withActor(actor, () =>
+ driver.update({ uid: 'whatever' } as Record),
+ ),
+ ).rejects.toMatchObject({ statusCode: 400 });
+ });
+});
+
+// ── upsert ──────────────────────────────────────────────────────────
+
+describe('SubdomainDriver.upsert', () => {
+ it('creates when no row matches the args', async () => {
+ const { actor } = await makeUser();
+ const sub = uniqueSubdomain('ups');
+ const result = (await withActor(actor, () =>
+ driver.upsert({
+ object: {
+ subdomain: sub,
+ root_dir: `/${actor.user!.username}/Public`,
+ },
+ }),
+ )) as Record | null;
+ expect(result?.subdomain).toBe(sub);
+ });
+
+ it('updates when an existing row resolves via id.subdomain', async () => {
+ const { actor } = await makeUser();
+ const username = actor.user!.username!;
+ const sub = uniqueSubdomain('ups-existing');
+ await withActor(actor, () =>
+ driver.create({
+ object: { subdomain: sub, root_dir: `/${username}/Public` },
+ }),
+ );
+
+ const result = (await withActor(actor, () =>
+ driver.upsert({
+ id: { subdomain: sub },
+ object: { root_dir: `/${username}/Documents` },
+ }),
+ )) as Record | null;
+
+ const rootDir = result!.root_dir as Record | null;
+ expect(rootDir?.path).toBe(`/${username}/Documents`);
+ });
+});
+
+// ── delete ──────────────────────────────────────────────────────────
+
+describe('SubdomainDriver.delete', () => {
+ it('deletes an owned subdomain and reports success', async () => {
+ const { actor } = await makeUser();
+ const sub = uniqueSubdomain('del');
+ const created = (await withActor(actor, () =>
+ driver.create({
+ object: {
+ subdomain: sub,
+ root_dir: `/${actor.user!.username}/Public`,
+ },
+ }),
+ )) as Record;
+
+ const result = (await withActor(actor, () =>
+ driver.delete({ uid: created.uid }),
+ )) as { success: boolean; uid: string };
+
+ expect(result.success).toBe(true);
+ expect(result.uid).toBe(created.uid);
+ expect(
+ await server.stores.subdomain.getBySubdomain(sub),
+ ).toBeNull();
+ });
+
+ it("refuses to delete another user's subdomain with 403", async () => {
+ const a = await makeUser();
+ const b = await makeUser();
+ const sub = uniqueSubdomain('cross-del');
+ const created = (await withActor(a.actor, () =>
+ driver.create({
+ object: {
+ subdomain: sub,
+ root_dir: `/${a.actor.user!.username}/Public`,
+ },
+ }),
+ )) as Record;
+
+ await expect(
+ withActor(b.actor, () => driver.delete({ uid: created.uid })),
+ ).rejects.toMatchObject({ statusCode: 403 });
+
+ // a's row is still there.
+ expect(
+ await server.stores.subdomain.getBySubdomain(sub),
+ ).not.toBeNull();
+ });
+
+ it('returns 404 for a non-existent subdomain', async () => {
+ const { actor } = await makeUser();
+ await expect(
+ withActor(actor, () =>
+ driver.delete({ uid: 'nonexistent-uuid' }),
+ ),
+ ).rejects.toMatchObject({ statusCode: 404 });
+ });
+});