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>
191 lines
7 KiB
TypeScript
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 });
|
|
});
|
|
});
|