Phase 1: Add ESLint/Prettier config + baseline auto-fixes

This commit is contained in:
Krystie
2026-03-22 11:00:25 +01:00
parent 41a0cdee7e
commit e2c67a8fe8
90 changed files with 4008 additions and 3066 deletions

View File

@@ -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)

View File

@@ -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();

View File

@@ -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` }],
});
}

View File

@@ -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)

View File

@@ -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);

View File

@@ -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();
});

View File

@@ -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');

View File

@@ -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)

View File

@@ -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);
});
});

View File

@@ -9,7 +9,7 @@ const {
validateServiceConfig,
sanitizeString,
isValidPort,
isPrivateIP
isPrivateIP,
} = require('../input-validator');
// Helper: extract .errors from ValidationError

View File

@@ -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);

View File

@@ -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');

View File

@@ -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);

View File

@@ -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);

View File

@@ -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()');

View File

@@ -85,7 +85,7 @@ describe('Sites Routes', () => {
.send({
subdomain: 'INVALID SUBDOMAIN!',
targetUrl: 'https://example.com',
name: 'Test'
name: 'Test',
});
expect(res.statusCode).toBe(400);

View File

@@ -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',
});
}

View File

@@ -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);