Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
838 lines
27 KiB
JavaScript
838 lines
27 KiB
JavaScript
const fc = require('fast-check');
|
||
const ConfigManager = require('./config-manager');
|
||
const fs = require('fs').promises;
|
||
const path = require('path');
|
||
const os = require('os');
|
||
|
||
/**
|
||
* Property-based tests for ConfigManager
|
||
* These tests validate universal properties across all valid inputs
|
||
*/
|
||
describe('ConfigManager Property Tests', () => {
|
||
let manager;
|
||
let testDir;
|
||
|
||
beforeEach(async () => {
|
||
manager = new ConfigManager();
|
||
testDir = path.join(os.tmpdir(), `dashcaddy-prop-test-${Date.now()}`);
|
||
});
|
||
|
||
afterEach(async () => {
|
||
try {
|
||
await fs.rm(testDir, { recursive: true, force: true });
|
||
} catch (error) {
|
||
// Ignore cleanup errors
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Feature: dashcaddy-installer, Property 5: Configuration Persistence
|
||
* For any installation configuration, saving and then loading the configuration
|
||
* should produce an equivalent configuration object.
|
||
* Validates: Requirements 2.6, 10.1, 10.2
|
||
*/
|
||
describe('Property 5: Configuration Persistence', () => {
|
||
// Generator for valid configuration objects
|
||
const configArbitrary = () => fc.record({
|
||
installPath: fc.constant(testDir),
|
||
tier: fc.constantFrom('basic', 'intermediate', 'advanced'),
|
||
dashboardName: fc.string({ minLength: 1, maxLength: 50 }),
|
||
customTLD: fc.option(fc.string({ minLength: 2, maxLength: 10 }).map(s => '.' + s)),
|
||
caddyAdminUrl: fc.constant('http://localhost:2021'),
|
||
apiPort: fc.integer({ min: 1024, max: 65535 }),
|
||
dnsEnabled: fc.boolean(),
|
||
tailscaleEnabled: fc.boolean()
|
||
});
|
||
|
||
test('configuration round-trips correctly', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
configArbitrary(),
|
||
async (config) => {
|
||
// Save configuration
|
||
const saveResult = await manager.saveConfig(config, testDir);
|
||
if (!saveResult.success) return true; // Skip if save failed
|
||
|
||
// Load configuration
|
||
const loadResult = await manager.loadConfig(testDir);
|
||
if (!loadResult.exists) return false;
|
||
|
||
// Verify all original fields are preserved
|
||
const loaded = loadResult.config;
|
||
return (
|
||
loaded.installPath === config.installPath &&
|
||
loaded.tier === config.tier &&
|
||
loaded.dashboardName === config.dashboardName &&
|
||
loaded.apiPort === config.apiPort &&
|
||
loaded.dnsEnabled === config.dnsEnabled &&
|
||
loaded.tailscaleEnabled === config.tailscaleEnabled
|
||
);
|
||
}
|
||
),
|
||
{ numRuns: 100 }
|
||
);
|
||
});
|
||
|
||
test('saved config always includes metadata', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
configArbitrary(),
|
||
async (config) => {
|
||
await manager.saveConfig(config, testDir);
|
||
const result = await manager.loadConfig(testDir);
|
||
|
||
if (!result.exists) return true; // Skip if load failed
|
||
|
||
return (
|
||
typeof result.config.version === 'string' &&
|
||
typeof result.config.lastModified === 'string'
|
||
);
|
||
}
|
||
),
|
||
{ numRuns: 100 }
|
||
);
|
||
});
|
||
|
||
test('loading non-existent config always returns exists=false', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.string({ minLength: 1, maxLength: 20 }),
|
||
async (randomPath) => {
|
||
const nonExistentPath = path.join(testDir, randomPath);
|
||
const result = await manager.loadConfig(nonExistentPath);
|
||
|
||
return result.exists === false && result.config === null;
|
||
}
|
||
),
|
||
{ numRuns: 50 }
|
||
);
|
||
});
|
||
});
|
||
|
||
/**
|
||
* Feature: dashcaddy-installer, Property 3: Installation Path Validation
|
||
* For any selected folder path, the installer should correctly determine
|
||
* if the path is writable before proceeding with installation.
|
||
* Validates: Requirements 2.3, 2.4
|
||
*/
|
||
describe('Property 3: Installation Path Validation', () => {
|
||
test('validatePath returns consistent structure', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.string({ minLength: 1, maxLength: 50 }),
|
||
async (pathSegment) => {
|
||
const testPath = path.join(testDir, pathSegment);
|
||
const result = await manager.validatePath(testPath);
|
||
|
||
return (
|
||
typeof result === 'object' &&
|
||
typeof result.valid === 'boolean' &&
|
||
typeof result.message === 'string'
|
||
);
|
||
}
|
||
),
|
||
{ numRuns: 50 }
|
||
);
|
||
});
|
||
|
||
test('valid paths are consistently validated', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.string({ minLength: 1, maxLength: 20 }),
|
||
async (pathSegment) => {
|
||
const testPath = path.join(testDir, pathSegment);
|
||
|
||
// Validate twice
|
||
const result1 = await manager.validatePath(testPath);
|
||
const result2 = await manager.validatePath(testPath);
|
||
|
||
// Results should be consistent
|
||
return result1.valid === result2.valid;
|
||
}
|
||
),
|
||
{ numRuns: 30 }
|
||
);
|
||
});
|
||
});
|
||
|
||
/**
|
||
* Feature: dashcaddy-installer, Property 4: Directory Structure Creation
|
||
* For any valid installation path, the installer should create all required
|
||
* subdirectories (config, data, logs, caddyfile) and verify their existence.
|
||
* Validates: Requirements 2.5
|
||
*/
|
||
describe('Property 4: Directory Structure Creation', () => {
|
||
test('createDirectories creates all required directories', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.string({ minLength: 1, maxLength: 20 }),
|
||
async (pathSegment) => {
|
||
const installPath = path.join(testDir, pathSegment);
|
||
const result = await manager.createDirectories(installPath);
|
||
|
||
if (!result.success) return true; // Skip if creation failed
|
||
|
||
// Verify all required directories exist
|
||
const requiredDirs = ['config', 'data', 'logs', 'caddyfile'];
|
||
const checks = await Promise.all(
|
||
requiredDirs.map(async (dir) => {
|
||
const dirPath = path.join(installPath, dir);
|
||
try {
|
||
await fs.access(dirPath);
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
})
|
||
);
|
||
|
||
return checks.every(check => check === true);
|
||
}
|
||
),
|
||
{ numRuns: 50 }
|
||
);
|
||
});
|
||
|
||
test('createDirectories is idempotent', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.string({ minLength: 1, maxLength: 20 }).filter(s => !s.includes('\0') && !s.includes('|')),
|
||
async (pathSegment) => {
|
||
const installPath = path.join(testDir, pathSegment);
|
||
|
||
// Create directories twice
|
||
const result1 = await manager.createDirectories(installPath);
|
||
const result2 = await manager.createDirectories(installPath);
|
||
|
||
// Both should succeed (or both fail for invalid paths)
|
||
return result1.success === result2.success;
|
||
}
|
||
),
|
||
{ numRuns: 30 }
|
||
);
|
||
});
|
||
});
|
||
|
||
/**
|
||
* Feature: dashcaddy-installer, Property 8: DNS Credential Security
|
||
* For any DNS credentials, saving and then loading the credentials should
|
||
* preserve all fields, and the password should be encrypted in storage.
|
||
* Validates: Requirements 4.8, 10.3
|
||
*/
|
||
describe('Property 8: DNS Credential Security', () => {
|
||
const credentialsArbitrary = () => fc.record({
|
||
server: fc.ipV4(),
|
||
username: fc.string({ minLength: 1, maxLength: 20 }),
|
||
password: fc.string({ minLength: 8, maxLength: 50 }),
|
||
tld: fc.string({ minLength: 2, maxLength: 10 }).map(s => '.' + s)
|
||
});
|
||
|
||
test('DNS credentials round-trip correctly', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
credentialsArbitrary(),
|
||
async (credentials) => {
|
||
// Save credentials
|
||
const saveResult = await manager.saveDNSCredentials(credentials, testDir);
|
||
if (!saveResult.success) return true; // Skip if save failed
|
||
|
||
// Load credentials
|
||
const loadResult = await manager.loadDNSCredentials(testDir);
|
||
if (!loadResult.exists) return false;
|
||
|
||
// Verify all fields are preserved
|
||
const loaded = loadResult.credentials;
|
||
return (
|
||
loaded.server === credentials.server &&
|
||
loaded.username === credentials.username &&
|
||
loaded.password === credentials.password &&
|
||
loaded.tld === credentials.tld
|
||
);
|
||
}
|
||
),
|
||
{ numRuns: 100 }
|
||
);
|
||
});
|
||
|
||
test('saved credentials always include timestamp', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
credentialsArbitrary(),
|
||
async (credentials) => {
|
||
await manager.saveDNSCredentials(credentials, testDir);
|
||
const result = await manager.loadDNSCredentials(testDir);
|
||
|
||
if (!result.exists) return true; // Skip if load failed
|
||
|
||
return typeof result.credentials.savedAt === 'string';
|
||
}
|
||
),
|
||
{ numRuns: 100 }
|
||
);
|
||
});
|
||
});
|
||
|
||
/**
|
||
* Feature: dashcaddy-installer, Property 6: Disk Space Verification
|
||
* For any valid path, the installer should correctly determine available disk space
|
||
* and report consistent results across multiple checks.
|
||
* Validates: Requirements 2.4
|
||
*/
|
||
describe('Property 6: Disk Space Verification', () => {
|
||
test('getDiskSpace returns consistent structure', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.string({ minLength: 1, maxLength: 20 }).filter(s => !s.includes('\0') && !s.includes('|')),
|
||
async (pathSegment) => {
|
||
const testPath = path.join(testDir, pathSegment);
|
||
const result = await manager.getDiskSpace(testPath);
|
||
|
||
return (
|
||
typeof result === 'object' &&
|
||
typeof result.available === 'boolean' &&
|
||
(result.available ? typeof result.path === 'string' : typeof result.error === 'string')
|
||
);
|
||
}
|
||
),
|
||
{ numRuns: 50 }
|
||
);
|
||
});
|
||
|
||
test('getDiskSpace is idempotent for same path', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.string({ minLength: 1, maxLength: 20 }).filter(s => !s.includes('\0') && !s.includes('|')),
|
||
async (pathSegment) => {
|
||
const testPath = path.join(testDir, pathSegment);
|
||
|
||
// Check twice
|
||
const result1 = await manager.getDiskSpace(testPath);
|
||
const result2 = await manager.getDiskSpace(testPath);
|
||
|
||
// Results should be consistent
|
||
return result1.available === result2.available;
|
||
}
|
||
),
|
||
{ numRuns: 30 }
|
||
);
|
||
});
|
||
|
||
test('getDiskSpace returns available=true for existing writable paths', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.string({ minLength: 1, maxLength: 15 }).filter(s => /^[a-zA-Z0-9_-]+$/.test(s)),
|
||
async (pathSegment) => {
|
||
const testPath = path.join(testDir, pathSegment);
|
||
|
||
// Create the directory first
|
||
await fs.mkdir(testPath, { recursive: true });
|
||
|
||
const result = await manager.getDiskSpace(testPath);
|
||
|
||
return result.available === true;
|
||
}
|
||
),
|
||
{ numRuns: 30 }
|
||
);
|
||
});
|
||
});
|
||
|
||
/**
|
||
* Feature: dashcaddy-installer, Property 7: Tier Validation
|
||
* For any configuration, only valid tier values (basic, intermediate, advanced)
|
||
* should be accepted and persisted correctly.
|
||
* Validates: Requirements 3.1, 3.2, 3.3
|
||
*/
|
||
describe('Property 7: Tier Validation', () => {
|
||
const validTiers = ['basic', 'intermediate', 'advanced'];
|
||
|
||
test('valid tiers are preserved in config round-trip', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.constantFrom(...validTiers),
|
||
fc.string({ minLength: 1, maxLength: 20 }),
|
||
async (tier, dashboardName) => {
|
||
const config = {
|
||
installPath: testDir,
|
||
tier: tier,
|
||
dashboardName: dashboardName,
|
||
apiPort: 3001,
|
||
dnsEnabled: false,
|
||
tailscaleEnabled: false
|
||
};
|
||
|
||
await manager.saveConfig(config, testDir);
|
||
const result = await manager.loadConfig(testDir);
|
||
|
||
if (!result.exists) return true; // Skip if save/load failed
|
||
|
||
return result.config.tier === tier;
|
||
}
|
||
),
|
||
{ numRuns: 50 }
|
||
);
|
||
});
|
||
|
||
test('tier value is always one of valid options after load', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.constantFrom(...validTiers),
|
||
async (tier) => {
|
||
const config = {
|
||
installPath: testDir,
|
||
tier: tier,
|
||
dashboardName: 'Test',
|
||
apiPort: 3001,
|
||
dnsEnabled: false,
|
||
tailscaleEnabled: false
|
||
};
|
||
|
||
await manager.saveConfig(config, testDir);
|
||
const result = await manager.loadConfig(testDir);
|
||
|
||
if (!result.exists) return true;
|
||
|
||
return validTiers.includes(result.config.tier);
|
||
}
|
||
),
|
||
{ numRuns: 30 }
|
||
);
|
||
});
|
||
|
||
test('tier determines expected feature availability', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.constantFrom(...validTiers),
|
||
async (tier) => {
|
||
const config = {
|
||
installPath: testDir,
|
||
tier: tier,
|
||
dashboardName: 'Test',
|
||
apiPort: 3001,
|
||
dnsEnabled: tier === 'advanced',
|
||
tailscaleEnabled: tier === 'advanced'
|
||
};
|
||
|
||
await manager.saveConfig(config, testDir);
|
||
const result = await manager.loadConfig(testDir);
|
||
|
||
if (!result.exists) return true;
|
||
|
||
// Advanced tier should support DNS and Tailscale
|
||
if (tier === 'advanced') {
|
||
return result.config.dnsEnabled === true && result.config.tailscaleEnabled === true;
|
||
}
|
||
|
||
// Non-advanced tiers should have these disabled
|
||
return result.config.dnsEnabled === false && result.config.tailscaleEnabled === false;
|
||
}
|
||
),
|
||
{ numRuns: 30 }
|
||
);
|
||
});
|
||
});
|
||
|
||
/**
|
||
* Additional property tests for installation management
|
||
*/
|
||
describe('Installation Management Properties', () => {
|
||
// Generator for valid configuration objects (reused from above)
|
||
const configArbitrary = () => fc.record({
|
||
installPath: fc.constant(testDir),
|
||
tier: fc.constantFrom('basic', 'intermediate', 'advanced'),
|
||
dashboardName: fc.string({ minLength: 1, maxLength: 50 }),
|
||
apiPort: fc.integer({ min: 1024, max: 65535 }),
|
||
dnsEnabled: fc.boolean(),
|
||
tailscaleEnabled: fc.boolean()
|
||
});
|
||
|
||
test('installationExists is consistent with config file presence', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
configArbitrary(),
|
||
async (config) => {
|
||
// Use a unique directory for this test
|
||
const uniqueDir = path.join(testDir, `test-${Date.now()}-${Math.random()}`);
|
||
config.installPath = uniqueDir;
|
||
|
||
// Before saving, installation should not exist
|
||
const existsBefore = await manager.installationExists(uniqueDir);
|
||
|
||
// Save config
|
||
await manager.saveConfig(config, uniqueDir);
|
||
|
||
// After saving, installation should exist
|
||
const existsAfter = await manager.installationExists(uniqueDir);
|
||
|
||
return !existsBefore && existsAfter;
|
||
}
|
||
),
|
||
{ numRuns: 50 }
|
||
);
|
||
});
|
||
|
||
test('removeInstallation removes all files', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
configArbitrary(),
|
||
async (config) => {
|
||
// Create installation
|
||
await manager.createDirectories(testDir);
|
||
await manager.saveConfig(config, testDir);
|
||
|
||
// Verify it exists
|
||
const existsBefore = await manager.installationExists(testDir);
|
||
if (!existsBefore) return true; // Skip if creation failed
|
||
|
||
// Remove installation
|
||
const removeResult = await manager.removeInstallation(testDir);
|
||
if (!removeResult.success) return true; // Skip if removal failed
|
||
|
||
// Verify it no longer exists
|
||
const existsAfter = await manager.installationExists(testDir);
|
||
|
||
return !existsAfter;
|
||
}
|
||
),
|
||
{ numRuns: 30 }
|
||
);
|
||
});
|
||
|
||
test('listInstallationFiles returns array', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.string({ minLength: 1, maxLength: 20 }),
|
||
async (pathSegment) => {
|
||
const installPath = path.join(testDir, pathSegment);
|
||
const files = await manager.listInstallationFiles(installPath);
|
||
|
||
return Array.isArray(files);
|
||
}
|
||
),
|
||
{ numRuns: 50 }
|
||
);
|
||
});
|
||
});
|
||
|
||
/**
|
||
* Configuration Edge Cases
|
||
* Tests for special characters, boundary conditions, and data integrity
|
||
*/
|
||
describe('Configuration Edge Cases', () => {
|
||
test('special characters in dashboardName are preserved', async () => {
|
||
// Generator for strings with special characters
|
||
const specialCharArbitrary = fc.string({
|
||
unit: fc.constantFrom(
|
||
'a', 'Z', '0', ' ', '-', '_', '.', '!', '@', '#',
|
||
'é', 'ñ', '中', '日', '한', 'α', 'β'
|
||
),
|
||
minLength: 1,
|
||
maxLength: 30
|
||
});
|
||
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
specialCharArbitrary,
|
||
async (dashboardName) => {
|
||
const config = {
|
||
installPath: testDir,
|
||
tier: 'basic',
|
||
dashboardName: dashboardName,
|
||
apiPort: 3001,
|
||
dnsEnabled: false,
|
||
tailscaleEnabled: false
|
||
};
|
||
|
||
await manager.saveConfig(config, testDir);
|
||
const result = await manager.loadConfig(testDir);
|
||
|
||
if (!result.exists) return true;
|
||
|
||
return result.config.dashboardName === dashboardName;
|
||
}
|
||
),
|
||
{ numRuns: 100 }
|
||
);
|
||
});
|
||
|
||
test('port values at boundaries are handled correctly', async () => {
|
||
const boundaryPorts = fc.constantFrom(1024, 1025, 3000, 3001, 8080, 49151, 65534, 65535);
|
||
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
boundaryPorts,
|
||
async (port) => {
|
||
const config = {
|
||
installPath: testDir,
|
||
tier: 'basic',
|
||
dashboardName: 'Test',
|
||
apiPort: port,
|
||
dnsEnabled: false,
|
||
tailscaleEnabled: false
|
||
};
|
||
|
||
await manager.saveConfig(config, testDir);
|
||
const result = await manager.loadConfig(testDir);
|
||
|
||
if (!result.exists) return true;
|
||
|
||
return result.config.apiPort === port;
|
||
}
|
||
),
|
||
{ numRuns: 20 }
|
||
);
|
||
});
|
||
|
||
test('customTLD with various formats preserved correctly', async () => {
|
||
const tldArbitrary = fc.oneof(
|
||
fc.constant(undefined),
|
||
fc.constant(null),
|
||
fc.string({ minLength: 2, maxLength: 10 }).map(s => '.' + s.replace(/[^a-zA-Z]/g, 'x')),
|
||
fc.constantFrom('.local', '.home', '.lan', '.sami', '.test')
|
||
);
|
||
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
tldArbitrary,
|
||
async (customTLD) => {
|
||
const config = {
|
||
installPath: testDir,
|
||
tier: 'basic',
|
||
dashboardName: 'Test',
|
||
customTLD: customTLD,
|
||
apiPort: 3001,
|
||
dnsEnabled: false,
|
||
tailscaleEnabled: false
|
||
};
|
||
|
||
await manager.saveConfig(config, testDir);
|
||
const result = await manager.loadConfig(testDir);
|
||
|
||
if (!result.exists) return true;
|
||
|
||
// Both null/undefined should be equivalent after round-trip
|
||
if (customTLD === null || customTLD === undefined) {
|
||
return result.config.customTLD === null ||
|
||
result.config.customTLD === undefined ||
|
||
result.config.customTLD === customTLD;
|
||
}
|
||
|
||
return result.config.customTLD === customTLD;
|
||
}
|
||
),
|
||
{ numRuns: 50 }
|
||
);
|
||
});
|
||
|
||
test('boolean fields remain booleans after round-trip', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.boolean(),
|
||
fc.boolean(),
|
||
async (dnsEnabled, tailscaleEnabled) => {
|
||
const config = {
|
||
installPath: testDir,
|
||
tier: 'basic',
|
||
dashboardName: 'Test',
|
||
apiPort: 3001,
|
||
dnsEnabled: dnsEnabled,
|
||
tailscaleEnabled: tailscaleEnabled
|
||
};
|
||
|
||
await manager.saveConfig(config, testDir);
|
||
const result = await manager.loadConfig(testDir);
|
||
|
||
if (!result.exists) return true;
|
||
|
||
return (
|
||
typeof result.config.dnsEnabled === 'boolean' &&
|
||
typeof result.config.tailscaleEnabled === 'boolean' &&
|
||
result.config.dnsEnabled === dnsEnabled &&
|
||
result.config.tailscaleEnabled === tailscaleEnabled
|
||
);
|
||
}
|
||
),
|
||
{ numRuns: 50 }
|
||
);
|
||
});
|
||
|
||
test('config version is valid semver format', async () => {
|
||
const semverRegex = /^\d+\.\d+\.\d+$/;
|
||
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.constantFrom('basic', 'intermediate', 'advanced'),
|
||
async (tier) => {
|
||
const config = {
|
||
installPath: testDir,
|
||
tier: tier,
|
||
dashboardName: 'Test',
|
||
apiPort: 3001,
|
||
dnsEnabled: false,
|
||
tailscaleEnabled: false
|
||
};
|
||
|
||
await manager.saveConfig(config, testDir);
|
||
const result = await manager.loadConfig(testDir);
|
||
|
||
if (!result.exists) return true;
|
||
|
||
return semverRegex.test(result.config.version);
|
||
}
|
||
),
|
||
{ numRuns: 30 }
|
||
);
|
||
});
|
||
|
||
test('lastModified is valid ISO date string', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.constantFrom('basic', 'intermediate', 'advanced'),
|
||
async (tier) => {
|
||
const config = {
|
||
installPath: testDir,
|
||
tier: tier,
|
||
dashboardName: 'Test',
|
||
apiPort: 3001,
|
||
dnsEnabled: false,
|
||
tailscaleEnabled: false
|
||
};
|
||
|
||
await manager.saveConfig(config, testDir);
|
||
const result = await manager.loadConfig(testDir);
|
||
|
||
if (!result.exists) return true;
|
||
|
||
// Should be parseable as a date and not NaN
|
||
const date = new Date(result.config.lastModified);
|
||
return !isNaN(date.getTime());
|
||
}
|
||
),
|
||
{ numRuns: 30 }
|
||
);
|
||
});
|
||
|
||
test('multiple rapid saves preserve final state', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.array(fc.string({ minLength: 1, maxLength: 20 }), { minLength: 2, maxLength: 5 }),
|
||
async (dashboardNames) => {
|
||
// Rapidly save multiple configs
|
||
for (const name of dashboardNames) {
|
||
await manager.saveConfig({
|
||
installPath: testDir,
|
||
tier: 'basic',
|
||
dashboardName: name,
|
||
apiPort: 3001,
|
||
dnsEnabled: false,
|
||
tailscaleEnabled: false
|
||
}, testDir);
|
||
}
|
||
|
||
const result = await manager.loadConfig(testDir);
|
||
|
||
if (!result.exists) return true;
|
||
|
||
// Final saved name should be the last one
|
||
return result.config.dashboardName === dashboardNames[dashboardNames.length - 1];
|
||
}
|
||
),
|
||
{ numRuns: 30 }
|
||
);
|
||
});
|
||
});
|
||
|
||
/**
|
||
* DNS Credential Edge Cases
|
||
*/
|
||
describe('DNS Credential Edge Cases', () => {
|
||
test('passwords with special characters are preserved', async () => {
|
||
const specialPasswordArbitrary = fc.string({
|
||
unit: fc.constantFrom(
|
||
'a', 'Z', '0', '!', '@', '#', '$', '%', '^', '&', '*',
|
||
'(', ')', '-', '_', '=', '+', '[', ']', '{', '}', '|',
|
||
';', ':', "'", '"', '<', '>', ',', '.', '/', '?', '`', '~'
|
||
),
|
||
minLength: 8,
|
||
maxLength: 50
|
||
});
|
||
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
specialPasswordArbitrary,
|
||
async (password) => {
|
||
const credentials = {
|
||
server: '192.168.1.1',
|
||
username: 'admin',
|
||
password: password,
|
||
tld: '.local'
|
||
};
|
||
|
||
await manager.saveDNSCredentials(credentials, testDir);
|
||
const result = await manager.loadDNSCredentials(testDir);
|
||
|
||
if (!result.exists) return true;
|
||
|
||
return result.credentials.password === password;
|
||
}
|
||
),
|
||
{ numRuns: 100 }
|
||
);
|
||
});
|
||
|
||
test('various IP address formats in server field', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.ipV4(),
|
||
async (server) => {
|
||
const credentials = {
|
||
server: server,
|
||
username: 'admin',
|
||
password: 'password123',
|
||
tld: '.local'
|
||
};
|
||
|
||
await manager.saveDNSCredentials(credentials, testDir);
|
||
const result = await manager.loadDNSCredentials(testDir);
|
||
|
||
if (!result.exists) return true;
|
||
|
||
return result.credentials.server === server;
|
||
}
|
||
),
|
||
{ numRuns: 50 }
|
||
);
|
||
});
|
||
|
||
test('username with edge case lengths', async () => {
|
||
const usernameArbitrary = fc.string({
|
||
unit: fc.constantFrom('a', 'b', 'c', '1', '2', '_', '-'),
|
||
minLength: 1,
|
||
maxLength: 100
|
||
});
|
||
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
usernameArbitrary,
|
||
async (username) => {
|
||
const credentials = {
|
||
server: '192.168.1.1',
|
||
username: username,
|
||
password: 'password123',
|
||
tld: '.local'
|
||
};
|
||
|
||
await manager.saveDNSCredentials(credentials, testDir);
|
||
const result = await manager.loadDNSCredentials(testDir);
|
||
|
||
if (!result.exists) return true;
|
||
|
||
return result.credentials.username === username;
|
||
}
|
||
),
|
||
{ numRuns: 50 }
|
||
);
|
||
});
|
||
});
|
||
});
|