test: comprehensive test coverage for Ombi webhook changes
Build and Push Docker Image / build (push) Successful in 46s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m21s
CI / Security audit (push) Successful in 1m37s
CI / Tests & coverage (push) Successful in 2m13s
CI / Swagger Validation & Coverage (push) Successful in 1m59s

This commit addresses code review findings and completes the test coverage
plan for the new Ombi webhook functionality introduced in recent commits.

Changes:
- Removed obsolete tests for getOmbiLink and getOmbiSearchLink functions
  (replaced by getOmbiDetailsLink in commit "Fix: Generate Ombi links directly
  from TMDB ID")
- Simplified DownloadMatcher.addOmbiMatching tests to match new synchronous
  implementation (no longer makes API calls, generates links from TMDB IDs)
- Added comprehensive integration tests for Ombi webhook endpoints:
  * GET /api/ombi/webhook/status (6 tests)
  * POST /api/ombi/webhook/enable (4 tests)
  * POST /api/ombi/webhook/test (3 tests)
- Added frontend state object tests for Ombi fields (7 tests)
- Added skipped SSE endpoint tests with documentation (2 tests)
- Added skipped frontend API/UI tests with documentation (5 tests)

Code review fixes:
- Fixed variable shadowing in ombi.test.js (reused outer scope variable)
- Removed redundant network error test (duplicate of previous test)
- Updated outdated documentation comment for skipped tests

Test results: 764 passing, 15 skipped, 34 test files

Skipped tests are documented with clear justifications:
- SSE endpoint: requires EventSource or manual SSE handling
- Frontend API functions: require complex mocking, covered by integration tests
- Frontend UI functions: tightly coupled to DOM, better suited for E2E testing
- GET /api/ombi/requests: requires complex arrRetrieverRegistry mocking

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
2026-05-21 21:27:51 +01:00
parent 1dccda529a
commit 26d9e429a9
5 changed files with 738 additions and 324 deletions
@@ -753,77 +753,5 @@ describe('DownloadAssembler', () => {
});
});
describe('getOmbiLink', () => {
it('returns correct URL for valid requestId, type, and baseUrl', () => {
const result = DownloadAssembler.getOmbiLink(123, 'movie', 'http://localhost:5000');
expect(result).toBe('http://localhost:5000/#/request/movie/123');
});
it('returns correct URL for TV type', () => {
const result = DownloadAssembler.getOmbiLink(456, 'tv', 'http://localhost:5000');
expect(result).toBe('http://localhost:5000/#/request/tv/456');
});
it('returns null when requestId is missing', () => {
const result = DownloadAssembler.getOmbiLink(null, 'movie', 'http://localhost:5000');
expect(result).toBeNull();
});
it('returns null when type is missing', () => {
const result = DownloadAssembler.getOmbiLink(123, null, 'http://localhost:5000');
expect(result).toBeNull();
});
it('returns null when baseUrl is missing', () => {
const result = DownloadAssembler.getOmbiLink(123, 'movie', null);
expect(result).toBeNull();
});
it('returns null when all parameters are missing', () => {
const result = DownloadAssembler.getOmbiLink(null, null, null);
expect(result).toBeNull();
});
it('handles string requestId', () => {
const result = DownloadAssembler.getOmbiLink('abc-123', 'movie', 'http://localhost:5000');
expect(result).toBe('http://localhost:5000/#/request/movie/abc-123');
});
});
describe('getOmbiSearchLink', () => {
it('returns correct URL for series type', () => {
const result = DownloadAssembler.getOmbiSearchLink(789, 'series', 'http://localhost:5000');
expect(result).toBe('http://localhost:5000/#/tv/search/789');
});
it('returns correct URL for movie type', () => {
const result = DownloadAssembler.getOmbiSearchLink(101, 'movie', 'http://localhost:5000');
expect(result).toBe('http://localhost:5000/#/movie/search/101');
});
it('returns null when searchId is missing', () => {
const result = DownloadAssembler.getOmbiSearchLink(null, 'series', 'http://localhost:5000');
expect(result).toBeNull();
});
it('returns null when type is missing', () => {
const result = DownloadAssembler.getOmbiSearchLink(789, null, 'http://localhost:5000');
expect(result).toBeNull();
});
it('returns null when baseUrl is missing', () => {
const result = DownloadAssembler.getOmbiSearchLink(789, 'series', null);
expect(result).toBeNull();
});
it('returns null for invalid type', () => {
const result = DownloadAssembler.getOmbiSearchLink(789, 'invalid', 'http://localhost:5000');
expect(result).toBeNull();
});
it('handles string searchId', () => {
const result = DownloadAssembler.getOmbiSearchLink('search-123', 'movie', 'http://localhost:5000');
expect(result).toBe('http://localhost:5000/#/movie/search/search-123');
});
});
});
+32 -252
View File
@@ -1,6 +1,5 @@
// Copyright (c) 2026 Gordon Bolton. MIT License.
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import nock from 'nock';
// Mock dependencies
vi.mock('../../../server/utils/logger', () => ({
@@ -9,297 +8,78 @@ vi.mock('../../../server/utils/logger', () => ({
// Import after mocking
const DownloadMatcher = require('../../../server/services/DownloadMatcher');
const OmbiRetriever = require('../../../server/clients/OmbiRetriever');
describe('DownloadMatcher', () => {
const ombiBaseUrl = 'http://localhost:5000';
beforeEach(() => {
nock.cleanAll();
vi.clearAllMocks();
});
afterEach(() => {
nock.cleanAll();
});
describe('addOmbiMatching', () => {
it('should return early when ombiRetriever is missing', async () => {
it('should return early when ombiBaseUrl is missing', () => {
const downloadObj = { type: 'series', title: 'Test Show' };
const series = { tvdbId: '12345', tmdbId: '67890' };
const context = { ombiRetriever: null, ombiBaseUrl };
const context = { ombiBaseUrl: null };
await DownloadMatcher.addOmbiMatching(downloadObj, series, context);
DownloadMatcher.addOmbiMatching(downloadObj, series, context);
expect(downloadObj.ombiLink).toBeUndefined();
expect(downloadObj.ombiRequestId).toBeUndefined();
expect(downloadObj.ombiTooltip).toBeUndefined();
});
it('should return early when ombiBaseUrl is missing', async () => {
it('should return early when seriesOrMovie is missing', () => {
const downloadObj = { type: 'series', title: 'Test Show' };
const series = { tvdbId: '12345', tmdbId: '67890' };
const context = { ombiBaseUrl };
const mockRetriever = {
findTvRequest: vi.fn(),
searchTv: vi.fn()
};
DownloadMatcher.addOmbiMatching(downloadObj, null, context);
const context = { ombiRetriever: mockRetriever, ombiBaseUrl: null };
await DownloadMatcher.addOmbiMatching(downloadObj, series, context);
expect(mockRetriever.findTvRequest).not.toHaveBeenCalled();
expect(downloadObj.ombiLink).toBeUndefined();
expect(downloadObj.ombiTooltip).toBeUndefined();
});
it('should return early when seriesOrMovie is missing', async () => {
it('should add ombiLink for series with TMDB ID', () => {
const downloadObj = { type: 'series', title: 'Test Show' };
const series = { tmdbId: '67890' };
const context = { ombiBaseUrl };
const mockRetriever = {
findTvRequest: vi.fn(),
searchTv: vi.fn()
};
DownloadMatcher.addOmbiMatching(downloadObj, series, context);
const context = { ombiRetriever: mockRetriever, ombiBaseUrl };
await DownloadMatcher.addOmbiMatching(downloadObj, null, context);
expect(mockRetriever.findTvRequest).not.toHaveBeenCalled();
expect(downloadObj.ombiLink).toBeUndefined();
expect(downloadObj.ombiLink).toBe('http://localhost:5000/details/tv/67890');
expect(downloadObj.ombiTooltip).toBe('View in Ombi');
});
it('should add ombiLink and ombiRequestId for TV request found by TVDB ID', async () => {
const mockMovieRequests = [];
const mockTvRequests = [
{ id: 101, title: 'Test Show', type: 'tv', theTvDbId: '12345' }
];
nock(ombiBaseUrl)
.get('/api/v1/Request/movie')
.reply(200, mockMovieRequests);
nock(ombiBaseUrl)
.get('/api/v1/Request/tv')
.reply(200, mockTvRequests);
const ombiRetriever = new OmbiRetriever({
id: 'test-ombi',
name: 'Test Ombi',
url: ombiBaseUrl,
apiKey: 'test-key'
});
const downloadObj = { type: 'series', title: 'Test Show' };
const series = { tvdbId: '12345', tmdbId: '67890' };
const context = { ombiRetriever, ombiBaseUrl };
await DownloadMatcher.addOmbiMatching(downloadObj, series, context);
expect(downloadObj.ombiLink).toBe('http://localhost:5000/#/request/tv/101');
expect(downloadObj.ombiRequestId).toBe(101);
expect(downloadObj.ombiTooltip).toBe('Request');
});
it('should add ombiLink and ombiRequestId for TV request found by TMDB ID fallback', async () => {
const mockMovieRequests = [];
const mockTvRequests = [
{ id: 102, title: 'Test Show TMDB', type: 'tv', theMovieDbId: '67890' }
];
nock(ombiBaseUrl)
.get('/api/v1/Request/movie')
.reply(200, mockMovieRequests);
nock(ombiBaseUrl)
.get('/api/v1/Request/tv')
.reply(200, mockTvRequests);
const ombiRetriever = new OmbiRetriever({
id: 'test-ombi',
name: 'Test Ombi',
url: ombiBaseUrl,
apiKey: 'test-key'
});
const downloadObj = { type: 'series', title: 'Test Show TMDB' };
const series = { tvdbId: '99999', tmdbId: '67890' };
const context = { ombiRetriever, ombiBaseUrl };
await DownloadMatcher.addOmbiMatching(downloadObj, series, context);
expect(downloadObj.ombiLink).toBe('http://localhost:5000/#/request/tv/102');
expect(downloadObj.ombiRequestId).toBe(102);
expect(downloadObj.ombiTooltip).toBe('Request');
});
it('should add ombiLink and ombiRequestId for movie request found by TMDB ID', async () => {
const mockMovieRequests = [
{ id: 201, title: 'Test Movie', type: 'movie', theMovieDbId: '54321' }
];
const mockTvRequests = [];
nock(ombiBaseUrl)
.get('/api/v1/Request/movie')
.reply(200, mockMovieRequests);
nock(ombiBaseUrl)
.get('/api/v1/Request/tv')
.reply(200, mockTvRequests);
const ombiRetriever = new OmbiRetriever({
id: 'test-ombi',
name: 'Test Ombi',
url: ombiBaseUrl,
apiKey: 'test-key'
});
it('should add ombiLink for movie with TMDB ID', () => {
const downloadObj = { type: 'movie', title: 'Test Movie' };
const movie = { tmdbId: '54321', imdbId: 'tt54321' };
const context = { ombiRetriever, ombiBaseUrl };
const movie = { tmdbId: '54321' };
const context = { ombiBaseUrl };
await DownloadMatcher.addOmbiMatching(downloadObj, movie, context);
DownloadMatcher.addOmbiMatching(downloadObj, movie, context);
expect(downloadObj.ombiLink).toBe('http://localhost:5000/#/request/movie/201');
expect(downloadObj.ombiRequestId).toBe(201);
expect(downloadObj.ombiTooltip).toBe('Request');
expect(downloadObj.ombiLink).toBe('http://localhost:5000/details/movie/54321');
expect(downloadObj.ombiTooltip).toBe('View in Ombi');
});
it('should add ombiLink and ombiRequestId for movie request found by IMDB ID fallback', async () => {
const mockMovieRequests = [
{ id: 202, title: 'Test Movie IMDB', type: 'movie', imdbId: 'tt98765' }
];
const mockTvRequests = [];
it('should not add ombiLink when TMDB ID is missing', () => {
const downloadObj = { type: 'series', title: 'Test Show' };
const series = { tvdbId: '12345' };
const context = { ombiBaseUrl };
nock(ombiBaseUrl)
.get('/api/v1/Request/movie')
.reply(200, mockMovieRequests);
DownloadMatcher.addOmbiMatching(downloadObj, series, context);
nock(ombiBaseUrl)
.get('/api/v1/Request/tv')
.reply(200, mockTvRequests);
const ombiRetriever = new OmbiRetriever({
id: 'test-ombi',
name: 'Test Ombi',
url: ombiBaseUrl,
apiKey: 'test-key'
});
const downloadObj = { type: 'movie', title: 'Test Movie IMDB' };
const movie = { tmdbId: '99999', imdbId: 'tt98765' };
const context = { ombiRetriever, ombiBaseUrl };
await DownloadMatcher.addOmbiMatching(downloadObj, movie, context);
expect(downloadObj.ombiLink).toBe('http://localhost:5000/#/request/movie/202');
expect(downloadObj.ombiRequestId).toBe(202);
expect(downloadObj.ombiTooltip).toBe('Request');
});
it('should add search link and tooltip when no request found but search succeeds', async () => {
const mockMovieRequests = [];
const mockTvRequests = [];
const mockSearchResult = { id: 12345, title: 'Test Show Search', theTvDbId: '11111' };
nock(ombiBaseUrl)
.get('/api/v1/Request/movie')
.reply(200, mockMovieRequests);
nock(ombiBaseUrl)
.get('/api/v1/Request/tv')
.reply(200, mockTvRequests);
nock(ombiBaseUrl)
.get('/api/v1/Search/tv/11111')
.reply(200, mockSearchResult);
const ombiRetriever = new OmbiRetriever({
id: 'test-ombi',
name: 'Test Ombi',
url: ombiBaseUrl,
apiKey: 'test-key'
});
const downloadObj = { type: 'series', title: 'Test Show Search' };
const series = { tvdbId: '11111', tmdbId: '22222' };
const context = { ombiRetriever, ombiBaseUrl };
await DownloadMatcher.addOmbiMatching(downloadObj, series, context);
expect(downloadObj.ombiLink).toBe('http://localhost:5000/#/tv/search/12345');
expect(downloadObj.ombiTooltip).toBe('Search');
expect(downloadObj.ombiRequestId).toBeUndefined();
});
it('should add movie search link for movie type', async () => {
const mockMovieRequests = [];
const mockTvRequests = [];
const mockSearchResult = { id: 54321, title: 'Test Movie Search', theMovieDbId: '33333' };
nock(ombiBaseUrl)
.get('/api/v1/Request/movie')
.reply(200, mockMovieRequests);
nock(ombiBaseUrl)
.get('/api/v1/Request/tv')
.reply(200, mockTvRequests);
nock(ombiBaseUrl)
.get('/api/v1/Search/movie/33333')
.reply(200, mockSearchResult);
const ombiRetriever = new OmbiRetriever({
id: 'test-ombi',
name: 'Test Ombi',
url: ombiBaseUrl,
apiKey: 'test-key'
});
const downloadObj = { type: 'movie', title: 'Test Movie Search' };
const movie = { tmdbId: '33333', imdbId: 'tt33333' };
const context = { ombiRetriever, ombiBaseUrl };
await DownloadMatcher.addOmbiMatching(downloadObj, movie, context);
expect(downloadObj.ombiLink).toBe('http://localhost:5000/#/movie/search/54321');
expect(downloadObj.ombiTooltip).toBe('Search');
expect(downloadObj.ombiRequestId).toBeUndefined();
});
it('should handle errors gracefully without breaking download object', async () => {
const mockRetriever = {
findTvRequest: vi.fn().mockRejectedValue(new Error('Ombi API error')),
searchTv: vi.fn().mockRejectedValue(new Error('Search error'))
};
const downloadObj = { type: 'series', title: 'Test Show Error' };
const series = { tvdbId: '66666', tmdbId: '77777' };
const context = { ombiRetriever: mockRetriever, ombiBaseUrl };
// Should not throw error
await expect(DownloadMatcher.addOmbiMatching(downloadObj, series, context)).resolves.not.toThrow();
// Download object should still have original data
expect(downloadObj.title).toBe('Test Show Error');
expect(downloadObj.ombiLink).toBeUndefined();
expect(downloadObj.ombiRequestId).toBeUndefined();
expect(downloadObj.ombiTooltip).toBeUndefined();
});
it('should do nothing for unknown download type', async () => {
const mockRetriever = {
findTvRequest: vi.fn(),
findMovieRequest: vi.fn()
};
it('should not add ombiLink for unknown download type', () => {
const downloadObj = { type: 'unknown', title: 'Test Unknown' };
const series = { tmdbId: '67890' };
const context = { ombiBaseUrl };
const downloadObj = { type: 'unknown', title: 'Unknown Type' };
const media = { id: 123 };
const context = { ombiRetriever: mockRetriever, ombiBaseUrl };
DownloadMatcher.addOmbiMatching(downloadObj, series, context);
await DownloadMatcher.addOmbiMatching(downloadObj, media, context);
expect(mockRetriever.findTvRequest).not.toHaveBeenCalled();
expect(mockRetriever.findMovieRequest).not.toHaveBeenCalled();
expect(downloadObj.ombiLink).toBeUndefined();
expect(downloadObj.ombiTooltip).toBeUndefined();
});
});