import { readFileSync } from 'node:fs' import { sql } from 'drizzle-orm' 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 const fixture: unknown = JSON.parse(readFileSync('tests/fixtures/unleash-export.json', 'utf8')) interface ImportBody { report: { source: string dryRun: boolean counts: Record contextFieldsCreated: string[] unmatchedEnvironments: string[] flags: { key: string; action: string; warnings: { kind: string; detail: string }[] }[] } warnings: { kind: string; detail: string }[] } interface ExportBody { document: { flags: { key: string }[] contextFields: { key: string }[] } warnings: { detail: string }[] } interface ErrorBody { message: string } describeIfDb('unleash transfer', () => { const db = process.env.TEST_DATABASE_URL ? getTestDb() : undefined beforeEach(async () => { process.env.DATABASE_URL = process.env.TEST_DATABASE_URL await truncateAll(db!) await db!.execute(sql`TRUNCATE users, user_projects RESTART IDENTITY CASCADE`) }) async function target() { const app = await buildServer({ db }) const rootKey = await createRootKey(db!) const project = await createProject(app, rootKey, 'unleash-target') const adminKey = await createAdminKey(app, rootKey, project.id) return { app, project, rootKey, auth: { authorization: adminKey }, base: `/api/v1/admin/projects/${project.id}`, } } it('imports the fixture and reports everything it dropped', async () => { const { app, auth, base } = await target() const res = await app.inject({ method: 'POST', url: `${base}/transfer/import/unleash`, headers: auth, payload: { document: fixture }, }) expect(res.statusCode).toBe(200) const { report, warnings } = res.json() expect(report.source).toBe('unleash') expect(report.counts.flagsCreated).toBe(1) expect(report.contextFieldsCreated).toEqual( expect.arrayContaining(['plan', 'appVersion', 'userId']), ) /** staging is not an environment in a default project. */ expect(report.unmatchedEnvironments).toEqual(['staging']) const entry = report.flags.find((flag) => flag.key === 'new-checkout')! expect(entry.action).toBe('created') expect(entry.warnings.map((warning) => warning.kind)).toEqual( expect.arrayContaining(['unsupported-strategy', 'unknown-environment']), ) /** Document-level notes ride alongside the report, not inside it. */ expect(warnings.some((warning) => warning.detail.includes('legacy-banner'))).toBe(true) await app.close() }) it('the imported flag is no more on than the source said', async () => { const { app, auth, base } = await target() await app.inject({ method: 'POST', url: `${base}/transfer/import/unleash`, headers: auth, payload: { document: fixture }, }) const flag = await app.inject({ method: 'GET', url: `${base}/flags/new-checkout`, headers: auth, }) const state = flag.json<{ state: Record }>().state expect(state.development?.on).toBe(true) expect(state.production?.on).toBe(false) /** The 50% rollout must not have become an unconditional strategy. */ const dev = await app.inject({ method: 'GET', url: `${base}/flags/new-checkout/environments/development/strategies`, headers: auth, }) const strategies = dev.json<{ constraints: unknown[] }[]>() expect(strategies.filter((strategy) => strategy.constraints.length === 0)).toEqual([]) expect(strategies).toHaveLength(1) await app.close() }) it('the native import refuses an Unleash document and points at the right route', async () => { const { app, auth, base } = await target() const res = await app.inject({ method: 'POST', url: `${base}/transfer/import`, headers: auth, payload: { document: fixture }, }) expect(res.statusCode).toBe(400) expect(res.json().message).toContain('/transfer/import/unleash') await app.close() }) it('dryRun on the unleash route writes nothing', async () => { const { app, auth, base } = await target() const dry = await app.inject({ method: 'POST', url: `${base}/transfer/import/unleash`, headers: auth, payload: { document: fixture, dryRun: true }, }) expect(dry.json().report.dryRun).toBe(true) const after = await app.inject({ method: 'GET', url: `${base}/transfer/export`, headers: auth }) expect(after.json().document.flags).toEqual([]) expect(after.json().document.contextFields).toEqual([]) await app.close() }) it('environmentMap lands a staging-only strategy somewhere real', async () => { const { app, auth, base } = await target() const res = await app.inject({ method: 'POST', url: `${base}/transfer/import/unleash`, headers: auth, payload: { document: fixture, environmentMap: { staging: 'development' } }, }) expect(res.json().report.unmatchedEnvironments).toEqual([]) await app.close() }) it('rejects a document that is not an unleash export', async () => { const { app, auth, base } = await target() const res = await app.inject({ method: 'POST', url: `${base}/transfer/import/unleash`, headers: auth, payload: { document: { format: 'flagraft.export', version: 1, flags: [] } }, }) expect(res.statusCode).toBe(400) await app.close() }) it('exports in unleash shape and names what it could not express', async () => { const { app, auth, base } = await target() await app.inject({ method: 'POST', url: `${base}/context-fields`, headers: auth, payload: { key: 'tenant', type: 'string' }, }) await app.inject({ method: 'POST', url: `${base}/flags`, headers: auth, payload: { key: 'regexy', name: 'Regexy' }, }) await app.inject({ method: 'PUT', url: `${base}/flags/regexy/environments/development/strategies`, headers: auth, payload: { strategies: [ { constraints: [{ fieldKey: 'tenant', operator: 'in', values: ['acme'] }] }, { constraints: [{ fieldKey: 'tenant', operator: 'regex', values: ['^ac'] }] }, ], }, }) const res = await app.inject({ method: 'GET', url: `${base}/transfer/export/unleash`, headers: auth, }) expect(res.statusCode).toBe(200) const body = res.json<{ document: { features: { name: string }[] featureStrategies: { environment: string; constraints: { operator: string }[] }[] contextFields: { name: string }[] } warnings: { detail: string }[] }>() expect(body.document.features.map((feature) => feature.name)).toEqual(['regexy']) expect(body.document.contextFields.map((field) => field.name)).toEqual(['tenant']) /** The `in` strategy survives; the regex one does not. */ expect(body.document.featureStrategies).toHaveLength(1) expect(body.document.featureStrategies[0].constraints[0].operator).toBe('IN') expect(body.warnings).toHaveLength(1) expect(body.warnings[0].detail).toContain('regex') await app.close() }) it('counts strategies the adapter dropped, not just the ones the importer dropped', async () => { const { app, auth, base } = await target() /** Strategies with no environment: unplaceable, so dropped by the adapter. */ const res = await app.inject({ method: 'POST', url: `${base}/transfer/import/unleash`, headers: auth, payload: { document: { features: [{ name: 'odd', description: '', archived: false }], featureEnvironments: [{ featureName: 'odd', environment: 'production', enabled: true }], featureStrategies: [ { featureName: 'odd', parameters: {}, constraints: [] }, { featureName: 'odd', parameters: {}, constraints: [] }, ], contextFields: [], }, }, }) expect(res.statusCode).toBe(200) const { report } = res.json() expect(report.counts.flagsCreated).toBe(1) /** The number must match the list of warnings shown beside it. */ expect(report.counts.strategiesSkipped).toBe(2) expect(report.flags[0].warnings).toHaveLength(2) await app.close() }) it('rejects a malformed file with a readable message, not a wall of JSON', async () => { const { app, auth, base } = await target() const res = await app.inject({ method: 'POST', url: `${base}/transfer/import/unleash`, headers: auth, payload: { document: { features: [{ description: 'no name' }] } }, }) expect(res.statusCode).toBe(400) const message = res.json<{ message: string }>().message expect(message).toContain('Not an Unleash export') expect(message).not.toContain('invalid_type') expect(message).not.toContain('"code"') expect(message.length).toBeLessThan(400) await app.close() }) })