/** * Notification Route Tests * * Tests notification configuration, test delivery, and history endpoints. * Notifications are mounted at /api/notifications/ prefix. */ const request = require('supertest'); const fs = require('fs'); const path = require('path'); const os = require('os'); const testServicesFile = path.join(os.tmpdir(), `notifications-services-${Date.now()}.json`); const testConfigFile = path.join(os.tmpdir(), `notifications-config-${Date.now()}.json`); process.env.SERVICES_FILE = testServicesFile; process.env.CONFIG_FILE = testConfigFile; process.env.ENABLE_HEALTH_CHECKER = 'false'; process.env.NODE_ENV = 'test'; fs.writeFileSync(testServicesFile, '[]', 'utf8'); fs.writeFileSync(testConfigFile, '{}', 'utf8'); const app = require('../server'); describe('Notification Routes', () => { afterAll(() => { try { fs.unlinkSync(testServicesFile); } catch (e) { /* ignore */ } try { fs.unlinkSync(testConfigFile); } catch (e) { /* ignore */ } }); describe('GET /api/notifications/config', () => { test('should return 200 with config object', async () => { const res = await request(app).get('/api/notifications/config'); expect(res.statusCode).toBe(200); expect(res.body.success).toBe(true); expect(res.body).toHaveProperty('config'); expect(res.body.config).toHaveProperty('enabled'); expect(res.body.config).toHaveProperty('providers'); expect(res.body.config.providers).toHaveProperty('discord'); expect(res.body.config.providers).toHaveProperty('telegram'); expect(res.body.config.providers).toHaveProperty('ntfy'); }); test('should redact sensitive provider data', async () => { const res = await request(app).get('/api/notifications/config'); expect(res.statusCode).toBe(200); // Should show enabled/configured flags, not raw webhook URLs or tokens const discord = res.body.config.providers.discord; expect(discord).toHaveProperty('enabled'); expect(discord).toHaveProperty('configured'); expect(discord).not.toHaveProperty('webhookUrl'); }); }); describe('POST /api/notifications/config', () => { test('should return 200 when updating enabled state', async () => { const res = await request(app) .post('/api/notifications/config') .send({ enabled: true }); expect(res.statusCode).toBe(200); expect(res.body.success).toBe(true); expect(res.body.message).toContain('updated'); }); test('should return 200 when updating event settings', async () => { const res = await request(app) .post('/api/notifications/config') .send({ events: { containerDown: true, containerUp: false } }); expect(res.statusCode).toBe(200); expect(res.body.success).toBe(true); }); test('should reject invalid Discord webhook URL', async () => { const res = await request(app) .post('/api/notifications/config') .send({ providers: { discord: { enabled: true, webhookUrl: 'not-a-valid-url' } } }); expect(res.statusCode).toBe(400); }); test('should reject invalid ntfy topic', async () => { const res = await request(app) .post('/api/notifications/config') .send({ providers: { ntfy: { enabled: true, topic: 'invalid topic with spaces!!!' } } }); expect(res.statusCode).toBe(400); }); }); describe('POST /api/notifications/test', () => { test('should handle test with unknown provider', async () => { const res = await request(app) .post('/api/notifications/test') .send({ provider: 'unknown_provider' }); expect(res.statusCode).toBe(400); }); test('should handle test with no provider (tests all enabled)', async () => { const res = await request(app) .post('/api/notifications/test') .send({}); // When no providers are configured, should still return 200 // with sent: true (but results array may be empty or have failures) expect([200, 400]).toContain(res.statusCode); if (res.statusCode === 200) { expect(res.body.success).toBe(true); } }); test('should handle discord test gracefully when not configured', async () => { const res = await request(app) .post('/api/notifications/test') .send({ provider: 'discord' }); // Discord test without a webhook URL configured will fail // but should still return 200 with success: false expect(res.statusCode).toBe(200); expect(res.body).toHaveProperty('success'); expect(res.body.provider).toBe('discord'); }); }); describe('GET /api/notifications/history', () => { test('should return 200 with history array', async () => { const res = await request(app).get('/api/notifications/history'); expect(res.statusCode).toBe(200); expect(res.body.success).toBe(true); expect(res.body).toHaveProperty('history'); expect(Array.isArray(res.body.history)).toBe(true); expect(res.body).toHaveProperty('total'); expect(typeof res.body.total).toBe('number'); }); test('should respect limit query parameter', async () => { const res = await request(app) .get('/api/notifications/history') .query({ limit: 10 }); expect(res.statusCode).toBe(200); expect(res.body.success).toBe(true); expect(res.body.history.length).toBeLessThanOrEqual(10); }); }); describe('DELETE /api/notifications/history', () => { test('should clear notification history', async () => { const res = await request(app).delete('/api/notifications/history'); expect(res.statusCode).toBe(200); expect(res.body.success).toBe(true); expect(res.body.message).toContain('cleared'); }); }); });