99 lines
3.5 KiB
TypeScript
99 lines
3.5 KiB
TypeScript
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<ServiceAccountRepository>;
|
|
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 });
|
|
});
|
|
});
|