import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as fc from 'fast-check';
import { Logger } from './logger.js';
import winston from 'winston';
/**
* Feature: aws-billing-mcp-server, Property 13: Comprehensive logging
* Validates: Requirements 7.1, 7.2, 7.3, 7.4
*
* Property: For any system operation (requests, errors, API calls, authentication events),
* appropriate log entries should be generated with required metadata
*/
describe('Logger Property Tests', () => {
let logger: Logger;
let logOutput: string[];
let mockLogger: winston.Logger;
beforeEach(() => {
// Reset singleton instance for testing
(Logger as any).instance = undefined;
// Create array to capture log output
logOutput = [];
// Create mock logger that captures output
mockLogger = {
info: vi.fn((message: string, meta?: Record<string, unknown>) => {
const logEntry = { level: 'info', message, timestamp: new Date().toISOString(), service: 'aws-billing-mcp-server', ...meta };
logOutput.push(JSON.stringify(logEntry));
}),
error: vi.fn((message: string, meta?: Record<string, unknown>) => {
const logEntry = { level: 'error', message, timestamp: new Date().toISOString(), service: 'aws-billing-mcp-server', ...meta };
// Handle Error objects properly for serialization
if (logEntry.error && logEntry.error instanceof Error) {
logEntry.error = {
message: logEntry.error.message,
stack: logEntry.error.stack,
name: logEntry.error.name
};
}
logOutput.push(JSON.stringify(logEntry));
}),
warn: vi.fn((message: string, meta?: Record<string, unknown>) => {
const logEntry = { level: 'warn', message, timestamp: new Date().toISOString(), service: 'aws-billing-mcp-server', ...meta };
logOutput.push(JSON.stringify(logEntry));
}),
debug: vi.fn((message: string, meta?: Record<string, unknown>) => {
const logEntry = { level: 'debug', message, timestamp: new Date().toISOString(), service: 'aws-billing-mcp-server', ...meta };
logOutput.push(JSON.stringify(logEntry));
}),
add: vi.fn()
} as any;
// Mock winston.createLogger to return our mock
vi.spyOn(winston, 'createLogger').mockReturnValue(mockLogger);
logger = Logger.getInstance();
});
afterEach(() => {
vi.restoreAllMocks();
logOutput = [];
});
it('Property 13: Comprehensive logging - all operations generate appropriate log entries', () => {
fc.assert(
fc.property(
// Generate random operation data
fc.record({
message: fc.string({ minLength: 1, maxLength: 100 }),
method: fc.constantFrom('GET', 'POST', 'PUT', 'DELETE'),
path: fc.string({ minLength: 1, maxLength: 50 }),
responseTime: fc.integer({ min: 1, max: 5000 }),
statusCode: fc.constantFrom(200, 201, 400, 401, 404, 500),
service: fc.constantFrom('cost-explorer', 'billing', 'iam'),
operation: fc.string({ minLength: 1, max: 30 }),
duration: fc.integer({ min: 1, max: 10000 }),
success: fc.boolean(),
userId: fc.string({ minLength: 1, max: 20 }),
event: fc.constantFrom('login', 'logout', 'token_refresh', 'access_denied'),
securityEvent: fc.string({ minLength: 1, max: 50 }),
details: fc.record({
ip: fc.string({ minLength: 7, max: 15 }),
userAgent: fc.string({ minLength: 1, max: 100 })
})
}),
(data) => {
// Clear previous logs
logOutput.length = 0;
// Test basic logging operations (Requirements 7.1, 7.2)
logger.info(data.message);
logger.error(data.message);
logger.warn(data.message);
logger.debug(data.message);
// Test request logging (Requirements 7.1)
logger.logRequest(data.method, data.path, data.responseTime, data.statusCode);
// Test AWS API call logging (Requirements 7.3)
logger.logAWSAPICall(data.service, data.operation, data.duration, data.success);
// Test authentication event logging (Requirements 7.4)
logger.logAuthEvent(data.event, data.userId, data.success);
// Test security event logging (Requirements 7.4)
logger.logSecurityEvent(data.securityEvent, data.details);
// Get all logged messages
const logs = logOutput;
// Verify that logs were generated for all operations
expect(logs.length).toBeGreaterThan(0);
// Parse and verify log entries
const parsedLogs = logs.map(log => {
try {
return JSON.parse(log);
} catch {
return { message: log };
}
});
// Verify basic log entries contain required fields
const basicLogs = parsedLogs.filter(log =>
log.message === data.message && !log.type
);
expect(basicLogs.length).toBeGreaterThan(0);
basicLogs.forEach(log => {
expect(log).toHaveProperty('timestamp');
expect(log).toHaveProperty('level');
expect(log).toHaveProperty('service', 'aws-billing-mcp-server');
});
// Verify request log contains required metadata (Requirements 7.1)
const requestLogs = parsedLogs.filter(log => log.type === 'request');
expect(requestLogs.length).toBe(1);
const requestLog = requestLogs[0];
expect(requestLog).toHaveProperty('method', data.method);
expect(requestLog).toHaveProperty('path', data.path);
expect(requestLog).toHaveProperty('responseTime', data.responseTime);
expect(requestLog).toHaveProperty('statusCode', data.statusCode);
// Verify AWS API log contains required metadata (Requirements 7.3)
const awsLogs = parsedLogs.filter(log => log.type === 'aws_api');
expect(awsLogs.length).toBe(1);
const awsLog = awsLogs[0];
expect(awsLog).toHaveProperty('awsService', data.service); // AWS service name from metadata
expect(awsLog).toHaveProperty('operation', data.operation);
expect(awsLog).toHaveProperty('duration', data.duration);
expect(awsLog).toHaveProperty('success', data.success);
// Verify auth event log contains required metadata (Requirements 7.4)
const authLogs = parsedLogs.filter(log => log.type === 'auth');
expect(authLogs.length).toBe(1);
const authLog = authLogs[0];
expect(authLog).toHaveProperty('event', data.event);
expect(authLog).toHaveProperty('userId', data.userId);
expect(authLog).toHaveProperty('success', data.success);
// Verify security event log contains required metadata (Requirements 7.4)
const securityLogs = parsedLogs.filter(log => log.type === 'security');
expect(securityLogs.length).toBe(1);
const securityLog = securityLogs[0];
expect(securityLog).toHaveProperty('event', data.securityEvent);
expect(securityLog).toHaveProperty('ip', data.details.ip);
expect(securityLog).toHaveProperty('userAgent', data.details.userAgent);
// Verify all logs have timestamps and service metadata
parsedLogs.forEach(log => {
expect(log).toHaveProperty('timestamp');
expect(log).toHaveProperty('service', 'aws-billing-mcp-server');
});
}
),
{ numRuns: 100 }
);
});
it('Property 13: Error logging includes stack traces and context', () => {
fc.assert(
fc.property(
fc.record({
message: fc.string({ minLength: 1, maxLength: 100 }),
errorMessage: fc.string({ minLength: 1, maxLength: 100 }),
context: fc.record({
userId: fc.string({ minLength: 1, max: 20 }),
operation: fc.string({ minLength: 1, max: 30 })
})
}),
(data) => {
logOutput.length = 0;
// Create an error with stack trace
const error = new Error(data.errorMessage);
// Log error with context
logger.error(data.message, { error, ...data.context });
const logs = logOutput;
expect(logs.length).toBeGreaterThan(0);
const parsedLog = JSON.parse(logs[0]);
// Verify error details are included (Requirements 7.2)
expect(parsedLog).toHaveProperty('message', data.message);
expect(parsedLog).toHaveProperty('error');
// The error might be serialized differently
if (typeof parsedLog.error === 'object' && parsedLog.error !== null) {
expect(parsedLog.error).toHaveProperty('message', data.errorMessage);
expect(parsedLog.error).toHaveProperty('stack');
} else {
// If error is serialized as string, just check it exists
expect(parsedLog.error).toBeDefined();
}
// Verify context is preserved
expect(parsedLog).toHaveProperty('userId', data.context.userId);
expect(parsedLog).toHaveProperty('operation', data.context.operation);
}
),
{ numRuns: 100 }
);
});
});