Sync DNS2 production changes - removed obsolete test suite and refactored structure
This commit is contained in:
@@ -1,367 +0,0 @@
|
||||
/**
|
||||
* @jest-environment node
|
||||
* Comprehensive tests for auth-manager.js
|
||||
* Tests JWT generation/validation, API key management, and security boundaries
|
||||
*/
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
const crypto = require('crypto');
|
||||
const AuthManager = require('../auth-manager');
|
||||
const credentialManager = require('../credential-manager');
|
||||
|
||||
// Mock credential manager
|
||||
jest.mock('../credential-manager');
|
||||
jest.mock('../logger-utils', () => ({
|
||||
safeLog: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('AuthManager', () => {
|
||||
let authManager;
|
||||
|
||||
beforeEach(() => {
|
||||
authManager = new AuthManager();
|
||||
jest.clearAllMocks();
|
||||
credentialManager.save.mockResolvedValue(true);
|
||||
credentialManager.get.mockResolvedValue(null);
|
||||
credentialManager.delete.mockResolvedValue(true);
|
||||
credentialManager.list.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
describe('JWT Generation', () => {
|
||||
test('should generate valid JWT token', async () => {
|
||||
const payload = { sub: 'user123', role: 'admin' };
|
||||
const token = await authManager.generateJWT(payload);
|
||||
|
||||
expect(token).toBeDefined();
|
||||
expect(typeof token).toBe('string');
|
||||
expect(token.split('.')).toHaveLength(3); // JWT has 3 parts
|
||||
});
|
||||
|
||||
test('should include required claims in JWT', async () => {
|
||||
const payload = { sub: 'user123', role: 'admin' };
|
||||
const token = await authManager.generateJWT(payload);
|
||||
const decoded = jwt.decode(token);
|
||||
|
||||
expect(decoded.sub).toBe('user123');
|
||||
expect(decoded.role).toBe('admin');
|
||||
expect(decoded.iat).toBeDefined();
|
||||
expect(decoded.exp).toBeDefined();
|
||||
expect(decoded.scope).toEqual(['read', 'write']); // default scopes
|
||||
});
|
||||
|
||||
test('should respect custom expiration time', async () => {
|
||||
const payload = { sub: 'user123' };
|
||||
const token = await authManager.generateJWT(payload, '1h');
|
||||
const decoded = jwt.decode(token);
|
||||
|
||||
const expectedExp = decoded.iat + 3600; // 1 hour = 3600 seconds
|
||||
expect(decoded.exp).toBeCloseTo(expectedExp, -1); // Allow 1 sec tolerance
|
||||
});
|
||||
|
||||
test('should include custom scopes', async () => {
|
||||
const payload = { sub: 'user123', scope: ['read'] };
|
||||
const token = await authManager.generateJWT(payload);
|
||||
const decoded = jwt.decode(token);
|
||||
|
||||
expect(decoded.scope).toEqual(['read']);
|
||||
});
|
||||
|
||||
test('should reject JWT generation without sub claim', async () => {
|
||||
const payload = { role: 'admin' }; // Missing sub
|
||||
await expect(authManager.generateJWT(payload))
|
||||
.rejects.toThrow('JWT payload must include "sub"');
|
||||
});
|
||||
|
||||
test('should reject JWT generation with null payload', async () => {
|
||||
await expect(authManager.generateJWT(null))
|
||||
.rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('JWT Verification', () => {
|
||||
test('should verify valid JWT token', async () => {
|
||||
const payload = { sub: 'user123', role: 'admin' };
|
||||
const token = await authManager.generateJWT(payload);
|
||||
const verified = await authManager.verifyJWT(token);
|
||||
|
||||
expect(verified).toBeDefined();
|
||||
expect(verified.userId).toBe('user123');
|
||||
expect(verified.scope).toEqual(['read', 'write']);
|
||||
expect(verified.iat).toBeDefined();
|
||||
expect(verified.exp).toBeDefined();
|
||||
});
|
||||
|
||||
test('should reject invalid JWT token', async () => {
|
||||
const invalidToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.invalid.signature';
|
||||
const verified = await authManager.verifyJWT(invalidToken);
|
||||
|
||||
expect(verified).toBeNull();
|
||||
});
|
||||
|
||||
test('should reject malformed JWT token', async () => {
|
||||
const verified = await authManager.verifyJWT('not-a-jwt-token');
|
||||
expect(verified).toBeNull();
|
||||
});
|
||||
|
||||
test('should reject expired JWT token', async () => {
|
||||
const payload = { sub: 'user123' };
|
||||
const token = await authManager.generateJWT(payload, '-1s'); // Already expired
|
||||
|
||||
// Wait a tiny bit to ensure expiration
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const verified = await authManager.verifyJWT(token);
|
||||
expect(verified).toBeNull();
|
||||
});
|
||||
|
||||
test('should reject JWT with wrong signature', async () => {
|
||||
const payload = { sub: 'user123' };
|
||||
const wrongSecret = 'wrong-secret-key';
|
||||
const token = jwt.sign(payload, wrongSecret);
|
||||
|
||||
const verified = await authManager.verifyJWT(token);
|
||||
expect(verified).toBeNull();
|
||||
});
|
||||
|
||||
test('should handle empty token gracefully', async () => {
|
||||
const verified = await authManager.verifyJWT('');
|
||||
expect(verified).toBeNull();
|
||||
});
|
||||
|
||||
test('should handle null token gracefully', async () => {
|
||||
const verified = await authManager.verifyJWT(null);
|
||||
expect(verified).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('API Key Generation', () => {
|
||||
test('should generate valid API key', async () => {
|
||||
const result = await authManager.generateAPIKey('test-key');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.key).toMatch(/^dk_[a-f0-9]{32}_[a-f0-9]{64}$/);
|
||||
expect(result.id).toMatch(/^[a-f0-9]{32}$/);
|
||||
expect(result.name).toBe('test-key');
|
||||
expect(result.scopes).toEqual(['read', 'write']);
|
||||
expect(result.createdAt).toBeDefined();
|
||||
});
|
||||
|
||||
test('should generate unique API keys', async () => {
|
||||
const key1 = await authManager.generateAPIKey('key1');
|
||||
const key2 = await authManager.generateAPIKey('key2');
|
||||
|
||||
expect(key1.key).not.toBe(key2.key);
|
||||
expect(key1.id).not.toBe(key2.id);
|
||||
});
|
||||
|
||||
test('should accept custom scopes', async () => {
|
||||
const result = await authManager.generateAPIKey('readonly-key', ['read']);
|
||||
|
||||
expect(result.scopes).toEqual(['read']);
|
||||
});
|
||||
|
||||
test('should store API key in credential manager', async () => {
|
||||
await authManager.generateAPIKey('test-key');
|
||||
|
||||
expect(credentialManager.save).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/^auth\.apikey\./),
|
||||
expect.objectContaining({
|
||||
keySecret: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should store API key metadata', async () => {
|
||||
await authManager.generateAPIKey('test-key', ['read']);
|
||||
|
||||
expect(credentialManager.save).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/^auth\.metadata\./),
|
||||
expect.objectContaining({
|
||||
name: 'test-key',
|
||||
scopes: ['read'],
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should reject API key generation without name', async () => {
|
||||
await expect(authManager.generateAPIKey(''))
|
||||
.rejects.toThrow('API key name is required');
|
||||
|
||||
await expect(authManager.generateAPIKey(null))
|
||||
.rejects.toThrow('API key name is required');
|
||||
|
||||
await expect(authManager.generateAPIKey(undefined))
|
||||
.rejects.toThrow('API key name is required');
|
||||
});
|
||||
|
||||
test('should reject non-string name', async () => {
|
||||
await expect(authManager.generateAPIKey(123))
|
||||
.rejects.toThrow('API key name is required');
|
||||
|
||||
await expect(authManager.generateAPIKey({}))
|
||||
.rejects.toThrow('API key name is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('API Key Validation', () => {
|
||||
test('should validate correct API key', async () => {
|
||||
const { key, id } = await authManager.generateAPIKey('test-key');
|
||||
|
||||
// Mock credential manager to return the stored key
|
||||
credentialManager.get.mockResolvedValueOnce({
|
||||
keySecret: key.split('_')[2],
|
||||
});
|
||||
credentialManager.get.mockResolvedValueOnce({
|
||||
name: 'test-key',
|
||||
scopes: ['read', 'write'],
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const validated = await authManager.validateAPIKey(key);
|
||||
|
||||
expect(validated).toBeDefined();
|
||||
expect(validated.valid).toBe(true);
|
||||
expect(validated.keyId).toBe(id);
|
||||
expect(validated.scopes).toEqual(['read', 'write']);
|
||||
});
|
||||
|
||||
test('should reject malformed API key', async () => {
|
||||
const validated = await authManager.validateAPIKey('not-an-api-key');
|
||||
|
||||
expect(validated.valid).toBe(false);
|
||||
});
|
||||
|
||||
test('should reject API key with wrong prefix', async () => {
|
||||
const validated = await authManager.validateAPIKey('sk_abc123_def456');
|
||||
|
||||
expect(validated.valid).toBe(false);
|
||||
});
|
||||
|
||||
test('should reject non-existent API key', async () => {
|
||||
const fakeKey = `dk_${ crypto.randomBytes(16).toString('hex') }_${ crypto.randomBytes(32).toString('hex')}`;
|
||||
credentialManager.get.mockResolvedValue(null); // Key doesn't exist
|
||||
|
||||
const validated = await authManager.validateAPIKey(fakeKey);
|
||||
|
||||
expect(validated.valid).toBe(false);
|
||||
});
|
||||
|
||||
test('should reject revoked API key', async () => {
|
||||
const { key } = await authManager.generateAPIKey('test-key');
|
||||
|
||||
credentialManager.get.mockResolvedValueOnce({
|
||||
keySecret: key.split('_')[2],
|
||||
revoked: true, // Key is revoked
|
||||
});
|
||||
|
||||
const validated = await authManager.validateAPIKey(key);
|
||||
|
||||
expect(validated.valid).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle null key gracefully', async () => {
|
||||
const validated = await authManager.validateAPIKey(null);
|
||||
|
||||
expect(validated.valid).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle empty key gracefully', async () => {
|
||||
const validated = await authManager.validateAPIKey('');
|
||||
|
||||
expect(validated.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('API Key Revocation', () => {
|
||||
test('should revoke API key', async () => {
|
||||
const { id } = await authManager.generateAPIKey('test-key');
|
||||
|
||||
credentialManager.get.mockResolvedValue({
|
||||
keySecret: 'test-secret',
|
||||
});
|
||||
|
||||
const revoked = await authManager.revokeAPIKey(id);
|
||||
|
||||
expect(revoked).toBe(true);
|
||||
expect(credentialManager.save).toHaveBeenCalledWith(
|
||||
`auth.apikey.${id}`,
|
||||
expect.objectContaining({
|
||||
revoked: true,
|
||||
revokedAt: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should reject revoking non-existent key', async () => {
|
||||
credentialManager.get.mockResolvedValue(null);
|
||||
|
||||
await expect(authManager.revokeAPIKey('nonexistent'))
|
||||
.rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('API Key Listing', () => {
|
||||
test('should list all API keys with metadata', async () => {
|
||||
credentialManager.list.mockResolvedValue([
|
||||
'auth.metadata.key1',
|
||||
'auth.metadata.key2',
|
||||
]);
|
||||
|
||||
credentialManager.get.mockResolvedValueOnce({
|
||||
name: 'Key 1',
|
||||
scopes: ['read'],
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
});
|
||||
|
||||
credentialManager.get.mockResolvedValueOnce({
|
||||
name: 'Key 2',
|
||||
scopes: ['read', 'write'],
|
||||
createdAt: '2026-01-02T00:00:00Z',
|
||||
});
|
||||
|
||||
const keys = await authManager.listAPIKeys();
|
||||
|
||||
expect(keys).toHaveLength(2);
|
||||
expect(keys[0].name).toBe('Key 1');
|
||||
expect(keys[1].name).toBe('Key 2');
|
||||
});
|
||||
|
||||
test('should return empty array when no keys exist', async () => {
|
||||
credentialManager.list.mockResolvedValue([]);
|
||||
|
||||
const keys = await authManager.listAPIKeys();
|
||||
|
||||
expect(keys).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Security Boundaries', () => {
|
||||
test('should not log sensitive token data', async () => {
|
||||
const payload = { sub: 'user123' };
|
||||
const token = await authManager.generateJWT(payload);
|
||||
|
||||
// Logger should never be called with the actual token
|
||||
const { safeLog } = require('../logger-utils');
|
||||
const calls = safeLog.mock.calls.flat();
|
||||
expect(calls.some(arg => String(arg).includes(token))).toBe(false);
|
||||
});
|
||||
|
||||
test('should not log API key secrets', async () => {
|
||||
const { key } = await authManager.generateAPIKey('test-key');
|
||||
|
||||
const { safeLog } = require('../logger-utils');
|
||||
const calls = safeLog.mock.calls.flat();
|
||||
expect(calls.some(arg => String(arg).includes(key))).toBe(false);
|
||||
});
|
||||
|
||||
test('should generate cryptographically secure API keys', async () => {
|
||||
const key1 = await authManager.generateAPIKey('key1');
|
||||
const key2 = await authManager.generateAPIKey('key2');
|
||||
|
||||
// Keys should be unrelated (not sequential)
|
||||
expect(parseInt(key1.id, 16)).not.toBe(parseInt(key2.id, 16) + 1);
|
||||
expect(parseInt(key1.id, 16)).not.toBe(parseInt(key2.id, 16) - 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user