fix(audit): nettoyage accès, garde-fous build, unification quotas
Semaine 1 — fuites et accès : - /archive rendu accessible via sidebar (était invisible) - Suppression pages orphelines : /chat, /graph, /support, test-title-suggestions - Fixes i18n : search-modal (clé non interpolée), sidebar tooltips, export PDF - 15 locales mises à jour Semaine 2 — garde-fous : - 8 erreurs TS fixees → ignoreBuildErrors: false (build type-safe) - reactStrictMode: true (dev-only, zero impact prod) - reserveUsageOrThrow → wrapper deprecie vers reserveAiUsageOrThrow - auth-guard.ts : requireAuthOrResponse() + requireAdminOrResponse() Tests 212/212 | Lint 0 erreur | Build OK | tsc 0 erreur
This commit is contained in:
@@ -33,8 +33,28 @@ vi.mock('@/lib/byok', () => ({
|
||||
hasAnyActiveByok: vi.fn().mockResolvedValue(false),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/config', () => ({
|
||||
getSystemConfig: vi.fn().mockResolvedValue({
|
||||
AI_PROVIDER_CHAT: 'openai',
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/ai/provider-for-user', () => ({
|
||||
willUseByokForLane: vi.fn().mockResolvedValue({
|
||||
providerType: 'openai',
|
||||
usedByok: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/credits', () => ({
|
||||
reserveCreditsOrThrow: vi.fn(),
|
||||
resolveCreditCost: vi.fn().mockReturnValue(1),
|
||||
releaseCredits: vi.fn(),
|
||||
}))
|
||||
|
||||
import { redis } from '@/lib/redis'
|
||||
import { hasAnyActiveByok } from '@/lib/byok'
|
||||
import { willUseByokForLane } from '@/lib/ai/provider-for-user'
|
||||
import { reserveCreditsOrThrow } from '@/lib/credits'
|
||||
|
||||
function mockActiveSubscription(tier: string) {
|
||||
prismaMock.subscription.findUnique.mockResolvedValue({
|
||||
@@ -49,6 +69,14 @@ function mockActiveSubscription(tier: string) {
|
||||
describe('brainstorm host-pays billing (Story 3.4)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(willUseByokForLane).mockResolvedValue({
|
||||
providerType: 'openai',
|
||||
usedByok: false,
|
||||
})
|
||||
vi.mocked(reserveCreditsOrThrow).mockResolvedValue({
|
||||
cost: 1,
|
||||
unlimited: false,
|
||||
})
|
||||
})
|
||||
|
||||
describe('billingOwnerFromSession', () => {
|
||||
@@ -79,7 +107,11 @@ describe('brainstorm host-pays billing (Story 3.4)', () => {
|
||||
describe('checkSessionEntitlementOrThrow', () => {
|
||||
it('throws QuotaExceededError with guest metadata when host quota exhausted', async () => {
|
||||
mockActiveSubscription('BASIC')
|
||||
vi.mocked(redis.eval).mockResolvedValue(-1)
|
||||
vi.mocked(reserveCreditsOrThrow).mockRejectedValue(
|
||||
new QuotaExceededError('PRO', 'brainstorm_expand', 10, 10, false, {
|
||||
currentTier: 'BASIC',
|
||||
}),
|
||||
)
|
||||
|
||||
await expect(
|
||||
checkSessionEntitlementOrThrow('host-1', 'guest-9', true, 'brainstorm_expand'),
|
||||
@@ -90,49 +122,43 @@ describe('brainstorm host-pays billing (Story 3.4)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('increments host redis key when guest action bills host', async () => {
|
||||
it('increments host credits when guest action bills host', async () => {
|
||||
mockActiveSubscription('PRO')
|
||||
vi.mocked(redis.eval).mockResolvedValue(1)
|
||||
|
||||
await checkSessionEntitlementOrThrow('host-1', 'guest-9', true, 'brainstorm_expand')
|
||||
|
||||
expect(redis.eval).toHaveBeenCalled()
|
||||
const keyArg = String(vi.mocked(redis.eval).mock.calls[0]?.[2])
|
||||
expect(keyArg).toContain('host-1')
|
||||
expect(keyArg).not.toContain('guest-9')
|
||||
|
||||
const allKeyArgs = vi.mocked(redis.eval).mock.calls.map(call => String(call[2]))
|
||||
const guestKeyUsed = allKeyArgs.some(k => k.includes('guest-9'))
|
||||
expect(guestKeyUsed).toBe(false)
|
||||
expect(reserveCreditsOrThrow).toHaveBeenCalledTimes(1)
|
||||
const [billedUserId, , options] = vi.mocked(reserveCreditsOrThrow).mock.calls[0]
|
||||
expect(billedUserId).toBe('host-1')
|
||||
expect(options).toMatchObject({ feature: 'brainstorm_expand' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('host BYOK bypass (Story 3.5)', () => {
|
||||
it('reserveUsage still runs when host has BYOK (bypass is per-provider in route layer)', async () => {
|
||||
vi.mocked(hasAnyActiveByok).mockResolvedValue(true)
|
||||
it('does not debit credits when host has active BYOK', async () => {
|
||||
vi.mocked(willUseByokForLane).mockResolvedValue({
|
||||
providerType: 'openai',
|
||||
usedByok: true,
|
||||
})
|
||||
mockActiveSubscription('BASIC')
|
||||
vi.mocked(redis.eval).mockResolvedValue(-1)
|
||||
|
||||
await expect(
|
||||
checkSessionEntitlementOrThrow('host-1', 'guest-9', true, 'brainstorm_expand'),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('AC10: host BYOK + guest quota empty still bills host (guest has no quota, host pays)', async () => {
|
||||
vi.mocked(hasAnyActiveByok).mockResolvedValue(true)
|
||||
mockActiveSubscription('PRO')
|
||||
vi.mocked(redis.eval).mockResolvedValue(1)
|
||||
|
||||
// Guest's quota is empty (simulated by checking guest's quota returns 0)
|
||||
vi.mocked(redis.get).mockResolvedValue('0')
|
||||
vi.mocked(reserveCreditsOrThrow).mockRejectedValue(
|
||||
new Error('reserveCreditsOrThrow should not be called when BYOK is active'),
|
||||
)
|
||||
|
||||
await checkSessionEntitlementOrThrow('host-1', 'guest-9', true, 'brainstorm_expand')
|
||||
|
||||
// Verify that host's redis key was incremented, not guest's
|
||||
expect(redis.eval).toHaveBeenCalled()
|
||||
const keyArg = String(vi.mocked(redis.eval).mock.calls[0]?.[2])
|
||||
expect(keyArg).toContain('host-1')
|
||||
expect(keyArg).not.toContain('guest-9')
|
||||
expect(reserveCreditsOrThrow).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('AC10: guest action bills host, not guest', async () => {
|
||||
mockActiveSubscription('PRO')
|
||||
|
||||
await checkSessionEntitlementOrThrow('host-1', 'guest-9', true, 'brainstorm_expand')
|
||||
|
||||
expect(reserveCreditsOrThrow).toHaveBeenCalledTimes(1)
|
||||
const [billedUserId] = vi.mocked(reserveCreditsOrThrow).mock.calls[0]
|
||||
expect(billedUserId).toBe('host-1')
|
||||
expect(billedUserId).not.toBe('guest-9')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user