tests/integration/client-strategies.test.ts
Rama main · réplica de lectura del repositorio original.
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()
})
})