170 lines
6.6 KiB
TypeScript
170 lines
6.6 KiB
TypeScript
// Verification: docs/ai/context/verification-automation.md
|
|
// Source doc: apps/server/docs/ai-context/verifications/admin-flux-grants.md
|
|
//
|
|
// Covers the three user paths from the verification doc:
|
|
// Path 1: admin POST /api/admin/flux-grants → 200 + granted array, ledger row written
|
|
// Path 2: ?dryRun=true → preview returned, ledger unchanged
|
|
// Path 3: adminGuard rejects (401 no session / 403 not in allowlist / 403 unverified)
|
|
|
|
import type { Harness } from './_harness'
|
|
|
|
import { eq } from 'drizzle-orm'
|
|
import { afterAll, beforeEach, describe, expect, it } from 'vitest'
|
|
|
|
import { startVerificationContext } from './_harness'
|
|
|
|
const ADMIN_EMAIL = 'admin@example.com'
|
|
|
|
describe('verification: admin-flux-grants', () => {
|
|
let ctx: Harness
|
|
|
|
beforeEach(async () => {
|
|
ctx = await startVerificationContext({ adminEmails: ADMIN_EMAIL })
|
|
ctx.setConfig({ INITIAL_USER_FLUX: 0 })
|
|
// Admin user (caller). emailVerified is required by adminGuard.
|
|
await ctx.seedUser({ id: 'admin-1', email: ADMIN_EMAIL, balance: 0 })
|
|
})
|
|
|
|
afterAll(async () => {
|
|
// PGlite is per-context and per-test — letting it drop out of scope is enough.
|
|
})
|
|
|
|
describe('path 1: admin synchronously grants flux', () => {
|
|
it('credits 100 flux to each existing recipient and returns the per-email outcome buckets', async () => {
|
|
await ctx.seedUser({ id: 'recipient-1', email: 'rec1@example.com', balance: 0 })
|
|
await ctx.seedUser({ id: 'recipient-2', email: 'rec2@example.com', balance: 25 })
|
|
ctx.setSessionUser({ id: 'admin-1', email: ADMIN_EMAIL })
|
|
|
|
const res = await ctx.app.request('/api/admin/flux-grants', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
description: 'integration-test grant',
|
|
amount: 100,
|
|
emails: ['rec1@example.com', 'rec2@example.com'],
|
|
}),
|
|
})
|
|
|
|
expect(res.status).toBe(200)
|
|
const body = await res.json() as {
|
|
summary: { willGrant: number, totalFluxToIssue: number }
|
|
result: {
|
|
granted: { email: string, userId: string, fluxTransactionId: string, balanceAfter: number }[]
|
|
skipped: unknown[]
|
|
failed: unknown[]
|
|
}
|
|
}
|
|
|
|
expect(body.summary.willGrant).toBe(2)
|
|
expect(body.summary.totalFluxToIssue).toBe(200)
|
|
expect(body.result.failed).toEqual([])
|
|
expect(body.result.skipped).toEqual([])
|
|
expect(body.result.granted).toHaveLength(2)
|
|
|
|
const granted1 = body.result.granted.find(g => g.email === 'rec1@example.com')
|
|
const granted2 = body.result.granted.find(g => g.email === 'rec2@example.com')
|
|
expect(granted1?.balanceAfter).toBe(100)
|
|
expect(granted2?.balanceAfter).toBe(125)
|
|
expect(granted1?.fluxTransactionId).toBeTruthy()
|
|
|
|
// Ledger writes per recipient with type='promo' and the operator id in metadata.
|
|
const rec1Ledger = await ctx.db.query.fluxTransaction.findMany({
|
|
where: eq(ctx.schema.fluxTransaction.userId, 'recipient-1'),
|
|
})
|
|
expect(rec1Ledger).toHaveLength(1)
|
|
expect(rec1Ledger[0].type).toBe('promo')
|
|
expect(rec1Ledger[0].amount).toBe(100)
|
|
expect(rec1Ledger[0].balanceBefore).toBe(0)
|
|
expect(rec1Ledger[0].balanceAfter).toBe(100)
|
|
expect(rec1Ledger[0].description).toBe('integration-test grant')
|
|
const meta = rec1Ledger[0].metadata as { issuedByUserId?: string, description?: string }
|
|
expect(meta?.issuedByUserId).toBe('admin-1')
|
|
})
|
|
})
|
|
|
|
describe('path 2: dry-run preview', () => {
|
|
it('reports willGrant / notFound / duplicateInInput without writing a ledger row', async () => {
|
|
await ctx.seedUser({ id: 'recipient-1', email: 'rec1@example.com', balance: 0 })
|
|
ctx.setSessionUser({ id: 'admin-1', email: ADMIN_EMAIL })
|
|
|
|
const res = await ctx.app.request('/api/admin/flux-grants?dryRun=true', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
description: 'smoke',
|
|
amount: 100,
|
|
emails: [
|
|
'rec1@example.com',
|
|
'REC1@example.com', // case-variant duplicate
|
|
'ghost@nope.example', // not_found
|
|
'rec1@example.com', // exact duplicate
|
|
],
|
|
}),
|
|
})
|
|
|
|
expect(res.status).toBe(200)
|
|
const body = await res.json() as {
|
|
preview: {
|
|
totalEmails: number
|
|
willGrant: number
|
|
willSkip: { notFound: number, userDeleted: number, duplicateInInput: number }
|
|
totalFluxToIssue: number
|
|
}
|
|
}
|
|
expect(body.preview.totalEmails).toBe(4)
|
|
expect(body.preview.willGrant).toBe(1)
|
|
expect(body.preview.willSkip.notFound).toBe(1)
|
|
expect(body.preview.willSkip.duplicateInInput).toBe(2)
|
|
expect(body.preview.totalFluxToIssue).toBe(100)
|
|
|
|
// Ledger must be untouched.
|
|
const ledger = await ctx.db.query.fluxTransaction.findMany({
|
|
where: eq(ctx.schema.fluxTransaction.userId, 'recipient-1'),
|
|
})
|
|
expect(ledger).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('path 3: adminGuard rejects unauthorized callers', () => {
|
|
it('returns 401 when no session is attached', async () => {
|
|
ctx.setSessionUser(null)
|
|
|
|
const res = await ctx.app.request('/api/admin/flux-grants', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ description: 'x', amount: 1, emails: ['a@b.com'] }),
|
|
})
|
|
|
|
expect(res.status).toBe(401)
|
|
})
|
|
|
|
it('returns 403 when the session user is not in ADMIN_EMAILS', async () => {
|
|
await ctx.seedUser({ id: 'normie', email: 'normie@example.com', balance: 0 })
|
|
ctx.setSessionUser({ id: 'normie', email: 'normie@example.com', emailVerified: true })
|
|
|
|
const res = await ctx.app.request('/api/admin/flux-grants', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ description: 'x', amount: 1, emails: ['a@b.com'] }),
|
|
})
|
|
|
|
expect(res.status).toBe(403)
|
|
})
|
|
|
|
it('returns 403 when the admin email is in the allowlist but not yet verified', async () => {
|
|
// Same email as ADMIN_EMAIL but emailVerified=false. Guards against a
|
|
// fresh signup with an admin's address slipping past before they prove
|
|
// ownership.
|
|
ctx.setSessionUser({ id: 'admin-1', email: ADMIN_EMAIL, emailVerified: false })
|
|
|
|
const res = await ctx.app.request('/api/admin/flux-grants', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ description: 'x', amount: 1, emails: ['a@b.com'] }),
|
|
})
|
|
|
|
expect(res.status).toBe(403)
|
|
})
|
|
})
|
|
})
|