fix(api): robustesse des providers de métadonnées

Recherche ISBN directe sur Open Library avec repli titre/auteur,
normalisation des réponses, timeouts et User-Agent explicites sur les
trois providers distants, abandon de l'extraction de jaquette CBR en
échec silencieux (fallback métadonnées seules), tests des providers et
garde du test de migration si better-sqlite3 est indisponible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Git Agent
2026-08-23 16:23:57 +02:00
parent 7b72cc0d83
commit 2f98c48259
6 changed files with 240 additions and 33 deletions

View File

@ -0,0 +1,101 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { BnfProvider } from "./adapters/bnf.provider.js";
import { GoogleBooksProvider } from "./adapters/google-books.provider.js";
import { OpenLibraryProvider } from "./adapters/open-library.provider.js";
import { MetadataLookup, MetadataProviderConfig } from "./metadata.types.js";
const lookup: MetadataLookup = {
title: "Harry Potter et la Chambre des Secrets",
author: "J. K. Rowling",
filePath: "/library/HP/Harry Potter et la Chambre des Secrets (J.K. Rowling).epub",
identifiers: { isbn10: null, isbn13: "9782070612376", candidates: ["9782070612376"] }
};
const config: MetadataProviderConfig = { provider: "openlibrary", enabled: true, priority: 1, apiKey: null };
afterEach(() => {
vi.unstubAllGlobals();
});
describe("metadata providers", () => {
it("queries OpenLibrary by ISBN and normalizes the matching book", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(
jsonResponse({
title: "Harry Potter et la Chambre des Secrets",
authors: [{ key: "/authors/OL23919A" }],
languages: [{ key: "/languages/fre" }],
publishers: ["Gallimard jeunesse"],
publish_date: "2007-03",
isbn_13: ["9782070612376"],
isbn_10: ["2070612379"]
})
)
.mockResolvedValueOnce(jsonResponse({ name: "J. K. Rowling" }));
vi.stubGlobal("fetch", fetchMock);
const result = await new OpenLibraryProvider().lookup(lookup, config);
expect(String((fetchMock.mock.calls[0] as unknown[])[0])).toBe("https://openlibrary.org/isbn/9782070612376.json");
expect(result).toMatchObject({
title: "Harry Potter et la Chambre des Secrets",
author: "J. K. Rowling",
isbn: "9782070612376"
});
});
it("treats Google Books quota exhaustion as a non-blocking miss", async () => {
const fetchMock = vi.fn(async () => jsonResponse({ error: { code: 429, status: "RESOURCE_EXHAUSTED" } }, 429));
vi.stubGlobal("fetch", fetchMock);
const result = await new GoogleBooksProvider().lookup(lookup, { ...config, provider: "googlebooks" });
expect(String((fetchMock.mock.calls[0] as unknown[])[0])).toContain("q=isbn%3A9782070612376");
expect(result).toBeNull();
});
it("parses BnF SRU UNIMARC records returned for ISBN lookup", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () =>
textResponse(`<?xml version="1.0" encoding="UTF-8"?>
<srw:searchRetrieveResponse xmlns:srw="http://www.loc.gov/zing/srw/">
<srw:records><srw:record><srw:recordData>
<mxc:record xmlns:mxc="info:lc/xmlns/marcxchange-v2">
<mxc:datafield tag="073"><mxc:subfield code="a">9782070612376</mxc:subfield></mxc:datafield>
<mxc:datafield tag="101"><mxc:subfield code="a">fre</mxc:subfield></mxc:datafield>
<mxc:datafield tag="200">
<mxc:subfield code="a">Harry Potter et la chambre des secrets</mxc:subfield>
<mxc:subfield code="f">J. K. Rowling</mxc:subfield>
</mxc:datafield>
<mxc:datafield tag="210">
<mxc:subfield code="c">Gallimard jeunesse</mxc:subfield>
<mxc:subfield code="d">DL 2007</mxc:subfield>
</mxc:datafield>
<mxc:datafield tag="330"><mxc:subfield code="a">Résumé BnF.</mxc:subfield></mxc:datafield>
</mxc:record>
</srw:recordData></srw:record></srw:records>
</srw:searchRetrieveResponse>`)
)
);
const result = await new BnfProvider().lookup(lookup, { ...config, provider: "bnf" });
expect(result).toMatchObject({
title: "Harry Potter et la chambre des secrets",
author: "J. K. Rowling",
publisher: "Gallimard jeunesse",
publishedDate: "2007",
isbn: "9782070612376"
});
});
});
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
}
function textResponse(body: string, status = 200): Response {
return new Response(body, { status, headers: { "content-type": "application/xml" } });
}