chore: clean .gitignore cache
This commit is contained in:
Vendored
-408
@@ -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(),
|
||||
};
|
||||
}
|
||||
Vendored
-247
@@ -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 };
|
||||
Vendored
-397
@@ -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',
|
||||
};
|
||||
}
|
||||
Vendored
-280
@@ -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));
|
||||
}
|
||||
Vendored
-430
@@ -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',
|
||||
};
|
||||
}
|
||||
Vendored
-480
@@ -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>;
|
||||
}
|
||||
Vendored
-386
@@ -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(),
|
||||
};
|
||||
}
|
||||
Vendored
-147
@@ -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));
|
||||
}
|
||||
Vendored
-399
@@ -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}`);
|
||||
}
|
||||
}
|
||||
Vendored
-595
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user