Wiki/bridge/tests/routes/views-r4-jwt.test.ts
Corentin JOGUET 445dda260a feat(bridge): add Baserow user JWT auto-login for metadata endpoints — Patch 031
Service account pattern resolves 401 PERMISSION_DENIED on Baserow metadata
endpoints (/api/database/views/table/:id/, /api/database/tables/:id/) which
reject DB tokens. A dedicated Baserow user account logs in via token-auth,
JWT cached in memory with mutex-protected refresh before expiry.

Fallback graceful: if BASEROW_USER_EMAIL/PASSWORD absent, CRUD rows still work,
metadata endpoints return 500 BASEROW_USER_AUTH_NOT_CONFIGURED.

417 tests pass (was 392, +25). 0 TS errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 14:44:55 +02:00

191 lines
7 KiB
TypeScript

/**
* Tests routes /api/v1/views/* — Patch 031 JWT manager integration.
*
* Valide :
* - GET /api/v1/views/table/:slug fonctionne quand user JWT configure (mock manager)
* - GET /api/v1/views/table/:slug renvoie 500 clair quand creds absentes
* (le repo remonte BASEROW_USER_AUTH_NOT_CONFIGURED qui devient 500 via error-handler)
* - Slug resolution fonctionne avec le JWT manager active
*/
import { afterEach, describe, expect, it } from 'vitest';
import { View } from '../../src/domain/view.js';
import type { BaserowJwtManager } from '../../src/lib/baserow-jwt-manager.js';
import { BaserowAuthError, BaserowJwtManagerDisabled } from '../../src/lib/baserow-jwt-manager.js';
import type { RepoSet } from '../../src/lib/container.js';
import { errors } from '../../src/lib/errors.js';
import type { ViewDataResult } from '../../src/repos/baserow-views-repo.js';
import {
ADMIN_TOKEN,
READ_TOKEN,
buildTestApp,
installTestContainer,
resetTestContainer,
} from '../helpers/test-app.js';
// ---------------------------------------------------------------------------
// Stub JWT managers
// ---------------------------------------------------------------------------
/** JWT manager enabled — returns a fixed fake JWT. */
class StubJwtManagerEnabled implements BaserowJwtManager {
public callCount = 0;
isEnabled(): boolean { return true; }
async getToken(): Promise<string> {
this.callCount++;
return 'fake.jwt.token';
}
}
/** JWT manager that simulates unconfigured creds (503). */
class StubJwtManagerUnconfigured implements BaserowJwtManager {
isEnabled(): boolean { return false; }
async getToken(): Promise<string> {
throw new BaserowAuthError(
'BASEROW_USER_AUTH_NOT_CONFIGURED: set BASEROW_USER_EMAIL and BASEROW_USER_PASSWORD',
503,
);
}
}
// ---------------------------------------------------------------------------
// Fake repos
// ---------------------------------------------------------------------------
class FakeTablesRepo {
async list(_databaseId: number) { return []; }
async get(_tableId: number) { throw errors.notFound('Table', _tableId); }
}
class FakeFieldsRepo {
async list(_tableId: number) { return []; }
}
class FakeRowsRepo {
async list() { return { items: [], meta: { page: 1, per_page: 50, total: 0, total_pages: 1 } }; }
async get(_tableId: number, rowId: number) { throw errors.notFound('Row', rowId); }
}
class FakeViewsRepoJwt {
public listByTableCalls: number[] = [];
public jwtManager: BaserowJwtManager | undefined;
private views: View[];
constructor(views: View[] = [], jwtManager?: BaserowJwtManager) {
this.views = views;
this.jwtManager = jwtManager;
}
async list(tableId: number): Promise<View[]> {
return this.listByTable(tableId);
}
async listByTable(tableId: number): Promise<View[]> {
this.listByTableCalls.push(tableId);
// Simulate the repo's behavior: call jwtManager.getToken() if configured
if (this.jwtManager) {
await this.jwtManager.getToken(); // throws if not configured
}
return this.views;
}
async runGrid(_viewId: number, _tableId: number) {
return { items: [], meta: { page: 1, per_page: 50, total: 0, total_pages: 1 } };
}
async getViewData(_viewId: number, _tableId: number): Promise<ViewDataResult> {
return { items: [], meta: { page: 1, per_page: 100, total: 0, total_pages: 1 }, viewType: 'grid' };
}
}
function buildFakeRepos(viewsRepo: FakeViewsRepoJwt): RepoSet {
return {
tables: new FakeTablesRepo() as unknown as RepoSet['tables'],
fields: new FakeFieldsRepo() as unknown as RepoSet['fields'],
views: viewsRepo as unknown as RepoSet['views'],
rows: new FakeRowsRepo() as unknown as RepoSet['rows'],
};
}
afterEach(resetTestContainer);
// ---------------------------------------------------------------------------
// Tests — JWT manager enabled
// ---------------------------------------------------------------------------
describe('GET /api/v1/views/table/:tableId — JWT manager enabled', () => {
it('200 liste vide quand JWT manager configure et retourne token', async () => {
const jwtMgr = new StubJwtManagerEnabled();
const viewsRepo = new FakeViewsRepoJwt([], jwtMgr);
const repos = buildFakeRepos(viewsRepo);
const container = installTestContainer({ repos, baserowJwt: jwtMgr });
const app = buildTestApp(container);
const res = await app.request('/api/v1/views/table/5', {
headers: { Authorization: `Bearer ${READ_TOKEN}` },
});
expect(res.status).toBe(200);
const body = (await res.json()) as { data: unknown[]; total: number };
expect(body.data).toHaveLength(0);
expect(body.total).toBe(0);
});
it('200 liste avec vues quand JWT configure', async () => {
const jwtMgr = new StubJwtManagerEnabled();
const views = [
new View({ id: 1, name: 'Tous', type: 'grid', tableId: 5, order: 0 }),
new View({ id: 2, name: 'Actifs', type: 'grid', tableId: 5, order: 1 }),
];
const viewsRepo = new FakeViewsRepoJwt(views, jwtMgr);
const repos = buildFakeRepos(viewsRepo);
const container = installTestContainer({ repos, baserowJwt: jwtMgr });
const app = buildTestApp(container);
const res = await app.request('/api/v1/views/table/5', {
headers: { Authorization: `Bearer ${ADMIN_TOKEN}` },
});
expect(res.status).toBe(200);
const body = (await res.json()) as { data: Array<{ id: number }>; total: number };
expect(body.total).toBe(2);
expect(body.data[0]?.id).toBe(1);
});
it('JWT manager getToken() is called when listing views', async () => {
const jwtMgr = new StubJwtManagerEnabled();
const viewsRepo = new FakeViewsRepoJwt([], jwtMgr);
const repos = buildFakeRepos(viewsRepo);
const container = installTestContainer({ repos, baserowJwt: jwtMgr });
const app = buildTestApp(container);
await app.request('/api/v1/views/table/42', {
headers: { Authorization: `Bearer ${READ_TOKEN}` },
});
expect(jwtMgr.callCount).toBeGreaterThan(0);
expect(viewsRepo.listByTableCalls).toContain(42);
});
});
// ---------------------------------------------------------------------------
// Tests — JWT manager not configured
// ---------------------------------------------------------------------------
describe('GET /api/v1/views/table/:tableId — JWT manager not configured', () => {
it('500 quand creds JWT absentes', async () => {
const jwtMgr = new StubJwtManagerUnconfigured();
const viewsRepo = new FakeViewsRepoJwt([], jwtMgr);
const repos = buildFakeRepos(viewsRepo);
const container = installTestContainer({ repos, baserowJwt: jwtMgr });
const app = buildTestApp(container);
const res = await app.request('/api/v1/views/table/5', {
headers: { Authorization: `Bearer ${READ_TOKEN}` },
});
// The repo transforms 503 BaserowAuthError into an INTERNAL error (500)
expect(res.status).toBe(500);
});
it('disabled manager getToken() rejects with BaserowAuthError 503', async () => {
const mgr = new BaserowJwtManagerDisabled();
await expect(mgr.getToken()).rejects.toMatchObject({ statusCode: 503 });
});
});