"""Shared component library: persist datasheets, catalog listing.""" from __future__ import annotations from fastapi.testclient import TestClient from backend.services import projects as proj_svc from backend.services.datasheet_store import resolve_datasheet from backend.services.storage import LocalStorageBackend PDF = b"%PDF-1.4\n" + b"x" * 8000 def _client(tmp_path) -> TestClient: from backend.main import app app.state.storage = LocalStorageBackend(tmp_path) return TestClient(app) def test_library_alias_resolves_family_mpn(storage): from backend.services.datasheet_store import resolve_datasheet, store_datasheet_bytes store_datasheet_bytes( storage, PDF, "ESP32-S31-WROOM-3", extra_mpns=["ESP32-S31-WROOM-3-N16R16V"], ) assert resolve_datasheet(storage, "ESP32-S31-WROOM-3") assert resolve_datasheet(storage, "ESP32-S31-WROOM-3-N16R16V") assert proj_svc.library_has_datasheet(storage, "ESP32-S31-WROOM-3") def test_save_datasheet_also_stores_in_library(storage): meta = proj_svc.create_project(storage, "local", "board") key = proj_svc.save_datasheet(storage, "local", meta.id, "CH340E", PDF) assert key.endswith("CH340E.pdf") assert storage.exists(key) assert resolve_datasheet(storage, "CH340E") assert proj_svc.library_has_datasheet(storage, "CH340E") def test_library_catalog_lists_ics_passives_and_pdfs(storage): storage.write_json( "library/extracted/CH340E.json", { "mpn": "CH340E", "pintable": [{"pin": 1}, {"pin": 2}], "component_subtype": "usb-uart", "absolute_maximum_ratings": {"vcc": "5V"}, }, ) storage.write_json( "library/patterns/samsung_c.json", { "name": "Samsung CL10", "component_type": "capacitor", "description": "Samsung 0603 MLCC", "regex": r"^CL10", }, ) storage.write_json( "library/passives/CL10B474KA8NNNC.json", { "mpn": "CL10B474KA8NNNC", "specs": { "specs_type": "capacitor", "component_subtype": "mlcc", "values": {"capacitance": "470n"}, }, }, ) proj_svc.remember_datasheet(storage, "CH340E", PDF) cat = proj_svc.list_library_catalog(storage) assert len(cat["ics"]) == 1 assert cat["ics"][0]["mpn"] == "CH340E" assert cat["ics"][0]["pin_count"] == 2 assert cat["ics"][0]["has_datasheet"] is True assert cat["passives"][0]["mpn"] == "Samsung CL10" assert cat["passive_parts"][0]["mpn"] == "CL10B474KA8NNNC" assert cat["passive_parts"][0]["param_count"] >= 1 assert cat["simple"] == [] assert any(d["mpn"] == "CH340E" and d["has_extraction"] for d in cat["datasheets"]) def test_library_http_catalog_and_pdf(tmp_path): client = _client(tmp_path) empty = client.get("/api/library") assert empty.status_code == 200 body = empty.json() assert body["ics"] == [] assert body["datasheets"] == [] meta = client.post("/api/projects", json={"name": "lib"}).json() resp = client.post( f"/api/projects/{meta['id']}/upload/datasheets", params={"mpn": "MSPM0G3507"}, files={"file": ("msp.pdf", PDF, "application/pdf")}, ) assert resp.status_code == 200 catalog = client.get("/api/library").json() assert any(d["mpn"] == "MSPM0G3507" for d in catalog["datasheets"]) pdf = client.get("/api/library/datasheet/MSPM0G3507") assert pdf.status_code == 200 assert pdf.content.startswith(b"%PDF-") def test_fetch_datasheet_persists_to_library(tmp_path, monkeypatch): from backend.services.datasheet_finder import DatasheetHit async def fake_find(mpn, lcsc_id=None): return DatasheetHit( mpn, pdf_bytes=PDF, url="https://datasheet.lcsc.com/x.pdf", source="lcsc", ) monkeypatch.setattr( "backend.services.datasheet_finder.find_datasheet", fake_find, ) client = _client(tmp_path) resp = client.get("/api/datasheets/fetch", params={"mpn": "CH340E"}) assert resp.status_code == 200 assert resp.content.startswith(b"%PDF-") catalog = client.get("/api/library").json() assert any(d["mpn"] == "CH340E" for d in catalog["datasheets"]) assert client.get("/api/library/datasheet/CH340E").status_code == 200 def test_library_import_pdf_without_project(tmp_path): client = _client(tmp_path) before = client.get("/api/projects").json() resp = client.post( "/api/library/datasheets", data={"mpn": "CH340E", "kind": "ic"}, files={"file": ("ch.pdf", PDF, "application/pdf")}, ) assert resp.status_code == 200 body = resp.json() assert body["mpn"] == "CH340E" assert body["has_extraction"] is False assert body["component_type"] == "ic" assert body["status"] == "needs_pintable" assert client.get("/api/projects").json() == before catalog = client.get("/api/library").json() assert any(d["mpn"] == "CH340E" and d.get("has_component") for d in catalog["datasheets"]) ics = [c for c in catalog["ics"] if c["mpn"] == "CH340E"] assert len(ics) == 1 assert ics[0]["pin_count"] == 0 assert ics[0]["library_status"] == "needs_pintable" pdf = client.get("/api/library/datasheet/CH340E") assert pdf.status_code == 200 assert pdf.content.startswith(b"%PDF-") card = client.get("/api/library/components/ic/CH340E") assert card.status_code == 200 assert card.json()["status"] == "needs_pintable" from backend.main import app from backend.services.library import library_has_extraction assert library_has_extraction(app.state.storage, "CH340E") is None assert not app.state.storage.exists("library/extracted/CH340E.json") def test_library_import_rejects_non_pdf(tmp_path): client = _client(tmp_path) resp = client.post( "/api/library/datasheets", data={"mpn": "CH340E"}, files={"file": ("notes.txt", b"not a pdf", "text/plain")}, ) assert resp.status_code == 400 assert client.get("/api/library").json()["datasheets"] == [] def test_library_import_rejects_blank_mpn(tmp_path): client = _client(tmp_path) resp = client.post( "/api/library/datasheets", data={"mpn": " "}, files={"file": ("ch.pdf", PDF, "application/pdf")}, ) assert resp.status_code in (400, 422) def test_library_get_put_component(tmp_path): client = _client(tmp_path) from backend.main import app storage = app.state.storage storage.write_json( "library/extracted/CH340E.json", { "mpn": "CH340E", "component_subtype": "ic.interface.usb_uart_bridge", "pintable": [ {"number": "1", "name": "VCC"}, {"number": "2", "name": "GND"}, ], }, ) got = client.get("/api/library/components/ic/CH340E") assert got.status_code == 200 assert got.json()["component_subtype"] == "ic.interface.usb_uart_bridge" payload = got.json() payload["component_subtype"] = "ic.interface.usb_uart" saved = client.put("/api/library/components/ic/CH340E", json=payload) assert saved.status_code == 200 assert saved.json()["component_subtype"] == "ic.interface.usb_uart" again = client.get("/api/library/components/ic/CH340E").json() assert again["component_subtype"] == "ic.interface.usb_uart" def test_library_put_rejects_empty_pintable(tmp_path): client = _client(tmp_path) resp = client.put( "/api/library/components/ic/FAKEIC", json={"mpn": "FAKEIC", "pintable": []}, ) assert resp.status_code == 400 assert client.get("/api/library/components/ic/FAKEIC").status_code == 404 def test_library_import_ic_put_pintable_promotes_out_of_inbox(tmp_path): client = _client(tmp_path) assert client.post( "/api/library/datasheets", data={"mpn": "CH340E", "kind": "ic"}, files={"file": ("ch.pdf", PDF, "application/pdf")}, ).status_code == 200 payload = { "mpn": "CH340E", "component_subtype": "ic.interface.usb_uart_bridge", "pintable": [ {"number": "1", "name": "VCC"}, {"number": "2", "name": "GND"}, ], "absolute_maximum_ratings": [], "rules": [], } saved = client.put("/api/library/components/ic/CH340E", json=payload) assert saved.status_code == 200 from backend.main import app assert app.state.storage.exists("library/extracted/CH340E.json") assert not app.state.storage.exists("library/inbox/CH340E.json") extracted = app.state.storage.read_json("library/extracted/CH340E.json") assert "status" not in extracted assert extracted["pintable"][0]["name"] == "VCC" cat = client.get("/api/library").json() ic = next(c for c in cat["ics"] if c["mpn"] == "CH340E") assert ic["pin_count"] == 2 assert ic["library_status"] == "extracted" def test_library_import_passive_without_exam(tmp_path): client = _client(tmp_path) resp = client.post( "/api/library/datasheets", data={"mpn": "CL10B474KA8NNNC", "kind": "passive_part"}, files={"file": ("c.pdf", PDF, "application/pdf")}, ) assert resp.status_code == 200 assert resp.json()["component_type"] == "passive_part" assert client.get("/api/projects").json() == [] cat = client.get("/api/library").json() assert any(p["mpn"] == "CL10B474KA8NNNC" for p in cat["passive_parts"]) got = client.get("/api/library/components/passive_part/CL10B474KA8NNNC") assert got.status_code == 200 assert got.json()["specs"]["specs_type"] == "passive" def test_library_import_rejects_unknown_kind(tmp_path): client = _client(tmp_path) resp = client.post( "/api/library/datasheets", data={"mpn": "CH340E", "kind": "project"}, files={"file": ("ch.pdf", PDF, "application/pdf")}, ) assert resp.status_code == 400 assert client.get("/api/library").json()["ics"] == []