diff --git a/services/iam-service/src/__tests__/integration/setup.ts b/services/iam-service/src/__tests__/integration/setup.ts index 936e33f2..a71b85c2 100644 --- a/services/iam-service/src/__tests__/integration/setup.ts +++ b/services/iam-service/src/__tests__/integration/setup.ts @@ -70,12 +70,11 @@ export async function teardownTestDatabase() { if (prisma) { // Clear all data await prisma.$transaction([ - prisma.userPermissions.deleteMany(), - prisma.userRoles.deleteMany(), + prisma.userPermission.deleteMany(), + prisma.userRole.deleteMany(), prisma.refreshToken.deleteMany(), prisma.session.deleteMany(), prisma.mFADevice.deleteMany(), - prisma.backupCode.deleteMany(), prisma.authEvent.deleteMany(), prisma.user.deleteMany(), prisma.role.deleteMany(), @@ -89,7 +88,7 @@ export async function teardownTestDatabase() { prisma.accessReview.deleteMany(), prisma.complianceReport.deleteMany(), prisma.policy.deleteMany(), - prisma.riskAssessment.deleteMany(), + prisma.riskScore.deleteMany(), prisma.feature.deleteMany(), ]); @@ -145,6 +144,7 @@ export const testUtils = { const prisma = getTestPrisma(); const defaultRole = { name: `test-role-${Date.now()}`, + displayName: 'User', // Added displayName description: 'Test role for integration tests', createdAt: new Date(), updatedAt: new Date(), @@ -174,30 +174,37 @@ export const testUtils = { /** * Assign role to user */ - async assignRoleToUser(userId: string, roleId: string, expiresAt?: Date) { + assignRole: async (userId: string, roleName: string) => { const prisma = getTestPrisma(); - return await prisma.userRoles.create({ - data: { - userId, - roleId, - assignedAt: new Date(), - expiresAt, - }, - }); + const role = await prisma.role.findUnique({ where: { name: roleName } }); + if (role) { + return await prisma.userRole.create({ // Renamed prisma.userRoles to prisma.userRole + data: { + userId, + roleId: role.id, + }, + }); + } + return null; }, /** * Grant permission to user */ - async grantPermissionToUser(userId: string, permissionId: string, expiresAt?: Date) { + grantPermission: async (userId: string, resource: string, action: string, scope: string = 'own') => { const prisma = getTestPrisma(); - return await prisma.userPermissions.create({ + const permission = await prisma.permission.create({ + data: { + resource, + action, + scope, + }, + }); + + return await prisma.userPermission.create({ // Renamed prisma.userPermissions to prisma.userPermission data: { userId, - permissionId, - grantedBy: 'test-admin', - grantedAt: new Date(), - expiresAt, + permissionId: permission.id, }, }); }, diff --git a/services/iam-service/src/modules/access/review/review.controller.ts b/services/iam-service/src/modules/access/review/review.controller.ts index c3e0da12..7ff4fb0e 100644 --- a/services/iam-service/src/modules/access/review/review.controller.ts +++ b/services/iam-service/src/modules/access/review/review.controller.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import { z } from 'zod'; import { NotFoundError, BadRequestError } from '../../../errors/http-error'; -import { CreateAccessReviewDto } from '../access.dto'; +import { CreateAccessReviewDto, ReviewAccessItemDto } from '../access.dto'; import { accessReviewService } from './review.service'; diff --git a/services/iam-service/src/modules/auth/__tests__/auth.integration.test.ts b/services/iam-service/src/modules/auth/__tests__/auth.integration.test.ts index b301f3dd..c890633b 100644 --- a/services/iam-service/src/modules/auth/__tests__/auth.integration.test.ts +++ b/services/iam-service/src/modules/auth/__tests__/auth.integration.test.ts @@ -15,12 +15,11 @@ describe('AuthService Integration Tests', () => { // Clean up any existing test data await prisma.$transaction([ - prisma.userPermissions.deleteMany(), - prisma.userRoles.deleteMany(), + prisma.userPermission.deleteMany(), + prisma.userRole.deleteMany(), prisma.refreshToken.deleteMany(), prisma.session.deleteMany(), prisma.mFADevice.deleteMany(), - prisma.backupCode.deleteMany(), prisma.authEvent.deleteMany(), prisma.user.deleteMany(), ]); @@ -35,12 +34,11 @@ describe('AuthService Integration Tests', () => { beforeEach(async () => { // Clean up between tests await prisma.$transaction([ - prisma.userPermissions.deleteMany(), - prisma.userRoles.deleteMany(), + prisma.userPermission.deleteMany(), + prisma.userRole.deleteMany(), prisma.refreshToken.deleteMany(), prisma.session.deleteMany(), prisma.mFADevice.deleteMany(), - prisma.backupCode.deleteMany(), prisma.authEvent.deleteMany(), prisma.user.deleteMany(), ]); @@ -103,7 +101,7 @@ describe('AuthService Integration Tests', () => { expect(dbUser?.passwordHash).toMatch(/^\$2[ayb]\$.{56}$/); // bcrypt hash pattern // Verify password can be validated - const isValidPassword = await bcrypt.compare(registerData.password, dbUser!.passwordHash); + const isValidPassword = await bcrypt.compare(registerData.password, dbUser!.passwordHash || ''); expect(isValidPassword).toBe(true); }); diff --git a/services/iam-service/src/modules/mfa/__tests__/mfa.service.test.ts b/services/iam-service/src/modules/mfa/__tests__/mfa.service.test.ts index 38d940bf..e5dcbdf7 100644 --- a/services/iam-service/src/modules/mfa/__tests__/mfa.service.test.ts +++ b/services/iam-service/src/modules/mfa/__tests__/mfa.service.test.ts @@ -12,7 +12,7 @@ jest.mock('@goodgo/logger'); describe('MFAService', () => { let mfaService: MFAService; - let mockPrisma: jest.Mocked; + let mockPrisma: any; let mockEncryptionService: any; let mockQRCode: any; let mockSpeakeasy: any; @@ -32,13 +32,8 @@ describe('MFAService', () => { create: jest.fn(), update: jest.fn(), deleteMany: jest.fn(), - }, - backupCode: { - findMany: jest.fn(), - createMany: jest.fn(), - deleteMany: jest.fn(), + updateMany: jest.fn(), findFirst: jest.fn(), - update: jest.fn(), }, } as any; @@ -221,27 +216,25 @@ describe('MFAService', () => { // Arrange const userId = 'user-123'; + mockPrisma.mFADevice.updateMany.mockResolvedValue({ count: 2 }); mockPrisma.user.update.mockResolvedValue(undefined); - mockPrisma.mFADevice.deleteMany.mockResolvedValue({ count: 2 }); - mockPrisma.backupCode.deleteMany.mockResolvedValue({ count: 10 }); // Act await mfaService.disableMFA(userId); // Assert + expect(mockPrisma.mFADevice.updateMany).toHaveBeenCalledWith({ + where: { userId }, + data: { isActive: false }, + }); expect(mockPrisma.user.update).toHaveBeenCalledWith({ where: { id: userId }, data: { mfaEnabled: false, mfaSecret: null, + mfaBackupCodes: null, }, }); - expect(mockPrisma.mFADevice.deleteMany).toHaveBeenCalledWith({ - where: { userId }, - }); - expect(mockPrisma.backupCode.deleteMany).toHaveBeenCalledWith({ - where: { userId }, - }); }); }); @@ -271,13 +264,11 @@ describe('MFAService', () => { // Assert expect(mockPrisma.mFADevice.findMany).toHaveBeenCalledWith({ - where: { userId }, - select: { - id: true, - type: true, - name: true, - createdAt: true, + where: { + userId, + isActive: true, }, + orderBy: { createdAt: 'desc' }, }); expect(result).toEqual(mockDevices); }); @@ -289,87 +280,82 @@ describe('MFAService', () => { const userId = 'user-123'; const codes = ['code1', 'code2', 'code3']; - mockPrisma.backupCode.createMany.mockResolvedValue(undefined); + mockPrisma.user.update.mockResolvedValue(undefined); // Act await mfaService.storeBackupCodes(userId, codes); // Assert - expect(mockPrisma.backupCode.createMany).toHaveBeenCalledWith({ - data: codes.map(code => ({ - userId, - code: expect.any(String), // hashed code - used: false, - })), + expect(mockEncryptionService.encrypt).toHaveBeenCalledWith(JSON.stringify(codes)); + expect(mockPrisma.user.update).toHaveBeenCalledWith({ + where: { id: userId }, + data: { mfaBackupCodes: 'encrypted-secret' }, }); }); }); describe('getBackupCodes', () => { - it('should return unused backup codes', async () => { + it('should return backup codes', async () => { // Arrange const userId = 'user-123'; - const mockCodes = [ - { code: 'hashed-code1', used: false }, - { code: 'hashed-code2', used: true }, - { code: 'hashed-code3', used: false }, - ]; + const codes = ['code1', 'code3']; - mockPrisma.backupCode.findMany.mockResolvedValue(mockCodes); + mockPrisma.user.findUnique.mockResolvedValue({ + id: userId, + mfaBackupCodes: 'encrypted-codes', + }); + mockEncryptionService.decrypt.mockReturnValue(JSON.stringify(codes)); // Act const result = await mfaService.getBackupCodes(userId); // Assert - expect(mockPrisma.backupCode.findMany).toHaveBeenCalledWith({ - where: { userId, used: false }, - select: { code: true }, + expect(mockPrisma.user.findUnique).toHaveBeenCalledWith({ + where: { id: userId }, + select: { mfaBackupCodes: true }, }); - expect(result).toEqual(['hashed-code1', 'hashed-code3']); + expect(mockEncryptionService.decrypt).toHaveBeenCalledWith('encrypted-codes'); + expect(result).toEqual(codes); }); }); describe('validateBackupCode', () => { - it('should validate and mark backup code as used', async () => { + it('should validate and consume backup code', async () => { // Arrange const userId = 'user-123'; - const code = 'backup-code-123'; - const hashedCode = 'hashed-backup-code-123'; + const code = 'code1'; + const codes = ['code1', 'code2']; - const mockBackupCode = { - id: 'backup-1', - code: hashedCode, - used: false, - }; - - mockPrisma.backupCode.findFirst.mockResolvedValue(mockBackupCode); - mockPrisma.backupCode.update.mockResolvedValue({ - ...mockBackupCode, - used: true, + mockPrisma.user.findUnique.mockResolvedValue({ + id: userId, + mfaBackupCodes: 'encrypted-codes', }); - - // Mock bcrypt compare - const bcrypt = require('bcryptjs'); - bcrypt.compare = jest.fn().mockResolvedValue(true); + mockEncryptionService.decrypt.mockReturnValue(JSON.stringify(codes)); + mockPrisma.user.update.mockResolvedValue(undefined); // Act const result = await mfaService.validateBackupCode(userId, code); // Assert - expect(bcrypt.compare).toHaveBeenCalledWith(code, hashedCode); - expect(mockPrisma.backupCode.update).toHaveBeenCalledWith({ - where: { id: 'backup-1' }, - data: { used: true }, - }); expect(result).toBe(true); + // Should store remaining codes + expect(mockPrisma.user.update).toHaveBeenCalledWith({ + where: { id: userId }, + data: { mfaBackupCodes: expect.any(String) }, + }); }); it('should return false for invalid backup code', async () => { // Arrange const userId = 'user-123'; const code = 'invalid-code'; + const codes = ['code1', 'code2']; - mockPrisma.backupCode.findFirst.mockResolvedValue(null); + mockPrisma.user.findUnique.mockResolvedValue({ + id: userId, + mfaBackupCodes: 'encrypted-codes', + }); + mockEncryptionService.decrypt.mockReturnValue(JSON.stringify(codes)); // Act const result = await mfaService.validateBackupCode(userId, code); @@ -384,31 +370,28 @@ describe('MFAService', () => { // Arrange const userId = 'user-123'; - mockPrisma.backupCode.deleteMany.mockResolvedValue({ count: 5 }); - mockPrisma.backupCode.createMany.mockResolvedValue(undefined); + mockPrisma.user.update.mockResolvedValue(undefined); // Act const result = await mfaService.regenerateBackupCodes(userId); // Assert - expect(mockPrisma.backupCode.deleteMany).toHaveBeenCalledWith({ - where: { userId }, - }); - expect(mockPrisma.backupCode.createMany).toHaveBeenCalledWith({ - data: expect.any(Array), - }); + expect(mockPrisma.user.update).toHaveBeenCalled(); expect(result).toHaveLength(10); // Default 10 codes }); }); describe('hasBackupCodes', () => { - it('should return true when user has unused backup codes', async () => { + it('should return true when user has backup codes', async () => { // Arrange const userId = 'user-123'; + const codes = ['code1']; - mockPrisma.backupCode.findMany.mockResolvedValue([ - { id: 'code-1', used: false }, - ]); + mockPrisma.user.findUnique.mockResolvedValue({ + id: userId, + mfaBackupCodes: 'encrypted-codes', + }); + mockEncryptionService.decrypt.mockReturnValue(JSON.stringify(codes)); // Act const result = await mfaService.hasBackupCodes(userId); @@ -417,11 +400,14 @@ describe('MFAService', () => { expect(result).toBe(true); }); - it('should return false when user has no unused backup codes', async () => { + it('should return false when user has no backup codes', async () => { // Arrange const userId = 'user-123'; - mockPrisma.backupCode.findMany.mockResolvedValue([]); + mockPrisma.user.findUnique.mockResolvedValue({ + id: userId, + mfaBackupCodes: null, + }); // Act const result = await mfaService.hasBackupCodes(userId); diff --git a/services/iam-service/src/modules/rbac/__tests__/rbac.service.test.ts b/services/iam-service/src/modules/rbac/__tests__/rbac.service.test.ts index a5b44935..198e963a 100644 --- a/services/iam-service/src/modules/rbac/__tests__/rbac.service.test.ts +++ b/services/iam-service/src/modules/rbac/__tests__/rbac.service.test.ts @@ -13,10 +13,10 @@ jest.mock('@goodgo/logger'); describe('RBACService', () => { let rbacService: RBACService; - let mockPrisma: jest.Mocked; - let mockUserRepo: jest.Mocked; - let mockRoleRepo: jest.Mocked; - let mockPermissionRepo: jest.Mocked; + let mockPrisma: any; + let mockUserRepo: any; + let mockRoleRepo: any; + let mockPermissionRepo: any; let mockCacheService: any; beforeEach(() => { @@ -24,23 +24,26 @@ describe('RBACService', () => { jest.clearAllMocks(); // Setup mocks - mockPrisma = {} as jest.Mocked; + mockPrisma = { + userRole: { + upsert: jest.fn(), + deleteMany: jest.fn(), + }, + userPermission: { + upsert: jest.fn(), + }, + } as any; mockUserRepo = { findWithPermissions: jest.fn(), findWithRoles: jest.fn(), - } as any; + }; mockRoleRepo = { - assignRoleToUser: jest.fn(), - revokeRoleFromUser: jest.fn(), - getRolePermissions: jest.fn(), - } as any; + // Repos used for constructor but maybe not used for mutations + }; - mockPermissionRepo = { - grantUserPermission: jest.fn(), - denyUserPermission: jest.fn(), - } as any; + mockPermissionRepo = {}; mockCacheService = { keys: { @@ -50,6 +53,8 @@ describe('RBACService', () => { get: jest.fn(), set: jest.fn(), delete: jest.fn(), + del: jest.fn(), + delMany: jest.fn(), }; // Mock the database client @@ -303,16 +308,26 @@ describe('RBACService', () => { const userId = 'user-123'; const roleId = 'role-admin'; - mockRoleRepo.assignRoleToUser.mockResolvedValue(undefined); - mockCacheService.delete.mockResolvedValue(undefined); + mockPrisma.userRole.upsert.mockResolvedValue(undefined); + mockCacheService.delMany.mockResolvedValue(undefined); // Act await rbacService.assignRole(userId, roleId); // Assert - expect(mockRoleRepo.assignRoleToUser).toHaveBeenCalledWith(userId, roleId); - expect(mockCacheService.delete).toHaveBeenCalledWith('user:permissions:123'); - expect(mockCacheService.delete).toHaveBeenCalledWith('user:roles:123'); + expect(mockPrisma.userRole.upsert).toHaveBeenCalledWith({ + where: { userId_roleId: { userId, roleId } }, + create: { + userId, + roleId, + grantedBy: undefined, + expiresAt: undefined, + }, + update: { + expiresAt: undefined, + }, + }); + expect(mockCacheService.delMany).toHaveBeenCalled(); }); }); @@ -322,16 +337,17 @@ describe('RBACService', () => { const userId = 'user-123'; const roleId = 'role-admin'; - mockRoleRepo.revokeRoleFromUser.mockResolvedValue(undefined); - mockCacheService.delete.mockResolvedValue(undefined); + mockPrisma.userRole.deleteMany.mockResolvedValue(undefined); + mockCacheService.delMany.mockResolvedValue(undefined); // Act await rbacService.revokeRole(userId, roleId); // Assert - expect(mockRoleRepo.revokeRoleFromUser).toHaveBeenCalledWith(userId, roleId); - expect(mockCacheService.delete).toHaveBeenCalledWith('user:permissions:123'); - expect(mockCacheService.delete).toHaveBeenCalledWith('user:roles:123'); + expect(mockPrisma.userRole.deleteMany).toHaveBeenCalledWith({ + where: { userId, roleId }, + }); + expect(mockCacheService.delMany).toHaveBeenCalled(); }); }); @@ -339,24 +355,35 @@ describe('RBACService', () => { it('should grant permission to user successfully', async () => { // Arrange const userId = 'user-123'; - const resource = 'users'; + const permissionId = 'perm-1'; + const resource = 'users'; // Unused in direct implementation but passed in test const action = 'read'; - const scope = 'own'; - mockPermissionRepo.grantUserPermission.mockResolvedValue(undefined); - mockCacheService.delete.mockResolvedValue(undefined); + mockPrisma.userPermission.upsert.mockResolvedValue(undefined); + mockCacheService.del.mockResolvedValue(undefined); // Act - await rbacService.grantPermission(userId, resource, action, scope); + // Note: service.grantPermission takes (userId, permissionId, options) + // The test previously passed (userId, resource, action, scope) which is WRONG for the actual service signature + // Signature: grantPermission(userId: string, permissionId: string, options?: ...) + await rbacService.grantPermission(userId, permissionId); // Assert - expect(mockPermissionRepo.grantUserPermission).toHaveBeenCalledWith( - userId, - resource, - action, - scope - ); - expect(mockCacheService.delete).toHaveBeenCalledWith('user:permissions:123'); + expect(mockPrisma.userPermission.upsert).toHaveBeenCalledWith({ + where: { userId_permissionId: { userId, permissionId } }, + create: { + userId, + permissionId, + granted: true, + grantedBy: undefined, + expiresAt: undefined, + }, + update: { + granted: true, + expiresAt: undefined, + }, + }); + expect(mockCacheService.del).toHaveBeenCalled(); }); }); @@ -364,22 +391,28 @@ describe('RBACService', () => { it('should deny permission from user successfully', async () => { // Arrange const userId = 'user-123'; - const resource = 'users'; - const action = 'write'; + const permissionId = 'perm-1'; - mockPermissionRepo.denyUserPermission.mockResolvedValue(undefined); - mockCacheService.delete.mockResolvedValue(undefined); + mockPrisma.userPermission.upsert.mockResolvedValue(undefined); + mockCacheService.del.mockResolvedValue(undefined); // Act - await rbacService.denyPermission(userId, resource, action); + await rbacService.denyPermission(userId, permissionId); // Assert - expect(mockPermissionRepo.denyUserPermission).toHaveBeenCalledWith( - userId, - resource, - action - ); - expect(mockCacheService.delete).toHaveBeenCalledWith('user:permissions:123'); + expect(mockPrisma.userPermission.upsert).toHaveBeenCalledWith({ + where: { userId_permissionId: { userId, permissionId } }, + create: { + userId, + permissionId, + granted: false, + grantedBy: undefined, + }, + update: { + granted: false, + }, + }); + expect(mockCacheService.del).toHaveBeenCalled(); }); }); }); \ No newline at end of file diff --git a/services/iam-service/src/utils/__tests__/helpers.test.ts b/services/iam-service/src/utils/__tests__/helpers.test.ts index 027846f0..b3f03037 100644 --- a/services/iam-service/src/utils/__tests__/helpers.test.ts +++ b/services/iam-service/src/utils/__tests__/helpers.test.ts @@ -150,9 +150,9 @@ describe('Helpers', () => { }, ip: '127.0.0.1', socket: { remoteAddress: '127.0.0.1' }, - } as Partial; + } as unknown as Request; - const result = getClientIP(mockReq as Request); + const result = getClientIP(mockReq); expect(result).toBe('192.168.1.100'); }); @@ -162,9 +162,9 @@ describe('Helpers', () => { 'x-forwarded-for': ['192.168.1.100', '10.0.0.1'], }, ip: '127.0.0.1', - } as Partial; + } as unknown as Request; - const result = getClientIP(mockReq as Request); + const result = getClientIP(mockReq); expect(result).toBe('192.168.1.100'); }); @@ -175,9 +175,9 @@ describe('Helpers', () => { }, ip: '127.0.0.1', socket: { remoteAddress: '127.0.0.1' }, - } as Partial; + } as unknown as Request; - const result = getClientIP(mockReq as Request); + const result = getClientIP(mockReq); expect(result).toBe('10.0.0.50'); }); @@ -187,9 +187,9 @@ describe('Helpers', () => { 'x-real-ip': ['10.0.0.50'], }, ip: '127.0.0.1', - } as Partial; + } as unknown as Request; - const result = getClientIP(mockReq as Request); + const result = getClientIP(mockReq); expect(result).toBe('10.0.0.50'); }); @@ -198,9 +198,9 @@ describe('Helpers', () => { headers: {}, ip: '172.16.0.25', socket: { remoteAddress: '127.0.0.1' }, - } as Partial; + } as unknown as Request; - const result = getClientIP(mockReq as Request); + const result = getClientIP(mockReq); expect(result).toBe('172.16.0.25'); }); @@ -208,18 +208,18 @@ describe('Helpers', () => { const mockReq = { headers: {}, socket: { remoteAddress: '203.0.113.1' }, - } as Partial; + } as unknown as Request; - const result = getClientIP(mockReq as Request); + const result = getClientIP(mockReq); expect(result).toBe('203.0.113.1'); }); it('should return unknown when no IP found', () => { const mockReq = { headers: {}, - } as Partial; + } as unknown as Request; - const result = getClientIP(mockReq as Request); + const result = getClientIP(mockReq); expect(result).toBe('unknown'); }); });