// Copyright (c) 2026 Gordon Bolton. MIT License. /** * @vitest-environment jsdom * Tests for client/src/state.js * * Verifies the structure and initial values of the state object. * This ensures the Ombi-related state fields are properly defined. */ import { state } from '../../client/src/state.js'; describe('state object', () => { it('has ombiBaseUrl field initialized to null', () => { expect(state).toHaveProperty('ombiBaseUrl'); expect(state.ombiBaseUrl).toBeNull(); }); it('has ombiRequests field initialized to null', () => { expect(state).toHaveProperty('ombiRequests'); expect(state.ombiRequests).toBeNull(); }); it('has ombiWebhook field with correct structure', () => { expect(state).toHaveProperty('ombiWebhook'); expect(state.ombiWebhook).toEqual({ enabled: false, triggers: { requestAvailable: false, requestApproved: false, requestDeclined: false, requestPending: false, requestProcessing: false }, stats: null }); }); it('has ombiWebhook triggers with all required fields', () => { const { triggers } = state.ombiWebhook; expect(triggers).toHaveProperty('requestAvailable'); expect(triggers).toHaveProperty('requestApproved'); expect(triggers).toHaveProperty('requestDeclined'); expect(triggers).toHaveProperty('requestPending'); expect(triggers).toHaveProperty('requestProcessing'); }); it('has all Ombi trigger fields initialized to false', () => { const { triggers } = state.ombiWebhook; expect(triggers.requestAvailable).toBe(false); expect(triggers.requestApproved).toBe(false); expect(triggers.requestDeclined).toBe(false); expect(triggers.requestPending).toBe(false); expect(triggers.requestProcessing).toBe(false); }); it('has ombiWebhook stats initialized to null', () => { expect(state.ombiWebhook.stats).toBeNull(); }); it('has ombiWebhook enabled initialized to false', () => { expect(state.ombiWebhook.enabled).toBe(false); }); }); // --------------------------------------------------------------------------- // Enabled tests with robust mocking // --------------------------------------------------------------------------- import { enableOmbiWebhook as apiEnableOmbiWebhook, testOmbiWebhook as apiTestOmbiWebhook } from '../../client/src/api.js'; import { renderWebhookStatus, enableOmbiWebhook as uiEnableOmbiWebhook, testOmbiWebhook as uiTestOmbiWebhook } from '../../client/src/ui/webhooks.js'; const mockFetch = vi.fn().mockImplementation((url, init) => { if (url === '/api/ombi/webhook/enable') { return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) }); } if (url === '/api/ombi/webhook/test') { return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) }); } if (url === '/api/webhook/config') { return Promise.resolve({ ok: true, json: () => Promise.resolve({ valid: true }) }); } if (url === '/api/sonarr/notifications') { return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); } if (url === '/api/radarr/notifications') { return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); } if (url === '/api/ombi/webhook/status') { return Promise.resolve({ ok: true, json: () => Promise.resolve({ enabled: true, triggers: { requestAvailable: true, requestApproved: true, requestDeclined: true, requestPending: true, requestProcessing: true }, stats: { eventsReceived: 10, pollsSkipped: 5, lastWebhookTimestamp: Date.now() - 60000 } }) }); } if (url === '/api/webhook/metrics') { return Promise.resolve({ ok: true, json: () => Promise.resolve({}) }); } return Promise.resolve({ ok: false, status: 404 }); }); function setupDomForOmbiWebhooks() { document.body.innerHTML = `
`; } describe('frontend API functions (enableOmbiWebhook, testOmbiWebhook)', () => { beforeEach(() => { global.fetch = mockFetch; mockFetch.mockClear(); state.csrfToken = 'test-csrf-token'; }); afterEach(() => { delete global.fetch; }); it('enableOmbiWebhook makes POST request to /api/ombi/webhook/enable', async () => { const result = await apiEnableOmbiWebhook(); expect(mockFetch).toHaveBeenCalledWith('/api/ombi/webhook/enable', { method: 'POST', headers: { 'X-CSRF-Token': 'test-csrf-token' } }); expect(result).toEqual({ success: true }); }); it('testOmbiWebhook makes POST request to /api/ombi/webhook/test', async () => { const result = await apiTestOmbiWebhook(); expect(mockFetch).toHaveBeenCalledWith('/api/ombi/webhook/test', { method: 'POST', headers: { 'X-CSRF-Token': 'test-csrf-token' } }); expect(result).toEqual({ success: true }); }); }); describe('frontend UI functions (webhooks.js Ombi functions)', () => { beforeEach(() => { global.fetch = mockFetch; mockFetch.mockClear(); global.alert = vi.fn(); setupDomForOmbiWebhooks(); state.csrfToken = 'test-csrf-token'; // Set up default state for Ombi webhook state.ombiWebhook = { enabled: false, triggers: { requestAvailable: false, requestApproved: false, requestDeclined: false, requestPending: false, requestProcessing: false }, stats: null }; }); afterEach(() => { delete global.fetch; delete global.alert; document.body.innerHTML = ''; }); it('renderWebhookStatus renders Ombi webhook status correctly', () => { // 1. Test disabled state state.ombiWebhook.enabled = false; renderWebhookStatus(); expect(document.getElementById('ombi-status').textContent).toBe('○ Disabled'); expect(document.getElementById('enable-ombi-webhook').classList.contains('hidden')).toBe(false); expect(document.getElementById('test-ombi-webhook').classList.contains('hidden')).toBe(true); expect(document.getElementById('ombi-triggers').classList.contains('hidden')).toBe(true); // 2. Test enabled state with triggers and stats state.ombiWebhook.enabled = true; state.ombiWebhook.triggers.requestAvailable = true; state.ombiWebhook.triggers.requestApproved = true; state.ombiWebhook.stats = { eventsReceived: 42, pollsSkipped: 17, lastWebhookTimestamp: Date.now() - 3600000 // 1 hour ago }; renderWebhookStatus(); expect(document.getElementById('ombi-status').textContent).toBe('● Enabled'); expect(document.getElementById('enable-ombi-webhook').classList.contains('hidden')).toBe(true); expect(document.getElementById('test-ombi-webhook').classList.contains('hidden')).toBe(false); expect(document.getElementById('ombi-triggers').classList.contains('hidden')).toBe(false); // Check triggers rendering expect(document.getElementById('ombi-requestAvailable').textContent).toBe('✓'); expect(document.getElementById('ombi-requestApproved').textContent).toBe('✓'); expect(document.getElementById('ombi-requestDeclined').textContent).toBe('✗'); // Check stats rendering expect(document.getElementById('ombi-stats').classList.contains('hidden')).toBe(false); expect(document.getElementById('ombi-events').textContent).toBe('42'); expect(document.getElementById('ombi-polls').textContent).toBe('17'); expect(document.getElementById('ombi-last').textContent).toBe('1h ago'); }); it('enableOmbiWebhook UI handler calls API and updates state', async () => { // Mock the state returned by fetchWebhookStatus to enable it mockFetch.mockImplementation((url) => { if (url === '/api/ombi/webhook/enable') { return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) }); } if (url === '/api/ombi/webhook/status') { // Return updated state where it is enabled return Promise.resolve({ ok: true, json: () => Promise.resolve({ enabled: true, triggers: { requestAvailable: true, requestApproved: false, requestDeclined: false, requestPending: false, requestProcessing: false }, stats: null }) }); } // For all other config fetches, return basic values return Promise.resolve({ ok: true, json: () => Promise.resolve({}) }); }); await uiEnableOmbiWebhook(); // Should make POST call to enable expect(mockFetch).toHaveBeenCalledWith('/api/ombi/webhook/enable', { method: 'POST', headers: { 'X-CSRF-Token': 'test-csrf-token' } }); // State should be updated expect(state.ombiWebhook.enabled).toBe(true); // Render the webhook status to update the DOM renderWebhookStatus(); // UI should show enabled status expect(document.getElementById('ombi-status').textContent).toBe('● Enabled'); }); it('testOmbiWebhook UI handler calls API and updates state', async () => { await uiTestOmbiWebhook(); // Should make POST call to test expect(mockFetch).toHaveBeenCalledWith('/api/ombi/webhook/test', { method: 'POST', headers: { 'X-CSRF-Token': 'test-csrf-token' } }); // Should alert success expect(global.alert).toHaveBeenCalledWith('Ombi webhook test sent successfully!'); }); });