import { PermissionResolverService } from './permission-resolver.service'; import { PrismaService } from '../../prisma/prisma.service'; describe('PermissionResolverService', () => { let resolver: PermissionResolverService; let prisma: { user: { findUnique: jest.Mock } }; function activeUserWithPermissions(...keys: string[]) { return { isActive: true, passwordChangedAt: null as Date | null, role: { status: 'ACTIVE', permissions: keys.map((key) => ({ permission: { key } })), }, }; } beforeEach(() => { prisma = { user: { findUnique: jest.fn() } }; resolver = new PermissionResolverService( prisma as unknown as PrismaService, ); }); it('returns the granted permission keys for an active user with an active role', async () => { prisma.user.findUnique.mockResolvedValue( activeUserWithPermissions('colonies:read', 'tasks:create'), ); const permissions = await resolver.getPermissions('user-1'); expect(Array.from(permissions).sort()).toEqual( ['colonies:read', 'tasks:create'].sort(), ); }); it('returns no permissions for a deactivated user, even if their role is active', async () => { prisma.user.findUnique.mockResolvedValue({ isActive: false, role: { status: 'ACTIVE', permissions: [{ permission: { key: 'colonies:read' } }], }, }); const permissions = await resolver.getPermissions('user-1'); expect(permissions.size).toBe(0); }); it('returns no permissions when the role itself is deactivated', async () => { prisma.user.findUnique.mockResolvedValue({ isActive: true, role: { status: 'INACTIVE', permissions: [{ permission: { key: 'colonies:read' } }], }, }); const permissions = await resolver.getPermissions('user-1'); expect(permissions.size).toBe(0); }); it('returns no permissions when the user does not exist', async () => { prisma.user.findUnique.mockResolvedValue(null); const permissions = await resolver.getPermissions('missing'); expect(permissions.size).toBe(0); }); it('caches the result and does not re-query within the TTL', async () => { prisma.user.findUnique.mockResolvedValue( activeUserWithPermissions('colonies:read'), ); await resolver.getPermissions('user-1'); await resolver.getPermissions('user-1'); expect(prisma.user.findUnique).toHaveBeenCalledTimes(1); }); it('re-queries after invalidateUser', async () => { prisma.user.findUnique.mockResolvedValue( activeUserWithPermissions('colonies:read'), ); await resolver.getPermissions('user-1'); resolver.invalidateUser('user-1'); await resolver.getPermissions('user-1'); expect(prisma.user.findUnique).toHaveBeenCalledTimes(2); }); describe('isTokenValid', () => { const nowSeconds = Math.floor(Date.now() / 1000); it('returns true for an active user with no recorded password change', async () => { prisma.user.findUnique.mockResolvedValue( activeUserWithPermissions('colonies:read'), ); await expect(resolver.isTokenValid('user-1', nowSeconds)).resolves.toBe( true, ); }); it('returns false for a deactivated user', async () => { prisma.user.findUnique.mockResolvedValue({ isActive: false, passwordChangedAt: null, role: { status: 'ACTIVE', permissions: [{ permission: { key: 'colonies:read' } }], }, }); await expect(resolver.isTokenValid('user-1', nowSeconds)).resolves.toBe( false, ); }); it('returns false when the role is inactive', async () => { prisma.user.findUnique.mockResolvedValue({ isActive: true, passwordChangedAt: null, role: { status: 'INACTIVE', permissions: [{ permission: { key: 'colonies:read' } }], }, }); await expect(resolver.isTokenValid('user-1', nowSeconds)).resolves.toBe( false, ); }); it('returns false when the user does not exist', async () => { prisma.user.findUnique.mockResolvedValue(null); await expect(resolver.isTokenValid('missing', nowSeconds)).resolves.toBe( false, ); }); it('returns false when the token was issued before the last password change', async () => { const entry = activeUserWithPermissions('colonies:read'); entry.passwordChangedAt = new Date((nowSeconds + 60) * 1000); prisma.user.findUnique.mockResolvedValue(entry); await expect(resolver.isTokenValid('user-1', nowSeconds)).resolves.toBe( false, ); }); it('returns true when the token was issued after the last password change', async () => { const entry = activeUserWithPermissions('colonies:read'); entry.passwordChangedAt = new Date((nowSeconds - 60) * 1000); prisma.user.findUnique.mockResolvedValue(entry); await expect(resolver.isTokenValid('user-1', nowSeconds)).resolves.toBe( true, ); }); it('reads fresh every time and refreshes the cache for getPermissions', async () => { prisma.user.findUnique.mockResolvedValue( activeUserWithPermissions('colonies:read'), ); await resolver.getPermissions('user-1'); await resolver.isTokenValid('user-1', nowSeconds); await resolver.getPermissions('user-1'); // One read for the first getPermissions, one fresh read for the // validity check - the second getPermissions uses that fresh entry. expect(prisma.user.findUnique).toHaveBeenCalledTimes(2); }); it('sees a deactivation made elsewhere on the next request', async () => { prisma.user.findUnique.mockResolvedValueOnce( activeUserWithPermissions('colonies:read'), ); await expect(resolver.isTokenValid('user-1', nowSeconds)).resolves.toBe( true, ); prisma.user.findUnique.mockResolvedValueOnce({ ...activeUserWithPermissions('colonies:read'), isActive: false, }); await expect(resolver.isTokenValid('user-1', nowSeconds)).resolves.toBe( false, ); }); }); it('re-queries after invalidateAll', async () => { prisma.user.findUnique.mockResolvedValue( activeUserWithPermissions('colonies:read'), ); await resolver.getPermissions('user-1'); resolver.invalidateAll(); await resolver.getPermissions('user-1'); expect(prisma.user.findUnique).toHaveBeenCalledTimes(2); }); describe('hasAny', () => { it('returns true when the user holds at least one of the requested keys', async () => { prisma.user.findUnique.mockResolvedValue( activeUserWithPermissions('colonies:read'), ); await expect( resolver.hasAny('user-1', ['tasks:create', 'colonies:read']), ).resolves.toBe(true); }); it('returns false when the user holds none of the requested keys', async () => { prisma.user.findUnique.mockResolvedValue( activeUserWithPermissions('colonies:read'), ); await expect(resolver.hasAny('user-1', ['tasks:create'])).resolves.toBe( false, ); }); it('returns true when no keys are required', async () => { await expect(resolver.hasAny('user-1', [])).resolves.toBe(true); expect(prisma.user.findUnique).not.toHaveBeenCalled(); }); }); });