mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
265 lines
7.5 KiB
TypeScript
265 lines
7.5 KiB
TypeScript
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { ExecutionContext, UnauthorizedException, ForbiddenException } from '@nestjs/common';
|
|
import { Reflector } from '@nestjs/core';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { HttpService } from '@nestjs/axios';
|
|
import { IamGuard } from './iam-adapter';
|
|
import { of, throwError } from 'rxjs';
|
|
|
|
describe('IamGuard', () => {
|
|
let guard: IamGuard;
|
|
let httpService: HttpService;
|
|
let configService: ConfigService;
|
|
let reflector: Reflector;
|
|
|
|
const mockConfigService = {
|
|
get: jest.fn((key: string) => {
|
|
const config: Record<string, string> = {
|
|
IAM_API_URL: 'https://iam.test.com/api',
|
|
IAM_ENABLED: 'true',
|
|
IAM_API_KEY: 'test-api-key',
|
|
};
|
|
return config[key];
|
|
}),
|
|
};
|
|
|
|
const mockHttpService = {
|
|
post: jest.fn(),
|
|
};
|
|
|
|
const mockReflector = {
|
|
get: jest.fn(),
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
IamGuard,
|
|
{ provide: ConfigService, useValue: mockConfigService },
|
|
{ provide: HttpService, useValue: mockHttpService },
|
|
{ provide: Reflector, useValue: mockReflector },
|
|
],
|
|
}).compile();
|
|
|
|
guard = module.get<IamGuard>(IamGuard);
|
|
httpService = module.get<HttpService>(HttpService);
|
|
configService = module.get<ConfigService>(ConfigService);
|
|
reflector = module.get<Reflector>(Reflector);
|
|
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
const createMockContext = (token?: string, roles?: string[]): ExecutionContext => {
|
|
const request = {
|
|
headers: token ? { authorization: `Bearer ${token}` } : {},
|
|
user: undefined,
|
|
};
|
|
|
|
return {
|
|
switchToHttp: () => ({
|
|
getRequest: () => request,
|
|
}),
|
|
getHandler: () => ({}),
|
|
} as ExecutionContext;
|
|
};
|
|
|
|
describe('canActivate', () => {
|
|
it('should allow access when IAM is disabled', async () => {
|
|
mockConfigService.get.mockReturnValueOnce('false'); // IAM_ENABLED
|
|
|
|
const context = createMockContext();
|
|
const result = await guard.canActivate(context);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should throw UnauthorizedException when no token provided', async () => {
|
|
const context = createMockContext();
|
|
|
|
await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
|
|
});
|
|
|
|
it('should validate token and allow access', async () => {
|
|
const mockValidationResponse = {
|
|
data: {
|
|
valid: true,
|
|
payload: {
|
|
sub: 'user-123',
|
|
email: 'admin@test.com',
|
|
roles: ['ADMIN'],
|
|
permissions: ['read', 'write'],
|
|
exp: Date.now() + 3600000,
|
|
iat: Date.now(),
|
|
},
|
|
},
|
|
};
|
|
|
|
mockHttpService.post.mockReturnValue(of(mockValidationResponse));
|
|
mockReflector.get.mockReturnValue(null);
|
|
|
|
const context = createMockContext('valid-token');
|
|
const result = await guard.canActivate(context);
|
|
|
|
expect(result).toBe(true);
|
|
expect(mockHttpService.post).toHaveBeenCalledWith(
|
|
'https://iam.test.com/api/v1/auth/validate',
|
|
{ token: 'valid-token' },
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({
|
|
'X-API-Key': 'test-api-key',
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should throw UnauthorizedException for invalid token', async () => {
|
|
const mockValidationResponse = {
|
|
data: {
|
|
valid: false,
|
|
error: 'Token expired',
|
|
},
|
|
};
|
|
|
|
mockHttpService.post.mockReturnValue(of(mockValidationResponse));
|
|
|
|
const context = createMockContext('invalid-token');
|
|
|
|
await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
|
|
});
|
|
|
|
it('should check required roles', async () => {
|
|
const mockValidationResponse = {
|
|
data: {
|
|
valid: true,
|
|
payload: {
|
|
sub: 'user-123',
|
|
email: 'agent@test.com',
|
|
roles: ['AGENT'],
|
|
permissions: [],
|
|
exp: Date.now() + 3600000,
|
|
iat: Date.now(),
|
|
},
|
|
},
|
|
};
|
|
|
|
mockHttpService.post.mockReturnValue(of(mockValidationResponse));
|
|
mockReflector.get.mockReturnValue(['ADMIN', 'SUPERVISOR']);
|
|
|
|
const context = createMockContext('valid-token');
|
|
|
|
await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException);
|
|
});
|
|
|
|
it('should allow access when user has required role', async () => {
|
|
const mockValidationResponse = {
|
|
data: {
|
|
valid: true,
|
|
payload: {
|
|
sub: 'user-123',
|
|
email: 'admin@test.com',
|
|
roles: ['ADMIN'],
|
|
permissions: [],
|
|
exp: Date.now() + 3600000,
|
|
iat: Date.now(),
|
|
},
|
|
},
|
|
};
|
|
|
|
mockHttpService.post.mockReturnValue(of(mockValidationResponse));
|
|
mockReflector.get.mockReturnValue(['ADMIN', 'SUPERVISOR']);
|
|
|
|
const context = createMockContext('valid-token');
|
|
const result = await guard.canActivate(context);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should handle HTTP errors gracefully', async () => {
|
|
mockHttpService.post.mockReturnValue(
|
|
throwError(() => new Error('Network error')),
|
|
);
|
|
|
|
const context = createMockContext('valid-token');
|
|
|
|
await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
|
|
});
|
|
|
|
it('should attach user to request', async () => {
|
|
const mockValidationResponse = {
|
|
data: {
|
|
valid: true,
|
|
payload: {
|
|
sub: 'user-123',
|
|
email: 'admin@test.com',
|
|
roles: ['ADMIN'],
|
|
permissions: ['read', 'write'],
|
|
organizationId: 'org-456',
|
|
exp: Date.now() + 3600000,
|
|
iat: Date.now(),
|
|
},
|
|
},
|
|
};
|
|
|
|
mockHttpService.post.mockReturnValue(of(mockValidationResponse));
|
|
mockReflector.get.mockReturnValue(null);
|
|
|
|
const context = createMockContext('valid-token');
|
|
await guard.canActivate(context);
|
|
|
|
const request = context.switchToHttp().getRequest();
|
|
expect(request.user).toEqual({
|
|
userId: 'user-123',
|
|
email: 'admin@test.com',
|
|
roles: ['ADMIN'],
|
|
permissions: ['read', 'write'],
|
|
organizationId: 'org-456',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('token extraction', () => {
|
|
it('should extract token from Bearer header', async () => {
|
|
const mockValidationResponse = {
|
|
data: {
|
|
valid: true,
|
|
payload: {
|
|
sub: 'user-123',
|
|
email: 'test@test.com',
|
|
roles: [],
|
|
permissions: [],
|
|
exp: Date.now() + 3600000,
|
|
iat: Date.now(),
|
|
},
|
|
},
|
|
};
|
|
|
|
mockHttpService.post.mockReturnValue(of(mockValidationResponse));
|
|
mockReflector.get.mockReturnValue(null);
|
|
|
|
const context = createMockContext('my-token-123');
|
|
await guard.canActivate(context);
|
|
|
|
expect(mockHttpService.post).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
{ token: 'my-token-123' },
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
|
|
it('should reject malformed authorization header', async () => {
|
|
const request = {
|
|
headers: { authorization: 'InvalidFormat token' },
|
|
};
|
|
|
|
const context = {
|
|
switchToHttp: () => ({
|
|
getRequest: () => request,
|
|
}),
|
|
getHandler: () => ({}),
|
|
} as ExecutionContext;
|
|
|
|
await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
|
|
});
|
|
});
|
|
});
|