import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as fc from 'fast-check';
import { AuthManager, AuthSession, GoogleUserInfo, JWTPayload } from './auth-manager.js';
/**
* Feature: aws-billing-mcp-server, Property 9: Authentication state management
* Validates: Requirements 4.2, 4.3, 4.4
*
* Property: For any authentication event (success, failure, expiration), the system should
* maintain consistent session state and require appropriate authentication actions
*/
describe('Authentication State Management Property Tests', () => {
it('Property 9: Authentication state management - session lifecycle is consistent', () => {
fc.assert(
fc.property(
fc.record({
userId: fc.string({ minLength: 1, maxLength: 50 }),
email: fc.emailAddress(),
sessionDuration: fc.integer({ min: 1000, max: 86400000 }), // 1 second to 24 hours
permissions: fc.array(fc.constantFrom('billing:read', 'billing:write', 'admin', 'accounts:manage'), { minLength: 1, maxLength: 4 })
}),
(sessionData) => {
const sessionId = 'sess_' + Math.random().toString(36).substr(2);
const now = new Date();
const expiresAt = new Date(now.getTime() + sessionData.sessionDuration);
const session: AuthSession = {
id: sessionId,
userId: sessionData.userId,
email: sessionData.email,
accessToken: 'mock_access_token',
refreshToken: 'mock_refresh_token',
expiresAt,
permissions: sessionData.permissions,
createdAt: now,
updatedAt: now
};
// Property: Session should have all required fields
expect(session.id).toBeDefined();
expect(session.userId).toBe(sessionData.userId);
expect(session.email).toBe(sessionData.email);
expect(session.accessToken).toBeDefined();
expect(session.expiresAt).toBeInstanceOf(Date);
expect(session.permissions).toEqual(sessionData.permissions);
expect(session.createdAt).toBeInstanceOf(Date);
expect(session.updatedAt).toBeInstanceOf(Date);
// Property: Session expiration should be in the future when created
expect(session.expiresAt.getTime()).toBeGreaterThan(now.getTime());
// Property: Session ID should follow expected format
expect(session.id).toMatch(/^sess_[a-z0-9]+$/);
// Property: Email should be valid
expect(session.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
// Property: Permissions should be valid
const validPermissions = ['billing:read', 'billing:write', 'admin', 'accounts:manage'];
session.permissions.forEach(permission => {
expect(validPermissions).toContain(permission);
});
}
),
{ numRuns: 100 }
);
});
it('Property 9: Authentication state management - JWT tokens are consistent with sessions', () => {
fc.assert(
fc.property(
fc.record({
userId: fc.string({ minLength: 1, maxLength: 50 }),
email: fc.emailAddress(),
permissions: fc.array(fc.constantFrom('billing:read', 'billing:write', 'admin'), { minLength: 1, maxLength: 3 }),
sessionId: fc.string({ minLength: 10, maxLength: 30 })
}),
(data) => {
const now = new Date();
const expiresAt = new Date(now.getTime() + 3600000); // 1 hour
const session: AuthSession = {
id: data.sessionId,
userId: data.userId,
email: data.email,
accessToken: 'mock_token',
expiresAt,
permissions: data.permissions,
createdAt: now,
updatedAt: now
};
// Simulate JWT payload creation
const jwtPayload: JWTPayload = {
userId: session.userId,
email: session.email,
permissions: session.permissions,
sessionId: session.id,
iat: Math.floor(now.getTime() / 1000),
exp: Math.floor(expiresAt.getTime() / 1000)
};
// Property: JWT payload should match session data
expect(jwtPayload.userId).toBe(session.userId);
expect(jwtPayload.email).toBe(session.email);
expect(jwtPayload.permissions).toEqual(session.permissions);
expect(jwtPayload.sessionId).toBe(session.id);
// Property: JWT timestamps should be consistent
expect(jwtPayload.exp).toBeGreaterThan(jwtPayload.iat);
expect(jwtPayload.iat).toBeLessThanOrEqual(Math.floor(Date.now() / 1000));
// Property: Expiration should match session expiration
expect(jwtPayload.exp).toBe(Math.floor(session.expiresAt.getTime() / 1000));
}
),
{ numRuns: 100 }
);
});
it('Property 9: Authentication state management - session expiration is handled correctly', () => {
fc.assert(
fc.property(
fc.record({
userId: fc.string({ minLength: 1, maxLength: 50 }),
email: fc.emailAddress(),
sessionAge: fc.integer({ min: -86400000, max: 86400000 }) // -24h to +24h from now
}),
(data) => {
const now = new Date();
const sessionCreatedAt = new Date(now.getTime() - Math.abs(data.sessionAge));
const sessionExpiresAt = new Date(now.getTime() + data.sessionAge);
const session: AuthSession = {
id: 'sess_test',
userId: data.userId,
email: data.email,
accessToken: 'mock_token',
expiresAt: sessionExpiresAt,
permissions: ['billing:read'],
createdAt: sessionCreatedAt,
updatedAt: sessionCreatedAt
};
const isExpired = session.expiresAt < now;
const shouldBeValid = session.expiresAt >= now;
// Property: Session validity should be determined by expiration time
if (data.sessionAge > 0) {
expect(shouldBeValid).toBe(true);
expect(isExpired).toBe(false);
} else if (data.sessionAge < 0) {
expect(shouldBeValid).toBe(false);
expect(isExpired).toBe(true);
} else {
// sessionAge === 0, session expires exactly now (edge case)
// This could be either valid or invalid depending on timing precision
expect(isExpired).toBe(shouldBeValid === false);
}
// Property: Created time should be before expiration time
expect(session.createdAt.getTime()).toBeLessThanOrEqual(session.expiresAt.getTime());
}
),
{ numRuns: 100 }
);
});
it('Property 9: Authentication state management - Google user info validation', () => {
fc.assert(
fc.property(
fc.record({
sub: fc.string({ minLength: 10, maxLength: 30 }),
email: fc.emailAddress(),
name: fc.string({ minLength: 1, maxLength: 100 }),
picture: fc.option(fc.webUrl(), { nil: undefined }),
email_verified: fc.boolean()
}),
(userData) => {
const userInfo: GoogleUserInfo = {
sub: userData.sub,
email: userData.email,
name: userData.name,
picture: userData.picture,
email_verified: userData.email_verified
};
// Property: All required fields should be present
expect(userInfo.sub).toBeDefined();
expect(userInfo.email).toBeDefined();
expect(userInfo.name).toBeDefined();
expect(typeof userInfo.email_verified).toBe('boolean');
// Property: Email should be valid format
expect(userInfo.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
// Property: Sub (user ID) should be non-empty
expect(userInfo.sub.length).toBeGreaterThan(0);
// Property: Name should be non-empty
expect(userInfo.name.length).toBeGreaterThan(0);
// Property: Picture URL should be valid if present
if (userInfo.picture) {
expect(userInfo.picture).toMatch(/^https?:\/\/.+/);
}
}
),
{ numRuns: 100 }
);
});
it('Property 9: Authentication state management - session state transitions are valid', () => {
fc.assert(
fc.property(
fc.array(
fc.constantFrom('create', 'validate', 'refresh', 'expire', 'delete'),
{ minLength: 1, maxLength: 10 }
),
(actions) => {
let sessionExists = false;
let sessionValid = false;
let sessionExpired = false;
// Simulate session state transitions
actions.forEach(action => {
switch (action) {
case 'create':
sessionExists = true;
sessionValid = true;
sessionExpired = false;
break;
case 'validate':
// Validation only succeeds if session exists and is not expired
sessionValid = sessionExists && !sessionExpired;
break;
case 'refresh':
// Refresh only works if session exists (even if expired)
if (sessionExists) {
sessionValid = true;
sessionExpired = false;
}
break;
case 'expire':
if (sessionExists) {
sessionExpired = true;
sessionValid = false;
}
break;
case 'delete':
sessionExists = false;
sessionValid = false;
sessionExpired = false;
break;
}
// Property: State consistency rules
if (!sessionExists) {
expect(sessionValid).toBe(false);
expect(sessionExpired).toBe(false);
}
if (sessionExpired) {
expect(sessionValid).toBe(false);
}
if (sessionValid) {
expect(sessionExists).toBe(true);
expect(sessionExpired).toBe(false);
}
});
}
),
{ numRuns: 100 }
);
});
});
/**
* Feature: aws-billing-mcp-server, Property 10: Authorization validation
* Validates: Requirements 4.5
*
* Property: For any user permission scenario, access control should be consistently
* enforced based on the configured permission rules
*/
describe('Authorization Validation Property Tests', () => {
it('Property 10: Authorization validation - permission checks are consistent', () => {
fc.assert(
fc.property(
fc.record({
userPermissions: fc.array(
fc.constantFrom('billing:read', 'billing:write', 'admin', 'accounts:manage', 'reports:view'),
{ minLength: 0, maxLength: 5 }
),
requiredPermission: fc.constantFrom('billing:read', 'billing:write', 'admin', 'accounts:manage', 'reports:view')
}),
(data) => {
const hasDirectPermission = data.userPermissions.includes(data.requiredPermission);
const hasAdminPermission = data.userPermissions.includes('admin');
const shouldHaveAccess = hasDirectPermission || hasAdminPermission;
// Property: User should have access if they have the required permission directly
if (hasDirectPermission) {
expect(shouldHaveAccess).toBe(true);
}
// Property: Admin permission should grant access to everything
if (hasAdminPermission) {
expect(shouldHaveAccess).toBe(true);
}
// Property: Without required permission or admin, access should be denied
if (!hasDirectPermission && !hasAdminPermission) {
expect(shouldHaveAccess).toBe(false);
}
// Property: Permission check should be deterministic
const secondCheck = data.userPermissions.includes(data.requiredPermission) ||
data.userPermissions.includes('admin');
expect(shouldHaveAccess).toBe(secondCheck);
}
),
{ numRuns: 100 }
);
});
it('Property 10: Authorization validation - multiple permission checks work correctly', () => {
fc.assert(
fc.property(
fc.record({
userPermissions: fc.array(
fc.constantFrom('billing:read', 'billing:write', 'admin', 'accounts:manage'),
{ minLength: 0, maxLength: 4 }
),
requiredPermissions: fc.array(
fc.constantFrom('billing:read', 'billing:write', 'accounts:manage'),
{ minLength: 1, maxLength: 3 }
)
}),
(data) => {
const hasAnyDirectPermission = data.requiredPermissions.some(permission =>
data.userPermissions.includes(permission)
);
const hasAdminPermission = data.userPermissions.includes('admin');
const shouldHaveAccess = hasAnyDirectPermission || hasAdminPermission;
// Property: User should have access if they have any of the required permissions
if (hasAnyDirectPermission) {
expect(shouldHaveAccess).toBe(true);
}
// Property: Admin permission should grant access to any permission set
if (hasAdminPermission) {
expect(shouldHaveAccess).toBe(true);
}
// Property: Without any required permission or admin, access should be denied
if (!hasAnyDirectPermission && !hasAdminPermission) {
expect(shouldHaveAccess).toBe(false);
}
// Property: Check each required permission individually
data.requiredPermissions.forEach(requiredPermission => {
const hasThisPermission = data.userPermissions.includes(requiredPermission) || hasAdminPermission;
if (shouldHaveAccess && hasThisPermission) {
// If user has overall access and this specific permission, that's consistent
expect(true).toBe(true);
}
});
}
),
{ numRuns: 100 }
);
});
it('Property 10: Authorization validation - permission hierarchy is respected', () => {
fc.assert(
fc.property(
fc.array(
fc.constantFrom('billing:read', 'billing:write', 'admin', 'accounts:manage'),
{ minLength: 1, maxLength: 4 }
),
(userPermissions) => {
const hasAdmin = userPermissions.includes('admin');
const hasBillingWrite = userPermissions.includes('billing:write');
const hasBillingRead = userPermissions.includes('billing:read');
const hasAccountsManage = userPermissions.includes('accounts:manage');
// Property: Admin should have access to all operations
if (hasAdmin) {
expect(hasAdmin).toBe(true); // Admin can do admin things
// Admin should be able to do billing operations
const canDoBilling = true; // Admin can do anything
expect(canDoBilling).toBe(true);
}
// Property: billing:write should imply billing:read capabilities
if (hasBillingWrite) {
// In a real system, write permission often includes read permission
const canRead = hasBillingRead || hasBillingWrite || hasAdmin;
expect(canRead).toBe(true);
}
// Property: Specific permissions should be independent unless there's hierarchy
const allPermissions = ['billing:read', 'billing:write', 'admin', 'accounts:manage'];
allPermissions.forEach(permission => {
const hasPermission = userPermissions.includes(permission) || hasAdmin;
// Admin should have all permissions
if (hasAdmin) {
expect(hasPermission).toBe(true);
}
});
}
),
{ numRuns: 100 }
);
});
it('Property 10: Authorization validation - email-based permission assignment is consistent', () => {
fc.assert(
fc.property(
fc.record({
email: fc.emailAddress(),
isAdmin: fc.boolean()
}),
(data) => {
// Simulate permission assignment logic
const adminEmails = data.isAdmin ? [data.email] : [];
const isAdminUser = adminEmails.includes(data.email);
let expectedPermissions: string[];
if (isAdminUser) {
expectedPermissions = ['admin', 'billing:read', 'billing:write', 'accounts:manage'];
} else {
expectedPermissions = ['billing:read'];
}
// Property: Admin users should get admin permissions
if (data.isAdmin) {
expect(expectedPermissions).toContain('admin');
expect(expectedPermissions).toContain('billing:read');
expect(expectedPermissions).toContain('billing:write');
expect(expectedPermissions).toContain('accounts:manage');
}
// Property: Non-admin users should get default permissions
if (!data.isAdmin) {
expect(expectedPermissions).toContain('billing:read');
expect(expectedPermissions).not.toContain('admin');
expect(expectedPermissions).not.toContain('billing:write');
expect(expectedPermissions).not.toContain('accounts:manage');
}
// Property: All users should have at least billing:read
expect(expectedPermissions).toContain('billing:read');
// Property: Permission assignment should be deterministic
const secondAssignment = isAdminUser
? ['admin', 'billing:read', 'billing:write', 'accounts:manage']
: ['billing:read'];
expect(expectedPermissions).toEqual(secondAssignment);
}
),
{ numRuns: 100 }
);
});
it('Property 10: Authorization validation - permission validation is secure by default', () => {
fc.assert(
fc.property(
fc.record({
sessionExists: fc.boolean(),
sessionValid: fc.boolean(),
userPermissions: fc.array(fc.string({ minLength: 1, maxLength: 20 }), { minLength: 0, maxLength: 5 }),
requiredPermission: fc.string({ minLength: 1, maxLength: 20 })
}),
(data) => {
// Simulate permission check logic
const canAccess = data.sessionExists &&
data.sessionValid &&
(data.userPermissions.includes(data.requiredPermission) ||
data.userPermissions.includes('admin'));
// Property: Access should be denied if session doesn't exist
if (!data.sessionExists) {
expect(canAccess).toBe(false);
}
// Property: Access should be denied if session is invalid
if (!data.sessionValid) {
expect(canAccess).toBe(false);
}
// Property: Access should be denied if user lacks required permission
if (data.sessionExists && data.sessionValid &&
!data.userPermissions.includes(data.requiredPermission) &&
!data.userPermissions.includes('admin')) {
expect(canAccess).toBe(false);
}
// Property: Access should be granted only when all conditions are met
if (data.sessionExists && data.sessionValid &&
(data.userPermissions.includes(data.requiredPermission) ||
data.userPermissions.includes('admin'))) {
expect(canAccess).toBe(true);
}
// Property: Default should be to deny access (secure by default)
if (!data.sessionExists || !data.sessionValid) {
expect(canAccess).toBe(false);
}
}
),
{ numRuns: 100 }
);
});
});