test: cover publishing, authentication and storage persistence
This commit is contained in:
69
tests/security.test.ts
Normal file
69
tests/security.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
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('<script>alert(1)</script>')), /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/);
|
||||
});
|
||||
Reference in New Issue
Block a user