Files
image/node-hook/test/server.test.mjs
T

272 lines
11 KiB
JavaScript

import assert from 'node:assert/strict';
import { once } from 'node:events';
import test from 'node:test';
import { adminSignature, uploadSignature } from '../src/auth.mjs';
import { createApp } from '../src/server.mjs';
const noUploadStore = { async initialize() {}, async store() { throw new Error('must not upload'); } };
test('sync endpoint authenticates a scoped caller and passes only an optional commit', async (t) => {
const calls = [];
const service = {
async initialize() {},
async deploySync(value) {
calls.push(value);
return { changed: false };
},
async recordError() {},
};
const secret = 's'.repeat(32);
const { server } = await createApp({
maxBodyBytes: 4096,
syncClientSecrets: { core: secret },
}, { service, uploadStore: noUploadStore });
server.listen(0, '127.0.0.1');
await once(server, 'listening');
t.after(() => server.close());
const address = server.address();
const body = Buffer.from(JSON.stringify({ commit: 'a'.repeat(40) }));
const timestamp = String(Date.now());
const requestId = 'sync-request-1234';
const response = await fetch(`http://127.0.0.1:${address.port}/v1/sync`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-image-client': 'core',
'x-image-timestamp': timestamp,
'x-image-request-id': requestId,
'x-image-signature': adminSignature(secret, timestamp, requestId, body),
},
body,
});
assert.equal(response.status, 200);
assert.deepEqual(calls, [{ requestKey: `core:${requestId}`, expectedCommit: 'a'.repeat(40) }]);
});
test('upload endpoint accepts a short-lived body-bound grant and rejects replay or tampering', async (t) => {
const service = {
async initialize() {},
async recordError() {},
};
const calls = [];
const uploadStore = {
async initialize() {},
async store(value) {
calls.push(value);
return { duplicate: false, path: `icons/users/${value.client}/${value.filename}` };
},
};
const secret = 'u'.repeat(32);
const config = {
maxBodyBytes: 4096,
maxUploadBytes: 51200,
maxContentUploadBytes: 1048576,
syncClientSecrets: { core: 's'.repeat(32) },
uploadClientSecrets: { core2026: secret },
publicBases: ['https://sam-image.hided.net', 'https://sam.hided.net/image'],
};
const { server } = await createApp(config, { service, uploadStore });
server.listen(0, '127.0.0.1');
await once(server, 'listening');
t.after(() => server.close());
const address = server.address();
const pathname = `/v1/uploads/user-icons/core2026/${'a'.repeat(32)}.png`;
const body = Buffer.from('89504e470d0a1a0a00000000', 'hex');
const expires = String(Math.floor(Date.now() / 1000) + 60);
const requestId = 'upload-request-1234';
const signature = uploadSignature(secret, { expires, requestId, pathname, contentType: 'image/png', body });
const headers = {
'content-type': 'image/png',
'x-image-client': 'core2026',
'x-image-expires': expires,
'x-image-request-id': requestId,
'x-image-signature': signature,
};
const accepted = await fetch(`http://127.0.0.1:${address.port}${pathname}`, { method: 'PUT', headers, body });
assert.equal(accepted.status, 201);
assert.deepEqual(calls[0], {
requestKey: `core2026:${requestId}`,
category: 'user-icons',
client: 'core2026',
filename: `${'a'.repeat(32)}.png`,
body,
});
assert.deepEqual((await accepted.json()).urls, [
`https://sam-image.hided.net/icons/users/core2026/${'a'.repeat(32)}.png`,
`https://sam.hided.net/image/icons/users/core2026/${'a'.repeat(32)}.png`,
]);
const contentPath = `/v1/uploads/content/core2026/${'b'.repeat(32)}.webp`;
const contentBody = Buffer.concat([Buffer.from('RIFF'), Buffer.alloc(4), Buffer.from('WEBP'), Buffer.alloc(4)]);
const contentRequestId = 'content-request-1234';
const contentSignature = uploadSignature(secret, {
expires,
requestId: contentRequestId,
pathname: contentPath,
contentType: 'image/webp',
body: contentBody,
});
const contentResponse = await fetch(`http://127.0.0.1:${address.port}${contentPath}`, {
method: 'PUT',
headers: {
'content-type': 'image/webp',
'x-image-client': 'core2026',
'x-image-expires': expires,
'x-image-request-id': contentRequestId,
'x-image-signature': contentSignature,
},
body: contentBody,
});
assert.equal(contentResponse.status, 201);
assert.equal(calls[1].category, 'content');
assert.equal(calls[1].filename, `${'b'.repeat(32)}.webp`);
const tampered = await fetch(`http://127.0.0.1:${address.port}${pathname}`, {
method: 'PUT', headers, body: Buffer.from('89504e470d0a1a0affffffff', 'hex'),
});
assert.equal(tampered.status, 401);
const expired = '1000000000';
const expiredHeaders = {
...headers,
'x-image-expires': expired,
'x-image-signature': uploadSignature(secret, { expires: expired, requestId, pathname, contentType: 'image/png', body }),
};
const expiredResponse = await fetch(`http://127.0.0.1:${address.port}${pathname}`, {
method: 'PUT', headers: expiredHeaders, body,
});
assert.equal(expiredResponse.status, 401);
});
test('sync endpoint rejects unknown callers and body fields outside the sync contract', async (t) => {
const service = {
async initialize() {},
async deploySync() { throw new Error('must not deploy'); },
async recordError() {},
};
const secret = 's'.repeat(32);
const { server } = await createApp(
{ maxBodyBytes: 4096, syncClientSecrets: { core: secret } },
{ service, uploadStore: noUploadStore },
);
server.listen(0, '127.0.0.1');
await once(server, 'listening');
t.after(() => server.close());
const address = server.address();
const body = Buffer.from(JSON.stringify({ branch: 'preview' }));
const timestamp = String(Date.now());
const requestId = 'sync-request-5678';
const signedHeaders = {
'content-type': 'application/json',
'x-image-timestamp': timestamp,
'x-image-request-id': requestId,
'x-image-signature': adminSignature(secret, timestamp, requestId, body),
};
const unknown = await fetch(`http://127.0.0.1:${address.port}/v1/sync`, {
method: 'POST', headers: { ...signedHeaders, 'x-image-client': 'unknown' }, body,
});
assert.equal(unknown.status, 401);
const prototypeName = await fetch(`http://127.0.0.1:${address.port}/v1/sync`, {
method: 'POST', headers: { ...signedHeaders, 'x-image-client': 'toString' }, body,
});
assert.equal(prototypeName.status, 401);
const extraField = await fetch(`http://127.0.0.1:${address.port}/v1/sync`, {
method: 'POST', headers: { ...signedHeaders, 'x-image-client': 'core' }, body,
});
assert.equal(extraField.status, 400);
});
test('internal content access endpoint batches only valid uploaded content paths', async (t) => {
const service = { async initialize() {}, async recordError() {} };
const touched = [];
const assetStore = {
async initialize() {},
touch(path) { touched.push(path); return path.startsWith('uploads/'); },
close() {},
};
const { server } = await createApp({ maxBodyBytes: 4096 }, {
service, uploadStore: noUploadStore, assetStore,
});
server.listen(0, '127.0.0.1');
await once(server, 'listening');
t.after(() => server.close());
const address = server.address();
const validPath = `/uploads/core2026/${'e'.repeat(32)}.png?cache=1`;
const valid = await fetch(`http://127.0.0.1:${address.port}/v1/internal/content-access`, {
headers: { 'x-image-path': validPath },
});
assert.equal(valid.status, 204);
assert.deepEqual(touched, [`uploads/core2026/${'e'.repeat(32)}.png`]);
const legacyBase = await fetch(`http://127.0.0.1:${address.port}/v1/internal/content-access`, {
method: 'HEAD',
headers: { 'x-image-path': `/image/uploads/core/${'f'.repeat(32)}.webp` },
});
assert.equal(legacyBase.status, 204);
assert.deepEqual(touched, [
`uploads/core2026/${'e'.repeat(32)}.png`,
`uploads/core/${'f'.repeat(32)}.webp`,
]);
});
test('admin panel uses a separate login session and CSRF-protected asset actions', async (t) => {
const service = { async initialize() {}, async recordError() {} };
const actions = [];
const asset = {
path: `uploads/core/${'f'.repeat(32)}.webp`, category: 'content', client: 'core',
filename: `${'f'.repeat(32)}.webp`, sizeBytes: 10, digest: null,
createdAt: new Date(0).toISOString(), lastSeenAt: new Date(0).toISOString(),
state: 'candidate', candidateAt: new Date(0).toISOString(), quarantinedAt: null,
deletedAt: null, eligibleAt: new Date(0).toISOString(), deleteAvailableAt: null, deleteAvailable: false,
};
const assetStore = {
async initialize() {}, touch() { return false; }, close() {},
list() { return { total: 1, limit: 100, offset: 0, assets: [asset] }; },
summary() { return { totalCount: 1, totalBytes: 10, groups: { 'content:candidate': { count: 1, bytes: 10 } } }; },
async quarantine(path) { actions.push(['quarantine', path]); return { ...asset, state: 'quarantined' }; },
};
const config = {
maxBodyBytes: 4096,
adminPanelPassword: 'panel-password-value',
adminPanelSessionSecret: 'p'.repeat(32),
adminPanelSessionTtlMs: 8 * 3_600_000,
};
const { server } = await createApp(config, { service, uploadStore: noUploadStore, assetStore });
server.listen(0, '127.0.0.1');
await once(server, 'listening');
t.after(() => server.close());
const base = `http://127.0.0.1:${server.address().port}`;
const anonymousApi = await fetch(`${base}/admin/api/assets`);
assert.equal(anonymousApi.status, 401);
const loginBody = new URLSearchParams({ password: config.adminPanelPassword });
const login = await fetch(`${base}/admin/login`, {
method: 'POST', redirect: 'manual',
headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: loginBody,
});
assert.equal(login.status, 303);
const cookie = login.headers.get('set-cookie').split(';', 1)[0];
assert.match(login.headers.get('set-cookie'), /HttpOnly; Secure; SameSite=Strict/);
const dashboard = await fetch(`${base}/admin/`, { headers: { cookie } });
assert.equal(dashboard.status, 200);
assert.match(dashboard.headers.get('content-security-policy'), /frame-ancestors 'none'/);
const html = await dashboard.text();
const csrf = html.match(/name="csrf-token" content="([^"]+)"/)[1];
const list = await fetch(`${base}/admin/api/assets`, { headers: { cookie } });
assert.equal(list.status, 200);
assert.equal((await list.json()).assets[0].state, 'candidate');
const rejected = await fetch(`${base}/admin/api/assets/action`, {
method: 'POST', headers: { cookie, 'content-type': 'application/json' },
body: JSON.stringify({ path: asset.path, action: 'quarantine' }),
});
assert.equal(rejected.status, 403);
const accepted = await fetch(`${base}/admin/api/assets/action`, {
method: 'POST', headers: { cookie, 'content-type': 'application/json', 'x-csrf-token': csrf },
body: JSON.stringify({ path: asset.path, action: 'quarantine' }),
});
assert.equal(accepted.status, 200);
assert.deepEqual(actions, [['quarantine', asset.path]]);
});