visual-journal / src /app /api /shares /route.test.ts
Visual Journal deploy
Deploy f77fc10 to Docker Space
805101e
Raw
History Blame Contribute Delete
19.9 kB
import { POST as getShareContent } from './[token]/content/route';
import { GET as getShare } from './[token]/route';
import { POST } from './route';
import { resetAgentStateStoreForTests } from '@/lib/agent-state-runtime';
import { PAGE_PASSWORD_AUTH_ERROR_CODES } from '@/lib/page-password-auth';
import { createAccessToken } from '@/lib/server-runtime';
import { createImageShare } from '@/lib/share-store';
import { NextRequest } from 'next/server';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { afterEach, describe, it } from 'node:test';
const originalAppPassword = process.env.APP_PASSWORD;
const PAGE_PASSWORD_FIXTURE = ['customer', 'access', 'code'].join('-');
const VALID_PNG_BYTES = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR42mP8z8AABQMBgADeT7UAAAAASUVORK5CYII=',
'base64'
);
let previousCwd = '';
let tempDir = '';
async function withTempCwd() {
previousCwd = process.cwd();
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'share-route-'));
process.chdir(tempDir);
}
afterEach(async () => {
await resetAgentStateStoreForTests();
if (previousCwd) process.chdir(previousCwd);
if (tempDir) await fs.rm(tempDir, { recursive: true, force: true });
previousCwd = '';
tempDir = '';
if (originalAppPassword === undefined) {
delete process.env.APP_PASSWORD;
} else {
process.env.APP_PASSWORD = originalAppPassword;
}
});
function createShareRequest(form: FormData, options: { accessToken?: string | null; url?: string } = {}) {
const headers = new Headers();
const accessToken =
options.accessToken === undefined ? createAccessToken(PAGE_PASSWORD_FIXTURE) : options.accessToken;
if (accessToken) headers.set('Cookie', `gptImageAccess=${accessToken}`);
return new NextRequest(options.url ?? 'http://localhost/api/shares', { method: 'POST', headers, body: form });
}
function params(token: string) {
return { params: Promise.resolve({ token }) };
}
describe('POST /api/shares', { concurrency: false }, () => {
it('creates a share from an uploaded image without returning secrets', async () => {
await withTempCwd();
process.env.APP_PASSWORD = PAGE_PASSWORD_FIXTURE;
const form = new FormData();
form.set('sourceFilename', 'result.png');
form.set('accessCode', '12345678');
form.set('expiresInMinutes', '60');
form.set('image', new File([VALID_PNG_BYTES], 'result.png', { type: 'image/png' }));
const response = await POST(createShareRequest(form));
assert.equal(response.status, 201);
const body = await response.json();
assert.match(body.token, /^[a-f0-9]{24}$/);
assert.equal(body.accessCodeRequired, true);
assert.equal(body.url, `/share/${body.token}`);
assert.equal('accessCodeHash' in body, false);
assert.equal('accessCodeSalt' in body, false);
});
it('returns a same-deployment share path instead of an internal Docker listener origin', async () => {
await withTempCwd();
process.env.APP_PASSWORD = PAGE_PASSWORD_FIXTURE;
const form = new FormData();
form.set('sourceFilename', 'result.png');
form.set('image', new File([VALID_PNG_BYTES], 'result.png', { type: 'image/png' }));
const response = await POST(createShareRequest(form, { url: 'http://0.0.0.0:4783/api/shares' }));
assert.equal(response.status, 201);
const body = await response.json();
assert.equal(body.url, `/share/${body.token}`);
});
it('keeps a deployment path prefix in the returned share path', async () => {
await withTempCwd();
process.env.APP_PASSWORD = PAGE_PASSWORD_FIXTURE;
const form = new FormData();
form.set('sourceFilename', 'result.png');
form.set('image', new File([VALID_PNG_BYTES], 'result.png', { type: 'image/png' }));
const response = await POST(
createShareRequest(form, { url: 'https://internal.example.test/playground/api/shares' })
);
assert.equal(response.status, 201);
const body = await response.json();
assert.equal(body.url, `/playground/share/${body.token}`);
});
it('rejects unauthenticated share creation when a page access code is configured', async () => {
await withTempCwd();
process.env.APP_PASSWORD = PAGE_PASSWORD_FIXTURE;
const form = new FormData();
form.set('sourceFilename', 'result.png');
form.set('image', new File([VALID_PNG_BYTES], 'result.png', { type: 'image/png' }));
const response = await POST(createShareRequest(form, { accessToken: null }));
assert.equal(response.status, 401);
const body = await response.json();
assert.equal(body.code, PAGE_PASSWORD_AUTH_ERROR_CODES.missing);
});
it('rejects share creation with an invalid page access token', async () => {
await withTempCwd();
process.env.APP_PASSWORD = PAGE_PASSWORD_FIXTURE;
const form = new FormData();
form.set('sourceFilename', 'result.png');
form.set('image', new File([VALID_PNG_BYTES], 'result.png', { type: 'image/png' }));
const response = await POST(createShareRequest(form, { accessToken: 'invalid-access-token' }));
assert.equal(response.status, 401);
const body = await response.json();
assert.equal(body.code, PAGE_PASSWORD_AUTH_ERROR_CODES.invalid);
});
it('allows share creation when no page access code is configured', async () => {
await withTempCwd();
delete process.env.APP_PASSWORD;
const form = new FormData();
form.set('sourceFilename', 'result.png');
form.set('image', new File([VALID_PNG_BYTES], 'result.png', { type: 'image/png' }));
const response = await POST(createShareRequest(form, { accessToken: null }));
assert.equal(response.status, 201);
});
it('allows share creation when APP_PASSWORD is blank', async () => {
await withTempCwd();
process.env.APP_PASSWORD = ' ';
const form = new FormData();
form.set('sourceFilename', 'result.png');
form.set('image', new File([VALID_PNG_BYTES], 'result.png', { type: 'image/png' }));
const response = await POST(createShareRequest(form, { accessToken: null }));
assert.equal(response.status, 201);
});
it('treats blank access codes as public shares', async () => {
await withTempCwd();
process.env.APP_PASSWORD = PAGE_PASSWORD_FIXTURE;
const form = new FormData();
form.set('sourceFilename', 'result.png');
form.set('accessCode', ' ');
form.set('image', new File([VALID_PNG_BYTES], 'result.png', { type: 'image/png' }));
const response = await POST(createShareRequest(form));
assert.equal(response.status, 201);
const body = await response.json();
assert.equal(body.accessCodeRequired, false);
});
it('creates a public share with an expiry', async () => {
await withTempCwd();
process.env.APP_PASSWORD = PAGE_PASSWORD_FIXTURE;
const form = new FormData();
form.set('sourceFilename', 'result.png');
form.set('expiresInMinutes', '60');
form.set('image', new File([VALID_PNG_BYTES], 'result.png', { type: 'image/png' }));
const response = await POST(createShareRequest(form));
assert.equal(response.status, 201);
const body = await response.json();
assert.equal(body.accessCodeRequired, false);
assert.equal(typeof body.expiresAt, 'string');
assert.ok(Number.isFinite(new Date(body.expiresAt).getTime()));
});
it('rejects short access codes', async () => {
await withTempCwd();
process.env.APP_PASSWORD = PAGE_PASSWORD_FIXTURE;
const form = new FormData();
form.set('sourceFilename', 'result.png');
form.set('accessCode', '1234567');
form.set('image', new File([VALID_PNG_BYTES], 'result.png', { type: 'image/png' }));
const response = await POST(createShareRequest(form));
assert.equal(response.status, 400);
const body = await response.json();
assert.equal(body.code, 'invalid_access_code');
});
it('rejects missing image uploads', async () => {
await withTempCwd();
process.env.APP_PASSWORD = PAGE_PASSWORD_FIXTURE;
const form = new FormData();
form.set('sourceFilename', 'result.png');
const response = await POST(createShareRequest(form));
assert.equal(response.status, 400);
const body = await response.json();
assert.equal(body.code, 'image_required');
});
it('rejects bytes that are not a real image even when the upload declares image/png', async () => {
await withTempCwd();
process.env.APP_PASSWORD = PAGE_PASSWORD_FIXTURE;
const form = new FormData();
form.set('sourceFilename', 'result.png');
form.set('image', new File([Buffer.from('not-a-real-png')], 'result.png', { type: 'image/png' }));
const response = await POST(createShareRequest(form));
assert.equal(response.status, 400);
const body = await response.json();
assert.equal(body.code, 'invalid_image_content');
});
it('rejects invalid expiry values', async () => {
await withTempCwd();
process.env.APP_PASSWORD = PAGE_PASSWORD_FIXTURE;
const form = new FormData();
form.set('sourceFilename', 'result.png');
form.set('expiresInMinutes', '-1');
form.set('image', new File([VALID_PNG_BYTES], 'result.png', { type: 'image/png' }));
const response = await POST(createShareRequest(form));
assert.equal(response.status, 400);
const body = await response.json();
assert.equal(body.code, 'invalid_expiry');
});
it('rejects unsafe source filenames before storing share metadata', async () => {
await withTempCwd();
process.env.APP_PASSWORD = PAGE_PASSWORD_FIXTURE;
const form = new FormData();
form.set('sourceFilename', '../secret.png');
form.set('image', new File([VALID_PNG_BYTES], 'result.png', { type: 'image/png' }));
const response = await POST(createShareRequest(form));
assert.equal(response.status, 400);
const body = await response.json();
assert.equal(body.code, 'invalid_source_filename');
});
it('rejects non-string source filenames before storing share metadata', async () => {
await withTempCwd();
process.env.APP_PASSWORD = PAGE_PASSWORD_FIXTURE;
const form = new FormData();
form.set('sourceFilename', new File([Buffer.from('not-a-name')], 'name.txt', { type: 'text/plain' }));
form.set('image', new File([VALID_PNG_BYTES], 'result.png', { type: 'image/png' }));
const response = await POST(createShareRequest(form));
assert.equal(response.status, 400);
const body = await response.json();
assert.equal(body.code, 'invalid_source_filename');
});
it('rejects overlong source filenames before storing share metadata', async () => {
await withTempCwd();
process.env.APP_PASSWORD = PAGE_PASSWORD_FIXTURE;
const form = new FormData();
form.set('sourceFilename', `${'a'.repeat(201)}.png`);
form.set('image', new File([VALID_PNG_BYTES], 'result.png', { type: 'image/png' }));
const response = await POST(createShareRequest(form));
assert.equal(response.status, 400);
const body = await response.json();
assert.equal(body.code, 'invalid_source_filename');
});
});
describe('share metadata and content routes', { concurrency: false }, () => {
it('returns public metadata without hashes', async () => {
await withTempCwd();
const record = await createImageShare({
imageBuffer: Buffer.from('image-bytes'),
sourceFilename: 'image.png',
mimeType: 'image/png',
accessCode: '12345678',
expiresInMinutes: 60
});
const response = await getShare(
new Request(`http://localhost/api/shares/${record.token}`),
params(record.token)
);
assert.equal(response.status, 200);
const body = await response.json();
assert.equal(body.token, record.token);
assert.equal(body.accessCodeRequired, true);
assert.equal(body.expired, false);
assert.equal(body.sourceFilename, 'shared-image');
assert.equal('accessCodeHash' in body, false);
assert.equal('accessCodeSalt' in body, false);
});
it('marks expired metadata without serving content', async () => {
await withTempCwd();
const record = await createImageShare({
imageBuffer: Buffer.from('expired-image'),
sourceFilename: 'image.png',
mimeType: 'image/png',
accessCode: undefined,
expiresInMinutes: 1,
now: new Date(Date.now() - 120_000)
});
const response = await getShare(
new Request(`http://localhost/api/shares/${record.token}`),
params(record.token)
);
assert.equal(response.status, 200);
const body = await response.json();
assert.equal(body.expired, true);
});
it('serves protected content only with the correct access code', async () => {
await withTempCwd();
const record = await createImageShare({
imageBuffer: Buffer.from('protected-image'),
sourceFilename: 'image.png',
mimeType: 'image/png',
accessCode: '12345678',
expiresInMinutes: null
});
const missing = await getShareContent(
new Request(`http://localhost/api/shares/${record.token}/content`, {
method: 'POST',
body: JSON.stringify({})
}),
params(record.token)
);
assert.equal(missing.status, 401);
const wrong = await getShareContent(
new Request(`http://localhost/api/shares/${record.token}/content`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ accessCode: 'bad' })
}),
params(record.token)
);
assert.equal(wrong.status, 401);
const ok = await getShareContent(
new Request(`http://localhost/api/shares/${record.token}/content`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ accessCode: '12345678' })
}),
params(record.token)
);
assert.equal(ok.status, 200);
assert.equal(ok.headers.get('content-type'), 'image/png');
assert.match(ok.headers.get('cache-control') || '', /no-store/);
assert.equal(ok.headers.get('surrogate-control'), 'no-store');
assert.equal(ok.headers.get('x-content-type-options'), 'nosniff');
assert.equal(ok.headers.get('content-disposition'), 'inline; filename="shared-image.png"');
assert.equal(await ok.text(), 'protected-image');
});
it('uses the detected image MIME type for public content filenames', async () => {
await withTempCwd();
const record = await createImageShare({
imageBuffer: Buffer.from('public-image'),
sourceFilename: 'invoice.html',
mimeType: 'image/png',
accessCode: undefined,
expiresInMinutes: null
});
const response = await getShareContent(
new Request(`http://localhost/api/shares/${record.token}/content`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({})
}),
params(record.token)
);
assert.equal(response.status, 200);
assert.equal(response.headers.get('content-type'), 'image/png');
assert.equal(response.headers.get('x-content-type-options'), 'nosniff');
assert.equal(response.headers.get('content-disposition'), 'inline; filename="invoice.png"');
});
it('rate limits repeated wrong access codes', async () => {
await withTempCwd();
const record = await createImageShare({
imageBuffer: Buffer.from('protected-image'),
sourceFilename: 'image.png',
mimeType: 'image/png',
accessCode: '12345678',
expiresInMinutes: null
});
for (let attempt = 0; attempt < 10; attempt += 1) {
await getShareContent(
new Request(`http://localhost/api/shares/${record.token}/content`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ accessCode: 'bad-code' })
}),
params(record.token)
);
}
const response = await getShareContent(
new Request(`http://localhost/api/shares/${record.token}/content`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ accessCode: 'bad-code' })
}),
params(record.token)
);
assert.equal(response.status, 429);
});
it('clears stale access failures when a token is retried after the window', async () => {
await withTempCwd();
const record = await createImageShare({
imageBuffer: Buffer.from('protected-image'),
sourceFilename: 'image.png',
mimeType: 'image/png',
accessCode: '12345678',
expiresInMinutes: null
});
const originalNow = Date.now;
let now = 1_000_000;
Date.now = () => now;
try {
for (let attempt = 0; attempt < 10; attempt += 1) {
await getShareContent(
new Request(`http://localhost/api/shares/${record.token}/content`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ accessCode: 'bad-code' })
}),
params(record.token)
);
}
now += 15 * 60 * 1000 + 1;
const response = await getShareContent(
new Request(`http://localhost/api/shares/${record.token}/content`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ accessCode: '12345678' })
}),
params(record.token)
);
assert.equal(response.status, 200);
} finally {
Date.now = originalNow;
}
});
it('does not serve expired content', async () => {
await withTempCwd();
const record = await createImageShare({
imageBuffer: Buffer.from('expired-image'),
sourceFilename: 'image.png',
mimeType: 'image/png',
accessCode: undefined,
expiresInMinutes: 1,
now: new Date(Date.now() - 120_000)
});
const response = await getShareContent(
new Request(`http://localhost/api/shares/${record.token}/content`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({})
}),
params(record.token)
);
assert.equal(response.status, 410);
});
});