import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, rmSync, existsSync } from 'node:fs'; import { randomBytes } from 'node:crypto'; import path from 'node:path'; import { tmpdir } from 'node:os'; import sharp from 'sharp'; import { initialize } from '../src/server/initialize'; import { provisionOwner } from '../src/server/owner'; import { getAuth } from '../src/server/auth'; import { requireOwner, boundedBody } from '../src/server/http'; import { storeUpload, deleteUpload } from '../src/server/media'; import { readContent, saveContent } from '../src/server/content'; import { backup, restore } from '../src/server/backup'; import { withContentLock } from '../src/server/write-lock'; import { openDatabase } from '../src/server/db'; test('owner authentication, media validation, backup and restore', async t => { const root = mkdtempSync(path.join(tmpdir(), 'portfolio-security-')); process.env.DATA_DIR = path.join(root, 'data'); process.env.SITE_URL = 'http://localhost:39991'; process.env.BETTER_AUTH_SECRET = randomBytes(48).toString('base64url'); const connection = initialize(); t.after(() => { connection.sqlite.close(); rmSync(root, { recursive: true, force: true }); }); const email = 'owner@example.com'; const password = randomBytes(20).toString('base64url'); await provisionOwner(connection, email, password); await assert.rejects(() => provisionOwner(connection, 'other@example.com', password), /already exists/); const auth = getAuth(); const request = (endpoint: string, body: unknown) => new Request(`${process.env.SITE_URL}/api/auth/${endpoint}`, { method: 'POST', headers: { 'content-type': 'application/json', origin: process.env.SITE_URL!, 'x-forwarded-for': '127.0.0.1' }, body: JSON.stringify(body) }); const signup = await auth.handler(request('sign-up/email', { email: 'outsider@example.com', password, name: 'Outsider' })); assert.ok(signup.status >= 400); const response = await auth.handler(request('sign-in/email', { email, password })); assert.equal(response.status, 200, await response.clone().text()); const cookie = response.headers.getSetCookie().map(value => value.split(';')[0]).join('; '); assert.ok(cookie); assert.equal((await requireOwner(new Request(`${process.env.SITE_URL}/api/admin/settings`, { headers: { cookie } }))).email, email); await assert.rejects(() => requireOwner(new Request(`${process.env.SITE_URL}/api/admin/settings`)), /Sign in/); await assert.rejects(() => requireOwner(new Request(`${process.env.SITE_URL}/api/admin/settings`, { method: 'PUT', headers: { cookie, origin: 'https://evil.example' } })), /origin/); await assert.rejects(() => boundedBody(new Request('http://localhost', { method: 'POST', body: 'oversized' }), 2), /too large/); await provisionOwner(connection, email, randomBytes(20).toString('base64url'), true); await assert.rejects(() => requireOwner(new Request(`${process.env.SITE_URL}/api/admin/settings`, { headers: { cookie } })), /Sign in/); let throttled = false; for (let attempt = 0; attempt < 7; attempt++) { const failed = await auth.handler(request('sign-in/email', { email, password: 'deliberately-wrong-password' })); if (failed.status === 429) throttled = true; } assert.ok(throttled, 'Repeated login attempts must be rate limited'); await assert.rejects(() => storeUpload(connection, 'bad.png', Buffer.from('')), /valid/); const png = await sharp({ create: { width: 16, height: 16, channels: 3, background: '#557799' } }).png().toBuffer(); const upload = await storeUpload(connection, '../../picture.png', png); assert.equal(upload.originalName, 'picture.png'); assert.equal(upload.mime, 'image/webp'); const settings = readContent(connection).settings; settings.avatar = { src: upload.url, width: upload.width!, height: upload.height! }; saveContent(connection, 'settings', settings); assert.throws(() => deleteUpload(connection, upload.key), /still used/); const destination = path.join(root, 'backup'); await backup(connection, process.env.DATA_DIR!, destination); assert.ok(existsSync(path.join(destination, 'manifest.json'))); await withContentLock(async () => { await assert.rejects(() => withContentLock(() => undefined), /in progress/); }); const restoredDir = path.join(root, 'restored'); restore(destination, restoredDir); const restored = openDatabase(restoredDir); try { assert.equal(readContent(restored).settings.avatar.src, upload.url); assert.ok(existsSync(path.join(restoredDir, 'uploads', upload.key))); } finally { restored.sqlite.close(); } assert.throws(() => restore(destination, restoredDir), /new, empty/); });