201 lines
6.3 KiB
TypeScript
201 lines
6.3 KiB
TypeScript
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { ServiceAuthGuard } from '../../../src/shared/guards/service-auth.guard';
|
|
import {
|
|
generateServiceToken,
|
|
generateExpiredServiceToken,
|
|
TEST_SERVICE_JWT_SECRET,
|
|
} from '../../utils/test-utils';
|
|
|
|
describe('ServiceAuthGuard', () => {
|
|
let guard: ServiceAuthGuard;
|
|
|
|
const createMockContext = (headers: Record<string, string> = {}, connection: any = {}): ExecutionContext => {
|
|
const mockRequest = {
|
|
headers,
|
|
connection,
|
|
socket: connection,
|
|
};
|
|
|
|
return {
|
|
switchToHttp: () => ({
|
|
getRequest: () => mockRequest,
|
|
}),
|
|
} as ExecutionContext;
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
ServiceAuthGuard,
|
|
{
|
|
provide: ConfigService,
|
|
useValue: {
|
|
get: (key: string) => {
|
|
switch (key) {
|
|
case 'SERVICE_JWT_SECRET':
|
|
return TEST_SERVICE_JWT_SECRET;
|
|
case 'ALLOWED_SERVICES':
|
|
return 'identity-service,recovery-service';
|
|
default:
|
|
return undefined;
|
|
}
|
|
},
|
|
},
|
|
},
|
|
],
|
|
}).compile();
|
|
|
|
guard = module.get<ServiceAuthGuard>(ServiceAuthGuard);
|
|
});
|
|
|
|
describe('canActivate', () => {
|
|
it('should allow request with valid identity-service token', () => {
|
|
const token = generateServiceToken('identity-service');
|
|
const context = createMockContext({ 'x-service-token': token });
|
|
|
|
expect(guard.canActivate(context)).toBe(true);
|
|
});
|
|
|
|
it('should allow request with valid recovery-service token', () => {
|
|
const token = generateServiceToken('recovery-service');
|
|
const context = createMockContext({ 'x-service-token': token });
|
|
|
|
expect(guard.canActivate(context)).toBe(true);
|
|
});
|
|
|
|
it('should reject request without service token', () => {
|
|
const context = createMockContext({});
|
|
|
|
expect(() => guard.canActivate(context)).toThrow(UnauthorizedException);
|
|
expect(() => guard.canActivate(context)).toThrow('Missing service token');
|
|
});
|
|
|
|
it('should reject request with invalid token', () => {
|
|
const context = createMockContext({ 'x-service-token': 'invalid-token' });
|
|
|
|
expect(() => guard.canActivate(context)).toThrow(UnauthorizedException);
|
|
expect(() => guard.canActivate(context)).toThrow('Invalid service token');
|
|
});
|
|
|
|
it('should reject request with expired token', () => {
|
|
const token = generateExpiredServiceToken('identity-service');
|
|
const context = createMockContext({ 'x-service-token': token });
|
|
|
|
expect(() => guard.canActivate(context)).toThrow(UnauthorizedException);
|
|
});
|
|
|
|
it('should reject request from unauthorized service', () => {
|
|
const token = generateServiceToken('unknown-service');
|
|
const context = createMockContext({ 'x-service-token': token });
|
|
|
|
expect(() => guard.canActivate(context)).toThrow(UnauthorizedException);
|
|
expect(() => guard.canActivate(context)).toThrow('Service not authorized');
|
|
});
|
|
|
|
it('should reject token signed with wrong secret', () => {
|
|
const token = generateServiceToken('identity-service', 'wrong-secret');
|
|
const context = createMockContext({ 'x-service-token': token });
|
|
|
|
expect(() => guard.canActivate(context)).toThrow(UnauthorizedException);
|
|
});
|
|
|
|
it('should attach sourceService to request', () => {
|
|
const token = generateServiceToken('identity-service');
|
|
const mockRequest: any = {
|
|
headers: { 'x-service-token': token },
|
|
connection: {},
|
|
socket: {},
|
|
};
|
|
const context = {
|
|
switchToHttp: () => ({
|
|
getRequest: () => mockRequest,
|
|
}),
|
|
} as ExecutionContext;
|
|
|
|
guard.canActivate(context);
|
|
|
|
expect(mockRequest.sourceService).toBe('identity-service');
|
|
});
|
|
|
|
it('should extract client IP from x-forwarded-for header', () => {
|
|
const token = generateServiceToken('identity-service');
|
|
const mockRequest: any = {
|
|
headers: {
|
|
'x-service-token': token,
|
|
'x-forwarded-for': '203.0.113.195, 70.41.3.18, 150.172.238.178',
|
|
},
|
|
connection: {},
|
|
socket: {},
|
|
};
|
|
const context = {
|
|
switchToHttp: () => ({
|
|
getRequest: () => mockRequest,
|
|
}),
|
|
} as ExecutionContext;
|
|
|
|
guard.canActivate(context);
|
|
|
|
expect(mockRequest.sourceIp).toBe('203.0.113.195');
|
|
});
|
|
|
|
it('should extract client IP from x-real-ip header', () => {
|
|
const token = generateServiceToken('identity-service');
|
|
const mockRequest: any = {
|
|
headers: {
|
|
'x-service-token': token,
|
|
'x-real-ip': '192.168.1.100',
|
|
},
|
|
connection: {},
|
|
socket: {},
|
|
};
|
|
const context = {
|
|
switchToHttp: () => ({
|
|
getRequest: () => mockRequest,
|
|
}),
|
|
} as ExecutionContext;
|
|
|
|
guard.canActivate(context);
|
|
|
|
expect(mockRequest.sourceIp).toBe('192.168.1.100');
|
|
});
|
|
|
|
it('should extract client IP from connection.remoteAddress', () => {
|
|
const token = generateServiceToken('identity-service');
|
|
const mockRequest: any = {
|
|
headers: { 'x-service-token': token },
|
|
connection: { remoteAddress: '10.0.0.1' },
|
|
socket: {},
|
|
};
|
|
const context = {
|
|
switchToHttp: () => ({
|
|
getRequest: () => mockRequest,
|
|
}),
|
|
} as ExecutionContext;
|
|
|
|
guard.canActivate(context);
|
|
|
|
expect(mockRequest.sourceIp).toBe('10.0.0.1');
|
|
});
|
|
|
|
it('should return "unknown" when no IP can be determined', () => {
|
|
const token = generateServiceToken('identity-service');
|
|
const mockRequest: any = {
|
|
headers: { 'x-service-token': token },
|
|
connection: {},
|
|
socket: {},
|
|
};
|
|
const context = {
|
|
switchToHttp: () => ({
|
|
getRequest: () => mockRequest,
|
|
}),
|
|
} as ExecutionContext;
|
|
|
|
guard.canActivate(context);
|
|
|
|
expect(mockRequest.sourceIp).toBe('unknown');
|
|
});
|
|
});
|
|
});
|