Files
dashcaddy/dashcaddy-installer/src/main/config-manager.property.test.js
T
Sami f61e85d9a7 Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend,
DashCA certificate distribution, installer script, and deployment skills.
2026-03-05 02:26:12 -08:00

838 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 }
);
});
});
});