tests/integration/strategies.test.ts
Rama main · réplica de lectura del repositorio original.
import type { LightMyRequestResponse } from 'fastify'
import { beforeEach, describe, expect, it } from 'vitest'
import { buildServer } from '../../src/server.js'
import { createAdminKey, createProject, createRootKey } from '../helpers/fixtures.js'
import { getTestDb, truncateAll } from '../helpers/db.js'
const describeIfDb = process.env.TEST_DATABASE_URL ? describe : describe.skip
interface StrategyResponse {
id: string
position: number
constraints: { fieldKey: string; operator: string; values: string[] }[]
}
describeIfDb('targeting 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 + admin key + a flag + two context fields (tenant string, plan enum). */
async function setup() {
const app = await buildServer({ db })
const rootKey = await createRootKey(db!)
const project = await createProject(app, rootKey, 'strat-project')
const adminKey = await createAdminKey(app, rootKey, project.id)
const auth = { authorization: adminKey }
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}/context-fields`,
headers: auth,
payload: { key: 'tenant', type: 'string' },
})
await app.inject({
method: 'POST',
url: `/api/v1/admin/projects/${project.id}/context-fields`,
headers: auth,
payload: { key: 'plan', type: 'enum', enumValues: ['free', 'pro'] },
})
const url = `/api/v1/admin/projects/${project.id}/flags/lobby/environments/production/strategies`
const put = (body: object): Promise<LightMyRequestResponse> =>
app.inject({ method: 'PUT', url, headers: auth, payload: body })
const get = (): Promise<LightMyRequestResponse> =>
app.inject({ method: 'GET', url, headers: auth })
return { app, project, auth, url, put, get }
}
it('replaces and lists strategies in order', async () => {
const { app, put, get } = await setup()
const res = await put({
strategies: [
{ constraints: [{ fieldKey: 'tenant', operator: 'in', values: ['phyg'] }] },
{ constraints: [{ fieldKey: 'plan', operator: 'equals', values: ['pro'] }] },
],
})
expect(res.statusCode).toBe(200)
const list = await get()
expect(list.statusCode).toBe(200)
const rows = list.json<StrategyResponse[]>()
expect(rows).toHaveLength(2)
expect(rows[0].position).toBe(0)
expect(rows[0].constraints[0]).toMatchObject({ fieldKey: 'tenant', operator: 'in' })
expect(rows[1].constraints[0]).toMatchObject({ fieldKey: 'plan', operator: 'equals' })
await app.close()
})
it('a second PUT replaces the previous list', async () => {
const { app, put, get } = await setup()
await put({
strategies: [{ constraints: [{ fieldKey: 'tenant', operator: 'in', values: ['a'] }] }],
})
await put({
strategies: [{ constraints: [{ fieldKey: 'plan', operator: 'equals', values: ['free'] }] }],
})
const rows = (await get()).json<StrategyResponse[]>()
expect(rows).toHaveLength(1)
expect(rows[0].constraints[0].fieldKey).toBe('plan')
await app.close()
})
it('PUT with empty list clears strategies', async () => {
const { app, put, get } = await setup()
await put({
strategies: [{ constraints: [{ fieldKey: 'tenant', operator: 'in', values: ['a'] }] }],
})
await put({ strategies: [] })
expect((await get()).json<StrategyResponse[]>()).toHaveLength(0)
await app.close()
})
it('rejects an unknown context field with 400', async () => {
const { app, put } = await setup()
const res = await put({
strategies: [{ constraints: [{ fieldKey: 'nope', operator: 'in', values: ['x'] }] }],
})
expect(res.statusCode).toBe(400)
await app.close()
})
it('rejects an operator not valid for the field type with 400', async () => {
const { app, put } = await setup()
// 'before' is a date operator, not valid for a string field
const res = await put({
strategies: [{ constraints: [{ fieldKey: 'tenant', operator: 'before', values: ['x'] }] }],
})
expect(res.statusCode).toBe(400)
await app.close()
})
it('rejects an enum value outside the allowed set with 400', async () => {
const { app, put } = await setup()
const res = await put({
strategies: [{ constraints: [{ fieldKey: 'plan', operator: 'equals', values: ['gold'] }] }],
})
expect(res.statusCode).toBe(400)
await app.close()
})
it('rejects a catastrophic regex with 400 and says why', async () => {
const { app, put } = await setup()
const res = await put({
strategies: [
{ constraints: [{ fieldKey: 'tenant', operator: 'regex', values: ['(a+)+$'] }] },
],
})
expect(res.statusCode).toBe(400)
expect(res.json()).toMatchObject({
message: expect.stringContaining('can hang flag evaluation') as string,
})
await app.close()
})
it('still accepts an ordinary regex', async () => {
const { app, put } = await setup()
const res = await put({
strategies: [
{ constraints: [{ fieldKey: 'tenant', operator: 'regex', values: ['^acme-[0-9]+$'] }] },
],
})
expect(res.statusCode).toBe(200)
await app.close()
})
it('returns 404 for an unknown flag', async () => {
const { app, project, auth } = await setup()
const res = await app.inject({
method: 'GET',
url: `/api/v1/admin/projects/${project.id}/flags/ghost/environments/production/strategies`,
headers: auth,
})
expect(res.statusCode).toBe(404)
await app.close()
})
})