Phase 1: Add ESLint/Prettier config + baseline auto-fixes
This commit is contained in:
@@ -77,7 +77,7 @@ describe('API Endpoints', () => {
|
||||
name: 'Test Service',
|
||||
logo: '/assets/test.png',
|
||||
ip: 'localhost',
|
||||
tailscaleOnly: false
|
||||
tailscaleOnly: false,
|
||||
});
|
||||
|
||||
// Now get services
|
||||
@@ -87,7 +87,7 @@ describe('API Endpoints', () => {
|
||||
expect(res.body.length).toBe(1);
|
||||
expect(res.body[0]).toMatchObject({
|
||||
id: 'test-service',
|
||||
name: 'Test Service'
|
||||
name: 'Test Service',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,7 +113,7 @@ describe('API Endpoints', () => {
|
||||
name: 'Plex',
|
||||
logo: '/assets/plex.png',
|
||||
ip: 'localhost',
|
||||
tailscaleOnly: false
|
||||
tailscaleOnly: false,
|
||||
};
|
||||
|
||||
const res = await request(app)
|
||||
@@ -134,7 +134,7 @@ describe('API Endpoints', () => {
|
||||
test('should reject duplicate service IDs', async () => {
|
||||
const service = {
|
||||
id: 'duplicate',
|
||||
name: 'Duplicate Service'
|
||||
name: 'Duplicate Service',
|
||||
};
|
||||
|
||||
// Add first time
|
||||
@@ -153,7 +153,7 @@ describe('API Endpoints', () => {
|
||||
.post('/api/services')
|
||||
.send({
|
||||
// Missing 'id' and 'name'
|
||||
logo: '/assets/test.png'
|
||||
logo: '/assets/test.png',
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
@@ -164,7 +164,7 @@ describe('API Endpoints', () => {
|
||||
const maliciousService = {
|
||||
id: 'test<script>alert(1)</script>',
|
||||
name: '<img src=x onerror=alert(1)>',
|
||||
logo: '/assets/test.png'
|
||||
logo: '/assets/test.png',
|
||||
};
|
||||
|
||||
const res = await request(app)
|
||||
@@ -192,8 +192,8 @@ describe('API Endpoints', () => {
|
||||
promises.push(
|
||||
request(app).post('/api/services').send({
|
||||
id: `service-${i}`,
|
||||
name: `Service ${i}`
|
||||
})
|
||||
name: `Service ${i}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -215,11 +215,11 @@ describe('API Endpoints', () => {
|
||||
// Add test services
|
||||
await request(app).post('/api/services').send({
|
||||
id: 'service1',
|
||||
name: 'Service 1'
|
||||
name: 'Service 1',
|
||||
});
|
||||
await request(app).post('/api/services').send({
|
||||
id: 'service2',
|
||||
name: 'Service 2'
|
||||
name: 'Service 2',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -246,7 +246,7 @@ describe('API Endpoints', () => {
|
||||
// Try to delete the same service twice simultaneously
|
||||
const promises = [
|
||||
request(app).delete('/api/services/service1'),
|
||||
request(app).delete('/api/services/service1')
|
||||
request(app).delete('/api/services/service1'),
|
||||
];
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
@@ -263,7 +263,7 @@ describe('API Endpoints', () => {
|
||||
const services = [
|
||||
{ id: 'plex', name: 'Plex' },
|
||||
{ id: 'jellyfin', name: 'Jellyfin' },
|
||||
{ id: 'emby', name: 'Emby' }
|
||||
{ id: 'emby', name: 'Emby' },
|
||||
];
|
||||
|
||||
const res = await request(app)
|
||||
@@ -282,13 +282,13 @@ describe('API Endpoints', () => {
|
||||
// Add initial service
|
||||
await request(app).post('/api/services').send({
|
||||
id: 'old',
|
||||
name: 'Old Service'
|
||||
name: 'Old Service',
|
||||
});
|
||||
|
||||
// Import new services (should replace)
|
||||
const newServices = [
|
||||
{ id: 'new1', name: 'New Service 1' },
|
||||
{ id: 'new2', name: 'New Service 2' }
|
||||
{ id: 'new2', name: 'New Service 2' },
|
||||
];
|
||||
|
||||
await request(app).put('/api/services').send(newServices);
|
||||
@@ -360,7 +360,7 @@ describe('API Endpoints', () => {
|
||||
test('should save config', async () => {
|
||||
const config = {
|
||||
theme: 'dark',
|
||||
domain: 'test.local'
|
||||
domain: 'test.local',
|
||||
};
|
||||
|
||||
const res = await request(app)
|
||||
|
||||
@@ -12,7 +12,7 @@ const credentialManager = require('../credential-manager');
|
||||
// Mock credential manager
|
||||
jest.mock('../credential-manager');
|
||||
jest.mock('../logger-utils', () => ({
|
||||
safeLog: jest.fn()
|
||||
safeLog: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('AuthManager', () => {
|
||||
@@ -166,8 +166,8 @@ describe('AuthManager', () => {
|
||||
expect(credentialManager.save).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/^auth\.apikey\./),
|
||||
expect.objectContaining({
|
||||
keySecret: expect.any(String)
|
||||
})
|
||||
keySecret: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -179,8 +179,8 @@ describe('AuthManager', () => {
|
||||
expect.objectContaining({
|
||||
name: 'test-key',
|
||||
scopes: ['read'],
|
||||
createdAt: expect.any(String)
|
||||
})
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -210,12 +210,12 @@ describe('AuthManager', () => {
|
||||
|
||||
// Mock credential manager to return the stored key
|
||||
credentialManager.get.mockResolvedValueOnce({
|
||||
keySecret: key.split('_')[2]
|
||||
keySecret: key.split('_')[2],
|
||||
});
|
||||
credentialManager.get.mockResolvedValueOnce({
|
||||
name: 'test-key',
|
||||
scopes: ['read', 'write'],
|
||||
createdAt: new Date().toISOString()
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const validated = await authManager.validateAPIKey(key);
|
||||
@@ -239,7 +239,7 @@ describe('AuthManager', () => {
|
||||
});
|
||||
|
||||
test('should reject non-existent API key', async () => {
|
||||
const fakeKey = 'dk_' + crypto.randomBytes(16).toString('hex') + '_' + crypto.randomBytes(32).toString('hex');
|
||||
const fakeKey = `dk_${ crypto.randomBytes(16).toString('hex') }_${ crypto.randomBytes(32).toString('hex')}`;
|
||||
credentialManager.get.mockResolvedValue(null); // Key doesn't exist
|
||||
|
||||
const validated = await authManager.validateAPIKey(fakeKey);
|
||||
@@ -252,7 +252,7 @@ describe('AuthManager', () => {
|
||||
|
||||
credentialManager.get.mockResolvedValueOnce({
|
||||
keySecret: key.split('_')[2],
|
||||
revoked: true // Key is revoked
|
||||
revoked: true, // Key is revoked
|
||||
});
|
||||
|
||||
const validated = await authManager.validateAPIKey(key);
|
||||
@@ -278,7 +278,7 @@ describe('AuthManager', () => {
|
||||
const { id } = await authManager.generateAPIKey('test-key');
|
||||
|
||||
credentialManager.get.mockResolvedValue({
|
||||
keySecret: 'test-secret'
|
||||
keySecret: 'test-secret',
|
||||
});
|
||||
|
||||
const revoked = await authManager.revokeAPIKey(id);
|
||||
@@ -288,8 +288,8 @@ describe('AuthManager', () => {
|
||||
`auth.apikey.${id}`,
|
||||
expect.objectContaining({
|
||||
revoked: true,
|
||||
revokedAt: expect.any(String)
|
||||
})
|
||||
revokedAt: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -305,19 +305,19 @@ describe('AuthManager', () => {
|
||||
test('should list all API keys with metadata', async () => {
|
||||
credentialManager.list.mockResolvedValue([
|
||||
'auth.metadata.key1',
|
||||
'auth.metadata.key2'
|
||||
'auth.metadata.key2',
|
||||
]);
|
||||
|
||||
credentialManager.get.mockResolvedValueOnce({
|
||||
name: 'Key 1',
|
||||
scopes: ['read'],
|
||||
createdAt: '2026-01-01T00:00:00Z'
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
});
|
||||
|
||||
credentialManager.get.mockResolvedValueOnce({
|
||||
name: 'Key 2',
|
||||
scopes: ['read', 'write'],
|
||||
createdAt: '2026-01-02T00:00:00Z'
|
||||
createdAt: '2026-01-02T00:00:00Z',
|
||||
});
|
||||
|
||||
const keys = await authManager.listAPIKeys();
|
||||
|
||||
@@ -198,7 +198,7 @@ describe('cleanupOldBackups', () => {
|
||||
name: 'daily',
|
||||
status: 'success',
|
||||
timestamp: new Date(Date.now() - i * 86400000).toISOString(),
|
||||
locations: [{ type: 'local', path: `/tmp/fake-${i}.backup` }]
|
||||
locations: [{ type: 'local', path: `/tmp/fake-${i}.backup` }],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('Config Routes', () => {
|
||||
const validConfig = {
|
||||
tld: 'sami',
|
||||
theme: 'dark',
|
||||
timezone: 'America/New_York'
|
||||
timezone: 'America/New_York',
|
||||
};
|
||||
|
||||
const res = await request(app)
|
||||
@@ -76,7 +76,7 @@ describe('Config Routes', () => {
|
||||
test('should return 400 for config with invalid field values', async () => {
|
||||
const invalidConfig = {
|
||||
tld: 123, // tld must be a string
|
||||
dns: 'not-an-object' // dns must be an object
|
||||
dns: 'not-an-object', // dns must be an object
|
||||
};
|
||||
|
||||
const res = await request(app)
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('store', () => {
|
||||
'key-with-dashes',
|
||||
'key_with_underscores',
|
||||
'key:with:colons',
|
||||
'key/with/slashes'
|
||||
'key/with/slashes',
|
||||
];
|
||||
|
||||
for (const key of specialKeys) {
|
||||
@@ -83,8 +83,8 @@ describe('store', () => {
|
||||
'password!@#$%^&*()',
|
||||
'token\nwith\nnewlines',
|
||||
'json{"key":"value"}',
|
||||
'unicode=ƒöÉ=ƒöæG£à',
|
||||
'quotes"and\'apostrophes'
|
||||
'unicode=<EFBFBD><EFBFBD><EFBFBD>=<3D><><EFBFBD>G<EFBFBD><47>',
|
||||
'quotes"and\'apostrophes',
|
||||
];
|
||||
|
||||
for (let i = 0; i < specialValues.length; i++) {
|
||||
@@ -210,7 +210,7 @@ describe('getMetadata', () => {
|
||||
description: 'API Key',
|
||||
service: 'GitHub',
|
||||
expiresAt: '2026-12-31',
|
||||
createdBy: 'admin'
|
||||
createdBy: 'admin',
|
||||
};
|
||||
|
||||
await credentialManager.store('meta.complex', 'value', metadata);
|
||||
@@ -328,7 +328,7 @@ describe('Concurrent Access', () => {
|
||||
const promises = [
|
||||
credentialManager.store('concurrent.key', 'value1'),
|
||||
credentialManager.store('concurrent.key', 'value2'),
|
||||
credentialManager.store('concurrent.key', 'value3')
|
||||
credentialManager.store('concurrent.key', 'value3'),
|
||||
];
|
||||
|
||||
await Promise.all(promises);
|
||||
@@ -359,7 +359,7 @@ describe('Concurrent Access', () => {
|
||||
const promises = [
|
||||
credentialManager.retrieve('readwrite.key'),
|
||||
credentialManager.store('readwrite.key', 'updated'),
|
||||
credentialManager.retrieve('readwrite.key')
|
||||
credentialManager.retrieve('readwrite.key'),
|
||||
];
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
@@ -496,7 +496,7 @@ describe('Credential Manager - Extended Coverage', () => {
|
||||
const promises = [
|
||||
credentialManager.delete('delete.concurrent'),
|
||||
credentialManager.delete('delete.concurrent'),
|
||||
credentialManager.delete('delete.concurrent')
|
||||
credentialManager.delete('delete.concurrent'),
|
||||
];
|
||||
|
||||
// Should not throw
|
||||
@@ -532,7 +532,7 @@ describe('Credential Manager - Extended Coverage', () => {
|
||||
});
|
||||
|
||||
test('should handle unicode characters', async () => {
|
||||
const unicode = 'S+ásÑ+S+ûtòî =ƒÜÇ +à+¦+¡+¿+º +º+ä+¦+º+ä+à';
|
||||
const unicode = 'S+<EFBFBD>s<EFBFBD>+S+<2B>t<EFBFBD><74> =<3D><><EFBFBD> +<2B>+<2B>+<2B>+<2B>+<2B> +<2B>+<2B>+<2B>+<2B>+<2B>+<2B>';
|
||||
|
||||
const stored = await credentialManager.store('unicode.key', unicode);
|
||||
expect(stored).toBe(true);
|
||||
@@ -621,7 +621,7 @@ describe('Credential Manager - Extended Coverage', () => {
|
||||
description: 'Production database password',
|
||||
createdAt: new Date().toISOString(),
|
||||
owner: 'admin',
|
||||
tags: ['production', 'database']
|
||||
tags: ['production', 'database'],
|
||||
};
|
||||
|
||||
await credentialManager.store('meta.full', 'value', metadata);
|
||||
@@ -648,7 +648,7 @@ describe('Credential Manager - Extended Coverage', () => {
|
||||
test('should handle metadata with special characters', async () => {
|
||||
const metadata = {
|
||||
description: 'Test with "quotes" and \'apostrophes\'',
|
||||
notes: 'Line 1\nLine 2\tTabbed'
|
||||
notes: 'Line 1\nLine 2\tTabbed',
|
||||
};
|
||||
|
||||
await credentialManager.store('meta.special', 'value', metadata);
|
||||
|
||||
@@ -43,14 +43,14 @@ describe('encrypt / decrypt', () => {
|
||||
test('throws on tampered ciphertext', () => {
|
||||
const encrypted = cryptoUtils.encrypt('test');
|
||||
const parts = encrypted.split(':');
|
||||
parts[2] = 'AAAA' + parts[2].slice(4); // tamper with ciphertext
|
||||
parts[2] = `AAAA${ parts[2].slice(4)}`; // tamper with ciphertext
|
||||
expect(() => cryptoUtils.decrypt(parts.join(':'))).toThrow();
|
||||
});
|
||||
|
||||
test('throws on tampered authTag', () => {
|
||||
const encrypted = cryptoUtils.encrypt('test');
|
||||
const parts = encrypted.split(':');
|
||||
parts[1] = 'AAAA' + parts[1].slice(4); // tamper with auth tag
|
||||
parts[1] = `AAAA${ parts[1].slice(4)}`; // tamper with auth tag
|
||||
expect(() => cryptoUtils.decrypt(parts.join(':'))).toThrow();
|
||||
});
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ describe('DockerSecurity Module', () => {
|
||||
});
|
||||
|
||||
test('should handle very long image names', () => {
|
||||
const longName = 'registry.example.com/team/project/' + 'a'.repeat(100) + ':v1.2.3';
|
||||
const longName = `registry.example.com/team/project/${ 'a'.repeat(100) }:v1.2.3`;
|
||||
|
||||
dockerSecurity.setTrustedDigest(longName, 'sha256:long');
|
||||
expect(dockerSecurity.config.trustedDigests[longName]).toBe('sha256:long');
|
||||
|
||||
@@ -202,7 +202,7 @@ describe('Edge Case Tests', () => {
|
||||
.send({
|
||||
id: 'path-traversal',
|
||||
name: 'Path Traversal',
|
||||
logo: '../../../../../../etc/passwd'
|
||||
logo: '../../../../../../etc/passwd',
|
||||
});
|
||||
|
||||
// Should handle safely
|
||||
@@ -255,7 +255,7 @@ describe('Edge Case Tests', () => {
|
||||
test('should handle bulk import of 200 services', async () => {
|
||||
const bulkServices = Array.from({ length: 200 }, (_, i) => ({
|
||||
id: `bulk-${i}`,
|
||||
name: `Bulk Service ${i}`
|
||||
name: `Bulk Service ${i}`,
|
||||
}));
|
||||
|
||||
const res = await request(app)
|
||||
@@ -277,7 +277,7 @@ describe('Edge Case Tests', () => {
|
||||
.send({
|
||||
id: 'large-data',
|
||||
name: 'Large Data',
|
||||
description: largeData
|
||||
description: largeData,
|
||||
});
|
||||
|
||||
// Might reject due to size
|
||||
@@ -290,7 +290,7 @@ describe('Edge Case Tests', () => {
|
||||
const promises = Array.from({ length: 20 }, (_, i) =>
|
||||
request(app)
|
||||
.post('/api/services')
|
||||
.send({ id: `concurrent-${i}`, name: `Concurrent ${i}` })
|
||||
.send({ id: `concurrent-${i}`, name: `Concurrent ${i}` }),
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
@@ -317,7 +317,7 @@ describe('Edge Case Tests', () => {
|
||||
// Simultaneously add again and delete
|
||||
const [addRes, deleteRes] = await Promise.all([
|
||||
request(app).post('/api/services').send({ id: 'race', name: 'Race 2' }),
|
||||
request(app).delete('/api/services/race')
|
||||
request(app).delete('/api/services/race'),
|
||||
]);
|
||||
|
||||
// One should succeed, states should be consistent
|
||||
@@ -331,7 +331,7 @@ describe('Edge Case Tests', () => {
|
||||
|
||||
const [res1, res2] = await Promise.all([
|
||||
request(app).put('/api/services').send(set1),
|
||||
request(app).put('/api/services').send(set2)
|
||||
request(app).put('/api/services').send(set2),
|
||||
]);
|
||||
|
||||
// Both operations should complete
|
||||
@@ -463,7 +463,7 @@ describe('Edge Case Tests', () => {
|
||||
|
||||
test('should handle double-encoded JSON', async () => {
|
||||
const doubleEncoded = JSON.stringify(
|
||||
JSON.stringify({ id: 'double', name: 'Double Encoded' })
|
||||
JSON.stringify({ id: 'double', name: 'Double Encoded' }),
|
||||
);
|
||||
|
||||
const res = await request(app)
|
||||
@@ -525,7 +525,7 @@ describe('Edge Case Tests', () => {
|
||||
|
||||
test('should handle configuration with nested arrays', async () => {
|
||||
const config = {
|
||||
nested: [[['deep', 'array'], ['values']], [['more']]]
|
||||
nested: [[['deep', 'array'], ['values']], [['more']]],
|
||||
};
|
||||
|
||||
const res = await request(app)
|
||||
@@ -558,7 +558,7 @@ describe('Edge Case Tests', () => {
|
||||
// Delete twice at once
|
||||
const [res1, res2] = await Promise.all([
|
||||
request(app).delete('/api/services/delete-me'),
|
||||
request(app).delete('/api/services/delete-me')
|
||||
request(app).delete('/api/services/delete-me'),
|
||||
]);
|
||||
|
||||
// One should succeed (200), one should fail (404)
|
||||
|
||||
@@ -37,25 +37,25 @@ describe('evaluateHealth', () => {
|
||||
|
||||
test('returns false when expectedBodyPattern regex does not match', () => {
|
||||
expect(healthChecker.evaluateHealth(200, 'error occurred', {
|
||||
expectedBodyPattern: 'ok|healthy'
|
||||
expectedBodyPattern: 'ok|healthy',
|
||||
})).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true when expectedBodyPattern regex matches', () => {
|
||||
expect(healthChecker.evaluateHealth(200, 'status: healthy', {
|
||||
expectedBodyPattern: 'healthy'
|
||||
expectedBodyPattern: 'healthy',
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false when expectedBodyContains text is missing', () => {
|
||||
expect(healthChecker.evaluateHealth(200, 'some response', {
|
||||
expectedBodyContains: 'healthy'
|
||||
expectedBodyContains: 'healthy',
|
||||
})).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true when expectedBodyContains text is present', () => {
|
||||
expect(healthChecker.evaluateHealth(200, 'service is healthy', {
|
||||
expectedBodyContains: 'healthy'
|
||||
expectedBodyContains: 'healthy',
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
@@ -64,21 +64,21 @@ describe('evaluateHealth', () => {
|
||||
expect(healthChecker.evaluateHealth(200, 'healthy ok', {
|
||||
expectedStatusCodes: [200],
|
||||
expectedBodyPattern: 'healthy',
|
||||
expectedBodyContains: 'ok'
|
||||
expectedBodyContains: 'ok',
|
||||
})).toBe(true);
|
||||
|
||||
// Status fails
|
||||
expect(healthChecker.evaluateHealth(500, 'healthy ok', {
|
||||
expectedStatusCodes: [200],
|
||||
expectedBodyPattern: 'healthy',
|
||||
expectedBodyContains: 'ok'
|
||||
expectedBodyContains: 'ok',
|
||||
})).toBe(false);
|
||||
|
||||
// Body pattern fails
|
||||
expect(healthChecker.evaluateHealth(200, 'error', {
|
||||
expectedStatusCodes: [200],
|
||||
expectedBodyPattern: 'healthy',
|
||||
expectedBodyContains: 'error'
|
||||
expectedBodyContains: 'error',
|
||||
})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ const {
|
||||
validateServiceConfig,
|
||||
sanitizeString,
|
||||
isValidPort,
|
||||
isPrivateIP
|
||||
isPrivateIP,
|
||||
} = require('../input-validator');
|
||||
|
||||
// Helper: extract .errors from ValidationError
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('Integration Tests', () => {
|
||||
id: 'test-app',
|
||||
name: 'Test Application',
|
||||
logo: '/assets/test.png',
|
||||
url: 'https://test.test.local'
|
||||
url: 'https://test.test.local',
|
||||
};
|
||||
|
||||
const addRes = await request(app)
|
||||
@@ -81,7 +81,7 @@ describe('Integration Tests', () => {
|
||||
const updatedServices = [{
|
||||
...newService,
|
||||
status: 'online',
|
||||
responseTime: 150
|
||||
responseTime: 150,
|
||||
}];
|
||||
|
||||
const updateRes = await request(app)
|
||||
@@ -116,7 +116,7 @@ describe('Integration Tests', () => {
|
||||
name: template.name,
|
||||
logo: template.logo,
|
||||
port: 8096,
|
||||
subdomain: 'jellyfin'
|
||||
subdomain: 'jellyfin',
|
||||
};
|
||||
|
||||
// Step 3: Add configured service
|
||||
@@ -129,7 +129,7 @@ describe('Integration Tests', () => {
|
||||
// Step 4: Verify service is listed
|
||||
const servicesRes = await request(app).get('/api/services');
|
||||
expect(servicesRes.body).toContainEqual(
|
||||
expect.objectContaining({ id: 'jellyfin' })
|
||||
expect.objectContaining({ id: 'jellyfin' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -140,11 +140,11 @@ describe('Integration Tests', () => {
|
||||
const services = Array.from({ length: 5 }, (_, i) => ({
|
||||
id: `concurrent-${i}`,
|
||||
name: `Concurrent Service ${i}`,
|
||||
logo: `/assets/service-${i}.png`
|
||||
logo: `/assets/service-${i}.png`,
|
||||
}));
|
||||
|
||||
const deployPromises = services.map(service =>
|
||||
request(app).post('/api/services').send(service)
|
||||
request(app).post('/api/services').send(service),
|
||||
);
|
||||
|
||||
const results = await Promise.all(deployPromises);
|
||||
@@ -167,7 +167,7 @@ describe('Integration Tests', () => {
|
||||
const bulkServices = [
|
||||
{ id: 'plex', name: 'Plex' },
|
||||
{ id: 'jellyfin', name: 'Jellyfin' },
|
||||
{ id: 'emby', name: 'Emby' }
|
||||
{ id: 'emby', name: 'Emby' },
|
||||
];
|
||||
|
||||
const importRes = await request(app)
|
||||
@@ -180,7 +180,7 @@ describe('Integration Tests', () => {
|
||||
const updatedServices = [
|
||||
{ id: 'plex', name: 'Plex', status: 'online' },
|
||||
{ id: 'jellyfin', name: 'Jellyfin' },
|
||||
{ id: 'emby', name: 'Emby' }
|
||||
{ id: 'emby', name: 'Emby' },
|
||||
];
|
||||
|
||||
await request(app).put('/api/services').send(updatedServices);
|
||||
@@ -219,7 +219,7 @@ describe('Integration Tests', () => {
|
||||
const config = {
|
||||
domain: 'example.local',
|
||||
theme: 'dark',
|
||||
enableHealthCheck: false
|
||||
enableHealthCheck: false,
|
||||
};
|
||||
|
||||
const configRes = await request(app)
|
||||
@@ -232,7 +232,7 @@ describe('Integration Tests', () => {
|
||||
const service = {
|
||||
id: 'test',
|
||||
name: 'Test Service',
|
||||
subdomain: 'test'
|
||||
subdomain: 'test',
|
||||
};
|
||||
|
||||
await request(app).post('/api/services').send(service);
|
||||
@@ -282,7 +282,7 @@ describe('Integration Tests', () => {
|
||||
const service = {
|
||||
id: firstTemplateId,
|
||||
name: singleTemplateRes.body.template.name,
|
||||
logo: singleTemplateRes.body.template.logo
|
||||
logo: singleTemplateRes.body.template.logo,
|
||||
};
|
||||
|
||||
const deployRes = await request(app)
|
||||
@@ -310,7 +310,7 @@ describe('Integration Tests', () => {
|
||||
name: 'Plex Production',
|
||||
logo: template.logo,
|
||||
port: 32400,
|
||||
subdomain: 'plex'
|
||||
subdomain: 'plex',
|
||||
};
|
||||
|
||||
const deployRes = await request(app)
|
||||
@@ -322,7 +322,7 @@ describe('Integration Tests', () => {
|
||||
// Verify service exists
|
||||
const servicesRes = await request(app).get('/api/services');
|
||||
expect(servicesRes.body).toContainEqual(
|
||||
expect.objectContaining({ id: 'plex-prod' })
|
||||
expect.objectContaining({ id: 'plex-prod' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -367,7 +367,7 @@ describe('Integration Tests', () => {
|
||||
// Start with empty state
|
||||
const initialServices = [
|
||||
{ id: 'base1', name: 'Base 1' },
|
||||
{ id: 'base2', name: 'Base 2' }
|
||||
{ id: 'base2', name: 'Base 2' },
|
||||
];
|
||||
|
||||
await request(app).put('/api/services').send(initialServices);
|
||||
@@ -377,7 +377,7 @@ describe('Integration Tests', () => {
|
||||
request(app).post('/api/services').send({ id: 'new1', name: 'New 1' }),
|
||||
request(app).post('/api/services').send({ id: 'new2', name: 'New 2' }),
|
||||
request(app).delete('/api/services/base1'),
|
||||
request(app).post('/api/services').send({ id: 'new3', name: 'New 3' })
|
||||
request(app).post('/api/services').send({ id: 'new3', name: 'New 3' }),
|
||||
];
|
||||
|
||||
await Promise.all(operations);
|
||||
@@ -426,7 +426,7 @@ describe('Integration Tests', () => {
|
||||
const selectedApps = mediaApps.map(id => ({
|
||||
id,
|
||||
name: templates[id].name,
|
||||
logo: templates[id].logo
|
||||
logo: templates[id].logo,
|
||||
}));
|
||||
|
||||
// Step 3: Deploy all media apps
|
||||
@@ -451,7 +451,7 @@ describe('Integration Tests', () => {
|
||||
const config = {
|
||||
domain: 'homelab.local',
|
||||
theme: 'dark',
|
||||
enableHealthCheck: true
|
||||
enableHealthCheck: true,
|
||||
};
|
||||
|
||||
await request(app).post('/api/config').send(config);
|
||||
@@ -460,7 +460,7 @@ describe('Integration Tests', () => {
|
||||
const existingServices = [
|
||||
{ id: 'router', name: 'Router', logo: '/assets/router.png' },
|
||||
{ id: 'nas', name: 'NAS', logo: '/assets/nas.png' },
|
||||
{ id: 'pihole', name: 'Pi-hole', logo: '/assets/pihole.png' }
|
||||
{ id: 'pihole', name: 'Pi-hole', logo: '/assets/pihole.png' },
|
||||
];
|
||||
|
||||
await request(app).put('/api/services').send(existingServices);
|
||||
@@ -484,7 +484,7 @@ describe('Integration Tests', () => {
|
||||
const oldServices = [
|
||||
{ id: 'old1', name: 'Old Service 1' },
|
||||
{ id: 'old2', name: 'Old Service 2' },
|
||||
{ id: 'keep', name: 'Keep This' }
|
||||
{ id: 'keep', name: 'Keep This' },
|
||||
];
|
||||
|
||||
await request(app).put('/api/services').send(oldServices);
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('logger-utils', () => {
|
||||
username: 'admin',
|
||||
password: 'secret123',
|
||||
apiKey: 'abc-def-ghi',
|
||||
token: 'xyz123'
|
||||
token: 'xyz123',
|
||||
};
|
||||
|
||||
const result = sanitizeForLog(input);
|
||||
@@ -29,9 +29,9 @@ describe('logger-utils', () => {
|
||||
name: 'Alice',
|
||||
credentials: {
|
||||
password: 'secret',
|
||||
token: 'abc123'
|
||||
}
|
||||
}
|
||||
token: 'abc123',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = sanitizeForLog(input);
|
||||
@@ -44,7 +44,7 @@ describe('logger-utils', () => {
|
||||
test('should handle arrays', () => {
|
||||
const input = [
|
||||
{ name: 'user1', password: 'pass1' },
|
||||
{ name: 'user2', secret: 'pass2' }
|
||||
{ name: 'user2', secret: 'pass2' },
|
||||
];
|
||||
|
||||
const result = sanitizeForLog(input);
|
||||
@@ -63,7 +63,7 @@ describe('logger-utils', () => {
|
||||
test('should support additional sensitive keys', () => {
|
||||
const input = {
|
||||
email: 'user@example.com',
|
||||
ssn: '123-45-6789'
|
||||
ssn: '123-45-6789',
|
||||
};
|
||||
|
||||
const result = sanitizeForLog(input, ['ssn']);
|
||||
@@ -76,7 +76,7 @@ describe('logger-utils', () => {
|
||||
const input = {
|
||||
PASSWORD: 'secret',
|
||||
ApiKey: 'key123',
|
||||
Bearer_Token: 'token456'
|
||||
Bearer_Token: 'token456',
|
||||
};
|
||||
|
||||
const result = sanitizeForLog(input);
|
||||
@@ -125,7 +125,7 @@ describe('logger-utils', () => {
|
||||
test('should create safe log object with message and sanitized data', () => {
|
||||
const result = safeLog('User login', {
|
||||
username: 'alice',
|
||||
password: 'secret123'
|
||||
password: 'secret123',
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('message', 'User login');
|
||||
|
||||
@@ -72,8 +72,8 @@ describe('Notification Routes', () => {
|
||||
.send({
|
||||
events: {
|
||||
containerDown: true,
|
||||
containerUp: false
|
||||
}
|
||||
containerUp: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
@@ -87,9 +87,9 @@ describe('Notification Routes', () => {
|
||||
providers: {
|
||||
discord: {
|
||||
enabled: true,
|
||||
webhookUrl: 'not-a-valid-url'
|
||||
}
|
||||
}
|
||||
webhookUrl: 'not-a-valid-url',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
@@ -102,9 +102,9 @@ describe('Notification Routes', () => {
|
||||
providers: {
|
||||
ntfy: {
|
||||
enabled: true,
|
||||
topic: 'invalid topic with spaces!!!'
|
||||
}
|
||||
}
|
||||
topic: 'invalid topic with spaces!!!',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
|
||||
@@ -27,7 +27,7 @@ function makeStat(cpu = 10, memory = 50, timestamp = new Date().toISOString()) {
|
||||
memory: { usage: memory * 1024 * 1024, limit: 1024 * 1024 * 1024, percent: memory, usageMB: memory, limitMB: 1024 },
|
||||
network: { rxBytes: 0, txBytes: 0, rxMB: 0, txMB: 0 },
|
||||
disk: { readBytes: 0, writeBytes: 0, readMB: 0, writeMB: 0 },
|
||||
pids: 5
|
||||
pids: 5,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('getAggregatedStats', () => {
|
||||
const now = new Date().toISOString();
|
||||
resourceMonitor.stats.set('c1', {
|
||||
name: '/app',
|
||||
history: [makeStat(10, 50, now), makeStat(30, 50, now), makeStat(20, 50, now)]
|
||||
history: [makeStat(10, 50, now), makeStat(30, 50, now), makeStat(20, 50, now)],
|
||||
});
|
||||
const agg = resourceMonitor.getAggregatedStats('c1', 24);
|
||||
expect(agg.cpu.avg).toBe(20);
|
||||
@@ -107,7 +107,7 @@ describe('getAggregatedStats', () => {
|
||||
const now = new Date().toISOString();
|
||||
resourceMonitor.stats.set('c1', {
|
||||
name: '/app',
|
||||
history: [makeStat(10, 40, now), makeStat(10, 60, now), makeStat(10, 80, now)]
|
||||
history: [makeStat(10, 40, now), makeStat(10, 60, now), makeStat(10, 80, now)],
|
||||
});
|
||||
const agg = resourceMonitor.getAggregatedStats('c1', 24);
|
||||
expect(agg.memory.avg).toBe(60);
|
||||
@@ -239,7 +239,7 @@ describe('exportStats / importStats', () => {
|
||||
test('import restores stats from backup', () => {
|
||||
const backup = {
|
||||
stats: { 'c1': { name: '/app', history: [makeStat()] } },
|
||||
alerts: { 'c1': { enabled: true, cpuThreshold: 80 } }
|
||||
alerts: { 'c1': { enabled: true, cpuThreshold: 80 } },
|
||||
};
|
||||
resourceMonitor.importStats(backup);
|
||||
expect(resourceMonitor.stats.has('c1')).toBe(true);
|
||||
|
||||
@@ -150,7 +150,7 @@ describe('Sites Route Security', () => {
|
||||
.post('/api/site/external')
|
||||
.send({
|
||||
subdomain: 'test',
|
||||
externalUrl: 'https://evil.com/path{inject}'
|
||||
externalUrl: 'https://evil.com/path{inject}',
|
||||
});
|
||||
|
||||
// Should be rejected — either 400 (our validation) or 500 (URL constructor throws on {})
|
||||
@@ -164,7 +164,7 @@ describe('Sites Route Security', () => {
|
||||
.post('/api/site/external')
|
||||
.send({
|
||||
subdomain: 'test',
|
||||
externalUrl: 'https://evil.com/path\nreverse_proxy malicious:1234'
|
||||
externalUrl: 'https://evil.com/path\nreverse_proxy malicious:1234',
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
@@ -183,7 +183,7 @@ describe('Sites Route Security', () => {
|
||||
.post('/api/site/external')
|
||||
.send({
|
||||
subdomain: '../etc/passwd',
|
||||
externalUrl: 'https://example.com'
|
||||
externalUrl: 'https://example.com',
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
@@ -205,7 +205,7 @@ describe('Error Logs — No Stack Trace Leak', () => {
|
||||
'[2026-03-07 12:01:00] dns: DNS timeout',
|
||||
'Error: connect ECONNREFUSED 192.168.1.1:5380',
|
||||
' at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1234:16)',
|
||||
'================================================================================'
|
||||
'================================================================================',
|
||||
].join('\n');
|
||||
// Write to the server's error log file location
|
||||
// The server uses ctx.ERROR_LOG_FILE — we need to check what that resolves to
|
||||
@@ -334,10 +334,10 @@ describe('Backup Security', () => {
|
||||
files: {
|
||||
encryptionKey: {
|
||||
type: 'text',
|
||||
content: 'malicious-key-data'
|
||||
}
|
||||
}
|
||||
}
|
||||
content: 'malicious-key-data',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// The encryptionKey should be skipped (not in fileMapping)
|
||||
@@ -392,8 +392,8 @@ describe('Custom Volume Path Validation', () => {
|
||||
port: '32400',
|
||||
customVolumes: [{
|
||||
containerPath: '/config',
|
||||
hostPath: '/etc/shadow'
|
||||
}]
|
||||
hostPath: '/etc/shadow',
|
||||
}],
|
||||
});
|
||||
|
||||
// The deploy will likely fail for other reasons (no Docker, etc.)
|
||||
@@ -414,7 +414,7 @@ describe('Logo Delete Path Traversal', () => {
|
||||
// Write config with a malicious logo path
|
||||
const configWithMaliciousLogo = {
|
||||
customLogo: '/assets/../../etc/passwd',
|
||||
customLogoDark: '/assets/../../../root/.ssh/id_rsa'
|
||||
customLogoDark: '/assets/../../../root/.ssh/id_rsa',
|
||||
};
|
||||
await fsp.writeFile(testConfigFile, JSON.stringify(configWithMaliciousLogo), 'utf8');
|
||||
|
||||
@@ -439,7 +439,7 @@ describe('DNS Server SSRF Prevention', () => {
|
||||
.query({
|
||||
domain: 'test.sami',
|
||||
type: 'A',
|
||||
server: '169.254.169.254' // AWS metadata endpoint
|
||||
server: '169.254.169.254', // AWS metadata endpoint
|
||||
});
|
||||
|
||||
// Must never succeed — 400 (server rejected), 401 (no token), or 500 (dns not configured in test)
|
||||
@@ -452,7 +452,7 @@ describe('DNS Server SSRF Prevention', () => {
|
||||
.send({
|
||||
domain: 'test.sami',
|
||||
ipAddress: '192.168.1.1',
|
||||
server: '10.0.0.1' // Not a configured DNS server
|
||||
server: '10.0.0.1', // Not a configured DNS server
|
||||
});
|
||||
|
||||
expect(res.statusCode).not.toBe(200);
|
||||
@@ -463,7 +463,7 @@ describe('DNS Server SSRF Prevention', () => {
|
||||
.get('/api/dns/resolve')
|
||||
.query({
|
||||
domain: 'test.sami',
|
||||
server: '127.0.0.1'
|
||||
server: '127.0.0.1',
|
||||
});
|
||||
|
||||
expect(res.statusCode).not.toBe(200);
|
||||
@@ -503,7 +503,7 @@ describe('HTTP Fetch Response Size Limit', () => {
|
||||
test('server should define MAX_RESPONSE_SIZE constant', () => {
|
||||
// Read server.js and verify the limit is defined
|
||||
const serverSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'server.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'server.js'), 'utf8',
|
||||
);
|
||||
expect(serverSource).toContain('MAX_RESPONSE_SIZE');
|
||||
expect(serverSource).toContain('10 * 1024 * 1024');
|
||||
@@ -516,7 +516,7 @@ describe('HTTP Fetch Response Size Limit', () => {
|
||||
describe('Middleware Security', () => {
|
||||
test('middleware should set Secure flag on cookies', () => {
|
||||
const middlewareSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'middleware.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'middleware.js'), 'utf8',
|
||||
);
|
||||
// Verify the Set-Cookie string includes Secure
|
||||
expect(middlewareSource).toContain('; Secure;');
|
||||
@@ -529,7 +529,7 @@ describe('Middleware Security', () => {
|
||||
describe('Config Save Atomicity', () => {
|
||||
test('saveConfig should use state manager for locking', () => {
|
||||
const serverSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'server.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'server.js'), 'utf8',
|
||||
);
|
||||
// Verify saveConfig uses configStateManager.update (not raw fs.writeFile)
|
||||
expect(serverSource).toContain('configStateManager.update');
|
||||
@@ -542,7 +542,7 @@ describe('Config Save Atomicity', () => {
|
||||
describe('External URL Security', () => {
|
||||
test('sites.js should validate URL components for unsafe chars', () => {
|
||||
const sitesSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'sites.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'sites.js'), 'utf8',
|
||||
);
|
||||
// Verify the unsafe character regex exists
|
||||
expect(sitesSource).toContain('unsafeCaddyChars');
|
||||
@@ -556,7 +556,7 @@ describe('External URL Security', () => {
|
||||
describe('Credential Manager File Locking', () => {
|
||||
test('credential-manager should use proper-lockfile', () => {
|
||||
const cmSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'credential-manager.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'credential-manager.js'), 'utf8',
|
||||
);
|
||||
expect(cmSource).toContain('proper-lockfile');
|
||||
expect(cmSource).toContain('_lockedUpdate');
|
||||
@@ -569,7 +569,7 @@ describe('Credential Manager File Locking', () => {
|
||||
describe('TOTP Config File Security', () => {
|
||||
test('loadTotpConfig should delete secret from file data', () => {
|
||||
const serverSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'server.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'server.js'), 'utf8',
|
||||
);
|
||||
// Verify the secret deletion exists in loadTotpConfig
|
||||
expect(serverSource).toContain('delete loaded.secret');
|
||||
@@ -577,7 +577,7 @@ describe('TOTP Config File Security', () => {
|
||||
|
||||
test('totp verify-setup should not write secret to config file', () => {
|
||||
const totpSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'auth', 'totp.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'auth', 'totp.js'), 'utf8',
|
||||
);
|
||||
// Verify totpConfig.secret assignment is NOT present
|
||||
expect(totpSource).not.toContain('totpConfig.secret = pendingSecret');
|
||||
@@ -591,7 +591,7 @@ describe('TOTP Config File Security', () => {
|
||||
describe('Helpers — Volume Security', () => {
|
||||
test('helpers.js should validate hostPath against allowed roots', () => {
|
||||
const helpersSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'apps', 'helpers.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'apps', 'helpers.js'), 'utf8',
|
||||
);
|
||||
expect(helpersSource).toContain('allowedRoots');
|
||||
expect(helpersSource).toContain('platformPaths.dockerData');
|
||||
@@ -605,7 +605,7 @@ describe('Helpers — Volume Security', () => {
|
||||
describe('Error Logs — Response Format', () => {
|
||||
test('errorlogs.js should not include details field', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'errorlogs.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'errorlogs.js'), 'utf8',
|
||||
);
|
||||
// The parsed log object should only have timestamp, context, error
|
||||
// NOT details (which contains stack traces)
|
||||
@@ -622,7 +622,7 @@ describe('Error Logs — Response Format', () => {
|
||||
describe('Assets — Logo Path Safety', () => {
|
||||
test('assets.js should use path.basename for logo filename extraction', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'config', 'assets.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'config', 'assets.js'), 'utf8',
|
||||
);
|
||||
expect(source).toContain('path.basename(logoPath)');
|
||||
// Should NOT use string replace for path extraction
|
||||
@@ -636,7 +636,7 @@ describe('Assets — Logo Path Safety', () => {
|
||||
describe('Backup — Encryption Key Exclusion', () => {
|
||||
test('backup.js should not include encryptionKey in filesToBackup', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'config', 'backup.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'config', 'backup.js'), 'utf8',
|
||||
);
|
||||
// Should have a comment about deliberate exclusion
|
||||
expect(source).toContain('encryptionKey deliberately excluded');
|
||||
@@ -646,7 +646,7 @@ describe('Backup — Encryption Key Exclusion', () => {
|
||||
|
||||
test('backup.js restore fileMapping should not include encryptionKey', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'config', 'backup.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'config', 'backup.js'), 'utf8',
|
||||
);
|
||||
// The RESTORE route's fileMapping (after "encryptionKey excluded" comment) must not have it
|
||||
// The preview route's fileMapping is allowed to have it (informational only)
|
||||
@@ -659,7 +659,7 @@ describe('Backup — Encryption Key Exclusion', () => {
|
||||
|
||||
test('backup.js should require TOTP for sensitive restores', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'config', 'backup.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'config', 'backup.js'), 'utf8',
|
||||
);
|
||||
expect(source).toContain('sensitiveKeys');
|
||||
expect(source).toContain('totpCode');
|
||||
@@ -673,7 +673,7 @@ describe('Backup — Encryption Key Exclusion', () => {
|
||||
describe('DNS — Server Validation Function', () => {
|
||||
test('dns.js should define validateDnsServer', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'dns.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'dns.js'), 'utf8',
|
||||
);
|
||||
expect(source).toContain('function validateDnsServer');
|
||||
expect(source).toContain('configuredIps');
|
||||
@@ -687,7 +687,7 @@ describe('DNS — Server Validation Function', () => {
|
||||
describe('Containers — Verified Container Access', () => {
|
||||
test('containers.js update route should use getVerifiedContainer', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'containers.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'containers.js'), 'utf8',
|
||||
);
|
||||
// update and check-update should both use getVerifiedContainer
|
||||
const updateSection = source.substring(source.indexOf("'/:id/update'"));
|
||||
@@ -704,7 +704,7 @@ describe('Containers — Verified Container Access', () => {
|
||||
describe('Logs — Symlink Resolution', () => {
|
||||
test('logs.js should use realpath for symlink resolution', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'logs.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'logs.js'), 'utf8',
|
||||
);
|
||||
expect(source).toContain('fsp.realpath');
|
||||
expect(source).toContain('path.sep');
|
||||
@@ -712,7 +712,7 @@ describe('Logs — Symlink Resolution', () => {
|
||||
|
||||
test('logs.js container routes should verify container exists', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'logs.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'logs.js'), 'utf8',
|
||||
);
|
||||
// Both container/:id and stream/:id should have inspect + NotFoundError
|
||||
expect(source).toContain('container.inspect()');
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('Sites Routes', () => {
|
||||
.send({
|
||||
subdomain: 'INVALID SUBDOMAIN!',
|
||||
targetUrl: 'https://example.com',
|
||||
name: 'Test'
|
||||
name: 'Test',
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('StateManager', () => {
|
||||
stateManager = new StateManager(testFile, {
|
||||
lockRetries: 20,
|
||||
lockRetryInterval: 50,
|
||||
lockTimeout: 15000
|
||||
lockTimeout: 15000,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('StateManager', () => {
|
||||
test('write and read roundtrip', async () => {
|
||||
const testData = [
|
||||
{ id: '1', name: 'Test Service 1' },
|
||||
{ id: '2', name: 'Test Service 2' }
|
||||
{ id: '2', name: 'Test Service 2' },
|
||||
];
|
||||
|
||||
await stateManager.write(testData);
|
||||
@@ -88,7 +88,7 @@ describe('StateManager', () => {
|
||||
await stateManager.write([
|
||||
{ id: '1', name: 'Service 1' },
|
||||
{ id: '2', name: 'Service 2' },
|
||||
{ id: '3', name: 'Service 3' }
|
||||
{ id: '3', name: 'Service 3' },
|
||||
]);
|
||||
|
||||
await stateManager.removeItem('2');
|
||||
@@ -100,7 +100,7 @@ describe('StateManager', () => {
|
||||
|
||||
test('updateItem updates by ID', async () => {
|
||||
await stateManager.write([
|
||||
{ id: '1', name: 'Service 1', status: 'offline' }
|
||||
{ id: '1', name: 'Service 1', status: 'offline' },
|
||||
]);
|
||||
|
||||
await stateManager.updateItem('1', { status: 'online' });
|
||||
@@ -130,7 +130,7 @@ describe('StateManager', () => {
|
||||
stateManager.update(items => {
|
||||
items.push({ id: `service-${i}`, name: `Service ${i}` });
|
||||
return items;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ describe('StateManager', () => {
|
||||
await expect(
|
||||
stateManager.update(() => {
|
||||
throw new Error('Test error');
|
||||
})
|
||||
}),
|
||||
).rejects.toThrow('Test error');
|
||||
});
|
||||
});
|
||||
@@ -229,7 +229,7 @@ describe('StateManager', () => {
|
||||
id: `service-${i}`,
|
||||
name: `Service ${i}`,
|
||||
url: `https://service-${i}.example.com`,
|
||||
status: 'online'
|
||||
status: 'online',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ describe('configureAutoUpdate', () => {
|
||||
updateManager.configureAutoUpdate('c1', {
|
||||
enabled: true,
|
||||
schedule: 'daily',
|
||||
securityOnly: true
|
||||
securityOnly: true,
|
||||
});
|
||||
expect(updateManager.config.autoUpdate['c1'].schedule).toBe('daily');
|
||||
expect(updateManager.config.autoUpdate['c1'].securityOnly).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user