From 76fd1df041475385c6e24f6c3b3dd939894bc910 Mon Sep 17 00:00:00 2001 From: Sanket17052006 Date: Fri, 15 May 2026 13:31:47 +0530 Subject: [PATCH 1/5] migrate import_preview_json to FastAPI --- openlibrary/asgi_app.py | 2 + openlibrary/fastapi/importapi.py | 86 ++++++ openlibrary/plugins/importapi/import_ui.py | 2 + openlibrary/tests/fastapi/test_importapi.py | 305 ++++++++++++++++++++ 4 files changed, 395 insertions(+) create mode 100644 openlibrary/fastapi/importapi.py create mode 100644 openlibrary/tests/fastapi/test_importapi.py diff --git a/openlibrary/asgi_app.py b/openlibrary/asgi_app.py index f79de38f0ad..38e94f2a3df 100644 --- a/openlibrary/asgi_app.py +++ b/openlibrary/asgi_app.py @@ -215,6 +215,7 @@ def health() -> dict[str, str]: from openlibrary.fastapi.books import router as books_router from openlibrary.fastapi.cdn import router as cdn_router from openlibrary.fastapi.checkins import router as checkins_router + from openlibrary.fastapi.importapi import router as importapi_router from openlibrary.fastapi.internal.api import router as internal_router from openlibrary.fastapi.languages import router as languages_router from openlibrary.fastapi.lists import router as lists_router @@ -232,6 +233,7 @@ def health() -> dict[str, str]: app.include_router(books_router) app.include_router(cdn_router) app.include_router(checkins_router) + app.include_router(importapi_router) app.include_router(internal_router) app.include_router(languages_router) app.include_router(lists_router) diff --git a/openlibrary/fastapi/importapi.py b/openlibrary/fastapi/importapi.py new file mode 100644 index 00000000000..f31b904d265 --- /dev/null +++ b/openlibrary/fastapi/importapi.py @@ -0,0 +1,86 @@ +""" +FastAPI endpoints for import preview. +""" + +from __future__ import annotations + +from typing import Annotated + +from fastapi import APIRouter, Depends, Form, HTTPException, Query, status + +from openlibrary.accounts import get_current_user +from openlibrary.plugins.importapi.import_ui import ImportPreviewRequest + +router = APIRouter(tags=["import"]) + + +def check_import_permission() -> None: + """Check that the current user has admin/librarian/super-librarian role.""" + user = get_current_user() + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Authentication required", + ) + if not (user.is_admin() or user.is_librarian() or user.is_super_librarian()): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Insufficient permissions", + ) + + +def _build_preview_response( + source: str | None, + provider: str | None, + identifier: str | None, + save: bool, +) -> dict: + """Shared logic for GET and POST import preview handlers.""" + try: + req = ImportPreviewRequest.from_input({ + "source": source, + "provider": provider, + "identifier": identifier, + "save": "true" if save else "false", + }) + except ValueError as e: + return {"success": False, "error": str(e)} + + return dict(req.metadata_provider.do_import(req.identifier, req.save)) + + +@router.get("/import/preview") +def import_preview_json_get( + source: Annotated[str | None, Query( + description="Source in format 'provider:identifier' (e.g. 'amazon:ASIN')", + )] = None, + provider: Annotated[str | None, Query( + description="Metadata provider: amazon, ia, or marc", + )] = None, + identifier: Annotated[str | None, Query( + description="Identifier for the given provider", + )] = None, + _: Annotated[None, Depends(check_import_permission)] = None, +) -> dict: + """ + Preview import metadata without saving. + + Requires admin, librarian, or super-librarian role. + """ + return _build_preview_response(source, provider, identifier, save=False) + + +@router.post("/import/preview") +def import_preview_json_post( + source: Annotated[str | None, Form()] = None, + provider: Annotated[str | None, Form()] = None, + identifier: Annotated[str | None, Form()] = None, + save: Annotated[bool, Form()] = False, + _: Annotated[None, Depends(check_import_permission)] = None, +) -> dict: + """ + Import metadata, optionally saving it. + + Requires admin, librarian, or super-librarian role. + """ + return _build_preview_response(source, provider, identifier, save=save) diff --git a/openlibrary/plugins/importapi/import_ui.py b/openlibrary/plugins/importapi/import_ui.py index a90055373b9..461d117f1cd 100644 --- a/openlibrary/plugins/importapi/import_ui.py +++ b/openlibrary/plugins/importapi/import_ui.py @@ -4,6 +4,7 @@ import requests import web +from typing_extensions import deprecated from infogami.plugins.api.code import jsonapi from infogami.utils import delegate @@ -62,6 +63,7 @@ def POST(self): ) +@deprecated("migrated to fastapi") class import_preview_json(delegate.page): path = "/import/preview" encoding = "json" diff --git a/openlibrary/tests/fastapi/test_importapi.py b/openlibrary/tests/fastapi/test_importapi.py new file mode 100644 index 00000000000..f6bc80b7242 --- /dev/null +++ b/openlibrary/tests/fastapi/test_importapi.py @@ -0,0 +1,305 @@ +"""Tests for the FastAPI import preview endpoints.""" + +from unittest.mock import MagicMock, Mock + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from openlibrary.fastapi.importapi import router + + +def _raise(exc: Exception) -> None: + raise exc + + +def _make_mock_user( + is_admin: bool = True, + is_librarian: bool = False, + is_super_librarian: bool = False, +) -> Mock: + """Create a mock user with configurable role flags.""" + user = MagicMock() + user.is_admin.return_value = is_admin + user.is_librarian.return_value = is_librarian + user.is_super_librarian.return_value = is_super_librarian + return user + + +FAKE_IMPORT_RESULT = { + "edition": {"key": "/books/OL1M", "title": "Test Book"}, + "success": True, +} + + +@pytest.fixture +def client() -> TestClient: + """Return a TestClient for the import preview router.""" + app = FastAPI() + app.include_router(router) + return TestClient(app) + + +class TestImportPreviewAuth: + """Authentication and authorization tests.""" + + def test_get_requires_authentication(self, client, monkeypatch): + monkeypatch.setattr( + "openlibrary.fastapi.importapi.get_current_user", + lambda: None, + ) + response = client.get("/import/preview?source=amazon:ASIN123") + assert response.status_code == 401 + + def test_post_requires_authentication(self, client, monkeypatch): + monkeypatch.setattr( + "openlibrary.fastapi.importapi.get_current_user", + lambda: None, + ) + response = client.post("/import/preview", data={"source": "amazon:ASIN123"}) + assert response.status_code == 401 + + def test_get_forbidden_for_regular_user(self, client, monkeypatch): + monkeypatch.setattr( + "openlibrary.fastapi.importapi.get_current_user", + lambda: _make_mock_user(is_admin=False, is_librarian=False), + ) + response = client.get("/import/preview?source=amazon:ASIN123") + assert response.status_code == 403 + + def test_post_forbidden_for_regular_user(self, client, monkeypatch): + monkeypatch.setattr( + "openlibrary.fastapi.importapi.get_current_user", + lambda: _make_mock_user(is_admin=False, is_librarian=False), + ) + response = client.post("/import/preview", data={"source": "amazon:ASIN123"}) + assert response.status_code == 403 + + def test_get_allows_admin(self, client, monkeypatch): + monkeypatch.setattr( + "openlibrary.fastapi.importapi.get_current_user", + lambda: _make_mock_user(is_admin=True), + ) + mock_req = MagicMock() + mock_req.metadata_provider.do_import.return_value = FAKE_IMPORT_RESULT + monkeypatch.setattr( + "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", + lambda i: mock_req, + ) + response = client.get("/import/preview?source=amazon:ASIN123") + assert response.status_code == 200 + + def test_get_allows_librarian(self, client, monkeypatch): + monkeypatch.setattr( + "openlibrary.fastapi.importapi.get_current_user", + lambda: _make_mock_user(is_admin=False, is_librarian=True), + ) + mock_req = MagicMock() + mock_req.metadata_provider.do_import.return_value = FAKE_IMPORT_RESULT + monkeypatch.setattr( + "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", + lambda i: mock_req, + ) + response = client.get("/import/preview?source=amazon:ASIN123") + assert response.status_code == 200 + + def test_get_allows_super_librarian(self, client, monkeypatch): + monkeypatch.setattr( + "openlibrary.fastapi.importapi.get_current_user", + lambda: _make_mock_user( + is_admin=False, is_librarian=False, is_super_librarian=True + ), + ) + mock_req = MagicMock() + mock_req.metadata_provider.do_import.return_value = FAKE_IMPORT_RESULT + monkeypatch.setattr( + "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", + lambda i: mock_req, + ) + response = client.get("/import/preview?source=amazon:ASIN123") + assert response.status_code == 200 + + +class TestImportPreviewGet: + """GET /import/preview endpoint tests.""" + + def test_get_returns_import_result(self, client, monkeypatch): + monkeypatch.setattr( + "openlibrary.fastapi.importapi.get_current_user", + lambda: _make_mock_user(is_admin=True), + ) + mock_req = MagicMock() + mock_req.metadata_provider.do_import.return_value = { + "edition": {"key": "/books/OL1M", "title": "Test Book"}, + "success": True, + } + monkeypatch.setattr( + "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", + lambda i: mock_req, + ) + response = client.get("/import/preview?source=amazon:ASIN123") + assert response.status_code == 200 + data = response.json() + assert data["success"] is True + assert data["edition"]["key"] == "/books/OL1M" + + def test_get_does_not_save(self, client, monkeypatch): + monkeypatch.setattr( + "openlibrary.fastapi.importapi.get_current_user", + lambda: _make_mock_user(is_admin=True), + ) + captured = {} + + def fake_from_input(i): + captured["save"] = i.get("save", "false") + mock_req = MagicMock() + mock_req.metadata_provider.do_import.return_value = FAKE_IMPORT_RESULT + mock_req.save = False + return mock_req + + monkeypatch.setattr( + "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", + fake_from_input, + ) + response = client.get("/import/preview?source=amazon:ASIN123&save=true") + assert response.status_code == 200 + assert captured["save"] == "false" + + def test_get_invalid_source_returns_error(self, client, monkeypatch): + monkeypatch.setattr( + "openlibrary.fastapi.importapi.get_current_user", + lambda: _make_mock_user(is_admin=True), + ) + monkeypatch.setattr( + "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", + lambda i: _raise(ValueError("Invalid source provided")), + ) + response = client.get("/import/preview?source=invalid") + assert response.status_code == 200 + data = response.json() + assert data["success"] is False + assert "Invalid source provided" in data["error"] + + def test_get_with_provider_and_identifier(self, client, monkeypatch): + monkeypatch.setattr( + "openlibrary.fastapi.importapi.get_current_user", + lambda: _make_mock_user(is_admin=True), + ) + captured = {} + + def fake_from_input(i): + captured["params"] = i + mock_req = MagicMock() + mock_req.metadata_provider.do_import.return_value = FAKE_IMPORT_RESULT + mock_req.save = False + return mock_req + + monkeypatch.setattr( + "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", + fake_from_input, + ) + response = client.get( + "/import/preview?provider=amazon&identifier=1234567890" + ) + assert response.status_code == 200 + assert captured["params"]["provider"] == "amazon" + assert captured["params"]["identifier"] == "1234567890" + + def test_get_no_params_returns_error(self, client, monkeypatch): + monkeypatch.setattr( + "openlibrary.fastapi.importapi.get_current_user", + lambda: _make_mock_user(is_admin=True), + ) + monkeypatch.setattr( + "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", + lambda i: _raise(ValueError("Invalid source provided")), + ) + response = client.get("/import/preview") + assert response.status_code == 200 + data = response.json() + assert data["success"] is False + + +class TestImportPreviewPost: + """POST /import/preview endpoint tests.""" + + def test_post_returns_import_result(self, client, monkeypatch): + monkeypatch.setattr( + "openlibrary.fastapi.importapi.get_current_user", + lambda: _make_mock_user(is_admin=True), + ) + mock_req = MagicMock() + mock_req.metadata_provider.do_import.return_value = FAKE_IMPORT_RESULT + monkeypatch.setattr( + "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", + lambda i: mock_req, + ) + response = client.post( + "/import/preview", data={"source": "amazon:ASIN123"} + ) + assert response.status_code == 200 + + def test_post_can_save(self, client, monkeypatch): + monkeypatch.setattr( + "openlibrary.fastapi.importapi.get_current_user", + lambda: _make_mock_user(is_admin=True), + ) + captured = {} + + def fake_from_input(i): + captured["save"] = i.get("save", "false") + mock_req = MagicMock() + mock_req.metadata_provider.do_import.return_value = FAKE_IMPORT_RESULT + mock_req.save = captured["save"] == "true" + return mock_req + + monkeypatch.setattr( + "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", + fake_from_input, + ) + response = client.post( + "/import/preview", + data={"source": "amazon:ASIN123", "save": "true"}, + ) + assert response.status_code == 200 + assert captured["save"] == "true" + + def test_post_without_save_defaults_to_false(self, client, monkeypatch): + monkeypatch.setattr( + "openlibrary.fastapi.importapi.get_current_user", + lambda: _make_mock_user(is_admin=True), + ) + captured = {} + + def fake_from_input(i): + captured["save"] = i.get("save", "false") + mock_req = MagicMock() + mock_req.metadata_provider.do_import.return_value = FAKE_IMPORT_RESULT + mock_req.save = captured["save"] == "true" + return mock_req + + monkeypatch.setattr( + "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", + fake_from_input, + ) + response = client.post( + "/import/preview", data={"source": "amazon:ASIN123"} + ) + assert response.status_code == 200 + assert captured["save"] == "false" + + def test_post_invalid_source_returns_error(self, client, monkeypatch): + monkeypatch.setattr( + "openlibrary.fastapi.importapi.get_current_user", + lambda: _make_mock_user(is_admin=True), + ) + monkeypatch.setattr( + "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", + lambda i: _raise(ValueError("Invalid source provided")), + ) + response = client.post( + "/import/preview", data={"source": "invalid"} + ) + assert response.status_code == 200 + data = response.json() + assert data["success"] is False From 03456ef89b1eef977185bcc109d6c36e5f2c9800 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 08:12:39 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- openlibrary/fastapi/importapi.py | 41 +++++++++++++-------- openlibrary/tests/fastapi/test_importapi.py | 20 +++------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/openlibrary/fastapi/importapi.py b/openlibrary/fastapi/importapi.py index f31b904d265..38cbc55da1d 100644 --- a/openlibrary/fastapi/importapi.py +++ b/openlibrary/fastapi/importapi.py @@ -37,12 +37,14 @@ def _build_preview_response( ) -> dict: """Shared logic for GET and POST import preview handlers.""" try: - req = ImportPreviewRequest.from_input({ - "source": source, - "provider": provider, - "identifier": identifier, - "save": "true" if save else "false", - }) + req = ImportPreviewRequest.from_input( + { + "source": source, + "provider": provider, + "identifier": identifier, + "save": "true" if save else "false", + } + ) except ValueError as e: return {"success": False, "error": str(e)} @@ -51,15 +53,24 @@ def _build_preview_response( @router.get("/import/preview") def import_preview_json_get( - source: Annotated[str | None, Query( - description="Source in format 'provider:identifier' (e.g. 'amazon:ASIN')", - )] = None, - provider: Annotated[str | None, Query( - description="Metadata provider: amazon, ia, or marc", - )] = None, - identifier: Annotated[str | None, Query( - description="Identifier for the given provider", - )] = None, + source: Annotated[ + str | None, + Query( + description="Source in format 'provider:identifier' (e.g. 'amazon:ASIN')", + ), + ] = None, + provider: Annotated[ + str | None, + Query( + description="Metadata provider: amazon, ia, or marc", + ), + ] = None, + identifier: Annotated[ + str | None, + Query( + description="Identifier for the given provider", + ), + ] = None, _: Annotated[None, Depends(check_import_permission)] = None, ) -> dict: """ diff --git a/openlibrary/tests/fastapi/test_importapi.py b/openlibrary/tests/fastapi/test_importapi.py index f6bc80b7242..1d06f5f9bc8 100644 --- a/openlibrary/tests/fastapi/test_importapi.py +++ b/openlibrary/tests/fastapi/test_importapi.py @@ -106,9 +106,7 @@ def test_get_allows_librarian(self, client, monkeypatch): def test_get_allows_super_librarian(self, client, monkeypatch): monkeypatch.setattr( "openlibrary.fastapi.importapi.get_current_user", - lambda: _make_mock_user( - is_admin=False, is_librarian=False, is_super_librarian=True - ), + lambda: _make_mock_user(is_admin=False, is_librarian=False, is_super_librarian=True), ) mock_req = MagicMock() mock_req.metadata_provider.do_import.return_value = FAKE_IMPORT_RESULT @@ -198,9 +196,7 @@ def fake_from_input(i): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", fake_from_input, ) - response = client.get( - "/import/preview?provider=amazon&identifier=1234567890" - ) + response = client.get("/import/preview?provider=amazon&identifier=1234567890") assert response.status_code == 200 assert captured["params"]["provider"] == "amazon" assert captured["params"]["identifier"] == "1234567890" @@ -234,9 +230,7 @@ def test_post_returns_import_result(self, client, monkeypatch): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", lambda i: mock_req, ) - response = client.post( - "/import/preview", data={"source": "amazon:ASIN123"} - ) + response = client.post("/import/preview", data={"source": "amazon:ASIN123"}) assert response.status_code == 200 def test_post_can_save(self, client, monkeypatch): @@ -282,9 +276,7 @@ def fake_from_input(i): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", fake_from_input, ) - response = client.post( - "/import/preview", data={"source": "amazon:ASIN123"} - ) + response = client.post("/import/preview", data={"source": "amazon:ASIN123"}) assert response.status_code == 200 assert captured["save"] == "false" @@ -297,9 +289,7 @@ def test_post_invalid_source_returns_error(self, client, monkeypatch): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", lambda i: _raise(ValueError("Invalid source provided")), ) - response = client.post( - "/import/preview", data={"source": "invalid"} - ) + response = client.post("/import/preview", data={"source": "invalid"}) assert response.status_code == 200 data = response.json() assert data["success"] is False From ba69cdd61adf978d0799715b02778f9139402044 Mon Sep 17 00:00:00 2001 From: Sanket17052006 Date: Fri, 15 May 2026 22:57:37 +0530 Subject: [PATCH 3/5] preserve the legacy behaviour --- openlibrary/fastapi/importapi.py | 4 +-- openlibrary/tests/fastapi/test_importapi.py | 36 ++++++++++----------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/openlibrary/fastapi/importapi.py b/openlibrary/fastapi/importapi.py index 38cbc55da1d..37f83c136cb 100644 --- a/openlibrary/fastapi/importapi.py +++ b/openlibrary/fastapi/importapi.py @@ -51,7 +51,7 @@ def _build_preview_response( return dict(req.metadata_provider.do_import(req.identifier, req.save)) -@router.get("/import/preview") +@router.get("/import/preview.json") def import_preview_json_get( source: Annotated[ str | None, @@ -81,7 +81,7 @@ def import_preview_json_get( return _build_preview_response(source, provider, identifier, save=False) -@router.post("/import/preview") +@router.post("/import/preview.json") def import_preview_json_post( source: Annotated[str | None, Form()] = None, provider: Annotated[str | None, Form()] = None, diff --git a/openlibrary/tests/fastapi/test_importapi.py b/openlibrary/tests/fastapi/test_importapi.py index 1d06f5f9bc8..31a69e4ff55 100644 --- a/openlibrary/tests/fastapi/test_importapi.py +++ b/openlibrary/tests/fastapi/test_importapi.py @@ -48,7 +48,7 @@ def test_get_requires_authentication(self, client, monkeypatch): "openlibrary.fastapi.importapi.get_current_user", lambda: None, ) - response = client.get("/import/preview?source=amazon:ASIN123") + response = client.get("/import/preview.json?source=amazon:ASIN123") assert response.status_code == 401 def test_post_requires_authentication(self, client, monkeypatch): @@ -56,7 +56,7 @@ def test_post_requires_authentication(self, client, monkeypatch): "openlibrary.fastapi.importapi.get_current_user", lambda: None, ) - response = client.post("/import/preview", data={"source": "amazon:ASIN123"}) + response = client.post("/import/preview.json", data={"source": "amazon:ASIN123"}) assert response.status_code == 401 def test_get_forbidden_for_regular_user(self, client, monkeypatch): @@ -64,7 +64,7 @@ def test_get_forbidden_for_regular_user(self, client, monkeypatch): "openlibrary.fastapi.importapi.get_current_user", lambda: _make_mock_user(is_admin=False, is_librarian=False), ) - response = client.get("/import/preview?source=amazon:ASIN123") + response = client.get("/import/preview.json?source=amazon:ASIN123") assert response.status_code == 403 def test_post_forbidden_for_regular_user(self, client, monkeypatch): @@ -72,7 +72,7 @@ def test_post_forbidden_for_regular_user(self, client, monkeypatch): "openlibrary.fastapi.importapi.get_current_user", lambda: _make_mock_user(is_admin=False, is_librarian=False), ) - response = client.post("/import/preview", data={"source": "amazon:ASIN123"}) + response = client.post("/import/preview.json", data={"source": "amazon:ASIN123"}) assert response.status_code == 403 def test_get_allows_admin(self, client, monkeypatch): @@ -86,7 +86,7 @@ def test_get_allows_admin(self, client, monkeypatch): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", lambda i: mock_req, ) - response = client.get("/import/preview?source=amazon:ASIN123") + response = client.get("/import/preview.json?source=amazon:ASIN123") assert response.status_code == 200 def test_get_allows_librarian(self, client, monkeypatch): @@ -100,7 +100,7 @@ def test_get_allows_librarian(self, client, monkeypatch): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", lambda i: mock_req, ) - response = client.get("/import/preview?source=amazon:ASIN123") + response = client.get("/import/preview.json?source=amazon:ASIN123") assert response.status_code == 200 def test_get_allows_super_librarian(self, client, monkeypatch): @@ -114,12 +114,12 @@ def test_get_allows_super_librarian(self, client, monkeypatch): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", lambda i: mock_req, ) - response = client.get("/import/preview?source=amazon:ASIN123") + response = client.get("/import/preview.json?source=amazon:ASIN123") assert response.status_code == 200 class TestImportPreviewGet: - """GET /import/preview endpoint tests.""" + """GET /import/preview.json endpoint tests.""" def test_get_returns_import_result(self, client, monkeypatch): monkeypatch.setattr( @@ -135,7 +135,7 @@ def test_get_returns_import_result(self, client, monkeypatch): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", lambda i: mock_req, ) - response = client.get("/import/preview?source=amazon:ASIN123") + response = client.get("/import/preview.json?source=amazon:ASIN123") assert response.status_code == 200 data = response.json() assert data["success"] is True @@ -159,7 +159,7 @@ def fake_from_input(i): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", fake_from_input, ) - response = client.get("/import/preview?source=amazon:ASIN123&save=true") + response = client.get("/import/preview.json?source=amazon:ASIN123&save=true") assert response.status_code == 200 assert captured["save"] == "false" @@ -172,7 +172,7 @@ def test_get_invalid_source_returns_error(self, client, monkeypatch): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", lambda i: _raise(ValueError("Invalid source provided")), ) - response = client.get("/import/preview?source=invalid") + response = client.get("/import/preview.json?source=invalid") assert response.status_code == 200 data = response.json() assert data["success"] is False @@ -196,7 +196,7 @@ def fake_from_input(i): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", fake_from_input, ) - response = client.get("/import/preview?provider=amazon&identifier=1234567890") + response = client.get("/import/preview.json?provider=amazon&identifier=1234567890") assert response.status_code == 200 assert captured["params"]["provider"] == "amazon" assert captured["params"]["identifier"] == "1234567890" @@ -210,14 +210,14 @@ def test_get_no_params_returns_error(self, client, monkeypatch): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", lambda i: _raise(ValueError("Invalid source provided")), ) - response = client.get("/import/preview") + response = client.get("/import/preview.json") assert response.status_code == 200 data = response.json() assert data["success"] is False class TestImportPreviewPost: - """POST /import/preview endpoint tests.""" + """POST /import/preview.json endpoint tests.""" def test_post_returns_import_result(self, client, monkeypatch): monkeypatch.setattr( @@ -230,7 +230,7 @@ def test_post_returns_import_result(self, client, monkeypatch): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", lambda i: mock_req, ) - response = client.post("/import/preview", data={"source": "amazon:ASIN123"}) + response = client.post("/import/preview.json", data={"source": "amazon:ASIN123"}) assert response.status_code == 200 def test_post_can_save(self, client, monkeypatch): @@ -252,7 +252,7 @@ def fake_from_input(i): fake_from_input, ) response = client.post( - "/import/preview", + "/import/preview.json", data={"source": "amazon:ASIN123", "save": "true"}, ) assert response.status_code == 200 @@ -276,7 +276,7 @@ def fake_from_input(i): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", fake_from_input, ) - response = client.post("/import/preview", data={"source": "amazon:ASIN123"}) + response = client.post("/import/preview.json", data={"source": "amazon:ASIN123"}) assert response.status_code == 200 assert captured["save"] == "false" @@ -289,7 +289,7 @@ def test_post_invalid_source_returns_error(self, client, monkeypatch): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", lambda i: _raise(ValueError("Invalid source provided")), ) - response = client.post("/import/preview", data={"source": "invalid"}) + response = client.post("/import/preview.json", data={"source": "invalid"}) assert response.status_code == 200 data = response.json() assert data["success"] is False From d4c32600e531561e746f3f7c88812a029dbe8f4e Mon Sep 17 00:00:00 2001 From: Sanket17052006 Date: Thu, 28 May 2026 22:16:34 +0530 Subject: [PATCH 4/5] Add web.ctx.site=site.get() and improve tests --- openlibrary/fastapi/importapi.py | 57 +++-- openlibrary/tests/fastapi/test_importapi.py | 270 +++++++++----------- vendor/infogami | 2 +- 3 files changed, 162 insertions(+), 167 deletions(-) diff --git a/openlibrary/fastapi/importapi.py b/openlibrary/fastapi/importapi.py index 37f83c136cb..5c9b75a7ad4 100644 --- a/openlibrary/fastapi/importapi.py +++ b/openlibrary/fastapi/importapi.py @@ -6,27 +6,27 @@ from typing import Annotated -from fastapi import APIRouter, Depends, Form, HTTPException, Query, status +import web +from fastapi import APIRouter, Depends, Form, HTTPException, Query, Request, status from openlibrary.accounts import get_current_user +from openlibrary.fastapi.auth import AuthenticatedUser, require_authenticated_user from openlibrary.plugins.importapi.import_ui import ImportPreviewRequest +from openlibrary.utils.request_context import site, web_ctx_ip router = APIRouter(tags=["import"]) -def check_import_permission() -> None: - """Check that the current user has admin/librarian/super-librarian role.""" - user = get_current_user() - if user is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Authentication required", - ) - if not (user.is_admin() or user.is_librarian() or user.is_super_librarian()): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Insufficient permissions", - ) +def _client_ip(request: Request) -> str: + forwarded = request.headers.get("X-Forwarded-For") + if forwarded: + return forwarded.split(",")[0].strip() + real_ip = request.headers.get("X-Real-IP") + if real_ip: + return real_ip.strip() + if request.client: + return request.client.host + return "127.0.0.1" def _build_preview_response( @@ -34,8 +34,10 @@ def _build_preview_response( provider: str | None, identifier: str | None, save: bool, + request: Request, ) -> dict: - """Shared logic for GET and POST import preview handlers.""" + web.ctx.site = site.get() + try: req = ImportPreviewRequest.from_input( { @@ -48,11 +50,14 @@ def _build_preview_response( except ValueError as e: return {"success": False, "error": str(e)} - return dict(req.metadata_provider.do_import(req.identifier, req.save)) + with web_ctx_ip(_client_ip(request)): + return dict(req.metadata_provider.do_import(req.identifier, req.save)) @router.get("/import/preview.json") def import_preview_json_get( + request: Request, + _: Annotated[AuthenticatedUser, Depends(require_authenticated_user)], source: Annotated[ str | None, Query( @@ -71,27 +76,39 @@ def import_preview_json_get( description="Identifier for the given provider", ), ] = None, - _: Annotated[None, Depends(check_import_permission)] = None, ) -> dict: """ Preview import metadata without saving. Requires admin, librarian, or super-librarian role. """ - return _build_preview_response(source, provider, identifier, save=False) + user = get_current_user() + if not (user and (user.is_admin() or user.is_librarian() or user.is_super_librarian())): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Insufficient permissions", + ) + return _build_preview_response(source, provider, identifier, save=False, request=request) @router.post("/import/preview.json") def import_preview_json_post( + request: Request, + _: Annotated[AuthenticatedUser, Depends(require_authenticated_user)], source: Annotated[str | None, Form()] = None, provider: Annotated[str | None, Form()] = None, identifier: Annotated[str | None, Form()] = None, save: Annotated[bool, Form()] = False, - _: Annotated[None, Depends(check_import_permission)] = None, ) -> dict: """ Import metadata, optionally saving it. Requires admin, librarian, or super-librarian role. """ - return _build_preview_response(source, provider, identifier, save=save) + user = get_current_user() + if not (user and (user.is_admin() or user.is_librarian() or user.is_super_librarian())): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Insufficient permissions", + ) + return _build_preview_response(source, provider, identifier, save=save, request=request) diff --git a/openlibrary/tests/fastapi/test_importapi.py b/openlibrary/tests/fastapi/test_importapi.py index 31a69e4ff55..dcedea406a4 100644 --- a/openlibrary/tests/fastapi/test_importapi.py +++ b/openlibrary/tests/fastapi/test_importapi.py @@ -1,31 +1,14 @@ """Tests for the FastAPI import preview endpoints.""" -from unittest.mock import MagicMock, Mock +from unittest.mock import MagicMock, patch import pytest -from fastapi import FastAPI -from fastapi.testclient import TestClient - -from openlibrary.fastapi.importapi import router def _raise(exc: Exception) -> None: raise exc -def _make_mock_user( - is_admin: bool = True, - is_librarian: bool = False, - is_super_librarian: bool = False, -) -> Mock: - """Create a mock user with configurable role flags.""" - user = MagicMock() - user.is_admin.return_value = is_admin - user.is_librarian.return_value = is_librarian - user.is_super_librarian.return_value = is_super_librarian - return user - - FAKE_IMPORT_RESULT = { "edition": {"key": "/books/OL1M", "title": "Test Book"}, "success": True, @@ -33,119 +16,133 @@ def _make_mock_user( @pytest.fixture -def client() -> TestClient: - """Return a TestClient for the import preview router.""" - app = FastAPI() - app.include_router(router) - return TestClient(app) +def mock_site(): + """Mock the site ContextVar to avoid real infogami connection.""" + with patch("openlibrary.fastapi.importapi.site") as mock: + yield mock + + +@pytest.fixture +def mock_user_factory(mock_site, monkeypatch): + """Factory fixture to create and mock users with configurable roles. + + Usage: + user = mock_user_factory(is_admin=True) + user = mock_user_factory(is_librarian=True) + """ + + def create_user( + is_admin: bool = False, + is_librarian: bool = False, + is_super_librarian: bool = False, + ): + user = MagicMock() + user.is_admin.return_value = is_admin + user.is_librarian.return_value = is_librarian + user.is_super_librarian.return_value = is_super_librarian + monkeypatch.setattr("openlibrary.fastapi.importapi.get_current_user", lambda: user) + return user + + return create_user + + +@pytest.fixture +def mock_import_request(): + """Return a factory that creates a mock ImportPreviewRequest.""" + + def _make(save: bool = False): + mock_req = MagicMock() + mock_req.metadata_provider.do_import.return_value = FAKE_IMPORT_RESULT + mock_req.save = save + return mock_req + + return _make class TestImportPreviewAuth: """Authentication and authorization tests.""" - def test_get_requires_authentication(self, client, monkeypatch): - monkeypatch.setattr( - "openlibrary.fastapi.importapi.get_current_user", - lambda: None, - ) - response = client.get("/import/preview.json?source=amazon:ASIN123") + def test_get_requires_authentication(self, fastapi_client, mock_site): + response = fastapi_client.get("/import/preview.json?source=amazon:ASIN123") assert response.status_code == 401 - def test_post_requires_authentication(self, client, monkeypatch): - monkeypatch.setattr( - "openlibrary.fastapi.importapi.get_current_user", - lambda: None, + def test_post_requires_authentication(self, fastapi_client, mock_site): + response = fastapi_client.post( + "/import/preview.json", data={"source": "amazon:ASIN123"} ) - response = client.post("/import/preview.json", data={"source": "amazon:ASIN123"}) assert response.status_code == 401 - def test_get_forbidden_for_regular_user(self, client, monkeypatch): - monkeypatch.setattr( - "openlibrary.fastapi.importapi.get_current_user", - lambda: _make_mock_user(is_admin=False, is_librarian=False), - ) - response = client.get("/import/preview.json?source=amazon:ASIN123") + def test_get_forbidden_for_regular_user( + self, fastapi_client, mock_authenticated_user, mock_user_factory + ): + mock_user_factory() + response = fastapi_client.get("/import/preview.json?source=amazon:ASIN123") assert response.status_code == 403 - def test_post_forbidden_for_regular_user(self, client, monkeypatch): - monkeypatch.setattr( - "openlibrary.fastapi.importapi.get_current_user", - lambda: _make_mock_user(is_admin=False, is_librarian=False), + def test_post_forbidden_for_regular_user( + self, fastapi_client, mock_authenticated_user, mock_user_factory + ): + mock_user_factory() + response = fastapi_client.post( + "/import/preview.json", data={"source": "amazon:ASIN123"} ) - response = client.post("/import/preview.json", data={"source": "amazon:ASIN123"}) assert response.status_code == 403 - def test_get_allows_admin(self, client, monkeypatch): - monkeypatch.setattr( - "openlibrary.fastapi.importapi.get_current_user", - lambda: _make_mock_user(is_admin=True), - ) - mock_req = MagicMock() - mock_req.metadata_provider.do_import.return_value = FAKE_IMPORT_RESULT + def test_get_allows_admin( + self, fastapi_client, mock_authenticated_user, mock_user_factory, mock_import_request, monkeypatch + ): + mock_user_factory(is_admin=True) monkeypatch.setattr( "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", - lambda i: mock_req, + lambda i: mock_import_request(), ) - response = client.get("/import/preview.json?source=amazon:ASIN123") + response = fastapi_client.get("/import/preview.json?source=amazon:ASIN123") assert response.status_code == 200 - def test_get_allows_librarian(self, client, monkeypatch): - monkeypatch.setattr( - "openlibrary.fastapi.importapi.get_current_user", - lambda: _make_mock_user(is_admin=False, is_librarian=True), - ) - mock_req = MagicMock() - mock_req.metadata_provider.do_import.return_value = FAKE_IMPORT_RESULT + def test_get_allows_librarian( + self, fastapi_client, mock_authenticated_user, mock_user_factory, mock_import_request, monkeypatch + ): + mock_user_factory(is_librarian=True) monkeypatch.setattr( "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", - lambda i: mock_req, + lambda i: mock_import_request(), ) - response = client.get("/import/preview.json?source=amazon:ASIN123") + response = fastapi_client.get("/import/preview.json?source=amazon:ASIN123") assert response.status_code == 200 - def test_get_allows_super_librarian(self, client, monkeypatch): - monkeypatch.setattr( - "openlibrary.fastapi.importapi.get_current_user", - lambda: _make_mock_user(is_admin=False, is_librarian=False, is_super_librarian=True), - ) - mock_req = MagicMock() - mock_req.metadata_provider.do_import.return_value = FAKE_IMPORT_RESULT + def test_get_allows_super_librarian( + self, fastapi_client, mock_authenticated_user, mock_user_factory, mock_import_request, monkeypatch + ): + mock_user_factory(is_super_librarian=True) monkeypatch.setattr( "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", - lambda i: mock_req, + lambda i: mock_import_request(), ) - response = client.get("/import/preview.json?source=amazon:ASIN123") + response = fastapi_client.get("/import/preview.json?source=amazon:ASIN123") assert response.status_code == 200 class TestImportPreviewGet: """GET /import/preview.json endpoint tests.""" - def test_get_returns_import_result(self, client, monkeypatch): - monkeypatch.setattr( - "openlibrary.fastapi.importapi.get_current_user", - lambda: _make_mock_user(is_admin=True), - ) - mock_req = MagicMock() - mock_req.metadata_provider.do_import.return_value = { - "edition": {"key": "/books/OL1M", "title": "Test Book"}, - "success": True, - } - monkeypatch.setattr( + @pytest.fixture(autouse=True) + def _auth_setup(self, fastapi_client, mock_authenticated_user, mock_user_factory, monkeypatch): + self.client = fastapi_client + self.monkeypatch = monkeypatch + mock_user_factory(is_admin=True) + + def test_get_returns_import_result(self, mock_import_request): + self.monkeypatch.setattr( "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", - lambda i: mock_req, + lambda i: mock_import_request(), ) - response = client.get("/import/preview.json?source=amazon:ASIN123") + response = self.client.get("/import/preview.json?source=amazon:ASIN123") assert response.status_code == 200 data = response.json() assert data["success"] is True assert data["edition"]["key"] == "/books/OL1M" - def test_get_does_not_save(self, client, monkeypatch): - monkeypatch.setattr( - "openlibrary.fastapi.importapi.get_current_user", - lambda: _make_mock_user(is_admin=True), - ) + def test_get_does_not_save(self): captured = {} def fake_from_input(i): @@ -155,34 +152,26 @@ def fake_from_input(i): mock_req.save = False return mock_req - monkeypatch.setattr( + self.monkeypatch.setattr( "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", fake_from_input, ) - response = client.get("/import/preview.json?source=amazon:ASIN123&save=true") + response = self.client.get("/import/preview.json?source=amazon:ASIN123&save=true") assert response.status_code == 200 assert captured["save"] == "false" - def test_get_invalid_source_returns_error(self, client, monkeypatch): - monkeypatch.setattr( - "openlibrary.fastapi.importapi.get_current_user", - lambda: _make_mock_user(is_admin=True), - ) - monkeypatch.setattr( + def test_get_invalid_source_returns_error(self): + self.monkeypatch.setattr( "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", lambda i: _raise(ValueError("Invalid source provided")), ) - response = client.get("/import/preview.json?source=invalid") + response = self.client.get("/import/preview.json?source=invalid") assert response.status_code == 200 data = response.json() assert data["success"] is False assert "Invalid source provided" in data["error"] - def test_get_with_provider_and_identifier(self, client, monkeypatch): - monkeypatch.setattr( - "openlibrary.fastapi.importapi.get_current_user", - lambda: _make_mock_user(is_admin=True), - ) + def test_get_with_provider_and_identifier(self): captured = {} def fake_from_input(i): @@ -192,25 +181,21 @@ def fake_from_input(i): mock_req.save = False return mock_req - monkeypatch.setattr( + self.monkeypatch.setattr( "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", fake_from_input, ) - response = client.get("/import/preview.json?provider=amazon&identifier=1234567890") + response = self.client.get("/import/preview.json?provider=amazon&identifier=1234567890") assert response.status_code == 200 assert captured["params"]["provider"] == "amazon" assert captured["params"]["identifier"] == "1234567890" - def test_get_no_params_returns_error(self, client, monkeypatch): - monkeypatch.setattr( - "openlibrary.fastapi.importapi.get_current_user", - lambda: _make_mock_user(is_admin=True), - ) - monkeypatch.setattr( + def test_get_no_params_returns_error(self): + self.monkeypatch.setattr( "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", - lambda i: _raise(ValueError("Invalid source provided")), + lambda i: _raise(ValueError("No provider specified")), ) - response = client.get("/import/preview.json") + response = self.client.get("/import/preview.json") assert response.status_code == 200 data = response.json() assert data["success"] is False @@ -219,25 +204,23 @@ def test_get_no_params_returns_error(self, client, monkeypatch): class TestImportPreviewPost: """POST /import/preview.json endpoint tests.""" - def test_post_returns_import_result(self, client, monkeypatch): - monkeypatch.setattr( - "openlibrary.fastapi.importapi.get_current_user", - lambda: _make_mock_user(is_admin=True), - ) - mock_req = MagicMock() - mock_req.metadata_provider.do_import.return_value = FAKE_IMPORT_RESULT - monkeypatch.setattr( + @pytest.fixture(autouse=True) + def _auth_setup(self, fastapi_client, mock_authenticated_user, mock_user_factory, monkeypatch): + self.client = fastapi_client + self.monkeypatch = monkeypatch + mock_user_factory(is_admin=True) + + def test_post_returns_import_result(self, mock_import_request): + self.monkeypatch.setattr( "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", - lambda i: mock_req, + lambda i: mock_import_request(), + ) + response = self.client.post( + "/import/preview.json", data={"source": "amazon:ASIN123"} ) - response = client.post("/import/preview.json", data={"source": "amazon:ASIN123"}) assert response.status_code == 200 - def test_post_can_save(self, client, monkeypatch): - monkeypatch.setattr( - "openlibrary.fastapi.importapi.get_current_user", - lambda: _make_mock_user(is_admin=True), - ) + def test_post_can_save(self): captured = {} def fake_from_input(i): @@ -247,49 +230,44 @@ def fake_from_input(i): mock_req.save = captured["save"] == "true" return mock_req - monkeypatch.setattr( + self.monkeypatch.setattr( "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", fake_from_input, ) - response = client.post( + response = self.client.post( "/import/preview.json", data={"source": "amazon:ASIN123", "save": "true"}, ) assert response.status_code == 200 assert captured["save"] == "true" - def test_post_without_save_defaults_to_false(self, client, monkeypatch): - monkeypatch.setattr( - "openlibrary.fastapi.importapi.get_current_user", - lambda: _make_mock_user(is_admin=True), - ) + def test_post_without_save_defaults_to_false(self): captured = {} def fake_from_input(i): captured["save"] = i.get("save", "false") mock_req = MagicMock() mock_req.metadata_provider.do_import.return_value = FAKE_IMPORT_RESULT - mock_req.save = captured["save"] == "true" return mock_req - monkeypatch.setattr( + self.monkeypatch.setattr( "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", fake_from_input, ) - response = client.post("/import/preview.json", data={"source": "amazon:ASIN123"}) + response = self.client.post( + "/import/preview.json", data={"source": "amazon:ASIN123"} + ) assert response.status_code == 200 assert captured["save"] == "false" - def test_post_invalid_source_returns_error(self, client, monkeypatch): - monkeypatch.setattr( - "openlibrary.fastapi.importapi.get_current_user", - lambda: _make_mock_user(is_admin=True), - ) - monkeypatch.setattr( + def test_post_invalid_source_returns_error(self): + self.monkeypatch.setattr( "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", lambda i: _raise(ValueError("Invalid source provided")), ) - response = client.post("/import/preview.json", data={"source": "invalid"}) + response = self.client.post( + "/import/preview.json", data={"source": "invalid"} + ) assert response.status_code == 200 data = response.json() assert data["success"] is False diff --git a/vendor/infogami b/vendor/infogami index 1fee668753d..1fd8b760ff6 160000 --- a/vendor/infogami +++ b/vendor/infogami @@ -1 +1 @@ -Subproject commit 1fee668753d15ecc9c41361441706130f343be28 +Subproject commit 1fd8b760ff69bfe755bf8313b8ec48de9002cef4 From 3f0fd9532c322fbab6655ff571b4f93b3b58ebb2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 17:13:48 +0000 Subject: [PATCH 5/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- openlibrary/fastapi/importapi.py | 6 ++-- openlibrary/plugins/importapi/import_ui.py | 2 +- openlibrary/tests/fastapi/test_importapi.py | 40 ++++++--------------- 3 files changed, 13 insertions(+), 35 deletions(-) diff --git a/openlibrary/fastapi/importapi.py b/openlibrary/fastapi/importapi.py index 5c9b75a7ad4..716d77e44cd 100644 --- a/openlibrary/fastapi/importapi.py +++ b/openlibrary/fastapi/importapi.py @@ -18,11 +18,9 @@ def _client_ip(request: Request) -> str: - forwarded = request.headers.get("X-Forwarded-For") - if forwarded: + if forwarded := request.headers.get("X-Forwarded-For"): return forwarded.split(",")[0].strip() - real_ip = request.headers.get("X-Real-IP") - if real_ip: + if real_ip := request.headers.get("X-Real-IP"): return real_ip.strip() if request.client: return request.client.host diff --git a/openlibrary/plugins/importapi/import_ui.py b/openlibrary/plugins/importapi/import_ui.py index 461d117f1cd..e43c5d7e1a9 100644 --- a/openlibrary/plugins/importapi/import_ui.py +++ b/openlibrary/plugins/importapi/import_ui.py @@ -1,10 +1,10 @@ import json from dataclasses import dataclass from typing import Literal, cast, override +from warnings import deprecated import requests import web -from typing_extensions import deprecated from infogami.plugins.api.code import jsonapi from infogami.utils import delegate diff --git a/openlibrary/tests/fastapi/test_importapi.py b/openlibrary/tests/fastapi/test_importapi.py index dcedea406a4..1ae063a7e95 100644 --- a/openlibrary/tests/fastapi/test_importapi.py +++ b/openlibrary/tests/fastapi/test_importapi.py @@ -67,30 +67,20 @@ def test_get_requires_authentication(self, fastapi_client, mock_site): assert response.status_code == 401 def test_post_requires_authentication(self, fastapi_client, mock_site): - response = fastapi_client.post( - "/import/preview.json", data={"source": "amazon:ASIN123"} - ) + response = fastapi_client.post("/import/preview.json", data={"source": "amazon:ASIN123"}) assert response.status_code == 401 - def test_get_forbidden_for_regular_user( - self, fastapi_client, mock_authenticated_user, mock_user_factory - ): + def test_get_forbidden_for_regular_user(self, fastapi_client, mock_authenticated_user, mock_user_factory): mock_user_factory() response = fastapi_client.get("/import/preview.json?source=amazon:ASIN123") assert response.status_code == 403 - def test_post_forbidden_for_regular_user( - self, fastapi_client, mock_authenticated_user, mock_user_factory - ): + def test_post_forbidden_for_regular_user(self, fastapi_client, mock_authenticated_user, mock_user_factory): mock_user_factory() - response = fastapi_client.post( - "/import/preview.json", data={"source": "amazon:ASIN123"} - ) + response = fastapi_client.post("/import/preview.json", data={"source": "amazon:ASIN123"}) assert response.status_code == 403 - def test_get_allows_admin( - self, fastapi_client, mock_authenticated_user, mock_user_factory, mock_import_request, monkeypatch - ): + def test_get_allows_admin(self, fastapi_client, mock_authenticated_user, mock_user_factory, mock_import_request, monkeypatch): mock_user_factory(is_admin=True) monkeypatch.setattr( "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", @@ -99,9 +89,7 @@ def test_get_allows_admin( response = fastapi_client.get("/import/preview.json?source=amazon:ASIN123") assert response.status_code == 200 - def test_get_allows_librarian( - self, fastapi_client, mock_authenticated_user, mock_user_factory, mock_import_request, monkeypatch - ): + def test_get_allows_librarian(self, fastapi_client, mock_authenticated_user, mock_user_factory, mock_import_request, monkeypatch): mock_user_factory(is_librarian=True) monkeypatch.setattr( "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", @@ -110,9 +98,7 @@ def test_get_allows_librarian( response = fastapi_client.get("/import/preview.json?source=amazon:ASIN123") assert response.status_code == 200 - def test_get_allows_super_librarian( - self, fastapi_client, mock_authenticated_user, mock_user_factory, mock_import_request, monkeypatch - ): + def test_get_allows_super_librarian(self, fastapi_client, mock_authenticated_user, mock_user_factory, mock_import_request, monkeypatch): mock_user_factory(is_super_librarian=True) monkeypatch.setattr( "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", @@ -215,9 +201,7 @@ def test_post_returns_import_result(self, mock_import_request): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", lambda i: mock_import_request(), ) - response = self.client.post( - "/import/preview.json", data={"source": "amazon:ASIN123"} - ) + response = self.client.post("/import/preview.json", data={"source": "amazon:ASIN123"}) assert response.status_code == 200 def test_post_can_save(self): @@ -254,9 +238,7 @@ def fake_from_input(i): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", fake_from_input, ) - response = self.client.post( - "/import/preview.json", data={"source": "amazon:ASIN123"} - ) + response = self.client.post("/import/preview.json", data={"source": "amazon:ASIN123"}) assert response.status_code == 200 assert captured["save"] == "false" @@ -265,9 +247,7 @@ def test_post_invalid_source_returns_error(self): "openlibrary.fastapi.importapi.ImportPreviewRequest.from_input", lambda i: _raise(ValueError("Invalid source provided")), ) - response = self.client.post( - "/import/preview.json", data={"source": "invalid"} - ) + response = self.client.post("/import/preview.json", data={"source": "invalid"}) assert response.status_code == 200 data = response.json() assert data["success"] is False