mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-29 12:50:59 +00:00
Add tests for Peer, WebDAV, Workers, and WISP. (#3070)
This commit is contained in:
committed by
GitHub
parent
b4e9fa7688
commit
5bcb425926
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user