Files
Momento/memento-note/tests/unit/brainstorm-billing.test.ts
Antigravity a623454347
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m32s
CI / Deploy production (on server) (push) Has been skipped
perf: memo GridCard, fuse save fns, fix slash tab active color
2026-06-14 14:06:05 +00:00

158 lines
4.8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import {
billingOwnerFromSession,
getBillingOwner,
} from '@/lib/brainstorm-collab'
import {
checkSessionEntitlementOrThrow,
incrementUsageAsync,
QuotaExceededError,
} from '@/lib/entitlements'
const { prismaMock } = vi.hoisted(() => ({
prismaMock: {
subscription: { findUnique: vi.fn() },
brainstormSession: { findUnique: vi.fn() },
},
}))
vi.mock('@/lib/prisma', () => ({
prisma: prismaMock,
default: prismaMock,
}))
vi.mock('@/lib/redis', () => ({
redis: {
get: vi.fn(),
mget: vi.fn(),
eval: vi.fn(),
},
}))
vi.mock('@/lib/byok', () => ({
hasAnyActiveByok: vi.fn().mockResolvedValue(false),
}))
import { redis } from '@/lib/redis'
import { hasAnyActiveByok } from '@/lib/byok'
function mockActiveSubscription(tier: string) {
prismaMock.subscription.findUnique.mockResolvedValue({
userId: 'host-1',
tier: tier as any,
status: 'ACTIVE' as any,
currentPeriodStart: new Date(),
currentPeriodEnd: new Date(),
} as any)
}
describe('brainstorm host-pays billing (Story 3.4)', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('billingOwnerFromSession', () => {
it('returns host id for host actor', () => {
expect(billingOwnerFromSession('host-1', 'host-1')).toEqual({
billingOwnerId: 'host-1',
isGuestActor: false,
})
})
it('returns host id for guest actor', () => {
expect(billingOwnerFromSession('host-1', 'guest-9')).toEqual({
billingOwnerId: 'host-1',
isGuestActor: true,
})
})
})
describe('getBillingOwner', () => {
it('loads session and resolves billing owner', async () => {
prismaMock.brainstormSession.findUnique.mockResolvedValue({ userId: 'host-1' })
const result = await getBillingOwner('session-abc', 'guest-9')
expect(result).toEqual({ billingOwnerId: 'host-1', isGuestActor: true })
})
})
describe('checkSessionEntitlementOrThrow', () => {
it('throws QuotaExceededError with guest metadata when host quota exhausted', async () => {
mockActiveSubscription('BASIC')
vi.mocked(redis.eval).mockResolvedValue(-1)
await expect(
checkSessionEntitlementOrThrow('host-1', 'guest-9', true, 'brainstorm_expand'),
).rejects.toMatchObject({
billingOwnerId: 'host-1',
triggeredByUserId: 'guest-9',
isGuestActor: true,
})
})
it('increments host redis key 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)
})
})
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)
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')
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')
})
})
describe('QuotaExceededError.toJSON', () => {
it('includes isGuestActor in toJSON for 402 responses', () => {
const err = new QuotaExceededError('PRO', 'brainstorm_expand', 10, 10, false, {
billingOwnerId: 'host-1',
triggeredByUserId: 'guest-9',
isGuestActor: true,
})
expect(err.toJSON()).toEqual({
error: 'QUOTA_EXCEEDED',
feature: 'brainstorm_expand',
upgradeTier: 'PRO',
byokConfigured: false,
isGuestActor: true,
})
expect(err.billingOwnerId).toBe('host-1')
expect(err.triggeredByUserId).toBe('guest-9')
})
})
})