Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
@@ -0,0 +1,837 @@
|
||||
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 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user