mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
107 lines
4.0 KiB
TypeScript
107 lines
4.0 KiB
TypeScript
/**
|
|
* @fileoverview Owner-scoped tab-layout HTTP concurrency and validation contract.
|
|
*/
|
|
import Fastify from 'fastify';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { registerTabLayoutRoutes } from '../../src/web/routes/tab-layout-routes.js';
|
|
import { installRouteErrorHandler } from '../../src/web/route-error-handler.js';
|
|
import type { TabLayout } from '../../src/tab-layout.js';
|
|
|
|
const layout = (version = 3): TabLayout => ({
|
|
version,
|
|
groups: [],
|
|
ungrouped: [{ kind: 'session', id: 'mine' }],
|
|
updatedAt: '2026-08-16T00:00:00.000Z',
|
|
});
|
|
|
|
async function harness(username?: string, role: 'admin' | 'user' = 'user') {
|
|
const app = Fastify({ logger: false });
|
|
if (username) {
|
|
app.addHook('onRequest', async (req) => {
|
|
(req as unknown as { authUser: { username: string; role: 'admin' | 'user' } }).authUser = { username, role };
|
|
});
|
|
}
|
|
const service = {
|
|
get: vi.fn(async () => layout()),
|
|
put: vi.fn(async (_owner: string, desired: unknown, baseVersion: number) => ({
|
|
status: 'updated' as const,
|
|
layout: { ...(desired as TabLayout), version: baseVersion + 1 },
|
|
})),
|
|
};
|
|
registerTabLayoutRoutes(app, { tabLayouts: service } as never);
|
|
installRouteErrorHandler(app);
|
|
await app.ready();
|
|
return { app, service };
|
|
}
|
|
|
|
afterEach(() => vi.unstubAllEnvs());
|
|
|
|
describe('tab layout routes', () => {
|
|
it('maps single-user requests to @single and never accepts an owner override', async () => {
|
|
vi.stubEnv('CODEMAN_MULTIUSER', '0');
|
|
const { app, service } = await harness();
|
|
const response = await app.inject({ method: 'GET', url: '/api/tab-layout?owner=foreign' });
|
|
expect(response.statusCode).toBe(200);
|
|
expect(service.get).toHaveBeenCalledWith('@single');
|
|
await app.close();
|
|
});
|
|
|
|
it('uses the authenticated username in multi-user mode, including for admins', async () => {
|
|
vi.stubEnv('CODEMAN_MULTIUSER', '1');
|
|
const { app, service } = await harness('admin-a', 'admin');
|
|
await app.inject({ method: 'GET', url: '/api/tab-layout?owner=someone-else' });
|
|
expect(service.get).toHaveBeenCalledWith('admin-a');
|
|
await app.close();
|
|
});
|
|
|
|
it.each([2, 4])('returns 409 with the authoritative prepared layout when baseVersion=%s', async (baseVersion) => {
|
|
vi.stubEnv('CODEMAN_MULTIUSER', '1');
|
|
const { app, service } = await harness('alice');
|
|
service.put.mockResolvedValueOnce({ status: 'conflict', layout: layout(3) });
|
|
const response = await app.inject({
|
|
method: 'PUT',
|
|
url: '/api/tab-layout',
|
|
payload: { baseVersion, layout: layout(baseVersion) },
|
|
});
|
|
expect(response.statusCode).toBe(409);
|
|
expect(response.json().success).toBe(false);
|
|
expect(response.json().errorCode).toBe('CONFLICT');
|
|
expect(response.json().data.layout).toEqual(layout(3));
|
|
expect(service.put).toHaveBeenCalledWith('alice', layout(baseVersion), baseVersion);
|
|
await app.close();
|
|
});
|
|
|
|
it('rejects malformed writes before invoking the service', async () => {
|
|
const { app, service } = await harness();
|
|
const response = await app.inject({ method: 'PUT', url: '/api/tab-layout', payload: { baseVersion: -1 } });
|
|
expect(response.statusCode).toBe(400);
|
|
expect(service.put).not.toHaveBeenCalled();
|
|
await app.close();
|
|
});
|
|
|
|
it('rejects extra write keys before invoking the service', async () => {
|
|
const { app, service } = await harness();
|
|
const response = await app.inject({
|
|
method: 'PUT',
|
|
url: '/api/tab-layout',
|
|
payload: { baseVersion: 3, layout: layout(), owner: 'foreign' },
|
|
});
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(response.json().errorCode).toBe('INVALID_INPUT');
|
|
expect(service.put).not.toHaveBeenCalled();
|
|
await app.close();
|
|
});
|
|
|
|
it('has an explicit conservative body limit', async () => {
|
|
const { app } = await harness();
|
|
const response = await app.inject({
|
|
method: 'PUT',
|
|
url: '/api/tab-layout',
|
|
payload: { baseVersion: 3, layout: layout(), padding: 'x'.repeat(140 * 1024) },
|
|
});
|
|
expect(response.statusCode).toBe(413);
|
|
await app.close();
|
|
});
|
|
});
|