import { randomUUID } from 'node:crypto'; import { SubscriptionPlan, SubscriptionStatus } from '@prisma/client'; import { ENTITLEMENT_KEYS, ENTITLEMENT_SOURCES, EntitlementService, } from './entitlement.service'; describe('EntitlementService', () => { const now = new Date('2026-06-28T12:00:00.000Z'); const future = new Date('2026-07-28T12:00:00.000Z'); const past = new Date('2026-06-27T12:00:00.000Z'); const buildService = () => { const subscriptions = new Map(); const entitlements = new Map(); const entitlementId = (userId: string, entitlementKey: string) => `${userId}:${entitlementKey}`; const userSubscription = { findUnique: jest.fn().mockImplementation(async ({ where }) => { const subscription = subscriptions.get(where.userId); if (!subscription) { return null; } return { plan: subscription.plan, status: subscription.status, currentPeriodEnd: subscription.currentPeriodEnd, }; }), }; const userEntitlement = { findUnique: jest.fn().mockImplementation(async ({ where }) => { const key = where.userId_entitlementKey; const entitlement = entitlements.get( entitlementId(key.userId, key.entitlementKey), ); return entitlement ? { granted: entitlement.granted } : null; }), findMany: jest.fn().mockImplementation(async ({ where }) => Array.from(entitlements.values()) .filter((entitlement) => entitlement.userId === where.userId) .sort((left, right) => left.entitlementKey.localeCompare(right.entitlementKey), ) .map( ({ entitlementKey, granted, source, createdAt, updatedAt }) => ({ entitlementKey, granted, source, createdAt, updatedAt, }), ), ), upsert: jest .fn() .mockImplementation(async ({ where, update, create }) => { const key = where.userId_entitlementKey; const id = entitlementId(key.userId, key.entitlementKey); const existing = entitlements.get(id); const timestamp = new Date(now); const entitlement = existing ? { ...existing, ...update, updatedAt: timestamp } : { id: randomUUID(), ...create, createdAt: timestamp, updatedAt: timestamp, }; entitlements.set(id, entitlement); return { entitlementKey: entitlement.entitlementKey, granted: entitlement.granted, source: entitlement.source, createdAt: entitlement.createdAt, updatedAt: entitlement.updatedAt, }; }), updateMany: jest.fn().mockImplementation(async ({ where, data }) => { let count = 0; for (const [id, entitlement] of entitlements) { if ( entitlement.userId === where.userId && where.entitlementKey.in.includes(entitlement.entitlementKey) && entitlement.source === where.source ) { entitlements.set(id, { ...entitlement, ...data, updatedAt: new Date(now), }); count += 1; } } return { count }; }), createMany: jest.fn().mockImplementation(async ({ data }) => { let count = 0; for (const create of data) { const id = entitlementId(create.userId, create.entitlementKey); if (entitlements.has(id)) { continue; } entitlements.set(id, { id: randomUUID(), ...create, createdAt: new Date(now), updatedAt: new Date(now), }); count += 1; } return { count }; }), }; const prismaService = { userSubscription, userEntitlement, } as any; return { service: new EntitlementService(prismaService), subscriptions, entitlements, prismaService, setSubscription: ( userId: string, status: SubscriptionStatus, plan: SubscriptionPlan, currentPeriodEnd = future, ) => { subscriptions.set(userId, { userId, status, plan, currentPeriodEnd, providerCustomerId: 'provider-id-must-not-affect-plan', providerSubscriptionId: 'provider-subscription-id', }); }, setEntitlement: ( userId: string, entitlementKey: string, granted: boolean, source: string, ) => { entitlements.set(entitlementId(userId, entitlementKey), { id: randomUUID(), userId, entitlementKey, granted, source, createdAt: new Date(now), updatedAt: new Date(now), }); }, }; }; it('returns FREE when the user has no subscription', async () => { const { service } = buildService(); await expect(service.getEffectivePlan(randomUUID(), now)).resolves.toBe( SubscriptionPlan.FREE, ); }); it('returns PRO for an ACTIVE PRO subscription in its current period', async () => { const { service, setSubscription } = buildService(); const userId = randomUUID(); setSubscription( userId, SubscriptionStatus.ACTIVE, SubscriptionPlan.PRO, ); await expect(service.getEffectivePlan(userId, now)).resolves.toBe( SubscriptionPlan.PRO, ); }); it('returns PRO for a current TRIAL', async () => { const { service, setSubscription } = buildService(); const userId = randomUUID(); setSubscription( userId, SubscriptionStatus.TRIAL, SubscriptionPlan.FREE, ); await expect(service.getEffectivePlan(userId, now)).resolves.toBe( SubscriptionPlan.PRO, ); }); it('does not infer PRO from provider IDs on an ACTIVE FREE subscription', async () => { const { service, setSubscription } = buildService(); const userId = randomUUID(); setSubscription( userId, SubscriptionStatus.ACTIVE, SubscriptionPlan.FREE, ); await expect(service.getEffectivePlan(userId, now)).resolves.toBe( SubscriptionPlan.FREE, ); }); it('returns FREE for an ACTIVE PRO subscription past its period end', async () => { const { service, setSubscription } = buildService(); const userId = randomUUID(); setSubscription( userId, SubscriptionStatus.ACTIVE, SubscriptionPlan.PRO, past, ); await expect(service.getEffectivePlan(userId, now)).resolves.toBe( SubscriptionPlan.FREE, ); }); it.each([ SubscriptionStatus.PAST_DUE, SubscriptionStatus.CANCELLED, SubscriptionStatus.EXPIRED, ])('returns FREE for a current %s subscription', async (status) => { const { service, setSubscription } = buildService(); const userId = randomUUID(); setSubscription(userId, status, SubscriptionPlan.PRO); await expect(service.getEffectivePlan(userId, now)).resolves.toBe( SubscriptionPlan.FREE, ); }); it('allows a directly granted entitlement on the FREE plan', async () => { const { service } = buildService(); const userId = randomUUID(); await service.grantEntitlement( userId, ENTITLEMENT_KEYS.SMART_DOWNLOADS, ); await expect( service.hasEntitlement( userId, ENTITLEMENT_KEYS.SMART_DOWNLOADS, now, ), ).resolves.toBe(true); }); it('allows Pro feature access from the effective plan before a sync', async () => { const { service, setSubscription } = buildService(); const userId = randomUUID(); setSubscription( userId, SubscriptionStatus.ACTIVE, SubscriptionPlan.PRO, ); await expect( service.hasEntitlement(userId, ENTITLEMENT_KEYS.PRO_LIBRARY, now), ).resolves.toBe(true); }); it('denies a revoked entitlement on the FREE plan', async () => { const { service } = buildService(); const userId = randomUUID(); await service.revokeEntitlement( userId, ENTITLEMENT_KEYS.SMART_DOWNLOADS, ); await expect( service.hasEntitlement( userId, ENTITLEMENT_KEYS.SMART_DOWNLOADS, now, ), ).resolves.toBe(false); }); it('grants all Pro entitlements when syncing an effective PRO plan', async () => { const { service, setSubscription } = buildService(); const userId = randomUUID(); setSubscription( userId, SubscriptionStatus.ACTIVE, SubscriptionPlan.PRO, ); const result = await service.syncEntitlementsForSubscription(userId, now); expect(result).toHaveLength(EntitlementService.proEntitlementKeys.length); expect(result).toEqual( expect.arrayContaining( EntitlementService.proEntitlementKeys.map((entitlementKey) => expect.objectContaining({ entitlementKey, granted: true, source: ENTITLEMENT_SOURCES.SUBSCRIPTION, }), ), ), ); }); it('revokes only subscription-sourced Pro entitlements when syncing FREE', async () => { const { service, setEntitlement } = buildService(); const userId = randomUUID(); setEntitlement( userId, ENTITLEMENT_KEYS.PRO_LIBRARY, true, ENTITLEMENT_SOURCES.SUBSCRIPTION, ); setEntitlement( userId, ENTITLEMENT_KEYS.SMART_DOWNLOADS, true, ENTITLEMENT_SOURCES.MANUAL, ); const result = await service.syncEntitlementsForSubscription(userId, now); expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ entitlementKey: ENTITLEMENT_KEYS.PRO_LIBRARY, granted: false, source: ENTITLEMENT_SOURCES.SUBSCRIPTION, }), expect.objectContaining({ entitlementKey: ENTITLEMENT_KEYS.SMART_DOWNLOADS, granted: true, source: ENTITLEMENT_SOURCES.MANUAL, }), ]), ); }); it('does not create duplicate entitlements across repeated syncs', async () => { const { service, setSubscription, entitlements } = buildService(); const userId = randomUUID(); setSubscription( userId, SubscriptionStatus.TRIAL, SubscriptionPlan.PRO, ); await service.syncEntitlementsForSubscription(userId, now); await service.syncEntitlementsForSubscription(userId, now); expect( Array.from(entitlements.values()).filter( (entitlement) => entitlement.userId === userId, ), ).toHaveLength(EntitlementService.proEntitlementKeys.length); }); it('keeps entitlement reads, checks, and syncs scoped to the requested user', async () => { const { service, setEntitlement } = buildService(); const userId = randomUUID(); const otherUserId = randomUUID(); setEntitlement( otherUserId, ENTITLEMENT_KEYS.PRIORITY_SYNC, true, ENTITLEMENT_SOURCES.MANUAL, ); await expect( service.hasEntitlement(userId, ENTITLEMENT_KEYS.PRIORITY_SYNC, now), ).resolves.toBe(false); await expect(service.getUserEntitlements(userId)).resolves.toEqual([]); await service.syncEntitlementsForSubscription(userId, now); await expect(service.getUserEntitlements(otherUserId)).resolves.toEqual([ expect.objectContaining({ entitlementKey: ENTITLEMENT_KEYS.PRIORITY_SYNC, granted: true, }), ]); }); });