Add tests for Peer, WebDAV, Workers, and WISP. (#3070)
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
Notify HeyPuter / notify (push) Has been cancelled
release-please / release-please (push) Has been cancelled

This commit is contained in:
ProgrammerIn-wonderland
2026-05-10 21:44:30 -04:00
committed by GitHub
parent b4e9fa7688
commit 5bcb425926
5 changed files with 1396 additions and 0 deletions
@@ -0,0 +1,273 @@
import type { Request, Response } from 'express';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { PuterRouter } from '../../core/http/PuterRouter.js';
import { PuterServer } from '../../server.js';
import { setupTestServer } from '../../testUtil.js';
import { PEER_COSTS } from './costs.js';
import type { PeerController } from './PeerController.js';
let server: PuterServer;
let controller: PeerController;
beforeAll(async () => {
server = await setupTestServer({
peers: {
signaller_url: 'wss://signal.test',
fallback_ice: [{ urls: 'stun:stun.test' }],
internal_auth_secret: 'test-secret',
},
});
controller = server.controllers.peer as unknown as PeerController;
});
afterAll(async () => {
await server?.shutdown();
});
interface CapturedResponse {
statusCode: number;
body: unknown;
}
const makeReq = (init: {
body?: unknown;
headers?: Record<string, string>;
actor?: unknown;
method?: string;
}): Request => {
return {
body: init.body ?? {},
query: {},
headers: init.headers ?? {},
actor: init.actor,
method: init.method ?? 'POST',
} 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),
set: vi.fn(() => res),
end: vi.fn(() => res),
send: vi.fn((value: unknown) => {
captured.body = value;
return res;
}),
};
return { res: res as unknown as Response, captured };
};
describe('PeerController', () => {
describe('getReportedCosts', () => {
it('reports a row per PEER cost type with the configured rate', () => {
const rows = controller.getReportedCosts();
expect(rows).toEqual(
expect.arrayContaining([
{
usageType: 'turn:egress-bytes',
ucentsPerUnit: PEER_COSTS['turn:egress-bytes'],
unit: 'byte',
source: 'controller:peer',
},
]),
);
expect(rows.length).toBe(Object.keys(PEER_COSTS).length);
});
});
describe('signaller-info', () => {
it('returns the configured signaller URL and fallback ICE servers', () => {
const { res, captured } = makeRes();
const req = makeReq({ method: 'GET' });
const router = new PuterRouter();
controller.registerRoutes(router);
const signallerRoute = router.routes.find(
(r) => r.path === '/peer/signaller-info',
);
expect(signallerRoute).toBeDefined();
signallerRoute!.handler(req, res);
expect(captured.body).toEqual({
url: 'wss://signal.test',
fallbackIce: [{ urls: 'stun:stun.test' }],
});
});
it('returns null url and empty fallbackIce when peers config is absent', async () => {
const minimalServer = await setupTestServer();
const minimalController = minimalServer.controllers
.peer as unknown as PeerController;
try {
const router = new PuterRouter();
minimalController.registerRoutes(router);
const route = router.routes.find(
(r) => r.path === '/peer/signaller-info',
);
const { res, captured } = makeRes();
route!.handler(makeReq({ method: 'GET' }), res);
expect(captured.body).toEqual({
url: null,
fallbackIce: [],
});
} finally {
await minimalServer.shutdown();
}
});
});
describe('generate-turn', () => {
it('returns 503 when TURN is not configured', async () => {
const minimalServer = await setupTestServer();
const minimalController = minimalServer.controllers
.peer as unknown as PeerController;
try {
const router = new PuterRouter();
minimalController.registerRoutes(router);
const route = router.routes.find(
(r) => r.path === '/peer/generate-turn',
);
const req = makeReq({
actor: {
user: { uuid: '00000000-0000-0000-0000-000000000001' },
},
});
await expect(
route!.handler(req, makeRes().res),
).rejects.toMatchObject({ statusCode: 503 });
} finally {
await minimalServer.shutdown();
}
});
});
describe('ingest-usage', () => {
let ingestHandler: Function;
beforeAll(() => {
const router = new PuterRouter();
controller.registerRoutes(router);
const route = router.routes.find(
(r) => r.path === '/turn/ingest-usage',
);
ingestHandler = route!.handler;
});
it('rejects requests without valid internal auth secret', async () => {
const req = makeReq({
body: { records: [] },
headers: { 'x-puter-internal-auth': 'wrong-secret' },
});
await expect(ingestHandler(req, makeRes().res)).rejects.toMatchObject(
{ statusCode: 403 },
);
});
it('rejects requests with missing auth header', async () => {
const req = makeReq({ body: { records: [] } });
await expect(ingestHandler(req, makeRes().res)).rejects.toMatchObject(
{ statusCode: 403 },
);
});
it('rejects when records is not an array', async () => {
const req = makeReq({
body: { records: 'not-array' },
headers: { 'x-puter-internal-auth': 'test-secret' },
});
await expect(ingestHandler(req, makeRes().res)).rejects.toMatchObject(
{ statusCode: 400 },
);
});
it('returns ok for an empty records array', async () => {
const { res, captured } = makeRes();
const req = makeReq({
body: { records: [] },
headers: { 'x-puter-internal-auth': 'test-secret' },
});
await ingestHandler(req, res);
expect(captured.body).toEqual({ ok: true });
});
it('skips records with non-positive egressBytes', async () => {
const { res, captured } = makeRes();
const req = makeReq({
body: {
records: [
{ egressBytes: 0, userId: 'AAAAAAAAAAAAAAAAAAAAAA' },
{ egressBytes: -5, userId: 'AAAAAAAAAAAAAAAAAAAAAA' },
{ userId: 'AAAAAAAAAAAAAAAAAAAAAA' },
],
},
headers: { 'x-puter-internal-auth': 'test-secret' },
});
await ingestHandler(req, res);
expect(captured.body).toEqual({ ok: true });
});
it('skips records with missing or invalid userId', async () => {
const { res, captured } = makeRes();
const req = makeReq({
body: {
records: [
{ egressBytes: 100 },
{ egressBytes: 100, userId: '' },
{ egressBytes: 100, userId: 'not-valid-b64' },
],
},
headers: { 'x-puter-internal-auth': 'test-secret' },
});
await ingestHandler(req, res);
expect(captured.body).toEqual({ ok: true });
});
it('skips null and non-object records gracefully', async () => {
const { res, captured } = makeRes();
const req = makeReq({
body: {
records: [null, undefined, 42, 'string'],
},
headers: { 'x-puter-internal-auth': 'test-secret' },
});
await ingestHandler(req, res);
expect(captured.body).toEqual({ ok: true });
});
it('rejects when body is missing entirely', async () => {
const req = makeReq({
body: undefined,
headers: { 'x-puter-internal-auth': 'test-secret' },
});
await expect(ingestHandler(req, makeRes().res)).rejects.toMatchObject(
{ statusCode: 400 },
);
});
});
describe('route registration', () => {
it('registers all three expected routes', () => {
const router = new PuterRouter();
controller.registerRoutes(router);
const paths = router.routes.map((r) => r.path);
expect(paths).toContain('/peer/signaller-info');
expect(paths).toContain('/peer/generate-turn');
expect(paths).toContain('/turn/ingest-usage');
});
});
});
@@ -0,0 +1,654 @@
// This suite tests basic features of puter webdav. it is not a comprehensive webdav test suite unlike litmus
// but rather it performs some common sense checks to ensure that WebDAV support isn't irrevocably broken in puter
import type { Request, Response } from 'express';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { PuterRouter } from '../../core/http/PuterRouter.js';
import { PuterServer } from '../../server.js';
import { setupTestServer } from '../../testUtil.js';
import type { WebDAVController } from './WebDAVController.js';
let server: PuterServer;
let controller: WebDAVController;
let dispatchMiddleware: Function;
beforeAll(async () => {
server = await setupTestServer();
controller = server.controllers.webdav as unknown as WebDAVController;
const router = new PuterRouter();
controller.registerRoutes(router);
// WebDAVController registers a single `use()` middleware on the `dav`
// subdomain. Grab it to call directly in tests.
dispatchMiddleware = router.routes[0]!.handler;
});
afterAll(async () => {
await server?.shutdown();
});
interface CapturedResponse {
statusCode: number;
body: unknown;
headers: Record<string, string>;
ended: boolean;
}
const makeReq = (init: {
method: string;
path?: string;
body?: unknown;
headers?: Record<string, string>;
actor?: unknown;
socket?: unknown;
}): Request => {
return {
method: init.method,
path: init.path ?? '/',
body: init.body ?? {},
query: {},
headers: init.headers ?? {},
actor: init.actor,
socket: init.socket ?? {},
} as unknown as Request;
};
const makeRes = () => {
const captured: CapturedResponse = {
statusCode: 200,
body: undefined,
headers: {},
ended: false,
};
const res = {
json: vi.fn((value: unknown) => {
captured.body = value;
return res;
}),
status: vi.fn((code: number) => {
captured.statusCode = code;
return res;
}),
set: vi.fn((obj: Record<string, string> | string, val?: string) => {
if (typeof obj === 'string') {
captured.headers[obj.toLowerCase()] = val!;
} else {
for (const [k, v] of Object.entries(obj)) {
captured.headers[k.toLowerCase()] = v;
}
}
return res;
}),
setHeader: vi.fn((key: string, val: string) => {
captured.headers[key.toLowerCase()] = val;
return res;
}),
send: vi.fn((value: unknown) => {
captured.body = value;
return res;
}),
end: vi.fn(() => {
captured.ended = true;
return res;
}),
headersSent: false,
};
return { res: res as unknown as Response, captured };
};
const basicAuth = (user: string, pass: string) =>
`Basic ${Buffer.from(`${user}:${pass}`).toString('base64')}`;
const noop = vi.fn();
describe('WebDAVController', () => {
describe('route registration', () => {
it('registers a single catch-all use() route', () => {
const router = new PuterRouter();
controller.registerRoutes(router);
expect(router.routes.length).toBeGreaterThanOrEqual(1);
});
});
describe('authentication', () => {
it('returns 401 when no auth is provided and no session actor', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({ method: 'OPTIONS' }),
res,
noop,
);
expect(captured.statusCode).toBe(401);
expect(captured.headers['www-authenticate']).toContain('Basic');
});
it('returns 401 for malformed Basic auth (no colon)', async () => {
const { res, captured } = makeRes();
const encoded = Buffer.from('no-colon-here').toString('base64');
await dispatchMiddleware(
makeReq({
method: 'OPTIONS',
headers: { authorization: `Basic ${encoded}` },
}),
res,
noop,
);
expect(captured.statusCode).toBe(401);
});
it('returns 401 for invalid -token auth', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'OPTIONS',
headers: {
authorization: basicAuth('-token', 'bad-token-value'),
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(401);
});
it('returns 401 for non-existent username', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'OPTIONS',
headers: {
authorization: basicAuth(
'nonexistent-user-xyz',
'password',
),
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(401);
});
});
describe('OPTIONS (with session actor)', () => {
it('returns 200 with DAV headers when actor is present', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'OPTIONS',
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(200);
expect(captured.headers['dav']).toContain('1');
expect(captured.headers['allow']).toContain('PROPFIND');
expect(captured.headers['allow']).toContain('GET');
expect(captured.headers['allow']).toContain('PUT');
expect(captured.headers['allow']).toContain('DELETE');
});
});
describe('unsupported methods', () => {
it('returns 405 for unknown HTTP methods', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'PATCH',
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(405);
expect(captured.headers['allow']).toContain('PROPFIND');
});
});
describe('GET', () => {
it('returns 404 for a non-existent path', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'GET',
path: '/nonexistent-file-that-does-not-exist.txt',
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(404);
});
});
describe('PROPFIND', () => {
it('returns 207 multistatus for root path', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'PROPFIND',
path: '/',
headers: { depth: '0' },
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(207);
expect(captured.headers['content-type']).toContain(
'application/xml',
);
expect(captured.body).toContain('multistatus');
});
it('returns 404 for a non-existent path', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'PROPFIND',
path: '/does-not-exist',
headers: { depth: '0' },
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(404);
});
it('includes DAV XML properties in the root PROPFIND response', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'PROPFIND',
path: '/',
headers: { depth: '0' },
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res,
noop,
);
const xml = captured.body as string;
expect(xml).toContain('<D:response>');
expect(xml).toContain('<D:href>');
expect(xml).toContain('<D:resourcetype>');
expect(xml).toContain('<D:collection/>');
});
});
describe('MKCOL', () => {
it('rejects creating a collection at root', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'MKCOL',
path: '/',
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(403);
});
it('rejects MKCOL with a body', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'MKCOL',
path: '/new-collection',
headers: { 'content-length': '10' },
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(415);
});
});
describe('PUT', () => {
it('rejects macOS junk files (.DS_Store)', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'PUT',
path: '/some/dir/.DS_Store',
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(422);
});
it('rejects macOS resource fork files (._prefix)', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'PUT',
path: '/some/dir/._myfile.txt',
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(422);
});
});
describe('DELETE', () => {
it('rejects delete when ACL denies write access', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'DELETE',
path: '/nonexistent-file-to-delete.txt',
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(403);
});
});
describe('COPY', () => {
it('returns 400 when Destination header is missing', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'COPY',
path: '/some/file.txt',
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(400);
});
});
describe('MOVE', () => {
it('returns 400 when Destination header is missing', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'MOVE',
path: '/some/file.txt',
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(400);
});
});
describe('UNLOCK', () => {
it('returns 400 when Lock-Token header is missing', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'UNLOCK',
path: '/some/file.txt',
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(400);
});
it('returns 204 for an expired/unknown lock token (idempotent)', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'UNLOCK',
path: '/some/file.txt',
headers: {
'lock-token':
'<urn:uuid:00000000-0000-0000-0000-000000000099>',
},
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(204);
});
});
describe('LOCK', () => {
it('creates a new exclusive lock and returns XML with lock token', async () => {
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'LOCK',
path: '/lockable-file.txt',
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(200);
expect(captured.headers['content-type']).toContain(
'application/xml',
);
const xml = captured.body as string;
expect(xml).toContain('lockdiscovery');
expect(xml).toContain('urn:uuid:');
expect(xml).toContain('<D:exclusive/>');
expect(captured.headers['lock-token']).toContain('urn:uuid:');
});
it('rejects a second exclusive lock on the same path', async () => {
const uniquePath = `/double-lock-${Date.now()}.txt`;
// First lock
const { res: res1, captured: cap1 } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'LOCK',
path: uniquePath,
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res1,
noop,
);
expect(cap1.statusCode).toBe(200);
// Second lock — should be 423 Locked
const { res: res2, captured: cap2 } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'LOCK',
path: uniquePath,
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res2,
noop,
);
expect(cap2.statusCode).toBe(423);
});
it('refreshes an existing lock when If header provides the token', async () => {
const uniquePath = `/refresh-lock-${Date.now()}.txt`;
const { res: res1, captured: cap1 } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'LOCK',
path: uniquePath,
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res1,
noop,
);
expect(cap1.statusCode).toBe(200);
const lockToken = cap1.headers['lock-token']!.replace(
/[<>]/g,
'',
);
// Refresh
const { res: res2, captured: cap2 } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'LOCK',
path: uniquePath,
headers: { if: `(<${lockToken}>)` },
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res2,
noop,
);
expect(cap2.statusCode).toBe(200);
const xml = cap2.body as string;
expect(xml).toContain(lockToken);
});
});
describe('error handling', () => {
it('catches HttpError and returns its status code', async () => {
// GET on a non-existent file → HttpError(404) → 404 response
const { res, captured } = makeRes();
await dispatchMiddleware(
makeReq({
method: 'GET',
path: '/no-such-file',
actor: {
user: {
id: 1,
uuid: 'test-uuid',
username: 'test',
},
},
}),
res,
noop,
);
expect(captured.statusCode).toBe(404);
});
});
});
@@ -0,0 +1,218 @@
// This tests wisp controller but not wisp itself. That is out of process and out of this repo
// This simply tests the authentication methods that puter wisp expects and uses.
import type { Request, Response } from 'express';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { PuterRouter } from '../../core/http/PuterRouter.js';
import { PuterServer } from '../../server.js';
import { setupTestServer } from '../../testUtil.js';
import type { WispController } from './WispController.js';
let server: PuterServer;
let controller: WispController;
let createHandler: Function;
let verifyHandler: Function;
beforeAll(async () => {
server = await setupTestServer({
wisp: { server: 'wss://wisp.test' },
});
controller = server.controllers.wisp as unknown as WispController;
const router = new PuterRouter();
controller.registerRoutes(router);
createHandler = router.routes.find(
(r) => r.path === '/wisp/relay-token/create',
)!.handler;
verifyHandler = router.routes.find(
(r) => r.path === '/wisp/relay-token/verify',
)!.handler;
});
afterAll(async () => {
await server?.shutdown();
});
interface CapturedResponse {
statusCode: number;
body: unknown;
}
const makeReq = (init: {
body?: unknown;
headers?: Record<string, string>;
actor?: unknown;
}): Request => {
return {
body: init.body ?? {},
query: {},
headers: init.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),
set: vi.fn(() => res),
send: vi.fn((value: unknown) => {
captured.body = value;
return res;
}),
end: vi.fn(() => res),
};
return { res: res as unknown as Response, captured };
};
describe('WispController', () => {
describe('route registration', () => {
it('registers both expected routes', () => {
const router = new PuterRouter();
controller.registerRoutes(router);
const paths = router.routes.map((r) => r.path);
expect(paths).toContain('/wisp/relay-token/create');
expect(paths).toContain('/wisp/relay-token/verify');
});
});
describe('create', () => {
it('returns a token and server for an authenticated user', async () => {
const { res, captured } = makeRes();
const req = makeReq({
actor: {
user: {
uuid: '00000000-0000-0000-0000-000000000001',
},
},
});
await createHandler(req, res);
const body = captured.body as { token: string; server: string };
expect(body.token).toBeDefined();
expect(typeof body.token).toBe('string');
expect(body.token.length).toBeGreaterThan(0);
expect(body.server).toBe('wss://wisp.test');
});
it('returns a guest token when actor has no user uuid', async () => {
const { res, captured } = makeRes();
const req = makeReq({ actor: { user: {} } });
await createHandler(req, res);
const body = captured.body as { token: string; server: string };
expect(body.token).toBeDefined();
expect(typeof body.token).toBe('string');
expect(body.server).toBe('wss://wisp.test');
});
it('returns a guest token when actor is absent', async () => {
const { res, captured } = makeRes();
const req = makeReq({});
await createHandler(req, res);
const body = captured.body as { token: string; server: string };
expect(body.token).toBeDefined();
expect(body.server).toBe('wss://wisp.test');
});
it('returns null server when wisp config has no server', async () => {
const minServer = await setupTestServer();
const minController = minServer.controllers
.wisp as unknown as WispController;
try {
const router = new PuterRouter();
minController.registerRoutes(router);
const handler = router.routes.find(
(r) => r.path === '/wisp/relay-token/create',
)!.handler;
const { res, captured } = makeRes();
await handler(makeReq({ actor: { user: {} } }), res);
const body = captured.body as { token: string; server: unknown };
expect(body.server).toBeNull();
} finally {
await minServer.shutdown();
}
});
});
describe('verify', () => {
it('verifies a valid authenticated-user token', async () => {
const { res: createRes, captured: createCaptured } = makeRes();
await createHandler(
makeReq({
actor: {
user: {
uuid: '00000000-0000-0000-0000-000000000001',
},
},
}),
createRes,
);
const token = (createCaptured.body as { token: string }).token;
const { res, captured } = makeRes();
await verifyHandler(makeReq({ body: { token } }), res);
expect(captured.statusCode).toBe(200);
const body = captured.body as { allow: boolean };
expect(body.allow).toBe(true);
});
it('verifies a valid guest token', async () => {
const { res: createRes, captured: createCaptured } = makeRes();
await createHandler(makeReq({ actor: { user: {} } }), createRes);
const token = (createCaptured.body as { token: string }).token;
const { res, captured } = makeRes();
await verifyHandler(makeReq({ body: { token } }), res);
expect(captured.statusCode).toBe(200);
const body = captured.body as { allow: boolean };
expect(body.allow).toBe(true);
});
it('rejects when token is missing', async () => {
await expect(
verifyHandler(makeReq({ body: {} }), makeRes().res),
).rejects.toMatchObject({ statusCode: 400 });
});
it('rejects when token is not a string', async () => {
await expect(
verifyHandler(
makeReq({ body: { token: 12345 } }),
makeRes().res,
),
).rejects.toMatchObject({ statusCode: 400 });
});
it('rejects an invalid/tampered token', async () => {
await expect(
verifyHandler(
makeReq({ body: { token: 'not-a-valid-jwt' } }),
makeRes().res,
),
).rejects.toMatchObject({ statusCode: 403 });
});
it('rejects when body is undefined', async () => {
await expect(
verifyHandler(
makeReq({ body: undefined }),
makeRes().res,
),
).rejects.toMatchObject({ statusCode: 400 });
});
});
});
@@ -0,0 +1,244 @@
// This test checks everything in WorkerDriver up to the cloudflare level.
// Full deployments are not tested as this would require Cloudflare Workerd,
// however the interactions with the Puter API are
import {
afterAll,
beforeAll,
beforeEach,
describe,
expect,
it,
} from 'vitest';
import { Actor } from '../../core/actor.js';
import { runWithContext } from '../../core/context.js';
import { PuterServer } from '../../server.js';
import { setupTestServer } from '../../testUtil.js';
import type { WorkerDriver } from './WorkerDriver.js';
describe('WorkerDriver', () => {
let server: PuterServer;
let target: WorkerDriver;
beforeAll(async () => {
server = await setupTestServer();
target = server.drivers.workers as unknown as WorkerDriver;
});
afterAll(async () => {
await server?.shutdown();
});
let actor: Actor;
const makeActor = (overrides: Partial<Actor> = {}): Actor => ({
user: {
uuid: `test-user-${Math.random().toString(36).slice(2)}`,
id: 1,
username: 'test-user',
email: 'test@test.com',
email_confirmed: true,
},
app: { uid: 'test-app', id: 1 },
...overrides,
});
beforeEach(() => {
actor = makeActor();
});
const inCtx = <T>(fn: () => T | Promise<T>, withActor: Actor = actor) =>
runWithContext({ actor: withActor }, fn);
describe('actor scoping', () => {
it('rejects calls without an actor in context', async () => {
await expect(
runWithContext({}, () =>
target.create({
appId: 'test',
workerName: 'test',
filePath: '/test.js',
}),
),
).rejects.toMatchObject({ statusCode: 401 });
});
it('rejects calls with an actor missing user.id', async () => {
const noIdActor = { user: { uuid: 'uuid' } } as Actor;
await expect(
inCtx(
() =>
target.create({
appId: 'test',
workerName: 'test',
filePath: '/test.js',
}),
noIdActor,
),
).rejects.toMatchObject({ statusCode: 401 });
});
});
describe('create', () => {
it('rejects when workerName is missing', async () => {
await expect(
inCtx(() =>
target.create({
appId: 'test',
workerName: '',
filePath: '/test.js',
}),
),
).rejects.toMatchObject({ statusCode: 400 });
});
it('rejects when filePath is missing', async () => {
await expect(
inCtx(() =>
target.create({
appId: 'test',
workerName: 'myworker',
filePath: '',
}),
),
).rejects.toMatchObject({ statusCode: 400 });
});
it('rejects invalid worker names (special characters)', async () => {
await expect(
inCtx(() =>
target.create({
appId: 'test',
workerName: 'invalid name!',
filePath: '/test.js',
}),
),
).rejects.toMatchObject({ statusCode: 400 });
});
it('rejects worker names with dots', async () => {
await expect(
inCtx(() =>
target.create({
appId: 'test',
workerName: 'my.worker',
filePath: '/test.js',
}),
),
).rejects.toMatchObject({ statusCode: 400 });
});
it('allows valid worker names with underscores and dashes', async () => {
// This will fail at the CF config check (503) rather than the
// name validation (400), proving the name was accepted.
await expect(
inCtx(() =>
target.create({
appId: 'test',
workerName: 'my-worker_01',
filePath: '/test.js',
}),
),
).rejects.toMatchObject({ statusCode: 503 });
});
it('lowercases the worker name', async () => {
await expect(
inCtx(() =>
target.create({
appId: 'test',
workerName: 'MyWorker',
filePath: '/test.js',
}),
),
).rejects.toMatchObject({ statusCode: 503 });
});
it('returns 503 when Cloudflare Workers is not configured', async () => {
await expect(
inCtx(() =>
target.create({
appId: 'test',
workerName: 'validname',
filePath: '/test.js',
}),
),
).rejects.toMatchObject({ statusCode: 503 });
});
it('rejects reserved worker names', async () => {
const serverWithReserved = await setupTestServer({
reserved_words: ['admin', 'api'],
});
const driverWithReserved = serverWithReserved.drivers
.workers as unknown as WorkerDriver;
try {
await expect(
runWithContext({ actor }, () =>
driverWithReserved.create({
appId: 'test',
workerName: 'admin',
filePath: '/test.js',
}),
),
).rejects.toMatchObject({ statusCode: 400 });
} finally {
await serverWithReserved.shutdown();
}
});
});
describe('destroy', () => {
it('rejects when workerName is missing', async () => {
await expect(
inCtx(() => target.destroy({ workerName: '' })),
).rejects.toMatchObject({ statusCode: 400 });
});
it('returns 503 when Cloudflare Workers is not configured', async () => {
await expect(
inCtx(() => target.destroy({ workerName: 'validname' })),
).rejects.toMatchObject({ statusCode: 503 });
});
});
describe('getFilePaths', () => {
it('returns an empty array when the user has no workers', async () => {
const res = await inCtx(() => target.getFilePaths({}));
expect(res).toEqual([]);
});
it('returns an empty array when querying a non-existent worker name', async () => {
const res = await inCtx(() =>
target.getFilePaths({ workerName: 'nonexistent' }),
);
expect(res).toEqual([]);
});
});
describe('getLoggingUrl', () => {
it('returns null when loggingUrl is not configured', async () => {
const res = await inCtx(() => target.getLoggingUrl());
expect(res).toBeNull();
});
it('returns the configured loggingUrl', async () => {
const serverWithLogging = await setupTestServer({
workers: { loggingUrl: 'https://logs.test/view' },
});
const driverWithLogging = serverWithLogging.drivers
.workers as unknown as WorkerDriver;
try {
const res = await runWithContext({ actor }, () =>
driverWithLogging.getLoggingUrl(),
);
expect(res).toBe('https://logs.test/view');
} finally {
await serverWithLogging.shutdown();
}
});
});
describe('getReportedCosts', () => {
it('returns an empty array (workers have no cost reporting)', () => {
const rows = target.getReportedCosts();
expect(rows).toEqual([]);
});
});
});
@@ -38,6 +38,7 @@ const WORKER_SUBDOMAIN_PREFIX = 'workers.puter.';
// If the file hasn't been built, workers run without puter.js access.
let preamble = '';
let preambleError = false;
let preambleLineCount = 0;
try {
const preamblePath = path.join(
@@ -51,6 +52,7 @@ try {
console.warn(
'[workers] preamble not built — workers will not have puter.js injected.',
);
preambleError = true;
}
/**
@@ -78,6 +80,11 @@ export class WorkerDriver extends PuterDriver {
if (cfg.namespace) {
this.#cfBaseUrl += `/dispatch/namespaces/${cfg.namespace}`;
}
if (preambleError) {
throw new Error(
'[workers] preamble not build but workers configured to be enabled. Halting start',
);
}
}
this.#subscribeHotReload();
}