Files
ersatztv/web/src/api/libraries.test.ts
T
timothyandClaude Fable 5 8ddf0ae169
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m11s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m49s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
fix(api): resolve per-show scan by exact show id, not substring title (fixes #219)
POST /api/libraries/{id}/scan-show resolved the target show via
GetShowIdByTitle, an EF.Functions.Like "%title%" substring match with
no OrderBy - non-deterministic under duplicate/overlapping titles and
capable of scanning the wrong show. The Blazor UI never had this bug
(it always passed the exact show id); this endpoint shipped days ago
in PR #216 with no external consumers, so the contract break is safe.

BREAKING CHANGE: ScanShowRequest now takes `showId: int` instead of
`showTitle: string`. Replaced ITelevisionRepository.GetShowIdByTitle
with GetShowTitle(libraryId, showId), which also enforces the show
belongs to the given library. LibrariesController.ScanShow now returns
a genuine 404 ProblemDetails (via ApiResults.NotFoundProblem, the
established pre-check pattern from TemplateController.DeleteGroup)
when the show id doesn't exist in that library, then queues
QueueShowScanByLibraryId with the DB-resolved title.

SPA: libraries.ts ScanShowParams.showId replaces showTitle;
MediaDetailScreen.tsx passes show.id. Extended
ApiErrorResponseMetadataTests and OpenApiErrorResponseContractTests
with the new 404 contract for ScanShow. Regenerated v1.json / v1.d.ts
via scripts/update-openapi.sh + npm run generate:api.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:28:09 +02:00

41 lines
1.6 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { scanLibrary, scanShow } from './libraries';
function noContent(): Response {
return new Response(null, { status: 200 });
}
function lastCall(fetchMock: ReturnType<typeof vi.spyOn>) {
const call = fetchMock.mock.calls[fetchMock.mock.calls.length - 1];
return { init: call[1] as RequestInit | undefined, url: String(call[0]) };
}
describe('libraries api client', () => {
beforeEach(() => {
window.localStorage.clear();
vi.restoreAllMocks();
});
it('scanLibrary POSTs to the library scan endpoint', async () => {
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
await scanLibrary(4);
expect(fetchMock).toHaveBeenCalledWith('/api/libraries/4/scan', expect.objectContaining({ method: 'POST' }));
});
it('scanShow POSTs the show id and deepScan flag', async () => {
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
await scanShow(4, { deepScan: true, showId: 42 });
const { init, url } = lastCall(fetchMock);
expect(url).toBe('/api/libraries/4/scan-show');
expect(init?.method).toBe('POST');
expect(JSON.parse(String(init?.body))).toEqual({ deepScan: true, showId: 42 });
});
it('scanShow defaults deepScan to false when omitted', async () => {
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
await scanShow(9, { showId: 17 });
const { init } = lastCall(fetchMock);
expect(JSON.parse(String(init?.body))).toEqual({ deepScan: false, showId: 17 });
});
});