/** * Edge Case Tests * * Tests boundary conditions, invalid inputs, and extreme scenarios * Validates system behavior under unusual or stressful conditions */ const request = require('supertest'); const fs = require('fs'); const path = require('path'); const os = require('os'); // Create test instance with isolated environment const testServicesFile = path.join(os.tmpdir(), `edge-services-${Date.now()}.json`); const testConfigFile = path.join(os.tmpdir(), `edge-config-${Date.now()}.json`); // Set test environment process.env.SERVICES_FILE = testServicesFile; process.env.CONFIG_FILE = testConfigFile; process.env.ENABLE_HEALTH_CHECKER = 'false'; process.env.NODE_ENV = 'test'; // Initialize test files fs.writeFileSync(testServicesFile, '[]', 'utf8'); fs.writeFileSync(testConfigFile, '{}', 'utf8'); // Require app after environment setup const app = require('../server'); describe('Edge Case Tests', () => { beforeEach(async () => { // Reset state through the API to respect file locks await request(app).put('/api/services').send([]); fs.writeFileSync(testConfigFile, '{}', 'utf8'); }); afterAll(() => { // Cleanup test files try { fs.unlinkSync(testServicesFile); fs.unlinkSync(testConfigFile); } catch (e) { // Ignore cleanup errors } }); describe('Boundary Conditions', () => { test('should handle empty service ID', async () => { const res = await request(app) .post('/api/services') .send({ id: '', name: 'Empty ID Service' }); // Should reject empty ID expect(res.statusCode).toBeGreaterThanOrEqual(400); }); test('should handle very long service ID (1000 chars)', async () => { const longId = 'a'.repeat(1000); const res = await request(app) .post('/api/services') .send({ id: longId, name: 'Long ID' }); // Might accept or reject depending on validation expect([200, 400, 413]).toContain(res.statusCode); }); test('should handle very long service name (10000 chars)', async () => { const longName = 'Name '.repeat(2000); const res = await request(app) .post('/api/services') .send({ id: 'test', name: longName }); // Should handle gracefully expect([200, 400, 413]).toContain(res.statusCode); }); test('should handle service with exactly 0 properties', async () => { const res = await request(app) .post('/api/services') .send({}); // Should reject - missing required fields expect(res.statusCode).toBe(400); }); test('should handle service with 100+ properties', async () => { const service = { id: 'many-props', name: 'Many Props' }; for (let i = 0; i < 100; i++) { service[`prop${i}`] = `value${i}`; } const res = await request(app) .post('/api/services') .send(service); // Should handle extra properties gracefully expect([200, 400]).toContain(res.statusCode); }); }); describe('Invalid Input Types', () => { test('should handle null service ID', async () => { const res = await request(app) .post('/api/services') .send({ id: null, name: 'Null ID' }); expect(res.statusCode).toBeGreaterThanOrEqual(400); }); test('should handle number as service ID', async () => { const res = await request(app) .post('/api/services') .send({ id: 12345, name: 'Number ID' }); // Might convert to string or reject expect([200, 400]).toContain(res.statusCode); }); test('should handle array as service ID', async () => { const res = await request(app) .post('/api/services') .send({ id: ['array', 'id'], name: 'Array ID' }); expect(res.statusCode).toBeGreaterThanOrEqual(400); }); test('should handle object as service ID', async () => { const res = await request(app) .post('/api/services') .send({ id: { nested: 'object' }, name: 'Object ID' }); expect(res.statusCode).toBeGreaterThanOrEqual(400); }); test('should handle boolean as service name', async () => { const res = await request(app) .post('/api/services') .send({ id: 'bool-test', name: true }); // Might convert to string or reject expect([200, 400]).toContain(res.statusCode); }); test('should handle undefined properties', async () => { const res = await request(app) .post('/api/services') .send({ id: 'test', name: undefined }); expect(res.statusCode).toBeGreaterThanOrEqual(400); }); }); describe('Special Characters and Encoding', () => { test('should handle Unicode characters in service name', async () => { const res = await request(app) .post('/api/services') .send({ id: 'unicode', name: '🚀 Rocket Service 中文 العربية' }); // Should handle Unicode properly expect([200, 400]).toContain(res.statusCode); if (res.statusCode === 200) { const services = JSON.parse(fs.readFileSync(testServicesFile, 'utf8')); expect(services[0].name).toContain('🚀'); } }); test('should handle special characters in ID', async () => { const res = await request(app) .post('/api/services') .send({ id: 'test!@#$%^&*()', name: 'Special ID' }); // Might sanitize or reject expect([200, 400]).toContain(res.statusCode); }); test('should handle newlines in service name', async () => { const res = await request(app) .post('/api/services') .send({ id: 'newline', name: 'Line 1\nLine 2\nLine 3' }); expect([200, 400]).toContain(res.statusCode); }); test('should handle SQL injection attempt in ID', async () => { const res = await request(app) .post('/api/services') .send({ id: "'; DROP TABLE services; --", name: 'SQL Injection' }); // Should reject or sanitize expect([200, 400]).toContain(res.statusCode); // Verify file is still valid JSON const content = fs.readFileSync(testServicesFile, 'utf8'); expect(() => JSON.parse(content)).not.toThrow(); }); test('should handle path traversal attempt in logo', async () => { const res = await request(app) .post('/api/services') .send({ id: 'path-traversal', name: 'Path Traversal', logo: '../../../../../../etc/passwd' }); // Should handle safely expect([200, 400]).toContain(res.statusCode); }); test('should handle null bytes in input', async () => { const res = await request(app) .post('/api/services') .send({ id: 'null\x00byte', name: 'Test\x00Name' }); // Should reject or sanitize expect([200, 400]).toContain(res.statusCode); }); }); describe('Large Datasets', () => { test('should handle 100 services', async () => { // Add 100 services for (let i = 0; i < 100; i++) { await request(app) .post('/api/services') .send({ id: `service-${i}`, name: `Service ${i}` }); } // Verify all exist const res = await request(app).get('/api/services'); expect(res.statusCode).toBe(200); expect(res.body.length).toBe(100); }, 60000); test('should handle deleting from large dataset', async () => { // Add 50 services for (let i = 0; i < 50; i++) { await request(app) .post('/api/services') .send({ id: `bulk-${i}`, name: `Bulk ${i}` }); } // Delete 25 services for (let i = 0; i < 25; i++) { await request(app).delete(`/api/services/bulk-${i}`); } // Verify 25 remain const res = await request(app).get('/api/services'); expect(res.body.length).toBe(25); }, 30000); test('should handle bulk import of 200 services', async () => { const bulkServices = Array.from({ length: 200 }, (_, i) => ({ id: `bulk-${i}`, name: `Bulk Service ${i}` })); const res = await request(app) .put('/api/services') .send(bulkServices); expect(res.statusCode).toBe(200); // Verify all imported const getRes = await request(app).get('/api/services'); expect(getRes.body.length).toBe(200); }, 10000); // Longer timeout test('should handle service with very large property value (1MB)', async () => { const largeData = 'x'.repeat(1024 * 1024); // 1MB string const res = await request(app) .post('/api/services') .send({ id: 'large-data', name: 'Large Data', description: largeData }); // Might reject due to size expect([200, 413]).toContain(res.statusCode); }); }); describe('Concurrent Operations and Race Conditions', () => { test('should handle 20 concurrent POSTs without corruption', async () => { const promises = Array.from({ length: 20 }, (_, i) => request(app) .post('/api/services') .send({ id: `concurrent-${i}`, name: `Concurrent ${i}` }) ); const results = await Promise.all(promises); // With file locking, some may fail with 500 (lock contention) — that's expected const successes = results.filter(r => r.statusCode === 200); expect(successes.length).toBeGreaterThanOrEqual(1); // The critical check: file must be valid JSON (no corruption) const content = fs.readFileSync(testServicesFile, 'utf8'); expect(() => JSON.parse(content)).not.toThrow(); // And the count must match the number of successes const services = JSON.parse(content); expect(services.length).toBe(successes.length); }); test('should handle concurrent add and delete of same service', async () => { // Add a service await request(app) .post('/api/services') .send({ id: 'race', name: 'Race Service' }); // 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') ]); // One should succeed, states should be consistent const services = JSON.parse(fs.readFileSync(testServicesFile, 'utf8')); expect(() => JSON.parse(fs.readFileSync(testServicesFile, 'utf8'))).not.toThrow(); }); test('should handle concurrent bulk imports', async () => { const set1 = [{ id: 's1', name: 'Set 1' }]; const set2 = [{ id: 's2', name: 'Set 2' }]; const [res1, res2] = await Promise.all([ request(app).put('/api/services').send(set1), request(app).put('/api/services').send(set2) ]); // Both operations should complete expect([200]).toContain(res1.statusCode); expect([200]).toContain(res2.statusCode); // Final state should have one complete set (last write wins) const services = JSON.parse(fs.readFileSync(testServicesFile, 'utf8')); expect(services.length).toBeGreaterThanOrEqual(1); }); }); describe('File System Edge Cases', () => { test('should handle file with read-only after writing', async () => { // Add a service await request(app) .post('/api/services') .send({ id: 'readonly-test', name: 'Read Only' }); // Make file read-only fs.chmodSync(testServicesFile, 0o444); // Try to add another service const res = await request(app) .post('/api/services') .send({ id: 'should-fail', name: 'Should Fail' }); // Should fail with 500 error expect(res.statusCode).toBe(500); // Restore permissions for cleanup fs.chmodSync(testServicesFile, 0o666); }); test('should handle missing services file gracefully', async () => { // Delete the file fs.unlinkSync(testServicesFile); // Try to get services const res = await request(app).get('/api/services'); // Should either return empty array or create file expect([200, 500]).toContain(res.statusCode); // File should be recreated or error handled if (res.statusCode === 200) { expect(Array.isArray(res.body)).toBe(true); } }); test('should handle empty file (0 bytes)', async () => { // Create empty file fs.writeFileSync(testServicesFile, '', 'utf8'); const res = await request(app).get('/api/services'); // Should handle gracefully expect([200, 500]).toContain(res.statusCode); }); test('should handle file with only whitespace', async () => { fs.writeFileSync(testServicesFile, ' \n\t\r ', 'utf8'); const res = await request(app).get('/api/services'); // Should handle gracefully expect([200, 500]).toContain(res.statusCode); }); test('should handle file with BOM (Byte Order Mark)', async () => { const bomContent = '\uFEFF[]'; fs.writeFileSync(testServicesFile, bomContent, 'utf8'); const res = await request(app).get('/api/services'); // BOM may cause JSON parse to fail (500) or be handled (200) expect([200, 500]).toContain(res.statusCode); if (res.statusCode === 200) { expect(Array.isArray(res.body)).toBe(true); } }); }); describe('API Request Edge Cases', () => { test('should handle missing Content-Type header', async () => { const res = await request(app) .post('/api/services') .set('Content-Type', '') .send('{"id":"test","name":"Test"}'); // Should handle gracefully expect([200, 400]).toContain(res.statusCode); }); test('should handle wrong Content-Type (text/plain)', async () => { const res = await request(app) .post('/api/services') .set('Content-Type', 'text/plain') .send('{"id":"test","name":"Test"}'); // Might still parse or reject expect([200, 400, 415]).toContain(res.statusCode); }); test('should handle extremely nested JSON (50 levels)', async () => { let nested = { value: 'deep' }; for (let i = 0; i < 50; i++) { nested = { level: nested }; } const res = await request(app) .post('/api/services') .send({ id: 'nested', name: 'Nested', data: nested }); // Should handle or reject expect([200, 400]).toContain(res.statusCode); }); test('should handle request with circular reference (if possible)', async () => { // Can't send actual circular JSON, but test large nested structure const data = { id: 'circular', name: 'Test' }; const res = await request(app) .post('/api/services') .send(data); expect([200, 400]).toContain(res.statusCode); }); test('should handle double-encoded JSON', async () => { const doubleEncoded = JSON.stringify( JSON.stringify({ id: 'double', name: 'Double Encoded' }) ); const res = await request(app) .post('/api/services') .set('Content-Type', 'application/json') .send(doubleEncoded); // Should reject - wrong format expect([400, 500]).toContain(res.statusCode); }); }); describe('Template Edge Cases', () => { test('should handle requesting template with special chars in ID', async () => { const res = await request(app).get('/api/apps/templates/test%20space'); expect([404, 400]).toContain(res.statusCode); }); test('should handle requesting template with very long ID', async () => { const longId = 'a'.repeat(1000); const res = await request(app).get(`/api/apps/templates/${longId}`); expect([404, 414]).toContain(res.statusCode); }); test('should handle template with path traversal', async () => { const res = await request(app).get('/api/apps/templates/../../secrets'); expect([404, 400]).toContain(res.statusCode); }); }); describe('Configuration Edge Cases', () => { test('should handle empty configuration object', async () => { const res = await request(app) .post('/api/config') .send({}); expect(res.statusCode).toBe(200); // Verify empty config saved const config = JSON.parse(fs.readFileSync(testConfigFile, 'utf8')); expect(typeof config).toBe('object'); }); test('should handle configuration with 1000 properties', async () => { const largeConfig = {}; for (let i = 0; i < 1000; i++) { largeConfig[`setting${i}`] = `value${i}`; } const res = await request(app) .post('/api/config') .send(largeConfig); expect([200, 413]).toContain(res.statusCode); }); test('should handle configuration with nested arrays', async () => { const config = { nested: [[['deep', 'array'], ['values']], [['more']]] }; const res = await request(app) .post('/api/config') .send(config); expect(res.statusCode).toBe(200); }); }); describe('Delete Edge Cases', () => { test('should handle deleting non-existent service', async () => { const res = await request(app).delete('/api/services/does-not-exist'); expect(res.statusCode).toBe(404); }); test('should handle deleting with special characters in ID', async () => { const res = await request(app).delete('/api/services/test%2Fslash'); expect([404, 400]).toContain(res.statusCode); }); test('should handle deleting same service twice simultaneously', async () => { // Add service await request(app) .post('/api/services') .send({ id: 'delete-me', name: 'Delete Me' }); // Delete twice at once const [res1, res2] = await Promise.all([ request(app).delete('/api/services/delete-me'), request(app).delete('/api/services/delete-me') ]); // One should succeed (200), one should fail (404) const statuses = [res1.statusCode, res2.statusCode].sort(); expect(statuses).toContain(200); expect(statuses).toContain(404); }); }); describe('State Consistency Edge Cases', () => { test('should recover if file becomes corrupted mid-operation', async () => { // Add initial service await request(app) .post('/api/services') .send({ id: 'initial', name: 'Initial' }); // Corrupt the file fs.writeFileSync(testServicesFile, '{corrupted', 'utf8'); // Try to read const res = await request(app).get('/api/services'); // Should handle error gracefully expect([200, 500]).toContain(res.statusCode); }); test('should handle file replaced with directory', async () => { // Delete file fs.unlinkSync(testServicesFile); // Create directory with same name fs.mkdirSync(testServicesFile); // Try to read const res = await request(app).get('/api/services'); expect(res.statusCode).toBe(500); // Cleanup fs.rmdirSync(testServicesFile); }); }); });