import { beforeEach, describe, expect, it } from 'vitest' import { buildServer } from '../../src/server.js' import { createAdminKey, createClientKey, createProject, createRootKey, } from '../helpers/fixtures.js' import { getTestDb, truncateAll } from '../helpers/db.js' const describeIfDb = process.env.TEST_DATABASE_URL ? describe : describe.skip describeIfDb('client evaluation with strategies', () => { const db = process.env.TEST_DATABASE_URL ? getTestDb() : undefined beforeEach(async () => { process.env.DATABASE_URL = process.env.TEST_DATABASE_URL await truncateAll(db!) }) /** Project + a lobby flag enabled in production + a `tenant` field + a prod client key. */ async function setup() { const app = await buildServer({ db }) const rootKey = await createRootKey(db!) const project = await createProject(app, rootKey, 'eval-project') const adminKey = await createAdminKey(app, rootKey, project.id) const auth = { authorization: adminKey } const envRes = await app.inject({ method: 'GET', url: `/api/v1/admin/projects/${project.id}/environments`, headers: auth, }) const prod = envRes.json<{ id: string; slug: string }[]>().find((e) => e.slug === 'production')! await app.inject({ method: 'POST', url: `/api/v1/admin/projects/${project.id}/flags`, headers: auth, payload: { key: 'lobby', name: 'Lobby' }, }) await app.inject({ method: 'POST', url: `/api/v1/admin/projects/${project.id}/flags/lobby/environments/production/enable`, headers: auth, }) await app.inject({ method: 'POST', url: `/api/v1/admin/projects/${project.id}/context-fields`, headers: auth, payload: { key: 'tenant', type: 'string' }, }) const clientKey = await createClientKey(app, adminKey, project.id, prod.id) async function putStrategies(body: object) { return app.inject({ method: 'PUT', url: `/api/v1/admin/projects/${project.id}/flags/lobby/environments/production/strategies`, headers: auth, payload: body, }) } function evalLobby(query = '') { return app.inject({ method: 'GET', url: `/api/v1/client/features/lobby${query}`, headers: { authorization: clientKey }, }) } async function disableLobby() { return app.inject({ method: 'POST', url: `/api/v1/admin/projects/${project.id}/flags/lobby/environments/production/disable`, headers: auth, }) } async function deleteTenantField() { const list = await app.inject({ method: 'GET', url: `/api/v1/admin/projects/${project.id}/context-fields`, headers: auth, }) const tenant = list.json<{ id: string; key: string }[]>().find((f) => f.key === 'tenant')! return app.inject({ method: 'DELETE', url: `/api/v1/admin/projects/${project.id}/context-fields/${tenant.id}`, headers: auth, }) } return { app, putStrategies, evalLobby, disableLobby, deleteTenantField, clientKey } } it('enabled with no strategies is on for everyone (reason default)', async () => { const { app, evalLobby } = await setup() expect((await evalLobby()).json()).toMatchObject({ enabled: true, reason: 'default' }) await app.close() }) it('strategy tenant IN [phyg]: only phyg is on', async () => { const { app, putStrategies, evalLobby } = await setup() await putStrategies({ strategies: [{ constraints: [{ fieldKey: 'tenant', operator: 'in', values: ['phyg'] }] }], }) expect((await evalLobby('?tenant=phyg')).json()).toMatchObject({ enabled: true, reason: 'strategy-match', }) expect((await evalLobby('?tenant=acme')).json()).toMatchObject({ enabled: false, reason: 'default', }) expect((await evalLobby()).json()).toMatchObject({ enabled: false, reason: 'default' }) await app.close() }) it('disabling the flag overrides strategies (reason disabled)', async () => { const { app, putStrategies, evalLobby, disableLobby } = await setup() await putStrategies({ strategies: [{ constraints: [{ fieldKey: 'tenant', operator: 'in', values: ['phyg'] }] }], }) // matches while enabled expect((await evalLobby('?tenant=phyg')).json()).toMatchObject({ enabled: true }) // disabling the environment forces off regardless of strategies await disableLobby() expect((await evalLobby('?tenant=phyg')).json()).toMatchObject({ enabled: false, reason: 'disabled', }) await app.close() }) it('deleting a targeted field fails closed on the next evaluation (cache invalidated)', async () => { const { app, putStrategies, evalLobby, deleteTenantField } = await setup() await putStrategies({ strategies: [{ constraints: [{ fieldKey: 'tenant', operator: 'in', values: ['phyg'] }] }], }) // caches env state (including the tenant field type) via this read expect((await evalLobby('?tenant=phyg')).json()).toMatchObject({ enabled: true, reason: 'strategy-match', }) // deleting the field must invalidate the cache and the orphaned constraint must fail closed expect((await deleteTenantField()).statusCode).toBe(204) expect((await evalLobby('?tenant=phyg')).json()).toMatchObject({ enabled: false, reason: 'default', }) await app.close() }) it('bulk /client/features reflects strategy matches', async () => { const { app, putStrategies, clientKey } = await setup() await putStrategies({ strategies: [{ constraints: [{ fieldKey: 'tenant', operator: 'in', values: ['phyg'] }] }], }) const res = await app.inject({ method: 'GET', url: '/api/v1/client/features?tenant=phyg', headers: { authorization: clientKey }, }) const features = res.json<{ features: { name: string; enabled: boolean }[] }>().features expect(features.find((f) => f.name === 'lobby')?.enabled).toBe(true) await app.close() }) })