import { Authenticator } from '../../src/auth/authenticator.js';
import { FreshdeskConfig } from '../../src/core/types.js';
describe('Authenticator', () => {
const validConfig: FreshdeskConfig = {
domain: 'testcompany',
apiKey: 'test-api-key-123456789',
};
const validFullDomainConfig: FreshdeskConfig = {
domain: 'testcompany.freshdesk.com',
apiKey: 'test-api-key-123456789',
};
describe('Constructor and Initialization', () => {
it('should initialize successfully with valid config', () => {
expect(() => new Authenticator(validConfig)).not.toThrow();
});
it('should initialize successfully with full domain format', () => {
expect(() => new Authenticator(validFullDomainConfig)).not.toThrow();
});
it('should throw error when API key is missing', () => {
const config = { ...validConfig, apiKey: '' };
expect(() => new Authenticator(config)).toThrow('API key is required for Freshdesk authentication');
});
it('should throw error when API key is undefined', () => {
const config = { domain: 'testcompany' } as FreshdeskConfig;
expect(() => new Authenticator(config)).toThrow('API key is required for Freshdesk authentication');
});
it('should throw error when domain is missing', () => {
const config = { ...validConfig, domain: '' };
expect(() => new Authenticator(config)).toThrow('Domain is required for Freshdesk authentication');
});
it('should throw error when domain is undefined', () => {
const config = { apiKey: 'test-key' } as FreshdeskConfig;
expect(() => new Authenticator(config)).toThrow('Domain is required for Freshdesk authentication');
});
});
describe('Domain Validation', () => {
describe('Valid domain formats', () => {
const validDomains = [
'company',
'test-company',
'company123',
'123company',
'a1b2c3',
'company.freshdesk.com',
'test-company.freshdesk.com',
'company123.freshdesk.com',
'123company.freshdesk.com',
'a1b2c3.freshdesk.com',
];
validDomains.forEach(domain => {
it(`should accept valid domain: ${domain}`, () => {
const config = { ...validConfig, domain };
expect(() => new Authenticator(config)).not.toThrow();
});
});
});
describe('Invalid domain formats', () => {
const invalidDomains = [
'-company', // starts with hyphen
'company-', // ends with hyphen
'comp--any', // double hyphen
'company.', // ends with dot
'.company', // starts with dot
'comp any', // contains space
'company@test', // contains special characters
'company#test', // contains hash
'company$test', // contains dollar sign
'company%test', // contains percent
'company^test', // contains caret
'company&test', // contains ampersand
'company*test', // contains asterisk
'company+test', // contains plus
'company=test', // contains equals
'company[test]', // contains brackets
'company{test}', // contains braces
'company|test', // contains pipe
'company\\test', // contains backslash
'company:test', // contains colon
'company;test', // contains semicolon
'company"test', // contains quote
"company'test", // contains apostrophe
'company<test>', // contains angle brackets
'company?test', // contains question mark
'company/test', // contains slash
'company..test', // double dot
'company.test.com', // wrong domain format
'company.example.com', // wrong domain
'a', // single character
'', // empty string
' ', // whitespace only
];
invalidDomains.forEach(domain => {
it(`should reject invalid domain: "${domain}"`, () => {
const config = { ...validConfig, domain };
expect(() => new Authenticator(config)).toThrow();
});
});
});
describe('Security edge cases for domain validation', () => {
const maliciousDomains = [
'../../../etc/passwd',
'../../config',
'localhost',
'127.0.0.1',
'0.0.0.0',
'file:///etc/passwd',
'http://malicious.com',
'https://malicious.com',
'ftp://malicious.com',
'javascript:alert(1)',
'<script>alert(1)</script>',
'company; DROP TABLE users;',
"company'; DROP TABLE users; --",
'company\x00.freshdesk.com',
'company\r\n.freshdesk.com',
'company\t.freshdesk.com',
];
maliciousDomains.forEach(domain => {
it(`should reject potentially malicious domain: "${domain}"`, () => {
const config = { ...validConfig, domain };
expect(() => new Authenticator(config)).toThrow();
});
});
});
});
describe('API Key Validation', () => {
let authenticator: Authenticator;
beforeEach(() => {
authenticator = new Authenticator(validConfig);
});
it('should validate API key with normal length', () => {
expect(authenticator.validateApiKey()).toBe(true);
});
it('should validate single character API key', () => {
const config = { ...validConfig, apiKey: 'a' };
const auth = new Authenticator(config);
expect(auth.validateApiKey()).toBe(true);
});
it('should validate maximum length API key (64 characters)', () => {
const longKey = 'a'.repeat(64);
const config = { ...validConfig, apiKey: longKey };
const auth = new Authenticator(config);
expect(auth.validateApiKey()).toBe(true);
});
it('should invalidate API key exceeding maximum length', () => {
const tooLongKey = 'a'.repeat(65);
const config = { ...validConfig, apiKey: tooLongKey };
const auth = new Authenticator(config);
expect(auth.validateApiKey()).toBe(false);
});
describe('Security edge cases for API key validation', () => {
const securityTestCases = [
{
name: 'API key with null bytes',
key: 'test\x00key',
shouldPass: false
},
{
name: 'API key with control characters',
key: 'test\r\nkey',
shouldPass: false
},
{
name: 'API key with unicode characters',
key: 'test🔑key',
shouldPass: false
},
{
name: 'API key with SQL injection attempt',
key: "'; DROP TABLE users; --",
shouldPass: false
},
{
name: 'API key with script injection attempt',
key: '<script>alert(1)</script>',
shouldPass: false
},
{
name: 'Normal API key with special but valid characters',
key: 'abc123_-+=.',
shouldPass: true
},
];
securityTestCases.forEach(({ name, key, shouldPass }) => {
it(`should ${shouldPass ? 'accept' : 'handle'} ${name}`, () => {
const config = { ...validConfig, apiKey: key };
const auth = new Authenticator(config);
if (shouldPass) {
expect(auth.validateApiKey()).toBe(key.length <= 64);
} else {
// The constructor should not throw for these cases as they're handled in validation
expect(() => new Authenticator(config)).not.toThrow();
// But validation should pass based on length only (current implementation)
expect(auth.validateApiKey()).toBe(key.length > 0 && key.length <= 64);
}
});
});
});
});
describe('API Key Masking', () => {
it('should mask short API keys (8 characters or less)', () => {
const shortKeys = ['a', 'ab', 'abc', 'abcd', 'abcde', 'abcdef', 'abcdefg', 'abcdefgh'];
shortKeys.forEach(key => {
const config = { ...validConfig, apiKey: key };
const auth = new Authenticator(config);
expect(auth.maskApiKey()).toBe('****');
});
});
it('should mask long API keys showing first and last 4 characters', () => {
const longKey = 'abcdefghijklmnop';
const config = { ...validConfig, apiKey: longKey };
const auth = new Authenticator(config);
expect(auth.maskApiKey()).toBe('abcd...mnop');
});
it('should mask 9-character API key', () => {
const key = 'abcdefghi';
const config = { ...validConfig, apiKey: key };
const auth = new Authenticator(config);
expect(auth.maskApiKey()).toBe('abcd...fghi');
});
it('should mask very long API key', () => {
const veryLongKey = 'abcdefghijklmnopqrstuvwxyz1234567890';
const config = { ...validConfig, apiKey: veryLongKey };
const auth = new Authenticator(config);
expect(auth.maskApiKey()).toBe('abcd...7890');
});
describe('Security considerations for API key masking', () => {
it('should not leak API key content in error messages', () => {
const sensitiveKey = 'super-secret-api-key-123456';
const config = { ...validConfig, apiKey: sensitiveKey };
const auth = new Authenticator(config);
const masked = auth.maskApiKey();
expect(masked).not.toContain('super-secret');
expect(masked).not.toContain('api-key');
expect(masked).toBe('supe...3456');
});
it('should handle API key with special characters in masking', () => {
const specialKey = 'abc!@#$%^&*()xyz';
const config = { ...validConfig, apiKey: specialKey };
const auth = new Authenticator(config);
expect(auth.maskApiKey()).toBe('abc!...)xyz');
});
});
});
describe('Auth Header Generation', () => {
let authenticator: Authenticator;
beforeEach(() => {
authenticator = new Authenticator(validConfig);
});
it('should generate correct basic auth header', () => {
const headers = authenticator.getAuthHeader();
expect(headers).toHaveProperty('Authorization');
expect(headers).toHaveProperty('Content-Type');
expect(headers['Content-Type']).toBe('application/json');
// Verify the authorization header format
expect(headers['Authorization']).toMatch(/^Basic [A-Za-z0-9+/]+=*$/);
});
it('should generate correct base64 encoded credentials', () => {
const headers = authenticator.getAuthHeader();
const authHeader = headers['Authorization'];
expect(authHeader).toBeDefined();
const base64Credentials = authHeader!.replace('Basic ', '');
const decodedCredentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
expect(decodedCredentials).toBe(`${validConfig.apiKey}:X`);
});
it('should use "X" as password for Freshdesk Basic Auth', () => {
const headers = authenticator.getAuthHeader();
const authHeader = headers['Authorization'];
expect(authHeader).toBeDefined();
const base64Credentials = authHeader!.replace('Basic ', '');
const decodedCredentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
expect(decodedCredentials.split(':')[1]).toBe('X');
});
describe('Security tests for auth header generation', () => {
it('should properly encode API keys with special characters', () => {
const specialKey = 'key:with!special@chars#$%^&*()';
const config = { ...validConfig, apiKey: specialKey };
const auth = new Authenticator(config);
const headers = auth.getAuthHeader();
const authHeader = headers['Authorization'];
expect(authHeader).toBeDefined();
const base64Credentials = authHeader!.replace('Basic ', '');
const decodedCredentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
expect(decodedCredentials).toBe(`${specialKey}:X`);
});
it('should handle API keys with unicode characters', () => {
const unicodeKey = 'key-with-🔑-unicode';
const config = { ...validConfig, apiKey: unicodeKey };
const auth = new Authenticator(config);
const headers = auth.getAuthHeader();
const authHeader = headers['Authorization'];
expect(authHeader).toBeDefined();
const base64Credentials = authHeader!.replace('Basic ', '');
const decodedCredentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
expect(decodedCredentials).toBe(`${unicodeKey}:X`);
});
it('should not expose API key in plain text within header object', () => {
const headers = authenticator.getAuthHeader();
const headerString = JSON.stringify(headers);
expect(headerString).not.toContain(validConfig.apiKey);
});
it('should generate consistent headers for same API key', () => {
const headers1 = authenticator.getAuthHeader();
const headers2 = authenticator.getAuthHeader();
expect(headers1).toEqual(headers2);
});
});
});
describe('Base URL Construction', () => {
it('should construct correct URL for subdomain-only format', () => {
const auth = new Authenticator(validConfig);
expect(auth.getBaseUrl()).toBe('https://testcompany.freshdesk.com/api/v2');
});
it('should construct correct URL for full domain format', () => {
const auth = new Authenticator(validFullDomainConfig);
expect(auth.getBaseUrl()).toBe('https://testcompany.freshdesk.com/api/v2');
});
it('should handle domain with hyphens', () => {
const config = { ...validConfig, domain: 'test-company-123' };
const auth = new Authenticator(config);
expect(auth.getBaseUrl()).toBe('https://test-company-123.freshdesk.com/api/v2');
});
it('should handle domain with numbers', () => {
const config = { ...validConfig, domain: '123company456' };
const auth = new Authenticator(config);
expect(auth.getBaseUrl()).toBe('https://123company456.freshdesk.com/api/v2');
});
describe('Security tests for base URL construction', () => {
it('should not allow URL injection through domain', () => {
// These should be caught by domain validation, but testing URL construction
const validDomain = 'testcompany';
const config = { ...validConfig, domain: validDomain };
const auth = new Authenticator(config);
const baseUrl = auth.getBaseUrl();
expect(baseUrl).toBe('https://testcompany.freshdesk.com/api/v2');
expect(baseUrl).not.toContain('../');
expect(baseUrl).not.toContain('///'); // Triple slash is bad
expect(baseUrl).not.toContain('http://'); // Should be HTTPS only
});
it('should always use HTTPS protocol', () => {
const auth = new Authenticator(validConfig);
const baseUrl = auth.getBaseUrl();
expect(baseUrl).toMatch(/^https:\/\//);
expect(baseUrl).not.toMatch(/^http:\/\//);
});
it('should maintain consistent URL structure', () => {
const auth = new Authenticator(validConfig);
const baseUrl = auth.getBaseUrl();
expect(baseUrl).toMatch(/^https:\/\/[a-zA-Z0-9-]+\.freshdesk\.com\/api\/v2$/);
});
it('should not allow protocol injection', () => {
// Domain validation should prevent this, but testing URL construction
const validDomain = 'testcompany';
const config = { ...validConfig, domain: validDomain };
const auth = new Authenticator(config);
const baseUrl = auth.getBaseUrl();
expect(baseUrl).not.toContain('file://');
expect(baseUrl).not.toContain('ftp://');
expect(baseUrl).not.toContain('javascript:');
expect(baseUrl).not.toContain('data:');
});
});
});
describe('Domain Getter', () => {
it('should return the configured domain', () => {
const auth = new Authenticator(validConfig);
expect(auth.getDomain()).toBe(validConfig.domain);
});
it('should return full domain when configured with full domain', () => {
const auth = new Authenticator(validFullDomainConfig);
expect(auth.getDomain()).toBe(validFullDomainConfig.domain);
});
it('should return exact domain value without modification', () => {
const domainWithHyphens = 'test-company-123';
const config = { ...validConfig, domain: domainWithHyphens };
const auth = new Authenticator(config);
expect(auth.getDomain()).toBe(domainWithHyphens);
});
});
describe('Integration Tests', () => {
it('should work end-to-end with valid configuration', () => {
const auth = new Authenticator(validConfig);
expect(auth.validateApiKey()).toBe(true);
expect(auth.getDomain()).toBe(validConfig.domain);
expect(auth.getBaseUrl()).toBe('https://testcompany.freshdesk.com/api/v2');
expect(auth.maskApiKey()).toMatch(/^.{4}\.\.\..{4}$/);
const headers = auth.getAuthHeader();
expect(headers).toHaveProperty('Authorization');
expect(headers).toHaveProperty('Content-Type');
});
it('should maintain security properties across all methods', () => {
const sensitiveKey = 'very-secret-api-key-12345';
const config = { ...validConfig, apiKey: sensitiveKey };
const auth = new Authenticator(config);
// API key should not appear in plain text in any output
const maskedKey = auth.maskApiKey();
const headers = auth.getAuthHeader();
const baseUrl = auth.getBaseUrl();
const domain = auth.getDomain();
expect(maskedKey).not.toContain('very-secret');
expect(JSON.stringify(headers)).not.toContain(sensitiveKey);
expect(baseUrl).not.toContain(sensitiveKey);
expect(domain).not.toContain(sensitiveKey);
});
});
describe('Error Handling and Edge Cases', () => {
it('should handle empty string inputs gracefully during validation', () => {
// Constructor should throw for empty strings
expect(() => new Authenticator({ domain: '', apiKey: 'test' })).toThrow();
expect(() => new Authenticator({ domain: 'test', apiKey: '' })).toThrow();
});
it('should handle whitespace-only inputs', () => {
expect(() => new Authenticator({ domain: ' ', apiKey: 'test' })).toThrow();
expect(() => new Authenticator({ domain: 'test', apiKey: ' ' })).toThrow();
});
it('should handle null and undefined inputs', () => {
expect(() => new Authenticator({ domain: null as any, apiKey: 'test' })).toThrow();
expect(() => new Authenticator({ domain: 'test', apiKey: null as any })).toThrow();
expect(() => new Authenticator({ domain: undefined as any, apiKey: 'test' })).toThrow();
expect(() => new Authenticator({ domain: 'test', apiKey: undefined as any })).toThrow();
});
it('should maintain immutability of config after initialization', () => {
const config = { ...validConfig };
const auth = new Authenticator(config);
// Modify original config
config.apiKey = 'modified-key';
config.domain = 'modified-domain';
// Authenticator should still have original values
expect(auth.getDomain()).toBe(validConfig.domain);
expect(auth.maskApiKey()).toBe('test...6789');
});
});
});