d3d085d614
Build and Push Docker Image / build (push) Successful in 1m29s
Docs Check / Markdown lint (push) Successful in 1m51s
Licence Check / Licence compatibility and copyright header verification (push) Failing after 2m3s
CI / Security audit (push) Successful in 2m54s
CI / Swagger Validation & Coverage (push) Successful in 3m6s
Docs Check / Mermaid diagram parse check (push) Successful in 3m13s
CI / Tests & coverage (push) Successful in 3m31s
- Add request filters UI (type, status, sort, search) - Implement dual-layer filtering (server + client) - Add ombiFilters utility for consistent filtering logic - Persist filter preferences in localStorage - Add SSE support for real-time Ombi request updates - Add webhook endpoints for Ombi integration - Update OpenAPI spec for new endpoints - Add unit tests for filter logic and UI - Add integration tests for Ombi routes
236 lines
8.1 KiB
JavaScript
236 lines
8.1 KiB
JavaScript
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
const {
|
|
getRequestStatus,
|
|
filterByType,
|
|
filterByStatus,
|
|
filterBySearch,
|
|
sortRequests,
|
|
applyRequestFilters
|
|
} = require('../../server/utils/ombiFilters');
|
|
|
|
function makeRequest(overrides = {}) {
|
|
return {
|
|
id: 1,
|
|
title: 'Test Request',
|
|
requestedDate: '2026-05-21T10:00:00.000Z',
|
|
available: false,
|
|
approved: false,
|
|
denied: false,
|
|
requested: true,
|
|
mediaType: 'movie',
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// getRequestStatus
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('getRequestStatus', () => {
|
|
it('returns available when available is true', () => {
|
|
expect(getRequestStatus(makeRequest({ available: true }))).toBe('available');
|
|
});
|
|
|
|
it('returns denied when denied is true', () => {
|
|
expect(getRequestStatus(makeRequest({ denied: true }))).toBe('denied');
|
|
});
|
|
|
|
it('returns approved when approved is true', () => {
|
|
expect(getRequestStatus(makeRequest({ approved: true }))).toBe('approved');
|
|
});
|
|
|
|
it('returns pending when requested is true', () => {
|
|
expect(getRequestStatus(makeRequest({ requested: true }))).toBe('pending');
|
|
});
|
|
|
|
it('returns unknown for empty object', () => {
|
|
expect(getRequestStatus({})).toBe('unknown');
|
|
});
|
|
|
|
it('returns unknown for null', () => {
|
|
expect(getRequestStatus(null)).toBe('unknown');
|
|
});
|
|
|
|
it('follows priority: available > denied > approved > pending', () => {
|
|
expect(getRequestStatus(makeRequest({ available: true, denied: true }))).toBe('available');
|
|
expect(getRequestStatus(makeRequest({ denied: true, approved: true }))).toBe('denied');
|
|
expect(getRequestStatus(makeRequest({ approved: true, requested: true }))).toBe('approved');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// filterByType
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('filterByType', () => {
|
|
const movie = makeRequest({ mediaType: 'movie' });
|
|
const tv = makeRequest({ mediaType: 'tv', id: 2 });
|
|
|
|
it('returns all when types is empty', () => {
|
|
expect(filterByType([movie, tv], [])).toEqual([movie, tv]);
|
|
});
|
|
|
|
it('returns all when types includes "all"', () => {
|
|
expect(filterByType([movie, tv], ['all'])).toEqual([movie, tv]);
|
|
});
|
|
|
|
it('filters to movies only', () => {
|
|
expect(filterByType([movie, tv], ['movie'])).toEqual([movie]);
|
|
});
|
|
|
|
it('filters to tv only', () => {
|
|
expect(filterByType([movie, tv], ['tv'])).toEqual([tv]);
|
|
});
|
|
|
|
it('is case-insensitive', () => {
|
|
expect(filterByType([movie, tv], ['MOVIE'])).toEqual([movie]);
|
|
});
|
|
|
|
it('handles empty array', () => {
|
|
expect(filterByType([], ['movie'])).toEqual([]);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// filterByStatus
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('filterByStatus', () => {
|
|
const pending = makeRequest({ requested: true });
|
|
const approved = makeRequest({ approved: true, requested: true, id: 2 });
|
|
const available = makeRequest({ available: true, id: 3 });
|
|
|
|
it('returns all when statuses is empty', () => {
|
|
expect(filterByStatus([pending, approved], [])).toEqual([pending, approved]);
|
|
});
|
|
|
|
it('filters by single status', () => {
|
|
expect(filterByStatus([pending, approved], ['approved'])).toEqual([approved]);
|
|
});
|
|
|
|
it('filters by multiple statuses', () => {
|
|
expect(filterByStatus([pending, approved, available], ['pending', 'available'])).toEqual([pending, available]);
|
|
});
|
|
|
|
it('is case-insensitive', () => {
|
|
expect(filterByStatus([pending], ['PENDING'])).toEqual([pending]);
|
|
});
|
|
|
|
it('handles empty array', () => {
|
|
expect(filterByStatus([], ['pending'])).toEqual([]);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// filterBySearch
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('filterBySearch', () => {
|
|
const batman = makeRequest({ title: 'The Batman' });
|
|
const superman = makeRequest({ title: 'Superman', id: 2 });
|
|
|
|
it('returns all when query is empty', () => {
|
|
expect(filterBySearch([batman, superman], '')).toEqual([batman, superman]);
|
|
});
|
|
|
|
it('returns all when query is whitespace', () => {
|
|
expect(filterBySearch([batman, superman], ' ')).toEqual([batman, superman]);
|
|
});
|
|
|
|
it('filters by case-insensitive substring', () => {
|
|
expect(filterBySearch([batman, superman], 'bat')).toEqual([batman]);
|
|
expect(filterBySearch([batman, superman], 'BAT')).toEqual([batman]);
|
|
});
|
|
|
|
it('handles missing title', () => {
|
|
expect(filterBySearch([makeRequest({ title: undefined })], 'test')).toEqual([]);
|
|
});
|
|
|
|
it('handles empty array', () => {
|
|
expect(filterBySearch([], 'test')).toEqual([]);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// sortRequests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('sortRequests', () => {
|
|
const oldReq = makeRequest({ id: 1, title: 'Alpha', requestedDate: '2026-01-01T00:00:00.000Z' });
|
|
const midReq = makeRequest({ id: 2, title: 'Beta', requestedDate: '2026-05-01T00:00:00.000Z' });
|
|
const newReq = makeRequest({ id: 3, title: 'Charlie', requestedDate: '2026-10-01T00:00:00.000Z' });
|
|
|
|
it('sorts newest to oldest by default', () => {
|
|
const sorted = sortRequests([oldReq, newReq, midReq], 'requestedDate_desc');
|
|
expect(sorted.map(r => r.id)).toEqual([3, 2, 1]);
|
|
});
|
|
|
|
it('sorts oldest to newest', () => {
|
|
const sorted = sortRequests([oldReq, newReq, midReq], 'requestedDate_asc');
|
|
expect(sorted.map(r => r.id)).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
it('sorts A-Z', () => {
|
|
const sorted = sortRequests([midReq, oldReq, newReq], 'title_asc');
|
|
expect(sorted.map(r => r.title)).toEqual(['Alpha', 'Beta', 'Charlie']);
|
|
});
|
|
|
|
it('sorts Z-A', () => {
|
|
const sorted = sortRequests([midReq, oldReq, newReq], 'title_desc');
|
|
expect(sorted.map(r => r.title)).toEqual(['Charlie', 'Beta', 'Alpha']);
|
|
});
|
|
|
|
it('defaults to requestedDate_desc for unknown sort mode', () => {
|
|
const sorted = sortRequests([oldReq, newReq], 'invalid');
|
|
expect(sorted.map(r => r.id)).toEqual([3, 1]);
|
|
});
|
|
|
|
it('handles missing requestedDate by treating as epoch 0', () => {
|
|
const noDate = makeRequest({ id: 4, requestedDate: undefined });
|
|
const sorted = sortRequests([midReq, noDate], 'requestedDate_desc');
|
|
expect(sorted[0]).toBe(midReq);
|
|
expect(sorted[1]).toBe(noDate);
|
|
});
|
|
|
|
it('handles missing title', () => {
|
|
const noTitle = makeRequest({ id: 4, title: undefined });
|
|
const withTitle = makeRequest({ id: 5, title: 'Zebra' });
|
|
const sorted = sortRequests([noTitle, withTitle], 'title_asc');
|
|
expect(sorted[0]).toBe(noTitle);
|
|
expect(sorted[1]).toBe(withTitle);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// applyRequestFilters
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('applyRequestFilters', () => {
|
|
const moviePending = makeRequest({ id: 1, title: 'The Batman', mediaType: 'movie', requested: true, approved: false });
|
|
const tvApproved = makeRequest({ id: 2, title: 'Superman Show', mediaType: 'tv', approved: true, requested: false });
|
|
const movieAvailable = makeRequest({ id: 3, title: 'Batman Returns', mediaType: 'movie', available: true });
|
|
|
|
it('applies all filters together', () => {
|
|
const result = applyRequestFilters(
|
|
[moviePending, tvApproved, movieAvailable],
|
|
{ types: ['movie'], statuses: ['pending', 'available'], sort: 'title_asc', search: 'bat' }
|
|
);
|
|
expect(result.map(r => r.id)).toEqual([3, 1]);
|
|
});
|
|
|
|
it('returns unfiltered when no options provided', () => {
|
|
const result = applyRequestFilters([moviePending, tvApproved], {});
|
|
expect(result).toEqual([moviePending, tvApproved]);
|
|
});
|
|
|
|
it('returns empty array when no matches', () => {
|
|
const result = applyRequestFilters(
|
|
[moviePending],
|
|
{ types: ['tv'] }
|
|
);
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|