chore: clean .gitignore cache

This commit is contained in:
GitHub Actions
2026-01-26 19:21:33 +00:00
parent 1b1b3a70b1
commit e5f0fec5db
1483 changed files with 0 additions and 472793 deletions
-408
View File
@@ -1,408 +0,0 @@
/**
* Access List (ACL) Test Fixtures
*
* Mock data for Access List E2E tests.
* Provides various ACL configurations for testing CRUD operations,
* rule management, and validation scenarios.
*
* The backend expects AccessList with:
* - type: 'whitelist' | 'blacklist' | 'geo_whitelist' | 'geo_blacklist'
* - ip_rules: JSON string of {cidr, description} objects
* - country_codes: comma-separated ISO country codes (for geo types)
*
* @example
* ```typescript
* import { emptyAccessList, allowOnlyAccessList, invalidACLConfigs } from './fixtures/access-lists';
*
* test('create access list with allow rules', async ({ testData }) => {
* const { id } = await testData.createAccessList(allowOnlyAccessList);
* });
* ```
*/
import { generateUniqueId, generateIPAddress, generateCIDR } from './test-data';
import type { AccessListData } from '../utils/TestDataManager';
import * as crypto from 'crypto';
/**
* Generate an unbiased random integer in [0, 9999] using rejection sampling.
* Avoids modulo bias from 16-bit source.
*/
function getRandomIntBelow10000(): number {
const maxExclusive = 10000;
const limit = 60000; // Largest multiple of 10000 <= 65535
while (true) {
const value = crypto.randomBytes(2).readUInt16BE(0);
if (value < limit) {
return value % maxExclusive;
}
}
}
/**
* ACL type - matches backend ValidAccessListTypes
*/
export type ACLType = 'whitelist' | 'blacklist' | 'geo_whitelist' | 'geo_blacklist';
/**
* Single ACL IP rule configuration (matches backend AccessListRule)
*/
export interface ACLRule {
/** CIDR notation IP or range */
cidr: string;
/** Optional description */
description?: string;
}
/**
* Complete access list configuration (matches backend AccessList model)
*/
export interface AccessListConfig extends AccessListData {
/** Optional description */
description?: string;
}
/**
* Empty access list (whitelist with no rules)
* Useful for testing empty state
*/
export const emptyAccessList: AccessListConfig = {
name: 'Empty ACL',
type: 'whitelist',
ipRules: [],
description: 'Access list with no rules',
};
/**
* Allow-only access list (whitelist)
* Contains CIDR ranges for private networks
*/
export const allowOnlyAccessList: AccessListConfig = {
name: 'Allow Only ACL',
type: 'whitelist',
ipRules: [
{ cidr: '192.168.1.0/24', description: 'Local network' },
{ cidr: '10.0.0.0/8', description: 'Private network' },
{ cidr: '172.16.0.0/12', description: 'Docker network' },
],
description: 'Access list with only allow rules',
};
/**
* Deny-only access list (blacklist)
* Blocks specific IP ranges
*/
export const denyOnlyAccessList: AccessListConfig = {
name: 'Deny Only ACL',
type: 'blacklist',
ipRules: [
{ cidr: '192.168.100.0/24', description: 'Blocked subnet' },
{ cidr: '10.255.0.1/32', description: 'Specific blocked IP' },
{ cidr: '203.0.113.0/24', description: 'TEST-NET-3' },
],
description: 'Access list with deny rules (blacklist)',
};
/**
* Whitelist for specific IPs
* Only allows traffic from specific IP ranges
*/
export const mixedRulesAccessList: AccessListConfig = {
name: 'Mixed Rules ACL',
type: 'whitelist',
ipRules: [
{ cidr: '192.168.1.100/32', description: 'Allowed specific IP' },
{ cidr: '10.0.0.0/8', description: 'Allow internal' },
],
description: 'Access list with whitelisted IPs',
};
/**
* Allow all access list (whitelist with 0.0.0.0/0)
*/
export const allowAllAccessList: AccessListConfig = {
name: 'Allow All ACL',
type: 'whitelist',
ipRules: [{ cidr: '0.0.0.0/0', description: 'Allow all' }],
description: 'Access list that allows all traffic',
};
/**
* Deny all access list (blacklist with 0.0.0.0/0)
*/
export const denyAllAccessList: AccessListConfig = {
name: 'Deny All ACL',
type: 'blacklist',
ipRules: [{ cidr: '0.0.0.0/0', description: 'Deny all' }],
description: 'Access list that denies all traffic',
};
/**
* Geo whitelist with country codes
* Only allows traffic from specific countries
*/
export const geoWhitelistAccessList: AccessListConfig = {
name: 'Geo Whitelist ACL',
type: 'geo_whitelist',
countryCodes: 'US,CA,GB',
description: 'Access list allowing only US, Canada, and UK',
};
/**
* Geo blacklist with country codes
* Blocks traffic from specific countries
*/
export const geoBlacklistAccessList: AccessListConfig = {
name: 'Geo Blacklist ACL',
type: 'geo_blacklist',
countryCodes: 'CN,RU,KP',
description: 'Access list blocking China, Russia, and North Korea',
};
/**
* Local network only access list
* Restricts to RFC1918 private networks
*/
export const localNetworkAccessList: AccessListConfig = {
name: 'Local Network ACL',
type: 'whitelist',
localNetworkOnly: true,
description: 'Access list restricted to local/private networks',
};
/**
* Single IP access list
* Most restrictive - only one IP allowed
*/
export const singleIPAccessList: AccessListConfig = {
name: 'Single IP ACL',
type: 'whitelist',
ipRules: [{ cidr: '192.168.1.50/32', description: 'Only allowed IP' }],
description: 'Access list for single IP address',
};
/**
* Access list with many rules
* For testing performance and UI with large lists
*/
export const manyRulesAccessList: AccessListConfig = {
name: 'Many Rules ACL',
type: 'whitelist',
ipRules: Array.from({ length: 50 }, (_, i) => ({
cidr: `10.${Math.floor(i / 256)}.${i % 256}.0/24`,
description: `Rule ${i + 1}`,
})),
description: 'Access list with many rules for stress testing',
};
/**
* IPv6 access list
* Contains IPv6 addresses
*/
export const ipv6AccessList: AccessListConfig = {
name: 'IPv6 ACL',
type: 'whitelist',
ipRules: [
{ cidr: '::1/128', description: 'Localhost IPv6' },
{ cidr: 'fe80::/10', description: 'Link-local' },
{ cidr: '2001:db8::/32', description: 'Documentation range' },
],
description: 'Access list with IPv6 rules',
};
/**
* Disabled access list
* For testing enable/disable functionality
*/
export const disabledAccessList: AccessListConfig = {
name: 'Disabled ACL',
type: 'blacklist',
ipRules: [{ cidr: '0.0.0.0/0' }],
description: 'Disabled access list',
enabled: false,
};
/**
* Invalid ACL configurations for validation testing
*/
export const invalidACLConfigs = {
/** Empty name */
emptyName: {
name: '',
type: 'whitelist' as const,
ipRules: [{ cidr: '192.168.1.0/24' }],
},
/** Name too long */
nameTooLong: {
name: 'A'.repeat(256),
type: 'whitelist' as const,
ipRules: [{ cidr: '192.168.1.0/24' }],
},
/** Invalid type */
invalidType: {
name: 'Invalid Type ACL',
type: 'invalid_type' as ACLType,
ipRules: [{ cidr: '192.168.1.0/24' }],
},
/** Invalid IP address */
invalidIP: {
name: 'Invalid IP ACL',
type: 'whitelist' as const,
ipRules: [{ cidr: '999.999.999.999' }],
},
/** Invalid CIDR */
invalidCIDR: {
name: 'Invalid CIDR ACL',
type: 'whitelist' as const,
ipRules: [{ cidr: '192.168.1.0/99' }],
},
/** Empty CIDR value */
emptyCIDR: {
name: 'Empty CIDR ACL',
type: 'whitelist' as const,
ipRules: [{ cidr: '' }],
},
/** XSS in name */
xssInName: {
name: '<script>alert(1)</script>',
type: 'whitelist' as const,
ipRules: [{ cidr: '192.168.1.0/24' }],
},
/** SQL injection in name */
sqlInjectionInName: {
name: "'; DROP TABLE access_lists; --",
type: 'whitelist' as const,
ipRules: [{ cidr: '192.168.1.0/24' }],
},
/** Geo type without country codes */
geoWithoutCountryCodes: {
name: 'Geo No Countries ACL',
type: 'geo_whitelist' as const,
countryCodes: '',
},
/** Invalid country code */
invalidCountryCode: {
name: 'Invalid Country ACL',
type: 'geo_whitelist' as const,
countryCodes: 'XX,YY,ZZ',
},
};
/**
* Generate a unique access list configuration
* Creates an ACL with unique name to avoid conflicts
* @param overrides - Optional configuration overrides
* @returns AccessListConfig with unique name
*
* @example
* ```typescript
* const acl = generateAccessList({ type: 'blacklist' });
* ```
*/
export function generateAccessList(
overrides: Partial<AccessListConfig> = {}
): AccessListConfig {
const id = generateUniqueId();
return {
name: `ACL-${id}`,
type: 'whitelist',
ipRules: [
{ cidr: generateCIDR(24) },
],
description: `Generated access list ${id}`,
...overrides,
};
}
/**
* Generate whitelist for specific IPs
* @param allowedIPs - Array of IP/CIDR addresses to whitelist
* @returns AccessListConfig
*/
export function generateAllowListForIPs(allowedIPs: string[]): AccessListConfig {
return {
name: `AllowList-${generateUniqueId()}`,
type: 'whitelist',
ipRules: allowedIPs.map((ip) => ({
cidr: ip.includes('/') ? ip : `${ip}/32`,
})),
description: `Whitelist for ${allowedIPs.length} IPs`,
};
}
/**
* Generate blacklist for specific IPs
* @param deniedIPs - Array of IP/CIDR addresses to blacklist
* @returns AccessListConfig
*/
export function generateDenyListForIPs(deniedIPs: string[]): AccessListConfig {
return {
name: `DenyList-${generateUniqueId()}`,
type: 'blacklist',
ipRules: deniedIPs.map((ip) => ({
cidr: ip.includes('/') ? ip : `${ip}/32`,
})),
description: `Blacklist for ${deniedIPs.length} IPs`,
};
}
/**
* Generate multiple unique access lists
* @param count - Number of access lists to generate
* @param overrides - Optional configuration overrides for all lists
* @returns Array of AccessListConfig
*/
export function generateAccessLists(
count: number,
overrides: Partial<AccessListConfig> = {}
): AccessListConfig[] {
return Array.from({ length: count }, () => generateAccessList(overrides));
}
/**
* Expected API response for access list (matches backend AccessList model)
*/
export interface AccessListAPIResponse {
id: number;
uuid: string;
name: string;
type: ACLType;
ip_rules: string;
country_codes: string;
local_network_only: boolean;
enabled: boolean;
description: string;
created_at: string;
updated_at: string;
}
/**
* Mock API response for testing
*/
export function mockAccessListResponse(
config: Partial<AccessListConfig> = {}
): AccessListAPIResponse {
const id = generateUniqueId();
return {
id: parseInt(id) || getRandomIntBelow10000(),
uuid: `acl-${id}`,
name: config.name || `ACL-${id}`,
type: config.type || 'whitelist',
ip_rules: config.ipRules ? JSON.stringify(config.ipRules) : '[]',
country_codes: config.countryCodes || '',
local_network_only: config.localNetworkOnly || false,
enabled: config.enabled !== false,
description: config.description || '',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
}
-247
View File
@@ -1,247 +0,0 @@
/**
* Auth Fixtures - Per-test user creation with role-based authentication
*
* This module extends the base Playwright test with fixtures for:
* - TestDataManager with automatic cleanup
* - Per-test user creation (admin, regular, guest roles)
* - Isolated authentication state per test
*
* @example
* ```typescript
* import { test, expect } from './fixtures/auth-fixtures';
*
* test('admin can access settings', async ({ page, adminUser }) => {
* await page.goto('/login');
* await page.locator('input[type="email"]').fill(adminUser.email);
* await page.locator('input[type="password"]').fill('TestPass123!');
* await page.getByRole('button', { name: /sign in/i }).click();
* await page.waitForURL('/');
* await page.goto('/settings');
* await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
* });
* ```
*/
import { test as base, expect } from '@bgotink/playwright-coverage';
import { request as playwrightRequest } from '@playwright/test';
import { existsSync, readFileSync } from 'fs';
import { TestDataManager } from '../utils/TestDataManager';
import { STORAGE_STATE } from '../constants';
/**
* Represents a test user with authentication details
*/
export interface TestUser {
/** User ID in the database */
id: string;
/** User's email address (namespaced) */
email: string;
/** Authentication token for API calls */
token: string;
/** User's role */
role: 'admin' | 'user' | 'guest';
}
/**
* Custom fixtures for authentication tests
*/
interface AuthFixtures {
/** Default authenticated user (admin role) */
authenticatedUser: TestUser;
/** Explicit admin user fixture */
adminUser: TestUser;
/** Regular user (non-admin) */
regularUser: TestUser;
/** Guest user (read-only) */
guestUser: TestUser;
/** Test data manager with automatic cleanup */
testData: TestDataManager;
}
/**
* Default password used for test users
* Strong password that meets typical validation requirements
*/
const TEST_PASSWORD = 'TestPass123!';
/**
* Extended Playwright test with authentication fixtures
*/
export const test = base.extend<AuthFixtures>({
/**
* TestDataManager fixture with automatic cleanup
*
* FIXED: Now creates an authenticated API context using stored auth state.
* This ensures API calls (like createUser, deleteUser) inherit the admin
* session established by auth.setup.ts.
*
* Previous Issue: The base `request` fixture was unauthenticated, causing
* "Admin access required" errors on protected endpoints.
*/
testData: async ({ baseURL }, use, testInfo) => {
// Defensive check: Verify auth state file exists (created by auth.setup.ts)
if (!existsSync(STORAGE_STATE)) {
throw new Error(
`Auth state file not found at ${STORAGE_STATE}. ` +
'Ensure auth.setup has run first. Check that dependencies: ["setup"] is configured.'
);
}
// Validate cookie domain matches baseURL to catch configuration issues early
try {
const savedState = JSON.parse(readFileSync(STORAGE_STATE, 'utf-8'));
const cookies = savedState.cookies || [];
const authCookie = cookies.find((c: { name: string }) => c.name === 'auth_token');
if (authCookie?.domain && baseURL) {
const expectedHost = new URL(baseURL).hostname;
const cookieDomain = authCookie.domain.replace(/^\./, ''); // Remove leading dot
if (cookieDomain !== expectedHost) {
console.warn(
`⚠️ TestDataManager: Cookie domain mismatch detected!\n` +
` Cookie domain: "${authCookie.domain}"\n` +
` Base URL host: "${expectedHost}"\n` +
` API calls will likely fail with 401/403.\n` +
` Fix: Set PLAYWRIGHT_BASE_URL=http://localhost:8080 in your environment.`
);
}
}
} catch (err) {
console.warn('⚠️ Could not validate cookie domain:', err instanceof Error ? err.message : err);
}
// Create an authenticated API request context using stored auth state
// This inherits the admin session from auth.setup.ts
const authenticatedContext = await playwrightRequest.newContext({
baseURL,
storageState: STORAGE_STATE,
extraHTTPHeaders: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
const manager = new TestDataManager(authenticatedContext, testInfo.title);
try {
await use(manager);
} finally {
// Ensure cleanup runs even if test fails
await manager.cleanup();
// Dispose the API context to release resources
await authenticatedContext.dispose();
}
},
/**
* Default authenticated user (admin role)
* Use this for tests that need a logged-in admin user
*/
authenticatedUser: async ({ testData }, use) => {
const user = await testData.createUser({
name: `Test Admin ${Date.now()}`,
email: `admin-${Date.now()}@test.local`,
password: TEST_PASSWORD,
role: 'admin',
});
await use({
...user,
role: 'admin',
});
},
/**
* Explicit admin user fixture
* Same as authenticatedUser but with explicit naming for clarity
*/
adminUser: async ({ testData }, use) => {
const user = await testData.createUser({
name: `Test Admin ${Date.now()}`,
email: `admin-${Date.now()}@test.local`,
password: TEST_PASSWORD,
role: 'admin',
});
await use({
...user,
role: 'admin',
});
},
/**
* Regular user (non-admin) fixture
* Use for testing permission restrictions
*/
regularUser: async ({ testData }, use) => {
const user = await testData.createUser({
name: `Test User ${Date.now()}`,
email: `user-${Date.now()}@test.local`,
password: TEST_PASSWORD,
role: 'user',
});
await use({
...user,
role: 'user',
});
},
/**
* Guest user (read-only) fixture
* Use for testing read-only access
*/
guestUser: async ({ testData }, use) => {
const user = await testData.createUser({
name: `Test Guest ${Date.now()}`,
email: `guest-${Date.now()}@test.local`,
password: TEST_PASSWORD,
role: 'guest',
});
await use({
...user,
role: 'guest',
});
},
});
/**
* Helper function to log in a user via the UI
* @param page - Playwright Page instance
* @param user - Test user to log in
*/
export async function loginUser(
page: import('@playwright/test').Page,
user: TestUser
): Promise<void> {
await page.goto('/login');
await page.locator('input[type="email"]').fill(user.email);
await page.locator('input[type="password"]').fill(TEST_PASSWORD);
await page.getByRole('button', { name: /sign in/i }).click();
await page.waitForURL('/');
}
/**
* Helper function to log out the current user
* @param page - Playwright Page instance
*/
export async function logoutUser(page: import('@playwright/test').Page): Promise<void> {
// Use text-based selector that handles emoji prefix (🚪 Logout)
// The button text contains "Logout" - use case-insensitive text matching
const logoutButton = page.getByText(/logout/i).first();
// Wait for the logout button to be visible and click it
await logoutButton.waitFor({ state: 'visible', timeout: 10000 });
await logoutButton.click();
// Wait for redirect to login page
await page.waitForURL(/\/login/, { timeout: 15000 });
}
/**
* Re-export expect from @playwright/test for convenience
*/
export { expect } from '@bgotink/playwright-coverage';
/**
* Re-export the default test password for use in tests
*/
export { TEST_PASSWORD };
-397
View File
@@ -1,397 +0,0 @@
/**
* Certificate Test Fixtures
*
* Mock data for SSL Certificate E2E tests.
* Provides various certificate configurations for testing CRUD operations,
* ACME challenges, and validation scenarios.
*
* @example
* ```typescript
* import { letsEncryptCertificate, customCertificateMock, expiredCertificate } from './fixtures/certificates';
*
* test('upload custom certificate', async ({ testData }) => {
* const { id } = await testData.createCertificate(customCertificateMock);
* });
* ```
*/
import { generateDomain, generateUniqueId } from './test-data';
/**
* Certificate type
*/
export type CertificateType = 'letsencrypt' | 'custom' | 'self-signed';
/**
* ACME challenge type
*/
export type ChallengeType = 'http-01' | 'dns-01';
/**
* Certificate status
*/
export type CertificateStatus =
| 'pending'
| 'valid'
| 'expired'
| 'revoked'
| 'error';
/**
* Certificate configuration interface
*/
export interface CertificateConfig {
/** Domains covered by the certificate */
domains: string[];
/** Certificate type */
type: CertificateType;
/** PEM-encoded certificate (for custom certs) */
certificate?: string;
/** PEM-encoded private key (for custom certs) */
privateKey?: string;
/** PEM-encoded intermediate certificates */
intermediates?: string;
/** DNS provider ID (for dns-01 challenge) */
dnsProviderId?: string;
/** Force renewal even if not expiring */
forceRenewal?: boolean;
/** ACME email for notifications */
acmeEmail?: string;
}
/**
* Self-signed test certificate and key
* Valid for testing purposes only - DO NOT use in production
*/
export const selfSignedTestCert = {
certificate: `-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAJC1HiIAZAiUMA0GCSqGSIb3Qw0BBQUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMjQwMTAxMDAwMDAwWhcNMjkwMTAxMDAwMDAwWjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAzUCFQIzxZqSU5LNHJ3m1R8fU3VpMfmTc1DJfKSBnBH4HKvC2vN7T9N9P
test-certificate-data-placeholder-for-testing-purposes-only
-----END CERTIFICATE-----`,
privateKey: `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDNQIVAjPFmpJTk
s0cnebVHx9TdWkx+ZNzUMl8pIGcEfgcq8La83tP030/0
test-private-key-data-placeholder-for-testing-purposes-only
-----END PRIVATE KEY-----`,
};
/**
* Let's Encrypt certificate mock
* Simulates a certificate obtained via ACME
*/
export const letsEncryptCertificate: CertificateConfig = {
domains: ['app.example.com'],
type: 'letsencrypt',
acmeEmail: 'admin@example.com',
};
/**
* Let's Encrypt certificate with multiple domains (SAN)
*/
export const multiDomainLetsEncrypt: CertificateConfig = {
domains: ['app.example.com', 'www.example.com', 'api.example.com'],
type: 'letsencrypt',
acmeEmail: 'admin@example.com',
};
/**
* Wildcard certificate mock
* Uses DNS-01 challenge (required for wildcards)
*/
export const wildcardCertificate: CertificateConfig = {
domains: ['*.example.com', 'example.com'],
type: 'letsencrypt',
acmeEmail: 'admin@example.com',
dnsProviderId: '', // Will be set dynamically in tests
};
/**
* Custom certificate mock
* Uses self-signed certificate for testing
*/
export const customCertificateMock: CertificateConfig = {
domains: ['custom.example.com'],
type: 'custom',
certificate: selfSignedTestCert.certificate,
privateKey: selfSignedTestCert.privateKey,
};
/**
* Custom certificate with intermediate chain
*/
export const customCertWithChain: CertificateConfig = {
domains: ['chain.example.com'],
type: 'custom',
certificate: selfSignedTestCert.certificate,
privateKey: selfSignedTestCert.privateKey,
intermediates: `-----BEGIN CERTIFICATE-----
MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
intermediate-certificate-placeholder-for-testing
-----END CERTIFICATE-----`,
};
/**
* Expired certificate mock
* For testing expiration handling
*/
export const expiredCertificate = {
domains: ['expired.example.com'],
type: 'custom' as CertificateType,
status: 'expired' as CertificateStatus,
expiresAt: new Date(Date.now() - 86400000).toISOString(), // Yesterday
certificate: `-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAJC1HiIAZAiUMA0GCSqGSIb3Qw0BBQUAMEUxCzAJBgNV
expired-certificate-placeholder
-----END CERTIFICATE-----`,
privateKey: selfSignedTestCert.privateKey,
};
/**
* Certificate expiring soon
* For testing renewal warnings
*/
export const expiringCertificate = {
domains: ['expiring.example.com'],
type: 'letsencrypt' as CertificateType,
status: 'valid' as CertificateStatus,
expiresAt: new Date(Date.now() + 7 * 86400000).toISOString(), // 7 days from now
};
/**
* Revoked certificate mock
*/
export const revokedCertificate = {
domains: ['revoked.example.com'],
type: 'letsencrypt' as CertificateType,
status: 'revoked' as CertificateStatus,
revokedAt: new Date().toISOString(),
revocationReason: 'Key compromise',
};
/**
* Invalid certificate configurations for validation testing
*/
export const invalidCertificates = {
/** Empty domains */
emptyDomains: {
domains: [],
type: 'letsencrypt' as CertificateType,
},
/** Invalid domain format */
invalidDomain: {
domains: ['not a valid domain!'],
type: 'letsencrypt' as CertificateType,
},
/** Missing certificate for custom type */
missingCertificate: {
domains: ['custom.example.com'],
type: 'custom' as CertificateType,
privateKey: selfSignedTestCert.privateKey,
},
/** Missing private key for custom type */
missingPrivateKey: {
domains: ['custom.example.com'],
type: 'custom' as CertificateType,
certificate: selfSignedTestCert.certificate,
},
/** Invalid certificate PEM format */
invalidCertificatePEM: {
domains: ['custom.example.com'],
type: 'custom' as CertificateType,
certificate: 'not a valid PEM certificate',
privateKey: selfSignedTestCert.privateKey,
},
/** Invalid private key PEM format */
invalidPrivateKeyPEM: {
domains: ['custom.example.com'],
type: 'custom' as CertificateType,
certificate: selfSignedTestCert.certificate,
privateKey: 'not a valid PEM private key',
},
/** Mismatched certificate and key */
mismatchedCertKey: {
domains: ['custom.example.com'],
type: 'custom' as CertificateType,
certificate: selfSignedTestCert.certificate,
privateKey: `-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDifferent-key
-----END PRIVATE KEY-----`,
},
/** Wildcard without DNS provider */
wildcardWithoutDNS: {
domains: ['*.example.com'],
type: 'letsencrypt' as CertificateType,
// dnsProviderId is missing - required for wildcards
},
/** Too many domains */
tooManyDomains: {
domains: Array.from({ length: 150 }, (_, i) => `domain${i}.example.com`),
type: 'letsencrypt' as CertificateType,
},
/** XSS in domain */
xssInDomain: {
domains: ['<script>alert(1)</script>.example.com'],
type: 'letsencrypt' as CertificateType,
},
/** Invalid ACME email */
invalidAcmeEmail: {
domains: ['app.example.com'],
type: 'letsencrypt' as CertificateType,
acmeEmail: 'not-an-email',
},
};
/**
* Generate a unique certificate configuration
* @param overrides - Optional configuration overrides
* @returns CertificateConfig with unique domain
*
* @example
* ```typescript
* const cert = generateCertificate({ type: 'custom' });
* ```
*/
export function generateCertificate(
overrides: Partial<CertificateConfig> = {}
): CertificateConfig {
const baseCert: CertificateConfig = {
domains: [generateDomain('cert')],
type: 'letsencrypt',
acmeEmail: `admin-${generateUniqueId()}@test.local`,
...overrides,
};
// Add certificate/key for custom type
if (baseCert.type === 'custom' && !baseCert.certificate) {
baseCert.certificate = selfSignedTestCert.certificate;
baseCert.privateKey = selfSignedTestCert.privateKey;
}
return baseCert;
}
/**
* Generate wildcard certificate configuration
* @param baseDomain - Base domain for the wildcard
* @param dnsProviderId - DNS provider ID for DNS-01 challenge
* @returns CertificateConfig for wildcard
*/
export function generateWildcardCertificate(
baseDomain?: string,
dnsProviderId?: string
): CertificateConfig {
const domain = baseDomain || `${generateUniqueId()}.test.local`;
return {
domains: [`*.${domain}`, domain],
type: 'letsencrypt',
acmeEmail: `admin-${generateUniqueId()}@test.local`,
dnsProviderId,
};
}
/**
* Generate multiple unique certificates
* @param count - Number of certificates to generate
* @param overrides - Optional configuration overrides
* @returns Array of CertificateConfig
*/
export function generateCertificates(
count: number,
overrides: Partial<CertificateConfig> = {}
): CertificateConfig[] {
return Array.from({ length: count }, () => generateCertificate(overrides));
}
/**
* Expected API response for certificate creation
*/
export interface CertificateAPIResponse {
id: string;
domains: string[];
type: CertificateType;
status: CertificateStatus;
issuer?: string;
expires_at?: string;
issued_at?: string;
acme_email?: string;
dns_provider_id?: string;
created_at: string;
updated_at: string;
}
/**
* Mock API response for testing
*/
export function mockCertificateResponse(
config: Partial<CertificateConfig> = {}
): CertificateAPIResponse {
const id = generateUniqueId();
const domains = config.domains || [generateDomain('cert')];
const isLetsEncrypt = config.type !== 'custom';
return {
id,
domains,
type: config.type || 'letsencrypt',
status: 'valid',
issuer: isLetsEncrypt ? "Let's Encrypt Authority X3" : 'Self-Signed',
expires_at: new Date(Date.now() + 90 * 86400000).toISOString(), // 90 days
issued_at: new Date().toISOString(),
acme_email: config.acmeEmail,
dns_provider_id: config.dnsProviderId,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
}
/**
* Mock ACME challenge data
*/
export interface ACMEChallengeData {
type: ChallengeType;
token: string;
keyAuthorization: string;
domain: string;
status: 'pending' | 'processing' | 'valid' | 'invalid';
}
/**
* Generate mock ACME HTTP-01 challenge
*/
export function mockHTTP01Challenge(domain: string): ACMEChallengeData {
return {
type: 'http-01',
token: `mock-token-${generateUniqueId()}`,
keyAuthorization: `mock-auth-${generateUniqueId()}`,
domain,
status: 'pending',
};
}
/**
* Generate mock ACME DNS-01 challenge
*/
export function mockDNS01Challenge(domain: string): ACMEChallengeData {
return {
type: 'dns-01',
token: `mock-token-${generateUniqueId()}`,
keyAuthorization: `mock-dns-auth-${generateUniqueId()}`,
domain: `_acme-challenge.${domain}`,
status: 'pending',
};
}
-280
View File
@@ -1,280 +0,0 @@
/**
* DNS Provider Test Fixtures
*
* Shared test data for DNS Provider E2E tests.
* These fixtures provide consistent test data across test files.
*/
import * as crypto from 'crypto';
/**
* Expected provider types from the API
*/
export const mockProviderTypes = {
/** Built-in providers from Lego/Caddy */
built_in: [
'cloudflare',
'route53',
'digitalocean',
'googleclouddns',
'azuredns',
'godaddy',
'namecheap',
'hetzner',
'vultr',
'dnsimple',
],
/** Custom providers from Phase 2 */
custom: ['manual', 'webhook', 'rfc2136', 'script'],
};
/**
* Mock provider data for creating test providers
*/
export const mockCloudflareProvider = {
name: 'Test Cloudflare',
provider_type: 'cloudflare',
credentials: {
api_token: 'test-token-12345',
},
};
export const mockManualProvider = {
name: 'Test Manual Provider',
provider_type: 'manual',
credentials: {},
};
export const mockWebhookProvider = {
name: 'Test Webhook Provider',
provider_type: 'webhook',
credentials: {
create_url: 'https://example.com/dns/create',
delete_url: 'https://example.com/dns/delete',
},
};
export const mockRfc2136Provider = {
name: 'Test RFC2136 Provider',
provider_type: 'rfc2136',
credentials: {
nameserver: 'ns.example.com:53',
tsig_key_name: 'ddns.example.com',
tsig_key: 'base64-encoded-key==',
tsig_algorithm: 'hmac-sha256',
},
};
export const mockScriptProvider = {
name: 'Test Script Provider',
provider_type: 'script',
credentials: {
script_path: '/usr/local/bin/dns-update.sh',
},
};
/**
* Mock API responses for testing
*/
export const mockTypesResponse = {
types: [
{
type: 'cloudflare',
name: 'Cloudflare',
description: 'DNS provider using Cloudflare API',
fields: [
{
name: 'api_token',
label: 'API Token',
type: 'password',
required: true,
},
],
},
{
type: 'manual',
name: 'Manual (No Automation)',
description: 'Manual DNS record creation - no automation',
fields: [],
},
{
type: 'webhook',
name: 'Webhook (HTTP)',
description: 'DNS provider using HTTP webhooks',
fields: [
{
name: 'create_url',
label: 'Create URL',
type: 'url',
required: true,
},
{
name: 'delete_url',
label: 'Delete URL',
type: 'url',
required: true,
},
],
},
],
total: 3,
};
/**
* Mock manual DNS challenge data
*/
export const mockManualChallenge = {
id: 1,
provider_id: 1,
fqdn: '_acme-challenge.example.com',
value: 'mock-challenge-token-value-abc123',
status: 'pending',
ttl: 300,
expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
created_at: new Date().toISOString(),
dns_propagated: false,
last_check_at: null,
};
export const mockExpiredChallenge = {
...mockManualChallenge,
id: 2,
status: 'expired',
expires_at: new Date(Date.now() - 60000).toISOString(),
created_at: new Date(Date.now() - 11 * 60 * 1000).toISOString(),
};
export const mockVerifiedChallenge = {
...mockManualChallenge,
id: 3,
status: 'verified',
dns_propagated: true,
};
/**
* Helper function to create a mock provider via API
*/
export async function createTestProvider(
request: { post: (url: string, options: { data: unknown }) => Promise<{ ok: () => boolean; json: () => Promise<unknown> }> },
providerData: typeof mockManualProvider
): Promise<{ id: number; uuid: string }> {
const response = await request.post('/api/v1/dns-providers', {
data: providerData,
});
if (!response.ok()) {
throw new Error('Failed to create test provider');
}
return response.json() as Promise<{ id: number; uuid: string }>;
}
/**
* Helper function to clean up test provider
*/
export async function deleteTestProvider(
request: { delete: (url: string) => Promise<unknown> },
providerId: number
): Promise<void> {
await request.delete(`/api/v1/dns-providers/${providerId}`);
}
/**
* DNS provider type options
*/
export type DnsProviderType = 'cloudflare' | 'manual' | 'route53' | 'webhook' | 'rfc2136' | 'script' | 'digitalocean' | 'googleclouddns' | 'azuredns' | 'godaddy' | 'namecheap' | 'hetzner' | 'vultr' | 'dnsimple';
/**
* DNS provider configuration interface
*/
export interface DnsProviderConfig {
/** Provider name */
name: string;
/** Provider type */
provider_type: DnsProviderType;
/** Provider credentials (type-specific) */
credentials: Record<string, string>;
/** Optional description */
description?: string;
/** Enable/disable the provider */
enabled?: boolean;
}
/**
* Generate a unique DNS provider configuration
* Creates a DNS provider with unique name to avoid conflicts
* @param overrides - Optional configuration overrides
* @returns DnsProviderConfig with unique name
*
* @example
* ```typescript
* const provider = generateDnsProvider({ provider_type: 'cloudflare' });
* ```
*/
export function generateDnsProvider(
overrides: Partial<DnsProviderConfig> = {}
): DnsProviderConfig {
const timestamp = Date.now().toString(36);
const random = crypto.randomBytes(4).toString('hex');
const uniqueId = `${timestamp}-${random}`;
const providerType = overrides.provider_type || 'manual';
// Generate type-specific credentials
let credentials: Record<string, string> = {};
switch (providerType) {
case 'cloudflare':
credentials = { api_token: `test-token-${uniqueId}` };
break;
case 'route53':
credentials = {
access_key_id: `AKIATEST${uniqueId.toUpperCase()}`,
secret_access_key: `secretkey${uniqueId}`,
region: 'us-east-1',
};
break;
case 'webhook':
credentials = {
create_url: `https://example.com/dns/${uniqueId}/create`,
delete_url: `https://example.com/dns/${uniqueId}/delete`,
};
break;
case 'rfc2136':
credentials = {
nameserver: 'ns.example.com:53',
tsig_key_name: `ddns-${uniqueId}.example.com`,
tsig_key: 'base64-encoded-key==',
tsig_algorithm: 'hmac-sha256',
};
break;
case 'script':
credentials = { script_path: '/usr/local/bin/dns-update.sh' };
break;
case 'digitalocean':
credentials = { api_token: `do-token-${uniqueId}` };
break;
case 'manual':
default:
credentials = {};
break;
}
return {
name: `DNS-${providerType}-${uniqueId}`,
provider_type: providerType,
credentials,
...overrides,
};
}
/**
* Generate multiple unique DNS providers
* @param count - Number of DNS providers to generate
* @param overrides - Optional configuration overrides for all providers
* @returns Array of DnsProviderConfig
*/
export function generateDnsProviders(
count: number,
overrides: Partial<DnsProviderConfig> = {}
): DnsProviderConfig[] {
return Array.from({ length: count }, () => generateDnsProvider(overrides));
}
-430
View File
@@ -1,430 +0,0 @@
/**
* Encryption Management Test Fixtures
*
* Shared test data for Encryption Management E2E tests.
* These fixtures provide consistent test data for testing encryption key
* rotation, status display, and validation scenarios.
*/
// ============================================================================
// Encryption Status Types
// ============================================================================
/**
* Encryption status interface matching API response
*/
export interface EncryptionStatus {
current_version: number;
next_key_configured: boolean;
legacy_key_count: number;
providers_on_current_version: number;
providers_on_older_versions: number;
}
/**
* Extended encryption status with additional metadata
*/
export interface EncryptionStatusExtended extends EncryptionStatus {
last_rotation_at?: string;
next_rotation_recommended?: string;
key_algorithm?: string;
key_size?: number;
}
/**
* Key rotation result interface
*/
export interface KeyRotationResult {
success: boolean;
new_version: number;
providers_updated: number;
providers_failed: number;
message: string;
timestamp: string;
}
/**
* Key validation result interface
*/
export interface KeyValidationResult {
valid: boolean;
current_key_ok: boolean;
next_key_ok: boolean;
legacy_keys_ok: boolean;
errors?: string[];
}
// ============================================================================
// Healthy Status Fixtures
// ============================================================================
/**
* Healthy encryption status - all providers on current version
*/
export const healthyEncryptionStatus: EncryptionStatus = {
current_version: 2,
next_key_configured: true,
legacy_key_count: 0,
providers_on_current_version: 5,
providers_on_older_versions: 0,
};
/**
* Healthy encryption status with extended metadata
*/
export const healthyEncryptionStatusExtended: EncryptionStatusExtended = {
...healthyEncryptionStatus,
last_rotation_at: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days ago
next_rotation_recommended: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000).toISOString(), // 60 days from now
key_algorithm: 'AES-256-GCM',
key_size: 256,
};
/**
* Initial setup status - first key version
*/
export const initialEncryptionStatus: EncryptionStatus = {
current_version: 1,
next_key_configured: false,
legacy_key_count: 0,
providers_on_current_version: 0,
providers_on_older_versions: 0,
};
// ============================================================================
// Status Requiring Action Fixtures
// ============================================================================
/**
* Status indicating rotation is needed - providers on older versions
*/
export const needsRotationStatus: EncryptionStatus = {
current_version: 1,
next_key_configured: true,
legacy_key_count: 1,
providers_on_current_version: 3,
providers_on_older_versions: 2,
};
/**
* Status with many providers needing update
*/
export const manyProvidersOutdatedStatus: EncryptionStatus = {
current_version: 3,
next_key_configured: true,
legacy_key_count: 2,
providers_on_current_version: 5,
providers_on_older_versions: 10,
};
/**
* Status without next key configured
*/
export const noNextKeyStatus: EncryptionStatus = {
current_version: 2,
next_key_configured: false,
legacy_key_count: 0,
providers_on_current_version: 5,
providers_on_older_versions: 0,
};
/**
* Status with legacy keys that should be cleaned up
*/
export const legacyKeysStatus: EncryptionStatus = {
current_version: 4,
next_key_configured: true,
legacy_key_count: 3,
providers_on_current_version: 8,
providers_on_older_versions: 0,
};
// ============================================================================
// Key Rotation Result Fixtures
// ============================================================================
/**
* Successful key rotation result
*/
export const successfulRotationResult: KeyRotationResult = {
success: true,
new_version: 3,
providers_updated: 5,
providers_failed: 0,
message: 'Key rotation completed successfully',
timestamp: new Date().toISOString(),
};
/**
* Partial success rotation result (some providers failed)
*/
export const partialRotationResult: KeyRotationResult = {
success: true,
new_version: 3,
providers_updated: 4,
providers_failed: 1,
message: 'Key rotation completed with 1 provider requiring manual update',
timestamp: new Date().toISOString(),
};
/**
* Failed key rotation result
*/
export const failedRotationResult: KeyRotationResult = {
success: false,
new_version: 2, // unchanged
providers_updated: 0,
providers_failed: 5,
message: 'Key rotation failed: Unable to decrypt existing credentials',
timestamp: new Date().toISOString(),
};
// ============================================================================
// Key Validation Result Fixtures
// ============================================================================
/**
* Successful key validation result
*/
export const validKeyValidationResult: KeyValidationResult = {
valid: true,
current_key_ok: true,
next_key_ok: true,
legacy_keys_ok: true,
};
/**
* Validation result with next key not configured
*/
export const noNextKeyValidationResult: KeyValidationResult = {
valid: true,
current_key_ok: true,
next_key_ok: false,
legacy_keys_ok: true,
errors: ['Next encryption key is not configured'],
};
/**
* Validation result with errors
*/
export const invalidKeyValidationResult: KeyValidationResult = {
valid: false,
current_key_ok: false,
next_key_ok: false,
legacy_keys_ok: false,
errors: [
'Current encryption key is invalid or corrupted',
'Next encryption key is not configured',
'Legacy key at version 1 cannot be loaded',
],
};
/**
* Validation result with legacy key issues
*/
export const legacyKeyIssuesValidationResult: KeyValidationResult = {
valid: true,
current_key_ok: true,
next_key_ok: true,
legacy_keys_ok: false,
errors: ['Legacy key at version 0 is missing - some old credentials may be unrecoverable'],
};
// ============================================================================
// Rotation History Types and Fixtures
// ============================================================================
/**
* Rotation history entry interface
*/
export interface RotationHistoryEntry {
id: number;
from_version: number;
to_version: number;
providers_updated: number;
providers_failed: number;
initiated_by: string;
initiated_at: string;
completed_at: string;
status: 'completed' | 'partial' | 'failed';
notes?: string;
}
/**
* Mock rotation history entries
*/
export const rotationHistory: RotationHistoryEntry[] = [
{
id: 3,
from_version: 1,
to_version: 2,
providers_updated: 5,
providers_failed: 0,
initiated_by: 'admin@example.com',
initiated_at: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
completed_at: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000 + 5000).toISOString(),
status: 'completed',
},
{
id: 2,
from_version: 0,
to_version: 1,
providers_updated: 3,
providers_failed: 0,
initiated_by: 'admin@example.com',
initiated_at: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString(),
completed_at: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000 + 3000).toISOString(),
status: 'completed',
},
{
id: 1,
from_version: 0,
to_version: 1,
providers_updated: 2,
providers_failed: 1,
initiated_by: 'admin@example.com',
initiated_at: new Date(Date.now() - 120 * 24 * 60 * 60 * 1000).toISOString(),
completed_at: new Date(Date.now() - 120 * 24 * 60 * 60 * 1000 + 8000).toISOString(),
status: 'partial',
notes: 'One provider had invalid credentials and was skipped',
},
];
/**
* Empty rotation history (initial setup)
*/
export const emptyRotationHistory: RotationHistoryEntry[] = [];
// ============================================================================
// UI Status Messages
// ============================================================================
/**
* Status message configurations for different encryption states
*/
export const statusMessages = {
healthy: {
title: 'Encryption Status: Healthy',
description: 'All credentials are encrypted with the current key version.',
severity: 'success' as const,
},
needsRotation: {
title: 'Rotation Recommended',
description: 'Some providers are using older encryption keys.',
severity: 'warning' as const,
},
noNextKey: {
title: 'Next Key Not Configured',
description: 'Configure a next key before rotation is needed.',
severity: 'info' as const,
},
criticalIssue: {
title: 'Encryption Issue Detected',
description: 'There are issues with the encryption configuration.',
severity: 'error' as const,
},
};
// ============================================================================
// API Helper Functions
// ============================================================================
/**
* Get encryption status via API
*/
export async function getEncryptionStatus(
request: { get: (url: string) => Promise<{ ok: () => boolean; json: () => Promise<unknown> }> }
): Promise<EncryptionStatus> {
const response = await request.get('/api/v1/admin/encryption/status');
if (!response.ok()) {
throw new Error('Failed to get encryption status');
}
return response.json() as Promise<EncryptionStatus>;
}
/**
* Rotate encryption key via API
*/
export async function rotateEncryptionKey(
request: { post: (url: string, options?: { data?: unknown }) => Promise<{ ok: () => boolean; json: () => Promise<unknown> }> }
): Promise<KeyRotationResult> {
const response = await request.post('/api/v1/admin/encryption/rotate', {});
if (!response.ok()) {
const result = await response.json() as KeyRotationResult;
return result;
}
return response.json() as Promise<KeyRotationResult>;
}
/**
* Validate encryption keys via API
*/
export async function validateEncryptionKeys(
request: { post: (url: string, options?: { data?: unknown }) => Promise<{ ok: () => boolean; json: () => Promise<unknown> }> }
): Promise<KeyValidationResult> {
const response = await request.post('/api/v1/admin/encryption/validate', {});
return response.json() as Promise<KeyValidationResult>;
}
/**
* Get rotation history via API
*/
export async function getRotationHistory(
request: { get: (url: string) => Promise<{ ok: () => boolean; json: () => Promise<unknown> }> }
): Promise<RotationHistoryEntry[]> {
const response = await request.get('/api/v1/admin/encryption/history');
if (!response.ok()) {
throw new Error('Failed to get rotation history');
}
return response.json() as Promise<RotationHistoryEntry[]>;
}
// ============================================================================
// Test Scenario Helpers
// ============================================================================
/**
* Generate a status based on provider counts
*/
export function generateEncryptionStatus(
currentProviders: number,
outdatedProviders: number,
version: number = 2
): EncryptionStatus {
return {
current_version: version,
next_key_configured: true,
legacy_key_count: outdatedProviders > 0 ? 1 : 0,
providers_on_current_version: currentProviders,
providers_on_older_versions: outdatedProviders,
};
}
/**
* Create a mock rotation history entry
*/
export function createRotationHistoryEntry(
fromVersion: number,
toVersion: number,
success: boolean = true
): RotationHistoryEntry {
const now = new Date();
return {
id: Date.now(),
from_version: fromVersion,
to_version: toVersion,
providers_updated: success ? 5 : 2,
providers_failed: success ? 0 : 3,
initiated_by: 'test@example.com',
initiated_at: now.toISOString(),
completed_at: new Date(now.getTime() + 5000).toISOString(),
status: success ? 'completed' : 'partial',
};
}
-480
View File
@@ -1,480 +0,0 @@
/**
* Notification Provider Test Fixtures
*
* Shared test data for Notification Provider E2E tests.
* These fixtures provide consistent test data across notification-related test files.
*/
import * as crypto from 'crypto';
// ============================================================================
// Notification Provider Types
// ============================================================================
/**
* Supported notification provider types
*/
export type NotificationProviderType =
| 'discord'
| 'slack'
| 'gotify'
| 'telegram'
| 'generic'
| 'webhook';
/**
* Notification provider configuration interface
*/
export interface NotificationProviderConfig {
name: string;
type: NotificationProviderType;
url: string;
config?: string;
template?: string;
enabled: boolean;
notify_proxy_hosts: boolean;
notify_certs: boolean;
notify_uptime: boolean;
}
/**
* Notification provider response from API (includes ID)
*/
export interface NotificationProvider extends NotificationProviderConfig {
id: number;
created_at: string;
updated_at: string;
}
// ============================================================================
// Generator Functions
// ============================================================================
/**
* Generate a unique provider name
*/
export function generateProviderName(prefix: string = 'test-provider'): string {
return `${prefix}-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
}
/**
* Generate a unique webhook URL for testing
*/
export function generateWebhookUrl(service: string = 'webhook'): string {
return `https://${service}.test.local/notify/${Date.now()}`;
}
// ============================================================================
// Discord Provider Fixtures
// ============================================================================
/**
* Valid Discord notification provider configuration
*/
export const discordProvider: NotificationProviderConfig = {
name: generateProviderName('discord'),
type: 'discord',
url: 'https://discord.com/api/webhooks/123456789/abcdefghijklmnop',
enabled: true,
notify_proxy_hosts: true,
notify_certs: true,
notify_uptime: false,
};
/**
* Discord provider with all notifications enabled
*/
export const discordProviderAllEvents: NotificationProviderConfig = {
name: generateProviderName('discord-all'),
type: 'discord',
url: 'https://discord.com/api/webhooks/987654321/zyxwvutsrqponmlk',
enabled: true,
notify_proxy_hosts: true,
notify_certs: true,
notify_uptime: true,
};
/**
* Discord provider (disabled)
*/
export const discordProviderDisabled: NotificationProviderConfig = {
...discordProvider,
name: generateProviderName('discord-disabled'),
enabled: false,
};
// ============================================================================
// Slack Provider Fixtures
// ============================================================================
/**
* Valid Slack notification provider configuration
*/
export const slackProvider: NotificationProviderConfig = {
name: generateProviderName('slack'),
type: 'slack',
url: 'https://hooks.example.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX',
enabled: true,
notify_proxy_hosts: true,
notify_certs: false,
notify_uptime: true,
};
/**
* Slack provider with custom template
*/
export const slackProviderWithTemplate: NotificationProviderConfig = {
name: generateProviderName('slack-template'),
type: 'slack',
url: 'https://hooks.example.com/services/T11111111/B11111111/YYYYYYYYYYYYYYYYYYYYYYYY',
template: 'minimal',
enabled: true,
notify_proxy_hosts: true,
notify_certs: true,
notify_uptime: true,
};
// ============================================================================
// Gotify Provider Fixtures
// ============================================================================
/**
* Valid Gotify notification provider configuration
*/
export const gotifyProvider: NotificationProviderConfig = {
name: generateProviderName('gotify'),
type: 'gotify',
url: 'https://gotify.test.local/message?token=Axxxxxxxxxxxxxxxxx',
enabled: true,
notify_proxy_hosts: true,
notify_certs: true,
notify_uptime: false,
};
// ============================================================================
// Telegram Provider Fixtures
// ============================================================================
/**
* Valid Telegram notification provider configuration
*/
export const telegramProvider: NotificationProviderConfig = {
name: generateProviderName('telegram'),
type: 'telegram',
url: 'https://api.telegram.org/bot123456789:ABCdefGHIjklMNOpqrSTUvwxYZ/sendMessage?chat_id=987654321',
enabled: true,
notify_proxy_hosts: true,
notify_certs: true,
notify_uptime: true,
};
// ============================================================================
// Generic Webhook Provider Fixtures
// ============================================================================
/**
* Valid generic webhook notification provider configuration
*/
export const genericWebhookProvider: NotificationProviderConfig = {
name: generateProviderName('generic'),
type: 'generic',
url: 'https://webhook.test.local/notify',
config: JSON.stringify({ message: '{{.Message}}', priority: 'normal' }),
template: 'minimal',
enabled: true,
notify_proxy_hosts: true,
notify_certs: true,
notify_uptime: true,
};
/**
* Generic webhook with custom JSON config
*/
export const genericWebhookCustomConfig: NotificationProviderConfig = {
name: generateProviderName('generic-custom'),
type: 'generic',
url: 'https://custom-webhook.test.local/api/notify',
config: JSON.stringify({
title: '{{.Title}}',
body: '{{.Message}}',
source: 'charon',
severity: '{{.Severity}}',
}),
enabled: true,
notify_proxy_hosts: true,
notify_certs: false,
notify_uptime: false,
};
// ============================================================================
// Custom Webhook Provider Fixtures
// ============================================================================
/**
* Valid custom webhook notification provider configuration
*/
export const customWebhookProvider: NotificationProviderConfig = {
name: generateProviderName('webhook'),
type: 'webhook',
url: 'https://my-custom-api.test.local/notifications',
config: JSON.stringify({
method: 'POST',
headers: {
'X-Custom-Header': 'value',
'Content-Type': 'application/json',
},
body: {
event: '{{.Event}}',
message: '{{.Message}}',
timestamp: '{{.Timestamp}}',
},
}),
enabled: true,
notify_proxy_hosts: true,
notify_certs: true,
notify_uptime: true,
};
// ============================================================================
// Invalid Provider Fixtures (for validation testing)
// ============================================================================
/**
* Invalid provider configurations for testing validation
*/
export const invalidProviderConfigs = {
missingName: {
...discordProvider,
name: '',
},
missingUrl: {
...discordProvider,
url: '',
},
invalidUrl: {
...discordProvider,
url: 'not-a-valid-url',
},
invalidJsonConfig: {
...genericWebhookProvider,
config: 'invalid-json{',
},
nameWithSpecialChars: {
...discordProvider,
name: 'test<script>alert(1)</script>',
},
urlWithXss: {
...discordProvider,
url: 'javascript:alert(1)',
},
};
// ============================================================================
// Notification Template Types and Fixtures
// ============================================================================
/**
* Notification template interface
*/
export interface NotificationTemplate {
id?: number;
name: string;
content: string;
description?: string;
is_builtin?: boolean;
}
/**
* Built-in template names
*/
export const builtInTemplates = ['default', 'minimal', 'detailed', 'compact'];
/**
* Custom external template
*/
export const customTemplate: NotificationTemplate = {
name: 'custom-test-template',
content: `**{{.Title}}**
Event: {{.Event}}
Time: {{.Timestamp}}
Details: {{.Message}}`,
description: 'Custom test template for E2E testing',
};
/**
* Generate a unique template name
*/
export function generateTemplateName(): string {
return `test-template-${Date.now()}`;
}
/**
* Create a custom template with unique name
*/
export function createCustomTemplate(overrides: Partial<NotificationTemplate> = {}): NotificationTemplate {
return {
name: generateTemplateName(),
content: `**{{.Title}}**\n{{.Message}}`,
description: 'Auto-generated test template',
...overrides,
};
}
// ============================================================================
// Notification Event Types
// ============================================================================
/**
* Notification event types
*/
export type NotificationEvent =
| 'proxy_host_created'
| 'proxy_host_updated'
| 'proxy_host_deleted'
| 'certificate_issued'
| 'certificate_renewed'
| 'certificate_expired'
| 'uptime_down'
| 'uptime_recovered';
/**
* Event configuration for testing specific notification types
*/
export const eventConfigs = {
proxyHostsOnly: {
notify_proxy_hosts: true,
notify_certs: false,
notify_uptime: false,
},
certsOnly: {
notify_proxy_hosts: false,
notify_certs: true,
notify_uptime: false,
},
uptimeOnly: {
notify_proxy_hosts: false,
notify_certs: false,
notify_uptime: true,
},
allEvents: {
notify_proxy_hosts: true,
notify_certs: true,
notify_uptime: true,
},
noEvents: {
notify_proxy_hosts: false,
notify_certs: false,
notify_uptime: false,
},
};
// ============================================================================
// Mock Notification Test Responses
// ============================================================================
/**
* Mock notification test success response
*/
export const mockTestSuccess = {
success: true,
message: 'Test notification sent successfully',
};
/**
* Mock notification test failure response
*/
export const mockTestFailure = {
success: false,
message: 'Failed to send test notification: Connection refused',
error: 'ECONNREFUSED',
};
/**
* Mock notification preview response
*/
export const mockPreviewResponse = {
content: '**Test Notification**\nThis is a preview of your notification message.',
rendered_at: new Date().toISOString(),
};
// ============================================================================
// API Helper Functions
// ============================================================================
/**
* Create notification provider via API
*/
export async function createNotificationProvider(
request: { post: (url: string, options: { data: unknown }) => Promise<{ ok: () => boolean; json: () => Promise<unknown> }> },
config: NotificationProviderConfig
): Promise<NotificationProvider> {
const response = await request.post('/api/v1/notifications/providers', {
data: config,
});
if (!response.ok()) {
throw new Error('Failed to create notification provider');
}
return response.json() as Promise<NotificationProvider>;
}
/**
* Delete notification provider via API
*/
export async function deleteNotificationProvider(
request: { delete: (url: string) => Promise<{ ok: () => boolean }> },
providerId: number
): Promise<void> {
const response = await request.delete(`/api/v1/notifications/providers/${providerId}`);
if (!response.ok()) {
throw new Error(`Failed to delete notification provider: ${providerId}`);
}
}
/**
* Create external template via API
*/
export async function createExternalTemplate(
request: { post: (url: string, options: { data: unknown }) => Promise<{ ok: () => boolean; json: () => Promise<unknown> }> },
template: NotificationTemplate
): Promise<NotificationTemplate & { id: number }> {
const response = await request.post('/api/v1/notifications/external-templates', {
data: template,
});
if (!response.ok()) {
throw new Error('Failed to create external template');
}
return response.json() as Promise<NotificationTemplate & { id: number }>;
}
/**
* Delete external template via API
*/
export async function deleteExternalTemplate(
request: { delete: (url: string) => Promise<{ ok: () => boolean }> },
templateId: number
): Promise<void> {
const response = await request.delete(`/api/v1/notifications/external-templates/${templateId}`);
if (!response.ok()) {
throw new Error(`Failed to delete external template: ${templateId}`);
}
}
/**
* Test notification provider via API
*/
export async function testNotificationProvider(
request: { post: (url: string, options: { data: unknown }) => Promise<{ ok: () => boolean; json: () => Promise<unknown> }> },
providerId: number
): Promise<typeof mockTestSuccess | typeof mockTestFailure> {
const response = await request.post('/api/v1/notifications/providers/test', {
data: { provider_id: providerId },
});
return response.json() as Promise<typeof mockTestSuccess | typeof mockTestFailure>;
}
-386
View File
@@ -1,386 +0,0 @@
/**
* Proxy Host Test Fixtures
*
* Mock data for proxy host E2E tests.
* Provides various configurations for testing CRUD operations,
* validation, and edge cases.
*
* @example
* ```typescript
* import { basicProxyHost, proxyHostWithSSL, invalidProxyHosts } from './fixtures/proxy-hosts';
*
* test('create basic proxy host', async ({ testData }) => {
* const { id } = await testData.createProxyHost(basicProxyHost);
* });
* ```
*/
import {
generateDomain,
generateIPAddress,
generatePort,
generateUniqueId,
} from './test-data';
/**
* Proxy host configuration interface
*/
export interface ProxyHostConfig {
/** Domain name for the proxy */
domain: string;
/** Target hostname or IP */
forwardHost: string;
/** Target port */
forwardPort: number;
/** Friendly name for the proxy host */
name?: string;
/** Protocol scheme */
scheme: 'http' | 'https';
/** Enable WebSocket support */
websocketSupport: boolean;
/** Enable HTTP/2 */
http2Support?: boolean;
/** Enable gzip compression */
gzipEnabled?: boolean;
/** Custom headers */
customHeaders?: Record<string, string>;
/** Access list ID (if applicable) */
accessListId?: string;
/** Certificate ID (if applicable) */
certificateId?: string;
/** Enable HSTS */
hstsEnabled?: boolean;
/** Force SSL redirect */
forceSSL?: boolean;
/** Block exploits */
blockExploits?: boolean;
/** Custom Caddy configuration */
advancedConfig?: string;
/** Enable/disable the proxy */
enabled?: boolean;
}
/**
* Basic proxy host configuration
* Minimal setup for simple HTTP proxying
*/
export const basicProxyHost: ProxyHostConfig = {
domain: 'basic-app.example.com',
forwardHost: '192.168.1.100',
forwardPort: 3000,
scheme: 'http',
websocketSupport: false,
};
/**
* Proxy host with SSL enabled
* Uses HTTPS scheme and forces SSL redirect
*/
export const proxyHostWithSSL: ProxyHostConfig = {
domain: 'secure-app.example.com',
forwardHost: '192.168.1.100',
forwardPort: 443,
scheme: 'https',
websocketSupport: false,
hstsEnabled: true,
forceSSL: true,
};
/**
* Proxy host with WebSocket support
* For real-time applications
*/
export const proxyHostWithWebSocket: ProxyHostConfig = {
domain: 'ws-app.example.com',
forwardHost: '192.168.1.100',
forwardPort: 3000,
scheme: 'http',
websocketSupport: true,
};
/**
* Proxy host with full security features
* Includes SSL, HSTS, exploit blocking
*/
export const proxyHostFullSecurity: ProxyHostConfig = {
domain: 'secure-full.example.com',
forwardHost: '192.168.1.100',
forwardPort: 8080,
scheme: 'https',
websocketSupport: true,
http2Support: true,
hstsEnabled: true,
forceSSL: true,
blockExploits: true,
gzipEnabled: true,
};
/**
* Proxy host with custom headers
* For testing header injection
*/
export const proxyHostWithHeaders: ProxyHostConfig = {
domain: 'headers-app.example.com',
forwardHost: '192.168.1.100',
forwardPort: 3000,
scheme: 'http',
websocketSupport: false,
customHeaders: {
'X-Custom-Header': 'test-value',
'X-Forwarded-Proto': 'https',
'X-Real-IP': '{remote_host}',
},
};
/**
* Proxy host with access list
* Placeholder for ACL integration testing
*/
export const proxyHostWithAccessList: ProxyHostConfig = {
domain: 'restricted-app.example.com',
forwardHost: '192.168.1.100',
forwardPort: 3000,
scheme: 'http',
websocketSupport: false,
accessListId: '', // Will be set dynamically in tests
};
/**
* Proxy host with advanced Caddy configuration
* For testing custom configuration injection
*/
export const proxyHostWithAdvancedConfig: ProxyHostConfig = {
domain: 'advanced-app.example.com',
forwardHost: '192.168.1.100',
forwardPort: 3000,
scheme: 'http',
websocketSupport: false,
advancedConfig: `
header {
-Server
X-Robots-Tag "noindex, nofollow"
}
request_body {
max_size 10MB
}
`,
};
/**
* Disabled proxy host
* For testing enable/disable functionality
*/
export const disabledProxyHost: ProxyHostConfig = {
domain: 'disabled-app.example.com',
forwardHost: '192.168.1.100',
forwardPort: 3000,
scheme: 'http',
websocketSupport: false,
enabled: false,
};
/**
* Docker container proxy host
* Uses container name as forward host
*/
export const dockerProxyHost: ProxyHostConfig = {
domain: 'docker-app.example.com',
forwardHost: 'my-container',
forwardPort: 80,
scheme: 'http',
websocketSupport: false,
};
/**
* Wildcard domain proxy host
* For subdomain proxying
*/
export const wildcardProxyHost: ProxyHostConfig = {
domain: '*.apps.example.com',
forwardHost: '192.168.1.100',
forwardPort: 3000,
scheme: 'http',
websocketSupport: false,
};
/**
* Invalid proxy host configurations for validation testing
*/
export const invalidProxyHosts = {
/** Empty domain */
emptyDomain: {
domain: '',
forwardHost: '192.168.1.100',
forwardPort: 3000,
scheme: 'http' as const,
websocketSupport: false,
},
/** Invalid domain format */
invalidDomain: {
domain: 'not a valid domain!',
forwardHost: '192.168.1.100',
forwardPort: 3000,
scheme: 'http' as const,
websocketSupport: false,
},
/** Empty forward host */
emptyForwardHost: {
domain: 'valid.example.com',
forwardHost: '',
forwardPort: 3000,
scheme: 'http' as const,
websocketSupport: false,
},
/** Invalid IP address */
invalidIP: {
domain: 'valid.example.com',
forwardHost: '999.999.999.999',
forwardPort: 3000,
scheme: 'http' as const,
websocketSupport: false,
},
/** Port out of range (too low) */
portTooLow: {
domain: 'valid.example.com',
forwardHost: '192.168.1.100',
forwardPort: 0,
scheme: 'http' as const,
websocketSupport: false,
},
/** Port out of range (too high) */
portTooHigh: {
domain: 'valid.example.com',
forwardHost: '192.168.1.100',
forwardPort: 70000,
scheme: 'http' as const,
websocketSupport: false,
},
/** Negative port */
negativePort: {
domain: 'valid.example.com',
forwardHost: '192.168.1.100',
forwardPort: -1,
scheme: 'http' as const,
websocketSupport: false,
},
/** XSS in domain */
xssInDomain: {
domain: '<script>alert(1)</script>.example.com',
forwardHost: '192.168.1.100',
forwardPort: 3000,
scheme: 'http' as const,
websocketSupport: false,
},
/** SQL injection in domain */
sqlInjectionInDomain: {
domain: "'; DROP TABLE proxy_hosts; --",
forwardHost: '192.168.1.100',
forwardPort: 3000,
scheme: 'http' as const,
websocketSupport: false,
},
};
/**
* Generate a unique proxy host configuration
* Creates a proxy host with unique domain to avoid conflicts
* @param overrides - Optional configuration overrides
* @returns ProxyHostConfig with unique domain
*
* @example
* ```typescript
* const host = generateProxyHost({ websocketSupport: true });
* ```
*/
export function generateProxyHost(
overrides: Partial<ProxyHostConfig> = {}
): ProxyHostConfig {
const domain = generateDomain('proxy');
return {
domain,
forwardHost: generateIPAddress(),
forwardPort: generatePort({ min: 3000, max: 9000 }),
name: `Test Host ${Date.now()}`,
scheme: 'http',
websocketSupport: false,
...overrides,
};
}
/**
* Generate multiple unique proxy hosts
* @param count - Number of proxy hosts to generate
* @param overrides - Optional configuration overrides for all hosts
* @returns Array of ProxyHostConfig
*/
export function generateProxyHosts(
count: number,
overrides: Partial<ProxyHostConfig> = {}
): ProxyHostConfig[] {
return Array.from({ length: count }, () => generateProxyHost(overrides));
}
/**
* Proxy host for load balancing tests
* Multiple backends configuration
*/
export const loadBalancedProxyHost = {
domain: 'lb-app.example.com',
backends: [
{ host: '192.168.1.101', port: 3000, weight: 1 },
{ host: '192.168.1.102', port: 3000, weight: 1 },
{ host: '192.168.1.103', port: 3000, weight: 2 },
],
scheme: 'http' as const,
websocketSupport: false,
healthCheck: {
path: '/health',
interval: '10s',
timeout: '5s',
},
};
/**
* Expected API response for proxy host creation
*/
export interface ProxyHostAPIResponse {
id: string;
uuid: string;
domain: string;
forward_host: string;
forward_port: number;
scheme: string;
websocket_support: boolean;
enabled: boolean;
created_at: string;
updated_at: string;
}
/**
* Mock API response for testing
*/
export function mockProxyHostResponse(
config: Partial<ProxyHostConfig> = {}
): ProxyHostAPIResponse {
const id = generateUniqueId();
return {
id,
uuid: `uuid-${id}`,
domain: config.domain || generateDomain('proxy'),
forward_host: config.forwardHost || '192.168.1.100',
forward_port: config.forwardPort || 3000,
scheme: config.scheme || 'http',
websocket_support: config.websocketSupport || false,
enabled: config.enabled !== false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
}
-147
View File
@@ -1,147 +0,0 @@
/**
* Security Test Fixtures
*
* Provides helper functions for enabling/disabling security modules
* and testing emergency access during E2E tests.
*/
import { APIRequestContext } from '@playwright/test';
/**
* Emergency token for E2E tests - must match docker-compose.e2e.yml
*/
export const EMERGENCY_TOKEN =
process.env.CHARON_EMERGENCY_TOKEN || 'test-emergency-token-for-e2e-32chars';
/**
* Emergency server configuration for E2E tests
*/
export const EMERGENCY_SERVER = {
baseURL: 'http://localhost:2019',
username: 'admin',
password: 'changeme',
};
/**
* Enable all security modules for testing.
* This simulates a production environment with full security enabled.
*
* @param request - Playwright APIRequestContext
*/
export async function enableSecurity(request: APIRequestContext): Promise<void> {
console.log('🔒 Enabling all security modules...');
const modules = [
{ key: 'security.acl.enabled', value: 'true' },
{ key: 'security.waf.enabled', value: 'true' },
{ key: 'security.rate_limit.enabled', value: 'true' },
{ key: 'feature.cerberus.enabled', value: 'true' },
];
for (const { key, value } of modules) {
await request.post('/api/v1/settings', {
data: { key, value },
});
console.log(` ✓ Enabled: ${key}`);
}
// Wait for settings to propagate
console.log(' ⏳ Waiting for security settings to propagate...');
await new Promise(resolve => setTimeout(resolve, 2000));
console.log(' ✅ Security enabled');
}
/**
* Disable all security modules using the emergency token.
* This is the proper way to recover from security lockouts.
*
* @param request - Playwright APIRequestContext
* @throws Error if emergency reset fails
*/
export async function disableSecurity(request: APIRequestContext): Promise<void> {
console.log('🔓 Disabling security using emergency token...');
const response = await request.post('/api/v1/emergency/security-reset', {
headers: {
'X-Emergency-Token': EMERGENCY_TOKEN,
},
});
if (!response.ok()) {
const body = await response.text();
throw new Error(`Emergency reset failed: ${response.status()} ${body}`);
}
const result = await response.json();
console.log(` ✅ Disabled modules: ${result.disabled_modules?.join(', ')}`);
// Wait for settings to propagate
console.log(' ⏳ Waiting for security reset to propagate...');
await new Promise(resolve => setTimeout(resolve, 2000));
console.log(' ✅ Security disabled');
}
/**
* Test if emergency token access is functional.
* This is useful for verifying the emergency bypass system is working.
*
* @param request - Playwright APIRequestContext
* @returns true if emergency token works, false otherwise
*/
export async function testEmergencyAccess(request: APIRequestContext): Promise<boolean> {
try {
const response = await request.post('/api/v1/emergency/security-reset', {
headers: {
'X-Emergency-Token': EMERGENCY_TOKEN,
},
});
return response.ok();
} catch (e) {
console.error(`Emergency access test failed: ${e}`);
return false;
}
}
/**
* Test emergency server access (Tier 2 break glass).
* This tests the separate emergency server on port 2019.
*
* @param request - Playwright APIRequestContext
* @returns true if emergency server is accessible, false otherwise
*/
export async function testEmergencyServerAccess(
request: APIRequestContext
): Promise<boolean> {
try {
// Create Basic Auth header
const authHeader =
'Basic ' +
Buffer.from(`${EMERGENCY_SERVER.username}:${EMERGENCY_SERVER.password}`).toString('base64');
const response = await request.post(`${EMERGENCY_SERVER.baseURL}/emergency/security-reset`, {
headers: {
Authorization: authHeader,
'X-Emergency-Token': EMERGENCY_TOKEN,
},
});
return response.ok();
} catch (e) {
console.error(`Emergency server access test failed: ${e}`);
return false;
}
}
/**
* Wait for security settings to propagate through the system.
* Some security changes take time to apply due to caching and module loading.
*
* @param durationMs - Duration to wait in milliseconds (default: 2000)
*/
export async function waitForSecurityPropagation(durationMs: number = 2000): Promise<void> {
console.log(` ⏳ Waiting ${durationMs}ms for security changes to propagate...`);
await new Promise(resolve => setTimeout(resolve, durationMs));
}
-399
View File
@@ -1,399 +0,0 @@
/**
* Settings Test Fixtures
*
* Shared test data for Settings E2E tests (System Settings, SMTP Settings, Account Settings).
* These fixtures provide consistent test data across settings-related test files.
*/
import * as crypto from 'crypto';
// ============================================================================
// SMTP Configuration Types and Fixtures
// ============================================================================
/**
* SMTP encryption types supported by the system
*/
export type SMTPEncryption = 'none' | 'ssl' | 'starttls';
/**
* SMTP configuration interface matching backend expectations
*/
export interface SMTPConfig {
host: string;
port: number;
username: string;
password: string;
from_address: string;
encryption: SMTPEncryption;
}
/**
* Valid SMTP configuration for successful test scenarios
*/
export const validSMTPConfig: SMTPConfig = {
host: 'smtp.test.local',
port: 587,
username: 'testuser',
password: 'testpass123',
from_address: 'noreply@test.local',
encryption: 'starttls',
};
/**
* Alternative valid SMTP configurations for different encryption types
*/
export const validSMTPConfigSSL: SMTPConfig = {
host: 'smtp-ssl.test.local',
port: 465,
username: 'ssluser',
password: 'sslpass456',
from_address: 'ssl-noreply@test.local',
encryption: 'ssl',
};
export const validSMTPConfigNoAuth: SMTPConfig = {
host: 'smtp-noauth.test.local',
port: 25,
username: '',
password: '',
from_address: 'noauth@test.local',
encryption: 'none',
};
/**
* Invalid SMTP configurations for validation testing
*/
export const invalidSMTPConfigs = {
missingHost: { ...validSMTPConfig, host: '' },
invalidPort: { ...validSMTPConfig, port: -1 },
portTooHigh: { ...validSMTPConfig, port: 99999 },
portZero: { ...validSMTPConfig, port: 0 },
invalidEmail: { ...validSMTPConfig, from_address: 'not-an-email' },
emptyEmail: { ...validSMTPConfig, from_address: '' },
invalidEmailMissingDomain: { ...validSMTPConfig, from_address: 'user@' },
invalidEmailMissingLocal: { ...validSMTPConfig, from_address: '@domain.com' },
};
/**
* Generate a unique test email address
*/
export function generateTestEmail(): string {
return `test-${Date.now()}-${crypto.randomBytes(4).toString('hex')}@test.local`;
}
// ============================================================================
// System Settings Types and Fixtures
// ============================================================================
/**
* SSL provider options
*/
export type SSLProvider = 'auto' | 'letsencrypt-staging' | 'letsencrypt-prod' | 'zerossl';
/**
* Domain link behavior options
*/
export type DomainLinkBehavior = 'same_tab' | 'new_tab' | 'new_window';
/**
* System settings interface
*/
export interface SystemSettings {
caddyAdminApi: string;
sslProvider: SSLProvider;
domainLinkBehavior: DomainLinkBehavior;
publicUrl: string;
language?: string;
}
/**
* Default system settings matching application defaults
*/
export const defaultSystemSettings: SystemSettings = {
caddyAdminApi: 'http://localhost:2019',
sslProvider: 'auto',
domainLinkBehavior: 'new_tab',
publicUrl: 'http://localhost:8080',
language: 'en',
};
/**
* System settings with production-like configuration
*/
export const productionSystemSettings: SystemSettings = {
caddyAdminApi: 'http://caddy:2019',
sslProvider: 'letsencrypt-prod',
domainLinkBehavior: 'new_tab',
publicUrl: 'https://charon.example.com',
language: 'en',
};
/**
* Invalid system settings for validation testing
*/
export const invalidSystemSettings = {
invalidCaddyApiUrl: { ...defaultSystemSettings, caddyAdminApi: 'not-a-url' },
emptyCaddyApiUrl: { ...defaultSystemSettings, caddyAdminApi: '' },
invalidPublicUrl: { ...defaultSystemSettings, publicUrl: 'not-a-valid-url' },
emptyPublicUrl: { ...defaultSystemSettings, publicUrl: '' },
};
/**
* Generate a valid public URL for testing
* @param valid - Whether to generate a valid or invalid URL
*/
export function generatePublicUrl(valid: boolean = true): string {
if (valid) {
return `https://charon-test-${Date.now()}.example.com`;
}
return 'not-a-valid-url';
}
/**
* Generate a unique Caddy admin API URL for testing
*/
export function generateCaddyApiUrl(): string {
return `http://caddy-test-${Date.now()}.local:2019`;
}
// ============================================================================
// Account Settings Types and Fixtures
// ============================================================================
/**
* User profile interface
*/
export interface UserProfile {
name: string;
email: string;
}
/**
* Password change request interface
*/
export interface PasswordChangeRequest {
currentPassword: string;
newPassword: string;
confirmPassword: string;
}
/**
* Certificate email settings interface
*/
export interface CertificateEmailSettings {
useAccountEmail: boolean;
customEmail?: string;
}
/**
* Valid user profile for testing
*/
export const validUserProfile: UserProfile = {
name: 'Test User',
email: 'testuser@example.com',
};
/**
* Generate a unique user profile for testing
*/
export function generateUserProfile(): UserProfile {
const timestamp = Date.now();
return {
name: `Test User ${timestamp}`,
email: `testuser-${timestamp}@test.local`,
};
}
/**
* Valid password change request
*/
export const validPasswordChange: PasswordChangeRequest = {
currentPassword: 'OldPassword123!',
newPassword: 'NewSecureP@ss456',
confirmPassword: 'NewSecureP@ss456',
};
/**
* Invalid password change requests for validation testing
*/
export const invalidPasswordChanges = {
wrongCurrentPassword: {
currentPassword: 'WrongPassword123!',
newPassword: 'NewSecureP@ss456',
confirmPassword: 'NewSecureP@ss456',
},
mismatchedPasswords: {
currentPassword: 'OldPassword123!',
newPassword: 'NewSecureP@ss456',
confirmPassword: 'DifferentPassword789!',
},
weakPassword: {
currentPassword: 'OldPassword123!',
newPassword: '123',
confirmPassword: '123',
},
emptyNewPassword: {
currentPassword: 'OldPassword123!',
newPassword: '',
confirmPassword: '',
},
emptyCurrentPassword: {
currentPassword: '',
newPassword: 'NewSecureP@ss456',
confirmPassword: 'NewSecureP@ss456',
},
};
/**
* Certificate email settings variations
*/
export const certificateEmailSettings = {
useAccountEmail: {
useAccountEmail: true,
} as CertificateEmailSettings,
useCustomEmail: {
useAccountEmail: false,
customEmail: 'certs@example.com',
} as CertificateEmailSettings,
invalidCustomEmail: {
useAccountEmail: false,
customEmail: 'not-an-email',
} as CertificateEmailSettings,
};
// ============================================================================
// Feature Flags Types and Fixtures
// ============================================================================
/**
* Feature flags interface
*/
export interface FeatureFlags {
cerberus_enabled: boolean;
crowdsec_console_enrollment: boolean;
uptime_monitoring: boolean;
}
/**
* Default feature flags (all disabled)
*/
export const defaultFeatureFlags: FeatureFlags = {
cerberus_enabled: false,
crowdsec_console_enrollment: false,
uptime_monitoring: false,
};
/**
* All features enabled
*/
export const allFeaturesEnabled: FeatureFlags = {
cerberus_enabled: true,
crowdsec_console_enrollment: true,
uptime_monitoring: true,
};
// ============================================================================
// API Key Types and Fixtures
// ============================================================================
/**
* API key response interface
*/
export interface ApiKeyResponse {
api_key: string;
created_at: string;
}
/**
* Mock API key for display testing
*/
export const mockApiKey: ApiKeyResponse = {
api_key: 'charon_api_key_mock_12345678901234567890',
created_at: new Date().toISOString(),
};
// ============================================================================
// System Health Types and Fixtures
// ============================================================================
/**
* System health status interface
*/
export interface SystemHealth {
status: 'healthy' | 'degraded' | 'unhealthy';
caddy: boolean;
database: boolean;
version: string;
uptime: number;
}
/**
* Healthy system status
*/
export const healthySystemStatus: SystemHealth = {
status: 'healthy',
caddy: true,
database: true,
version: '1.0.0-beta',
uptime: 86400,
};
/**
* Degraded system status
*/
export const degradedSystemStatus: SystemHealth = {
status: 'degraded',
caddy: true,
database: false,
version: '1.0.0-beta',
uptime: 3600,
};
/**
* Unhealthy system status
*/
export const unhealthySystemStatus: SystemHealth = {
status: 'unhealthy',
caddy: false,
database: false,
version: '1.0.0-beta',
uptime: 0,
};
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Create SMTP config via API
*/
export async function createSMTPConfig(
request: { post: (url: string, options: { data: unknown }) => Promise<{ ok: () => boolean; json: () => Promise<unknown> }> },
config: SMTPConfig
): Promise<void> {
const response = await request.post('/api/v1/settings/smtp', {
data: config,
});
if (!response.ok()) {
throw new Error('Failed to create SMTP config');
}
}
/**
* Update system setting via API
*/
export async function updateSystemSetting(
request: { post: (url: string, options: { data: unknown }) => Promise<{ ok: () => boolean; json: () => Promise<unknown> }> },
key: string,
value: unknown
): Promise<void> {
const response = await request.post('/api/v1/settings', {
data: { key, value },
});
if (!response.ok()) {
throw new Error(`Failed to update setting: ${key}`);
}
}
-595
View File
@@ -1,595 +0,0 @@
/**
* Test Data Generators - Common test data factories
*
* This module provides functions to generate realistic test data
* with unique identifiers to prevent collisions in parallel tests.
*
* @example
* ```typescript
* import { generateProxyHostData, generateUserData } from './fixtures/test-data';
*
* test('create proxy host', async ({ testData }) => {
* const hostData = generateProxyHostData();
* const { id } = await testData.createProxyHost(hostData);
* });
* ```
*/
import * as crypto from 'crypto';
/**
* Generate a unique identifier with optional prefix
* Uses timestamp + high-resolution time + random bytes for maximum uniqueness
* @param prefix - Optional prefix for the ID
* @returns Unique identifier string
*
* @example
* ```typescript
* const id = generateUniqueId('test');
* // Returns: "test-m1abc123-0042-deadbeef"
* ```
*/
export function generateUniqueId(prefix = ''): string {
const timestamp = Date.now().toString(36);
// Add high-resolution time component for nanosecond-level uniqueness
const hrTime = process.hrtime.bigint().toString(36).slice(-4);
const random = crypto.randomBytes(4).toString('hex');
return prefix ? `${prefix}-${timestamp}-${hrTime}-${random}` : `${timestamp}-${hrTime}-${random}`;
}
/**
* Generate a unique UUID v4
* @returns UUID string
*
* @example
* ```typescript
* const uuid = generateUUID();
* // Returns: "550e8400-e29b-41d4-a716-446655440000"
* ```
*/
export function generateUUID(): string {
return crypto.randomUUID();
}
/**
* Generate a valid private IP address (RFC 1918)
* Uses 10.x.x.x range to avoid conflicts with local networks
* @param options - Configuration options
* @returns Valid IP address string
*
* @example
* ```typescript
* const ip = generateIPAddress();
* // Returns: "10.42.128.15"
*
* const specificRange = generateIPAddress({ octet2: 100 });
* // Returns: "10.100.x.x"
* ```
*/
export function generateIPAddress(options: {
/** Second octet (0-255), random if not specified */
octet2?: number;
/** Third octet (0-255), random if not specified */
octet3?: number;
/** Fourth octet (1-254), random if not specified */
octet4?: number;
} = {}): string {
const o2 = options.octet2 ?? secureRandomInt(256);
const o3 = options.octet3 ?? secureRandomInt(256);
const o4 = options.octet4 ?? secureRandomInt(253) + 1; // 1-254
return `10.${o2}.${o3}.${o4}`;
}
/**
* Generate a valid CIDR range
* @param prefix - CIDR prefix (8-32)
* @returns Valid CIDR string
*
* @example
* ```typescript
* const cidr = generateCIDR(24);
* // Returns: "10.42.128.0/24"
* ```
*/
export function generateCIDR(prefix: number = 24): string {
const ip = generateIPAddress({ octet4: 0 });
return `${ip}/${prefix}`;
}
/**
* Generate a valid port number
* @param options - Configuration options
* @returns Valid port number
*
* @example
* ```typescript
* const port = generatePort();
* // Returns: 8080-65000 range
*
* const specificRange = generatePort({ min: 3000, max: 4000 });
* // Returns: 3000-4000 range
* ```
*/
export function generatePort(options: {
/** Minimum port (default: 8080) */
min?: number;
/** Maximum port (default: 65000) */
max?: number;
} = {}): number {
const { min = 8080, max = 65000 } = options;
return secureRandomInt(max - min + 1) + min;
}
/**
* Common ports for testing purposes
*/
export const commonPorts = {
http: 80,
https: 443,
ssh: 22,
mysql: 3306,
postgres: 5432,
redis: 6379,
mongodb: 27017,
node: 3000,
react: 3000,
vite: 5173,
backend: 8080,
proxy: 8081,
} as const;
/**
* Generate a password that meets common requirements
* - At least 12 characters
* - Contains uppercase, lowercase, numbers, and special characters
* @param length - Password length (default: 16)
* @returns Strong password string
*
* @example
* ```typescript
* const password = generatePassword();
* // Returns: "Xy7!kM2@pL9#nQ4$"
* ```
*/
function secureRandomInt(maxExclusive: number): number {
if (maxExclusive <= 0) {
throw new Error('maxExclusive must be positive');
}
const maxUint32 = 0xffffffff;
const limit = maxUint32 - ((maxUint32 + 1) % maxExclusive);
while (true) {
const random = crypto.randomBytes(4).readUInt32BE(0);
if (random <= limit) {
return random % maxExclusive;
}
}
}
function shuffleArraySecure<T>(arr: T[]): T[] {
for (let i = arr.length - 1; i > 0; i--) {
const j = secureRandomInt(i + 1);
const tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
return arr;
}
export function generatePassword(length: number = 16): string {
const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const lower = 'abcdefghijklmnopqrstuvwxyz';
const numbers = '0123456789';
const special = '!@#$%^&*';
const all = upper + lower + numbers + special;
// Ensure at least one of each type
let password = '';
password += upper[secureRandomInt(upper.length)];
password += lower[secureRandomInt(lower.length)];
password += numbers[secureRandomInt(numbers.length)];
password += special[secureRandomInt(special.length)];
// Fill the rest randomly
for (let i = 4; i < length; i++) {
password += all[secureRandomInt(all.length)];
}
// Shuffle the password using a cryptographically secure shuffle
return shuffleArraySecure(password.split('')).join('');
}
/**
* Common test passwords for different scenarios
*/
export const testPasswords = {
/** Valid strong password */
valid: 'TestPass123!',
/** Another valid password */
validAlt: 'SecureP@ss456',
/** Too short */
tooShort: 'Ab1!',
/** No uppercase */
noUppercase: 'password123!',
/** No lowercase */
noLowercase: 'PASSWORD123!',
/** No numbers */
noNumbers: 'Password!!',
/** No special characters */
noSpecial: 'Password123',
/** Common weak password */
weak: 'password',
} as const;
/**
* Counter for additional uniqueness within same millisecond
*/
let domainCounter = 0;
/**
* Generate a unique domain name for testing
* Uses timestamp + counter for uniqueness while keeping length reasonable
* @param subdomain - Optional subdomain prefix
* @returns Unique domain string guaranteed to be unique even in rapid calls
*/
export function generateDomain(subdomain = 'app'): string {
// Increment counter and wrap at 9999 to keep domain lengths reasonable
domainCounter = (domainCounter + 1) % 10000;
// Use shorter ID format: base36 timestamp (7 chars) + random (4 chars)
const timestamp = Date.now().toString(36).slice(-7);
const random = crypto.randomBytes(2).toString('hex');
// Format: subdomain-timestamp-random-counter.test.local
// Example: proxy-1abc123-a1b2-1.test.local (max ~35 chars)
return `${subdomain}-${timestamp}-${random}-${domainCounter}.test.local`;
}
/**
* Generate a unique email address for testing
* @param prefix - Optional email prefix
* @returns Unique email string
*/
export function generateEmail(prefix = 'test'): string {
const id = generateUniqueId();
return `${prefix}-${id}@test.local`;
}
/**
* Proxy host test data
*/
export interface ProxyHostTestData {
domain: string;
forwardHost: string;
forwardPort: number;
scheme: 'http' | 'https';
websocketSupport: boolean;
}
/**
* Generate proxy host test data with unique domain
* @param overrides - Optional overrides for default values
* @returns ProxyHostTestData object
*/
export function generateProxyHostData(
overrides: Partial<ProxyHostTestData> = {}
): ProxyHostTestData {
return {
domain: generateDomain('proxy'),
forwardHost: '192.168.1.100',
forwardPort: 3000,
scheme: 'http',
websocketSupport: false,
...overrides,
};
}
/**
* Generate proxy host data for Docker container
* @param containerName - Docker container name
* @param port - Container port
* @returns ProxyHostTestData object
*/
export function generateDockerProxyHostData(
containerName: string,
port = 80
): ProxyHostTestData {
return {
domain: generateDomain('docker'),
forwardHost: containerName,
forwardPort: port,
scheme: 'http',
websocketSupport: false,
};
}
/**
* Access list test data
*/
export interface AccessListTestData {
name: string;
rules: Array<{ type: 'allow' | 'deny'; value: string }>;
}
/**
* Generate access list test data with unique name
* @param overrides - Optional overrides for default values
* @returns AccessListTestData object
*/
export function generateAccessListData(
overrides: Partial<AccessListTestData> = {}
): AccessListTestData {
const id = generateUniqueId();
return {
name: `ACL-${id}`,
rules: [
{ type: 'allow', value: '192.168.1.0/24' },
{ type: 'deny', value: '0.0.0.0/0' },
],
...overrides,
};
}
/**
* Generate an allowlist that permits all traffic
* @param name - Optional name override
* @returns AccessListTestData object
*/
export function generateAllowAllAccessList(name?: string): AccessListTestData {
return {
name: name || `AllowAll-${generateUniqueId()}`,
rules: [{ type: 'allow', value: '0.0.0.0/0' }],
};
}
/**
* Generate a denylist that blocks specific IPs
* @param blockedIPs - Array of IPs to block
* @returns AccessListTestData object
*/
export function generateDenyListAccessList(blockedIPs: string[]): AccessListTestData {
return {
name: `DenyList-${generateUniqueId()}`,
rules: blockedIPs.map((ip) => ({ type: 'deny' as const, value: ip })),
};
}
/**
* Certificate test data
*/
export interface CertificateTestData {
domains: string[];
type: 'letsencrypt' | 'custom';
privateKey?: string;
certificate?: string;
}
/**
* Generate certificate test data with unique domains
* @param overrides - Optional overrides for default values
* @returns CertificateTestData object
*/
export function generateCertificateData(
overrides: Partial<CertificateTestData> = {}
): CertificateTestData {
return {
domains: [generateDomain('cert')],
type: 'letsencrypt',
...overrides,
};
}
/**
* Generate custom certificate test data
* Note: Uses placeholder values - in real tests, use actual cert/key
* @param domains - Domains for the certificate
* @returns CertificateTestData object
*/
export function generateCustomCertificateData(domains?: string[]): CertificateTestData {
return {
domains: domains || [generateDomain('custom-cert')],
type: 'custom',
// Placeholder - real tests should provide actual certificate data
privateKey: '-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQ...\n-----END PRIVATE KEY-----',
certificate: '-----BEGIN CERTIFICATE-----\nMIIDazCCAlOgAwIBAgIUa...\n-----END CERTIFICATE-----',
};
}
/**
* Generate wildcard certificate test data
* @param baseDomain - Base domain for the wildcard
* @returns CertificateTestData object
*/
export function generateWildcardCertificateData(baseDomain?: string): CertificateTestData {
const domain = baseDomain || `${generateUniqueId()}.test.local`;
return {
domains: [`*.${domain}`, domain],
type: 'letsencrypt',
};
}
/**
* User test data
*/
export interface UserTestData {
email: string;
password: string;
role: 'admin' | 'user' | 'guest';
name?: string;
}
/**
* Generate user test data with unique email
* @param overrides - Optional overrides for default values
* @returns UserTestData object
*/
export function generateUserData(overrides: Partial<UserTestData> = {}): UserTestData {
const id = generateUniqueId();
return {
email: generateEmail('user'),
password: 'TestPass123!',
role: 'user',
name: `Test User ${id}`,
...overrides,
};
}
/**
* Generate admin user test data
* @param overrides - Optional overrides
* @returns UserTestData object
*/
export function generateAdminUserData(overrides: Partial<UserTestData> = {}): UserTestData {
return generateUserData({ ...overrides, role: 'admin' });
}
/**
* Generate guest user test data
* @param overrides - Optional overrides
* @returns UserTestData object
*/
export function generateGuestUserData(overrides: Partial<UserTestData> = {}): UserTestData {
return generateUserData({ ...overrides, role: 'guest' });
}
/**
* DNS provider test data
*/
export interface DNSProviderTestData {
name: string;
type: 'manual' | 'cloudflare' | 'route53' | 'webhook' | 'rfc2136';
credentials?: Record<string, string>;
}
/**
* Generate DNS provider test data with unique name
* @param providerType - Type of DNS provider
* @param overrides - Optional overrides for default values
* @returns DNSProviderTestData object
*/
export function generateDNSProviderData(
providerType: DNSProviderTestData['type'] = 'manual',
overrides: Partial<DNSProviderTestData> = {}
): DNSProviderTestData {
const id = generateUniqueId();
const baseData: DNSProviderTestData = {
name: `DNS-${providerType}-${id}`,
type: providerType,
};
// Add type-specific credentials
switch (providerType) {
case 'cloudflare':
baseData.credentials = {
api_token: `test-token-${id}`,
};
break;
case 'route53':
baseData.credentials = {
access_key_id: `AKIATEST${id.toUpperCase()}`,
secret_access_key: `secretkey${id}`,
region: 'us-east-1',
};
break;
case 'webhook':
baseData.credentials = {
create_url: `https://example.com/dns/${id}/create`,
delete_url: `https://example.com/dns/${id}/delete`,
};
break;
case 'rfc2136':
baseData.credentials = {
nameserver: 'ns.example.com:53',
tsig_key_name: `ddns-${id}.example.com`,
tsig_key: 'base64-encoded-key==',
tsig_algorithm: 'hmac-sha256',
};
break;
case 'manual':
default:
baseData.credentials = {};
break;
}
return { ...baseData, ...overrides };
}
/**
* CrowdSec decision test data
*/
export interface CrowdSecDecisionTestData {
ip: string;
duration: string;
reason: string;
scope: 'ip' | 'range' | 'country';
}
/**
* Generate CrowdSec decision test data
* @param overrides - Optional overrides for default values
* @returns CrowdSecDecisionTestData object
*/
export function generateCrowdSecDecisionData(
overrides: Partial<CrowdSecDecisionTestData> = {}
): CrowdSecDecisionTestData {
return {
ip: `10.0.${secureRandomInt(255)}.${secureRandomInt(255)}`,
duration: '4h',
reason: 'Test ban - automated testing',
scope: 'ip',
...overrides,
};
}
/**
* Rate limit rule test data
*/
export interface RateLimitRuleTestData {
name: string;
requests: number;
window: string;
action: 'block' | 'throttle';
}
/**
* Generate rate limit rule test data
* @param overrides - Optional overrides for default values
* @returns RateLimitRuleTestData object
*/
export function generateRateLimitRuleData(
overrides: Partial<RateLimitRuleTestData> = {}
): RateLimitRuleTestData {
const id = generateUniqueId();
return {
name: `RateLimit-${id}`,
requests: 100,
window: '1m',
action: 'block',
...overrides,
};
}
/**
* Backup test data
*/
export interface BackupTestData {
name: string;
includeConfig: boolean;
includeCertificates: boolean;
includeDatabase: boolean;
}
/**
* Generate backup test data
* @param overrides - Optional overrides for default values
* @returns BackupTestData object
*/
export function generateBackupData(
overrides: Partial<BackupTestData> = {}
): BackupTestData {
const id = generateUniqueId();
return {
name: `Backup-${id}`,
includeConfig: true,
includeCertificates: true,
includeDatabase: true,
...overrides,
};
}