Release 0.7.0

This commit is contained in:
hnzio committed 2026-10-25 18:40:01 +01:00
1 parent d586fbd8ce
commit 6d96900d81
27 files changed
+2060 -25

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
import { execFileSync } from 'node:child_process';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import type { AuthUser } from './auth.js';
import { closeDatabase, getDatabase } from './database.js';
import {
authenticateTechnical,
createTechnicalAccess,
idempotentResponse,
revokeTechnicalAccess
} from './technical-access.js';
let directory: string;
let actor: AuthUser;
beforeEach(() => {
directory = mkdtempSync(path.join(tmpdir(), 'sendewerk-api-test-'));
process.env.SENDEWERK_DB_PATH = path.join(directory, 'test.sqlite');
process.env.SENDEWERK_UPLOAD_DIR = path.join(directory, 'uploads');
execFileSync(process.execPath, ['scripts/migrate.mjs'], { cwd: process.cwd(), env: process.env });
execFileSync(process.execPath, ['scripts/seed.mjs'], { cwd: process.cwd(), env: process.env });
const now = new Date().toISOString();
getDatabase()
.prepare(
`INSERT INTO users (id, email, email_normalized, display_name, password_hash, role,
password_changed_at_utc, created_at_utc, updated_at_utc) VALUES ('test-user', 'test@example.org',
'test@example.org', 'Test', 'not-used', 'barkeeper', ?, ?, ?)`
)
.run(now, now, now);
actor = {
id: 'test-user',
email: 'test@example.org',
displayName: 'Test',
role: 'barkeeper',
lockedAtUtc: null
};
});
afterEach(() => {
closeDatabase();
delete process.env.SENDEWERK_DB_PATH;
delete process.env.SENDEWERK_UPLOAD_DIR;
rmSync(directory, { recursive: true, force: true });
});
function request(token: string, key?: string) {
return new Request('https://sendewerk.test/api/v1/pieces', {
headers: { Authorization: `Bearer ${token}`, ...(key ? { 'Idempotency-Key': key } : {}) }
});
}
describe('technical API access', () => {
it('enforces permissions and revocation without storing the raw token', () => {
const created = createTechnicalAccess(actor, 'Leser', 'read');
expect(authenticateTechnical(request(created.token))).toMatchObject({
name: 'Leser',
permissions: 'read'
});
expect(() => authenticateTechnical(request(created.token), true)).toThrow('API_FORBIDDEN');
expect(
getDatabase()
.prepare('SELECT token_hash FROM technical_accesses WHERE id = ?')
.get(created.id)
).not.toMatchObject({ token_hash: created.token });
revokeTechnicalAccess(actor, created.id);
expect(() => authenticateTechnical(request(created.token))).toThrow('API_UNAUTHORIZED');
});
it('replays an identical idempotent write and rejects a changed payload', () => {
const created = createTechnicalAccess(actor, 'Schreiber', 'read_write');
const technical = authenticateTechnical(request(created.token), true);
let calls = 0;
const first = idempotentResponse(
technical,
request(created.token, 'command-1'),
{ title: 'A' },
() => ({ status: 201, body: { calls: ++calls } })
);
const replay = idempotentResponse(
technical,
request(created.token, 'command-1'),
{ title: 'A' },
() => ({ status: 201, body: { calls: ++calls } })
);
expect(first.duplicate).toBe(false);
expect(replay).toMatchObject({ duplicate: true, body: { calls: 1 } });
expect(calls).toBe(1);
expect(() =>
idempotentResponse(technical, request(created.token, 'command-1'), { title: 'B' }, () => ({
status: 201,
body: {}
}))
).toThrow('API_IDEMPOTENCY_CONFLICT');
});
});