add tests
Some checks failed
Contract Testing / contract-tests (push) Failing after 6m7s
Contract Testing / contract-snapshot (push) Failing after 4m46s

This commit is contained in:
2026-01-22 11:52:42 +01:00
parent 40bc15ff61
commit fb1221701d
112 changed files with 30625 additions and 1059 deletions

View File

@@ -0,0 +1,272 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext, NotFoundException, ServiceUnavailableException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { FeatureAvailabilityGuard, inferActionTypeFromHttpMethod } from './FeatureAvailabilityGuard';
import { PolicyService } from './PolicyService';
import { FEATURE_AVAILABILITY_METADATA_KEY, FeatureAvailabilityMetadata } from './RequireCapability';
class MockReflector {
getAllAndOverride = vi.fn();
}
class MockPolicyService {
getSnapshot = vi.fn();
}
describe('FeatureAvailabilityGuard', () => {
let guard: FeatureAvailabilityGuard;
let reflector: MockReflector;
let policyService: MockPolicyService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
FeatureAvailabilityGuard,
{
provide: Reflector,
useClass: MockReflector,
},
{
provide: PolicyService,
useClass: MockPolicyService,
},
],
}).compile();
guard = module.get<FeatureAvailabilityGuard>(FeatureAvailabilityGuard);
reflector = module.get<Reflector>(Reflector) as unknown as MockReflector;
policyService = module.get<PolicyService>(PolicyService) as unknown as MockPolicyService;
});
describe('canActivate', () => {
it('should return true when no metadata is found', async () => {
const mockContext = {
getHandler: () => () => {},
getClass: () => class {},
} as unknown as ExecutionContext;
reflector.getAllAndOverride.mockReturnValue(undefined);
const result = await guard.canActivate(mockContext);
expect(result).toBe(true);
expect(reflector.getAllAndOverride).toHaveBeenCalledWith(
FEATURE_AVAILABILITY_METADATA_KEY,
[mockContext.getHandler(), mockContext.getClass()]
);
});
it('should return true when feature is enabled', async () => {
const mockContext = {
getHandler: () => () => {},
getClass: () => class {},
} as unknown as ExecutionContext;
const metadata: FeatureAvailabilityMetadata = {
capabilityKey: 'test-feature',
actionType: 'view',
};
reflector.getAllAndOverride.mockReturnValue(metadata);
policyService.getSnapshot.mockResolvedValue({
policyVersion: 1,
operationalMode: 'normal',
capabilities: { 'test-feature': 'enabled' },
maintenanceAllowlist: { mutate: [], view: [] },
loadedFrom: 'defaults',
loadedAtIso: new Date().toISOString(),
});
const result = await guard.canActivate(mockContext);
expect(result).toBe(true);
});
it('should throw ServiceUnavailableException when in maintenance mode and not in allowlist', async () => {
const mockContext = {
getHandler: () => () => {},
getClass: () => class {},
} as unknown as ExecutionContext;
const metadata: FeatureAvailabilityMetadata = {
capabilityKey: 'test-feature',
actionType: 'mutate',
};
reflector.getAllAndOverride.mockReturnValue(metadata);
policyService.getSnapshot.mockResolvedValue({
policyVersion: 1,
operationalMode: 'maintenance',
capabilities: { 'test-feature': 'enabled' },
maintenanceAllowlist: { mutate: [], view: [] },
loadedFrom: 'defaults',
loadedAtIso: new Date().toISOString(),
});
await expect(guard.canActivate(mockContext)).rejects.toThrow(ServiceUnavailableException);
await expect(guard.canActivate(mockContext)).rejects.toThrow('Service temporarily unavailable');
});
it('should return true when in maintenance mode but in allowlist', async () => {
const mockContext = {
getHandler: () => () => {},
getClass: () => class {},
} as unknown as ExecutionContext;
const metadata: FeatureAvailabilityMetadata = {
capabilityKey: 'test-feature',
actionType: 'mutate',
};
reflector.getAllAndOverride.mockReturnValue(metadata);
policyService.getSnapshot.mockResolvedValue({
policyVersion: 1,
operationalMode: 'maintenance',
capabilities: { 'test-feature': 'enabled' },
maintenanceAllowlist: { mutate: ['test-feature'], view: [] },
loadedFrom: 'defaults',
loadedAtIso: new Date().toISOString(),
});
const result = await guard.canActivate(mockContext);
expect(result).toBe(true);
});
it('should throw NotFoundException when feature is disabled', async () => {
const mockContext = {
getHandler: () => () => {},
getClass: () => class {},
} as unknown as ExecutionContext;
const metadata: FeatureAvailabilityMetadata = {
capabilityKey: 'test-feature',
actionType: 'view',
};
reflector.getAllAndOverride.mockReturnValue(metadata);
policyService.getSnapshot.mockResolvedValue({
policyVersion: 1,
operationalMode: 'normal',
capabilities: { 'test-feature': 'disabled' },
maintenanceAllowlist: { mutate: [], view: [] },
loadedFrom: 'defaults',
loadedAtIso: new Date().toISOString(),
});
await expect(guard.canActivate(mockContext)).rejects.toThrow(NotFoundException);
await expect(guard.canActivate(mockContext)).rejects.toThrow('Not Found');
});
it('should throw NotFoundException when feature is hidden', async () => {
const mockContext = {
getHandler: () => () => {},
getClass: () => class {},
} as unknown as ExecutionContext;
const metadata: FeatureAvailabilityMetadata = {
capabilityKey: 'test-feature',
actionType: 'view',
};
reflector.getAllAndOverride.mockReturnValue(metadata);
policyService.getSnapshot.mockResolvedValue({
policyVersion: 1,
operationalMode: 'normal',
capabilities: { 'test-feature': 'hidden' },
maintenanceAllowlist: { mutate: [], view: [] },
loadedFrom: 'defaults',
loadedAtIso: new Date().toISOString(),
});
await expect(guard.canActivate(mockContext)).rejects.toThrow(NotFoundException);
await expect(guard.canActivate(mockContext)).rejects.toThrow('Not Found');
});
it('should throw NotFoundException when feature is coming_soon', async () => {
const mockContext = {
getHandler: () => () => {},
getClass: () => class {},
} as unknown as ExecutionContext;
const metadata: FeatureAvailabilityMetadata = {
capabilityKey: 'test-feature',
actionType: 'view',
};
reflector.getAllAndOverride.mockReturnValue(metadata);
policyService.getSnapshot.mockResolvedValue({
policyVersion: 1,
operationalMode: 'normal',
capabilities: { 'test-feature': 'coming_soon' },
maintenanceAllowlist: { mutate: [], view: [] },
loadedFrom: 'defaults',
loadedAtIso: new Date().toISOString(),
});
await expect(guard.canActivate(mockContext)).rejects.toThrow(NotFoundException);
await expect(guard.canActivate(mockContext)).rejects.toThrow('Not Found');
});
it('should throw NotFoundException when feature is not configured', async () => {
const mockContext = {
getHandler: () => () => {},
getClass: () => class {},
} as unknown as ExecutionContext;
const metadata: FeatureAvailabilityMetadata = {
capabilityKey: 'test-feature',
actionType: 'view',
};
reflector.getAllAndOverride.mockReturnValue(metadata);
policyService.getSnapshot.mockResolvedValue({
policyVersion: 1,
operationalMode: 'normal',
capabilities: {},
maintenanceAllowlist: { mutate: [], view: [] },
loadedFrom: 'defaults',
loadedAtIso: new Date().toISOString(),
});
await expect(guard.canActivate(mockContext)).rejects.toThrow(NotFoundException);
await expect(guard.canActivate(mockContext)).rejects.toThrow('Not Found');
});
});
describe('inferActionTypeFromHttpMethod', () => {
it('should return "view" for GET requests', () => {
expect(inferActionTypeFromHttpMethod('GET')).toBe('view');
});
it('should return "view" for HEAD requests', () => {
expect(inferActionTypeFromHttpMethod('HEAD')).toBe('view');
});
it('should return "view" for OPTIONS requests', () => {
expect(inferActionTypeFromHttpMethod('OPTIONS')).toBe('view');
});
it('should return "mutate" for POST requests', () => {
expect(inferActionTypeFromHttpMethod('POST')).toBe('mutate');
});
it('should return "mutate" for PUT requests', () => {
expect(inferActionTypeFromHttpMethod('PUT')).toBe('mutate');
});
it('should return "mutate" for PATCH requests', () => {
expect(inferActionTypeFromHttpMethod('PATCH')).toBe('mutate');
});
it('should return "mutate" for DELETE requests', () => {
expect(inferActionTypeFromHttpMethod('DELETE')).toBe('mutate');
});
it('should handle lowercase HTTP methods', () => {
expect(inferActionTypeFromHttpMethod('get')).toBe('view');
expect(inferActionTypeFromHttpMethod('post')).toBe('mutate');
});
});
});

View File

@@ -9,7 +9,7 @@ import { Reflector } from '@nestjs/core';
import { ActionType, FeatureState, PolicyService } from './PolicyService';
import { FEATURE_AVAILABILITY_METADATA_KEY, FeatureAvailabilityMetadata } from './RequireCapability';
type Evaluation = { allow: true } | { allow: false; reason: 'maintenance' | FeatureState | 'not_configured' };
type Evaluation = { allow: true; reason?: undefined } | { allow: false; reason: 'maintenance' | FeatureState | 'not_configured' };
@Injectable()
export class FeatureAvailabilityGuard implements CanActivate {

View File

@@ -0,0 +1,37 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PolicyController } from './PolicyController';
import { PolicyModule } from './PolicyModule';
import { PolicyService } from './PolicyService';
import { FeatureAvailabilityGuard } from './FeatureAvailabilityGuard';
describe('PolicyModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [PolicyModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should provide PolicyController', () => {
const controller = module.get<PolicyController>(PolicyController);
expect(controller).toBeDefined();
expect(controller).toBeInstanceOf(PolicyController);
});
it('should provide PolicyService', () => {
const service = module.get<PolicyService>(PolicyService);
expect(service).toBeDefined();
expect(service).toBeInstanceOf(PolicyService);
});
it('should provide FeatureAvailabilityGuard', () => {
const guard = module.get<FeatureAvailabilityGuard>(FeatureAvailabilityGuard);
expect(guard).toBeDefined();
expect(guard).toBeInstanceOf(FeatureAvailabilityGuard);
});
});

View File

@@ -0,0 +1,68 @@
import { SetMetadata } from '@nestjs/common';
import { RequireCapability, FEATURE_AVAILABILITY_METADATA_KEY, FeatureAvailabilityMetadata } from './RequireCapability';
import { ActionType } from './PolicyService';
// Mock SetMetadata
vi.mock('@nestjs/common', () => ({
SetMetadata: vi.fn(),
}));
describe('RequireCapability', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should call SetMetadata with correct key and metadata', () => {
const capabilityKey = 'test-feature';
const actionType: ActionType = 'view';
RequireCapability(capabilityKey, actionType);
expect(SetMetadata).toHaveBeenCalledWith(
FEATURE_AVAILABILITY_METADATA_KEY,
{
capabilityKey,
actionType,
} satisfies FeatureAvailabilityMetadata
);
});
it('should work with mutate action type', () => {
const capabilityKey = 'test-feature';
const actionType: ActionType = 'mutate';
RequireCapability(capabilityKey, actionType);
expect(SetMetadata).toHaveBeenCalledWith(
FEATURE_AVAILABILITY_METADATA_KEY,
{
capabilityKey,
actionType,
} satisfies FeatureAvailabilityMetadata
);
});
it('should work with different capability keys', () => {
const capabilityKey = 'another-feature';
const actionType: ActionType = 'view';
RequireCapability(capabilityKey, actionType);
expect(SetMetadata).toHaveBeenCalledWith(
FEATURE_AVAILABILITY_METADATA_KEY,
{
capabilityKey,
actionType,
} satisfies FeatureAvailabilityMetadata
);
});
it('should return a decorator function', () => {
const capabilityKey = 'test-feature';
const actionType: ActionType = 'view';
const decorator = RequireCapability(capabilityKey, actionType);
expect(typeof decorator).toBe('function');
});
});