import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import * as bcrypt from 'bcrypt'; import { TokenHandler } from '../../src/modules/identity/application/handlers/token.handler'; import { ServiceAccount } from '../../src/modules/identity/domain/entities/service-account.entity'; import { ServiceAccountRepository } from '../../src/modules/identity/domain/repositories/service-account.repository.interface'; describe('TokenHandler', () => { let mockRepo: Partial; let mockJwtService: any; let handler: TokenHandler; beforeEach(() => { mockRepo = { findByClientId: jest.fn(), }; mockJwtService = { signAsync: jest.fn().mockResolvedValue('signed-token'), }; handler = new TokenHandler(mockRepo as any, mockJwtService as any); }); it('should throw BadRequestException for invalid grant_type', async () => { await expect(handler.execute({ client_id: 'a', client_secret: 'b', grant_type: 'foo' })).rejects.toBeInstanceOf(BadRequestException); }); it('should throw UnauthorizedException when client_id not found', async () => { (mockRepo.findByClientId as jest.Mock).mockResolvedValue(null); await expect(handler.execute({ client_id: 'notfound', client_secret: 'x', grant_type: 'client_credentials' })).rejects.toBeInstanceOf(UnauthorizedException); }); it('should throw UnauthorizedException when service account disabled', async () => { const sa = ServiceAccount.restore({ id: 'sa-id', clientId: 'cid', clientSecretHash: await bcrypt.hash('secret', 1), name: 'Bot', role: 'BOT', permissions: ['DEBT_READ'], status: 'DISABLED', metadata: {}, }); (mockRepo.findByClientId as jest.Mock).mockResolvedValue(sa); await expect(handler.execute({ client_id: 'cid', client_secret: 'secret', grant_type: 'client_credentials' })).rejects.toBeInstanceOf(UnauthorizedException); }); it('should throw UnauthorizedException when secret mismatch', async () => { const sa = ServiceAccount.restore({ id: 'sa-id', clientId: 'cid', clientSecretHash: await bcrypt.hash('secret', 1), name: 'Bot', role: 'BOT', permissions: ['DEBT_READ'], status: 'ACTIVE', metadata: {}, }); (mockRepo.findByClientId as jest.Mock).mockResolvedValue(sa); await expect(handler.execute({ client_id: 'cid', client_secret: 'wrong', grant_type: 'client_credentials' })).rejects.toBeInstanceOf(UnauthorizedException); }); it('should sign JWT and return token for valid credentials', async () => { const hash = await bcrypt.hash('secret', 1); const sa = ServiceAccount.restore({ id: 'sa-id', clientId: 'cid', clientSecretHash: hash, name: 'Bot', role: 'BOT', permissions: ['DEBT_READ', 'DEBT_CREATE'], status: 'ACTIVE', metadata: {}, }); (mockRepo.findByClientId as jest.Mock).mockResolvedValue(sa); const result = await handler.execute({ client_id: 'cid', client_secret: 'secret', grant_type: 'client_credentials' }); expect(mockJwtService.signAsync).toHaveBeenCalledTimes(1); const signCallArgs = (mockJwtService.signAsync as jest.Mock).mock.calls[0]; const payload = signCallArgs[0]; const options = signCallArgs[1]; expect(payload).toMatchObject({ sub: 'sa-id', client_id: 'cid', role: 'BOT', permissions: expect.any(Array) }); expect(options).toMatchObject({ expiresIn: '1h' }); expect(result).toMatchObject({ access_token: 'signed-token', token_type: 'Bearer', expires_in: 3600 }); }); });