tests: add some more driver tests (#2944)

This commit is contained in:
Daniel Salazar
2026-05-07 10:04:39 -07:00
committed by GitHub
parent 5e01c13b31
commit bbe93defe9
6 changed files with 1855 additions and 0 deletions
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<string, string>;
}): 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<string, string> => ({
'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<string, string>,
}),
res,
);
expect(captured.statusCode).toBe(403);
});
});
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<string, unknown>)?.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();
});
});
@@ -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 <https://www.gnu.org/licenses/>.
*/
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 });
});
});
+496
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<string, unknown>) => Promise<Record<string, unknown>>;
read: (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
select: (args: Record<string, unknown>) => Promise<unknown[]>;
update: (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
upsert: (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
delete: (args: Record<string, unknown>) => Promise<{ success: boolean; uid: string }>;
isNameAvailable: (name: string) => Promise<boolean>;
};
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 <T>(actor: Actor, fn: () => Promise<T>): Promise<T> =>
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<Record<string, unknown>>;
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<Record<string, unknown>>;
// `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 },
);
});
});
@@ -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 <https://www.gnu.org/licenses/>.
*/
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 <T>(actor: Actor, fn: () => Promise<T>): Promise<T> =>
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<string, unknown> | 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<string, unknown> | 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<string, unknown>),
),
).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<string, unknown>;
const fetched = (await withActor(actor, () =>
driver.read({ uid: created.uid }),
)) as Record<string, unknown> | 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<string, unknown>;
const fetched = (await withActor(actor, () =>
driver.read({ id: created.uid }),
)) as Record<string, unknown> | 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<string, unknown>;
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<Record<string, unknown>>;
// 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<Record<string, unknown>>;
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<string, unknown>;
const unseen = (await withActor(actor, () =>
driver.create({ object: { value: { i: 'unseen' } } }),
)) as Record<string, unknown>;
await server.stores.notification.markShown(
seen.uid as string,
userId,
);
const result = (await withActor(actor, () =>
driver.select({ predicate: 'unseen' }),
)) as Array<Record<string, unknown>>;
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<string, unknown>;
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<Record<string, unknown>>;
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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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);
});
});
@@ -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 <https://www.gnu.org/licenses/>.
*/
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 <T>(actor: Actor, fn: () => Promise<T>): Promise<T> =>
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<string, unknown> | 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<string, unknown>),
),
).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<string, unknown>;
const fetched = (await withActor(actor, () =>
driver.read({ uid: created.uid }),
)) as Record<string, unknown> | 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<string, unknown> | 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<string, unknown>;
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<Record<string, unknown>>;
// 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<string, unknown>;
const updated = (await withActor(actor, () =>
driver.update({
uid: created.uid,
object: { root_dir: `/${username}/Documents` },
}),
)) as Record<string, unknown> | null;
expect(updated).not.toBeNull();
const rootDir = updated!.root_dir as Record<string, unknown> | 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<string, unknown>;
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<string, unknown>),
),
).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<string, unknown> | 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<string, unknown> | null;
const rootDir = result!.root_dir as Record<string, unknown> | 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<string, unknown>;
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<string, unknown>;
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 });
});
});