Release 0.7.0
This commit is contained in:
1 parent
d586fbd8ce
commit
6d96900d81
27 files changed
+2060
-25
No files matched your search
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user