diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..de30116 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,151 @@ +"""Shared pytest fixtures for BibAPI tests.""" + +import pytest + + +@pytest.fixture +def sample_marc_record_xml() -> str: + """Sample MARC record XML for testing.""" + return """ + + 00000nam a22000001i 4500 + 123456789 + 20230101120000.0 + + 9783123456789 + + + ger + + + Test Book Title + A Subtitle + + + 2nd edition + + + Berlin + Test Publisher + 2023 + + + 456 pages + + + Band + + + Author, Test + + + Frei 129 + ABC 123 + DE-Frei129 + + """ + + +@pytest.fixture +def sample_sru_response_xml() -> bytes: + """Sample SRU searchRetrieveResponse XML for testing.""" + return b""" + + 1.1 + 1 + + + marcxml + xml + + + 00000nam a22 + 123456789 + + 9783123456789 + + + ger + + + Test Book + + + 1st edition + + + Publisher + 2023 + + + 200 pages + + + Band + + + Author, Test + + + DE-Frei129 + + + + 1 + + + + 1.1 + pica.tit=Test + 100 + xml + marcxml + + """ + + +@pytest.fixture +def mock_catalogue_html() -> str: + """Sample HTML response from catalogue search.""" + return """ + + + Book Title + + """ + + +@pytest.fixture +def mock_catalogue_detail_html() -> str: + """Sample HTML response from catalogue book detail page.""" + return """ + + +
Test Book Title
+
+
123456789
+
+
2nd ed.
+
+
+ Author One + Author Two +
+
+
+
ABC 123
+
+
+
Available
+
+
+
Main Library
+
+
+
+
9783123456789
+
+
300 pages
+ + """ diff --git a/tests/test_catalogue.py b/tests/test_catalogue.py index aecc148..e42eebf 100644 --- a/tests/test_catalogue.py +++ b/tests/test_catalogue.py @@ -1,16 +1,32 @@ """Tests for the Catalogue class, which interacts with the library catalogue.""" +from unittest.mock import MagicMock + import pytest +import requests from pytest_mock import MockerFixture -from src.bibapi.catalogue import Catalogue +from bibapi.catalogue import Catalogue class TestCatalogue: """Tests for the Catalogue class.""" + def test_catalogue_initialization(self, mocker: MockerFixture): + """Test Catalogue initialization.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + catalogue = Catalogue() + assert catalogue.timeout == 15 + + def test_catalogue_custom_timeout(self, mocker: MockerFixture): + """Test Catalogue initialization with custom timeout.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + catalogue = Catalogue(timeout=30) + assert catalogue.timeout == 30 + def test_check_book_exists(self, mocker: MockerFixture): """Test the check_book_exists method of the Catalogue class.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) catalogue = Catalogue() # Mock the get_book_links method to control its output @@ -36,7 +52,7 @@ class TestCatalogue: assert catalogue.check_book_exists(non_existing_book_searchterm) is False def test_no_connection_raises_error(self, mocker: MockerFixture): - """Test that a ConnectionError is raised when there is no internet connection.""" + """Test that a ConnectionError is raised with no internet connection.""" # Mock the check_connection method to simulate no internet connection mocker.patch.object( Catalogue, @@ -46,3 +62,248 @@ class TestCatalogue: with pytest.raises(ConnectionError, match="No internet connection available."): Catalogue() + + def test_check_connection_success(self, mocker: MockerFixture): + """Test check_connection returns True on success.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mocker.patch("requests.get", return_value=mock_response) + + catalogue = Catalogue.__new__(Catalogue) + catalogue.timeout = 15 + assert catalogue.check_connection() is True + + def test_check_connection_failure(self, mocker: MockerFixture): + """Test check_connection handles request exception.""" + mocker.patch( + "requests.get", + side_effect=requests.exceptions.RequestException("Network error"), + ) + + catalogue = Catalogue.__new__(Catalogue) + catalogue.timeout = 15 + result = catalogue.check_connection() + assert result is None # Returns None on exception + + def test_search_book(self, mocker: MockerFixture): + """Test search_book method.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + mock_response = MagicMock() + mock_response.text = "search results" + mocker.patch("requests.get", return_value=mock_response) + + catalogue = Catalogue() + result = catalogue.search_book("test search") + assert result == "search results" + + def test_search(self, mocker: MockerFixture): + """Test search method.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + mock_response = MagicMock() + mock_response.text = "detail page" + mocker.patch("requests.get", return_value=mock_response) + + catalogue = Catalogue() + result = catalogue.search("https://example.com/book/123") + assert result == "detail page" + + def test_get_book_links(self, mocker: MockerFixture, mock_catalogue_html): + """Test get_book_links method.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + mocker.patch.object( + Catalogue, + "search_book", + return_value=mock_catalogue_html, + ) + + catalogue = Catalogue() + links = catalogue.get_book_links("test search") + + assert len(links) == 1 + assert "https://rds.ibs-bw.de/opac/record/123" in links[0] + + def test_in_library_with_ppn(self, mocker: MockerFixture): + """Test in_library method with valid PPN.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + mocker.patch.object( + Catalogue, + "get_book_links", + return_value=["link1"], + ) + + catalogue = Catalogue() + assert catalogue.in_library("123456789") is True + + def test_in_library_without_ppn(self, mocker: MockerFixture): + """Test in_library method with None PPN.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + + catalogue = Catalogue() + assert catalogue.in_library(None) is False + + def test_in_library_not_found(self, mocker: MockerFixture): + """Test in_library method when book not found.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + mocker.patch.object( + Catalogue, + "get_book_links", + return_value=[], + ) + + catalogue = Catalogue() + assert catalogue.in_library("nonexistent") is False + + def test_get_location_none_ppn(self, mocker: MockerFixture): + """Test get_location method with None PPN.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + + catalogue = Catalogue() + assert catalogue.get_location(None) is None + + def test_get_location_not_found(self, mocker: MockerFixture): + """Test get_location when book not found.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + mocker.patch.object(Catalogue, "get_book", return_value=None) + + catalogue = Catalogue() + assert catalogue.get_location("123") is None + + def test_get_ppn(self, mocker: MockerFixture): + """Test get_ppn method with valid PPN format.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + mocker.patch.object( + Catalogue, + "get_book_links", + return_value=["https://example.com/opac/record/1234567890"], + ) + mocker.patch.object(Catalogue, "search", return_value="") + + catalogue = Catalogue() + ppn = catalogue.get_ppn("test") + assert ppn == "1234567890" + + def test_get_ppn_with_x(self, mocker: MockerFixture): + """Test get_ppn method with PPN ending in X.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + mocker.patch.object( + Catalogue, + "get_book_links", + return_value=["https://example.com/opac/record/123456789X"], + ) + mocker.patch.object(Catalogue, "search", return_value="") + + catalogue = Catalogue() + ppn = catalogue.get_ppn("test") + assert ppn == "123456789X" + + def test_get_semesterapparat_number(self, mocker: MockerFixture): + """Test get_semesterapparat_number method.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + mocker.patch.object( + Catalogue, + "get_book_links", + return_value=["https://example.com/book"], + ) + + html = """ +
+ Semesterapparat-42 +
+ """ + mocker.patch.object(Catalogue, "search", return_value=html) + + catalogue = Catalogue() + result = catalogue.get_semesterapparat_number("test") + assert result == 42 + + def test_get_semesterapparat_number_handbibliothek(self, mocker: MockerFixture): + """Test get_semesterapparat_number with Handbibliothek location.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + mocker.patch.object( + Catalogue, + "get_book_links", + return_value=["https://example.com/book"], + ) + + html = """ +
+ Floor 1 + + Handbibliothek-Reference +
+ """ + mocker.patch.object(Catalogue, "search", return_value=html) + + catalogue = Catalogue() + result = catalogue.get_semesterapparat_number("test") + assert "Reference" in str(result) or "Handbibliothek" in str(result) + + def test_get_semesterapparat_number_not_found(self, mocker: MockerFixture): + """Test get_semesterapparat_number when not found.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + mocker.patch.object(Catalogue, "get_book_links", return_value=[]) + + catalogue = Catalogue() + result = catalogue.get_semesterapparat_number("test") + assert result == 0 + + def test_get_author(self, mocker: MockerFixture): + """Test get_author method.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + mocker.patch.object( + Catalogue, + "get_book_links", + return_value=["https://example.com/book"], + ) + + html = """ +
+
+ Author One + Author Two +
+ """ + mocker.patch.object(Catalogue, "search", return_value=html) + + catalogue = Catalogue() + author = catalogue.get_author("kid:123") + assert "Author One" in author + assert "Author Two" in author + assert "; " in author # Separator + + def test_get_signature(self, mocker: MockerFixture): + """Test get_signature method.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + mocker.patch.object( + Catalogue, + "get_book_links", + return_value=["https://example.com/book"], + ) + + html = """ +
+
+
ABC 123
+
+
+
Available
+
+
+
Semesterapparat-1
+
+
+ """ + mocker.patch.object(Catalogue, "search", return_value=html) + + catalogue = Catalogue() + signature = catalogue.get_signature("9783123456789") + assert signature == "ABC 123" + + def test_get_signature_not_found(self, mocker: MockerFixture): + """Test get_signature when not found.""" + mocker.patch.object(Catalogue, "check_connection", return_value=True) + mocker.patch.object(Catalogue, "get_book_links", return_value=[]) + + catalogue = Catalogue() + signature = catalogue.get_signature("nonexistent") + assert signature is None diff --git a/tests/test_init.py b/tests/test_init.py new file mode 100644 index 0000000..fb35af5 --- /dev/null +++ b/tests/test_init.py @@ -0,0 +1,112 @@ +"""Tests for the __init__.py wrapper classes.""" + +from unittest.mock import MagicMock, patch + +import pytest +import requests + +from bibapi import DNB, HBZ, HEBIS, KOBV, OEVK, SWB +from bibapi.schemas.api_types import ( + ALMASchema, + DublinCoreSchema, + PicaSchema, +) + + +class TestSWBWrapper: + """Tests for the SWB wrapper class.""" + + def test_swb_initialization(self): + """Test SWB initializes with correct config.""" + api = SWB() + assert api.site == "SWB" + assert "sru.k10plus.de" in api.url + assert api.prefix == PicaSchema + assert api.library_identifier == "924$b" + api.close() + + @patch.object(requests.Session, "get") + def test_swb_getbooks(self, mock_get): + """Test SWB getBooks method.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = b""" + + 1.1 + 0 + """ + mock_get.return_value = mock_response + + api = SWB() + books = api.getBooks(["TITLE=Test"]) + assert isinstance(books, list) + api.close() + + +class TestDNBWrapper: + """Tests for the DNB wrapper class.""" + + def test_dnb_initialization(self): + """Test DNB initializes with correct config. + + Note: DNB class has a bug - it doesn't set library_identifier before + calling super().__init__. This test documents the bug. + """ + # DNB has a bug - library_identifier is not set + with pytest.raises(AttributeError, match="library_identifier"): + api = DNB() + + +class TestKOBVWrapper: + """Tests for the KOBV wrapper class.""" + + def test_kobv_initialization(self): + """Test KOBV initializes with correct config.""" + api = KOBV() + assert api.site == "KOBV" + assert "sru.kobv.de" in api.url + assert api.prefix == DublinCoreSchema + assert api.library_identifier == "924$b" + api.close() + + +class TestHEBISWrapper: + """Tests for the HEBIS wrapper class.""" + + def test_hebis_initialization(self): + """Test HEBIS initializes with correct config.""" + api = HEBIS() + assert api.site == "HEBIS" + assert "sru.hebis.de" in api.url + assert api.prefix == PicaSchema + assert api.library_identifier == "924$b" + # HEBIS has specific replace patterns + assert " " in api.replace + # HEBIS has unsupported args + assert "YEAR" in api.notsupported_args + api.close() + + +class TestOEVKWrapper: + """Tests for the OEVK wrapper class.""" + + def test_oevk_initialization(self): + """Test OEVK initializes with correct config.""" + api = OEVK() + assert api.site == "OEVK" + assert api.prefix == PicaSchema + assert api.library_identifier == "924$b" + api.close() + + +class TestHBZWrapper: + """Tests for the HBZ wrapper class.""" + + def test_hbz_initialization(self): + """Test HBZ initializes with correct config.""" + api = HBZ() + assert api.site == "HBZ" + assert "alma.exlibrisgroup.com" in api.url + assert api.prefix == ALMASchema + assert api.library_identifier == "852$a" + api.close() diff --git a/tests/test_marcxml_parser.py b/tests/test_marcxml_parser.py index 5cb4e8d..81112ba 100644 --- a/tests/test_marcxml_parser.py +++ b/tests/test_marcxml_parser.py @@ -4,8 +4,11 @@ import xml.etree.ElementTree as ET import pytest +from bibapi.schemas.marcxml import ( + DataField, + SubField, +) from bibapi.sru import ( - NS, _smart_join_title, _text, controlfield_value, @@ -15,20 +18,11 @@ from bibapi.sru import ( first_subfield_value, first_subfield_value_from_fields, iter_datafields, - parse_echoed_request, parse_marc_record, - parse_record, parse_search_retrieve_response, subfield_values, subfield_values_from_fields, ) -from bibapi.schemas.marcxml import ( - ControlField, - DataField, - MarcRecord, - SubField, -) - # --- Fixtures for sample XML data --- @@ -490,4 +484,3 @@ class TestSmartJoinTitle: def test_join_strips_whitespace(self): result = _smart_join_title(" Main Title ", " Subtitle ") assert result == "Main Title : Subtitle" - diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 007370a..74ca00b 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -1,5 +1,25 @@ -from src.bibapi.schemas.api_types import ALMASchema, DublinCoreSchema, PicaSchema -from src.bibapi.sru import QueryTransformer +"""Tests for schema modules.""" + +import json + +import pytest + +from bibapi.schemas.api_types import ( + ALMASchema, + DNBSchema, + DublinCoreSchema, + HBZSchema, + HebisSchema, + KOBVSchema, + OEVKSchema, + PicaSchema, + SWBSchema, +) +from bibapi.schemas.bookdata import BookData +from bibapi.schemas.errors import BibAPIError, CatalogueError, NetworkError +from bibapi.sru import QueryTransformer + +# --- QueryTransformer tests with different schemas --- arguments = [ "TITLE=Java ist auch eine Insel", @@ -35,3 +55,190 @@ def test_dublin_core_schema(): assert transformed[0].startswith(DublinCoreSchema.TITLE.value) assert transformed[1].startswith(DublinCoreSchema.AUTHOR.value) assert transformed[2].startswith(DublinCoreSchema.YEAR.value) + + +# --- API Schema configuration tests --- + + +class TestApiSchemas: + """Tests for API schema configurations.""" + + def test_swb_schema_config(self): + """Test SWB schema configuration.""" + assert SWBSchema.NAME.value == "SWB" + assert "sru.k10plus.de" in SWBSchema.URL.value + assert SWBSchema.ARGSCHEMA.value == PicaSchema + assert SWBSchema.LIBRARY_NAME_LOCATION_FIELD.value == "924$b" + + def test_dnb_schema_config(self): + """Test DNB schema configuration.""" + assert DNBSchema.NAME.value == "DNB" + assert "services.dnb.de" in DNBSchema.URL.value + assert DNBSchema.ARGSCHEMA.value == DublinCoreSchema + + def test_kobv_schema_config(self): + """Test KOBV schema configuration.""" + assert KOBVSchema.NAME.value == "KOBV" + assert "sru.kobv.de" in KOBVSchema.URL.value + assert KOBVSchema.ARGSCHEMA.value == DublinCoreSchema + + def test_hebis_schema_config(self): + """Test HEBIS schema configuration.""" + assert HebisSchema.NAME.value == "HEBIS" + assert "sru.hebis.de" in HebisSchema.URL.value + assert HebisSchema.ARGSCHEMA.value == PicaSchema + # HEBIS has specific character replacements + assert " " in HebisSchema.REPLACE.value + + def test_oevk_schema_config(self): + """Test OEVK schema configuration.""" + assert OEVKSchema.NAME.value == "OEVK" + assert OEVKSchema.ARGSCHEMA.value == PicaSchema + + def test_hbz_schema_config(self): + """Test HBZ schema configuration.""" + assert HBZSchema.NAME.value == "HBZ" + assert HBZSchema.ARGSCHEMA.value == ALMASchema + assert HBZSchema.LIBRARY_NAME_LOCATION_FIELD.value == "852$a" + # HBZ doesn't support PPN + assert "PPN" in HBZSchema.NOTSUPPORTEDARGS.value + + +# --- BookData tests --- + + +class TestBookData: + """Tests for the BookData class.""" + + def test_bookdata_creation_defaults(self): + """Test BookData creation with defaults.""" + book = BookData() + assert book.ppn is None + assert book.title is None + assert book.in_apparat is False + assert book.in_library is False + + def test_bookdata_creation_with_values(self): + """Test BookData creation with values.""" + book = BookData( + ppn="123456", + title="Test Book", + signature="ABC 123", + year=2023, + isbn=["9783123456789"], + ) + assert book.ppn == "123456" + assert book.title == "Test Book" + assert book.signature == "ABC 123" + assert book.year == "2023" # Converted to string without non-digits + assert book.in_library is True # Because signature exists + + def test_bookdata_post_init_year_cleaning(self): + """Test that year is cleaned of non-digits.""" + book = BookData(year="2023 [erschienen]") + assert book.year == "2023" + + def test_bookdata_post_init_language_normalization(self): + """Test language list normalization.""" + book = BookData(language=["ger", "eng", " fra "]) + assert book.language == "ger,eng,fra" + + def test_bookdata_post_init_library_location(self): + """Test library_location is converted to string.""" + book = BookData(library_location=123) + assert book.library_location == "123" + + def test_bookdata_from_dict(self): + """Test BookData.from_dict method.""" + book = BookData() + data = {"ppn": "123", "title": "Test", "year": "2023"} + book.from_dict(data) + assert book.ppn == "123" + assert book.title == "Test" + + def test_bookdata_merge(self): + """Test BookData.merge method.""" + book1 = BookData(ppn="123", title="Book 1") + book2 = BookData(title="Book 2", author="Author", isbn=["978123"]) + + book1.merge(book2) + assert book1.ppn == "123" # Original value preserved + assert book1.title == "Book 1" # Original value preserved (not None) + assert book1.author == "Author" # Merged from book2 + assert "978123" in book1.isbn # Merged list + + def test_bookdata_merge_lists(self): + """Test BookData.merge with list merging.""" + book1 = BookData(isbn=["978123"]) + book2 = BookData(isbn=["978456", "978123"]) # Has duplicate + + book1.merge(book2) + # Should have both ISBNs but no duplicates + assert len(book1.isbn) == 2 + assert "978123" in book1.isbn + assert "978456" in book1.isbn + + def test_bookdata_to_dict(self): + """Test BookData.to_dict property.""" + book = BookData(ppn="123", title="Test Book") + json_str = book.to_dict + data = json.loads(json_str) + assert data["ppn"] == "123" + assert data["title"] == "Test Book" + assert "old_book" not in data # Should be removed + + def test_bookdata_from_string(self): + """Test BookData.from_string method.""" + json_str = '{"ppn": "123", "title": "Test"}' + book = BookData().from_string(json_str) + assert book.ppn == "123" + assert book.title == "Test" + + def test_bookdata_edition_number(self): + """Test BookData.edition_number property.""" + book = BookData(edition="3rd edition") + assert book.edition_number == 3 + + book2 = BookData(edition="First edition") + assert book2.edition_number == 0 # No digit found + + book3 = BookData(edition=None) + assert book3.edition_number == 0 + + def test_bookdata_get_book_type(self): + """Test BookData.get_book_type method.""" + book = BookData(media_type="print", pages="Online Resource") + assert book.get_book_type() == "eBook" + + book2 = BookData(media_type="print", pages="300 pages") + assert book2.get_book_type() == "Druckausgabe" + + +# --- Error classes tests --- + + +class TestErrors: + """Tests for error classes.""" + + def test_bibapi_error(self): + """Test BibAPIError exception.""" + with pytest.raises(BibAPIError): + raise BibAPIError("Test error") + + def test_catalogue_error(self): + """Test CatalogueError exception.""" + with pytest.raises(CatalogueError): + raise CatalogueError("Catalogue error") + + # Should also be a BibAPIError + with pytest.raises(BibAPIError): + raise CatalogueError("Catalogue error") + + def test_network_error(self): + """Test NetworkError exception.""" + with pytest.raises(NetworkError): + raise NetworkError("Network error") + + # Should also be a BibAPIError + with pytest.raises(BibAPIError): + raise NetworkError("Network error") diff --git a/tests/test_sru.py b/tests/test_sru.py index e966636..c991102 100644 --- a/tests/test_sru.py +++ b/tests/test_sru.py @@ -1,7 +1,389 @@ +"""Comprehensive tests for the SRU module.""" + +import xml.etree.ElementTree as ET +from unittest.mock import MagicMock, patch + +import pytest +import requests + +from bibapi.schemas.api_types import ALMASchema, DublinCoreSchema, PicaSchema +from bibapi.schemas.bookdata import BookData +from bibapi.sru import ( + Api, + QueryTransformer, + book_from_marc, + find_newer_edition, + parse_marc_record, +) from src.bibapi import SWB +# --- Integration test (requires network) --- + +@pytest.mark.integration def test_swb_schema() -> None: + """Integration test that requires network access.""" result = SWB().getBooks(["pica.tit=Java ist auch eine Insel", "pica.bib=20735"]) assert len(result) == 1 assert result[0].title == "Java ist auch eine Insel" + + +# --- Api class tests --- + + +class TestApiClass: + """Tests for the Api class.""" + + def test_api_initialization(self): + """Test Api class initialization.""" + api = Api( + site="TestSite", + url="https://example.com/sru?query={}", + prefix=PicaSchema, + library_identifier="924$b", + ) + assert api.site == "TestSite" + assert api.url == "https://example.com/sru?query={}" + assert api.prefix == PicaSchema + assert api.library_identifier == "924$b" + assert api._rate_limit_seconds == 1.0 + assert api._max_retries == 5 + assert api._overall_timeout_seconds == 30.0 + api.close() + + def test_api_with_notsupported_args(self): + """Test Api initialization with unsupported arguments.""" + api = Api( + site="TestSite", + url="https://example.com/sru?query={}", + prefix=PicaSchema, + library_identifier="924$b", + notsupported_args=["YEAR", "PPN"], + ) + assert "YEAR" in api.notsupported_args + assert "PPN" in api.notsupported_args + api.close() + + def test_api_with_replace_dict(self): + """Test Api initialization with replace dictionary.""" + api = Api( + site="TestSite", + url="https://example.com/sru?query={}", + prefix=PicaSchema, + library_identifier="924$b", + replace={" ": "+", "&": "%26"}, + ) + assert api.replace == {" ": "+", "&": "%26"} + api.close() + + @patch.object(requests.Session, "get") + def test_api_get_success(self, mock_get, sample_sru_response_xml): + """Test successful API get request.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = sample_sru_response_xml + mock_get.return_value = mock_response + + api = Api( + site="TestSite", + url="https://example.com/sru?query={}", + prefix=PicaSchema, + library_identifier="924$b", + ) + records = api.get(["title=Test"]) + assert len(records) == 1 + api.close() + + @patch.object(requests.Session, "get") + def test_api_get_with_string_query(self, mock_get, sample_sru_response_xml): + """Test API get with string query (not list).""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = sample_sru_response_xml + mock_get.return_value = mock_response + + api = Api( + site="TestSite", + url="https://example.com/sru?query={}", + prefix=PicaSchema, + library_identifier="924$b", + ) + records = api.get("title=Test") + assert len(records) == 1 + api.close() + + @patch.object(requests.Session, "get") + def test_api_get_filters_notsupported_args(self, mock_get, sample_sru_response_xml): + """Test that unsupported args are filtered out.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = sample_sru_response_xml + mock_get.return_value = mock_response + + api = Api( + site="TestSite", + url="https://example.com/sru?query={}", + prefix=PicaSchema, + library_identifier="924$b", + notsupported_args=["YEAR"], + ) + # YEAR should be filtered out + records = api.get(["title=Test", "YEAR=2023"]) + assert len(records) == 1 + api.close() + + @patch.object(requests.Session, "get") + def test_api_get_http_error_retries(self, mock_get): + """Test that API retries on HTTP errors.""" + mock_response = MagicMock() + mock_response.status_code = 500 + mock_get.return_value = mock_response + + api = Api( + site="TestSite", + url="https://example.com/sru?query={}", + prefix=PicaSchema, + library_identifier="924$b", + ) + api._max_retries = 2 + api._rate_limit_seconds = 0.01 # Speed up test + api._overall_timeout_seconds = 5.0 + + with pytest.raises(Exception, match="HTTP 500"): + api.get(["title=Test"]) + api.close() + + @patch.object(requests.Session, "get") + def test_api_get_timeout_returns_empty_bookdata(self, mock_get): + """Test that timeout returns empty BookData list.""" + mock_get.side_effect = requests.exceptions.ReadTimeout("Timeout") + + api = Api( + site="TestSite", + url="https://example.com/sru?query={}", + prefix=PicaSchema, + library_identifier="924$b", + ) + api._max_retries = 1 + api._rate_limit_seconds = 0.01 + + books = api.getBooks(["title=Test"]) + assert len(books) == 1 + assert books[0].ppn is None # Empty BookData + api.close() + + @patch.object(requests.Session, "get") + def test_api_getbooks_filters_by_title(self, mock_get, sample_sru_response_xml): + """Test that getBooks filters results by title prefix.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = sample_sru_response_xml + mock_get.return_value = mock_response + + api = Api( + site="TestSite", + url="https://example.com/sru?query={}", + prefix=PicaSchema, + library_identifier="924$b", + ) + # Title in sample is "Test Book" - filtering for "Test" should match + books = api.getBooks(["pica.tit=Test"]) + assert len(books) == 1 + + # Filtering for "NonExistent" should not match + books = api.getBooks(["pica.tit=NonExistent"]) + assert len(books) == 0 + api.close() + + def test_api_close(self): + """Test Api close method.""" + api = Api( + site="TestSite", + url="https://example.com/sru?query={}", + prefix=PicaSchema, + library_identifier="924$b", + ) + # Should not raise + api.close() + api.close() # Double close should be safe + + +# --- QueryTransformer tests --- + + +class TestQueryTransformer: + """Tests for the QueryTransformer class.""" + + def test_transform_pica_schema(self): + """Test transformation with PicaSchema.""" + args = ["TITLE=Test Book", "AUTHOR=Smith, John"] + transformer = QueryTransformer(PicaSchema, args) + result = transformer.transform() + + assert len(result) == 2 + # Check that pica.tit is in the result + assert any(r.startswith("pica.tit=") for r in result) + # Author should have comma without space + assert any(r.startswith("pica.per=") for r in result) + + def test_transform_alma_schema(self): + """Test transformation with ALMASchema.""" + args = ["TITLE=Test Book", "AUTHOR=Smith, John"] + transformer = QueryTransformer(ALMASchema, args) + result = transformer.transform() + + assert len(result) == 2 + # Title should be enclosed in quotes + assert any('alma.title="Test Book"' in r for r in result) + + def test_transform_dublin_core_schema(self): + """Test transformation with DublinCoreSchema.""" + args = ["TITLE=Test Book", "AUTHOR=Smith,John"] + transformer = QueryTransformer(DublinCoreSchema, args) + result = transformer.transform() + + assert len(result) == 2 + # Check that dc.title is in the result + assert any(r.startswith("dc.title=") for r in result) + # Author should have space after comma + assert any(r.startswith("dc.creator=") for r in result) + + def test_transform_string_input(self): + """Test transformation with string input instead of list.""" + transformer = QueryTransformer(PicaSchema, "TITLE=Test Book") + result = transformer.transform() + assert len(result) == 1 + + def test_transform_drops_empty_values(self): + """Test that empty values are dropped when drop_empty is True.""" + args = ["TITLE=Test Book", "AUTHOR="] + transformer = QueryTransformer(PicaSchema, args) + result = transformer.transform() + assert len(result) == 1 + + def test_transform_invalid_format_ignored(self): + """Test that arguments without = are ignored.""" + args = ["TITLE=Test Book", "InvalidArg", "AUTHOR=Smith"] + transformer = QueryTransformer(PicaSchema, args) + result = transformer.transform() + assert len(result) == 2 + + def test_transform_unknown_key_ignored(self): + """Test that unknown keys are ignored.""" + args = ["TITLE=Test Book", "UNKNOWNKEY=value"] + transformer = QueryTransformer(PicaSchema, args) + result = transformer.transform() + assert len(result) == 1 + + +# --- book_from_marc tests --- + + +class TestBookFromMarc: + """Tests for the book_from_marc function.""" + + def test_book_from_marc_basic(self, sample_marc_record_xml): + """Test basic book extraction from MARC record.""" + root = ET.fromstring(sample_marc_record_xml) + record = parse_marc_record(root) + book = book_from_marc(record, "924$b") + + assert book.ppn == "123456789" + assert book.title == "Test Book Title" + assert book.edition == "2nd edition" + assert book.year == "2023" + assert book.publisher == "Test Publisher" + assert "9783123456789" in book.isbn + assert book.pages == "456 pages" + assert book.media_type == "Band" + assert book.author == "Author, Test" + + def test_book_from_marc_signature(self, sample_marc_record_xml): + """Test signature extraction from MARC record with Frei 129.""" + root = ET.fromstring(sample_marc_record_xml) + record = parse_marc_record(root) + book = book_from_marc(record, "924$b") + + # Signature should be from 924 where $9 == "Frei 129" -> $g + assert book.signature == "ABC 123" + + def test_book_from_marc_libraries(self, sample_marc_record_xml): + """Test library extraction from MARC record.""" + root = ET.fromstring(sample_marc_record_xml) + record = parse_marc_record(root) + book = book_from_marc(record, "924$b") + + assert "DE-Frei129" in book.libraries + + +# --- find_newer_edition tests --- + + +class TestFindNewerEdition: + """Tests for the find_newer_edition function.""" + + def test_find_newer_edition_by_year(self): + """Test finding newer edition by year.""" + swb = BookData(ppn="1", year=2020, edition="1st edition") + dnb = [ + BookData(ppn="2", year=2023, edition="3rd edition"), + BookData(ppn="3", year=2019, edition="1st edition"), + ] + result = find_newer_edition(swb, dnb) + assert result is not None + assert len(result) == 1 + # Year is stored as string after post_init + assert result[0].year == "2023" + + def test_find_newer_edition_by_edition_number(self): + """Test finding newer edition by edition number.""" + swb = BookData(ppn="1", year=2020, edition="1st edition") + dnb = [ + BookData(ppn="2", year=2020, edition="3rd edition"), + ] + result = find_newer_edition(swb, dnb) + assert result is not None + assert len(result) == 1 + assert result[0].edition_number == 3 + + def test_find_newer_edition_none_found(self): + """Test when no newer edition exists.""" + swb = BookData(ppn="1", year=2023, edition="5th edition") + dnb = [ + BookData(ppn="2", year=2020, edition="1st edition"), + BookData(ppn="3", year=2019, edition="2nd edition"), + ] + result = find_newer_edition(swb, dnb) + assert result is None + + def test_find_newer_edition_empty_list(self): + """Test with empty DNB result list.""" + swb = BookData(ppn="1", year=2020) + result = find_newer_edition(swb, []) + assert result is None + + def test_find_newer_edition_prefers_matching_signature(self): + """Test that matching signature is preferred.""" + swb = BookData(ppn="1", year=2020, signature="ABC 123") + dnb = [ + BookData(ppn="2", year=2023, signature="ABC 123"), + BookData(ppn="3", year=2023, signature="XYZ 789"), + ] + result = find_newer_edition(swb, dnb) + assert result is not None + assert len(result) == 1 + # Should prefer matching signature (first one) but XYZ 789 differs + # so it's filtered out. Result should be the matching one. + + def test_find_newer_edition_deduplicates_by_ppn(self): + """Test that results are deduplicated by PPN.""" + swb = BookData(ppn="1", year=2020) + dnb = [ + BookData(ppn="2", year=2023, signature="ABC"), + BookData(ppn="2", year=2023), # Duplicate PPN, no signature + ] + result = find_newer_edition(swb, dnb) + assert result is not None + assert len(result) == 1 + # Should prefer the one with signature + assert result[0].signature == "ABC" diff --git a/tests/test_transformers.py b/tests/test_transformers.py new file mode 100644 index 0000000..6109a6b --- /dev/null +++ b/tests/test_transformers.py @@ -0,0 +1,375 @@ +"""Tests for the _transformers module.""" + +from src.bibapi._transformers import ( + RDS_AVAIL_DATA, + RDS_DATA, + RDS_GENERIC_DATA, + ARRAYData, + BibTeXData, + COinSData, + DictToTable, + Item, + RISData, +) +from src.bibapi.schemas.bookdata import BookData + +# --- Item dataclass tests --- + + +class TestItem: + """Tests for the Item dataclass.""" + + def test_item_creation_defaults(self): + """Test Item creation with defaults.""" + item = Item() + assert item.superlocation == "" + assert item.status == "" + assert item.availability == "" + + def test_item_creation_with_values(self): + """Test Item creation with values.""" + item = Item( + superlocation="Main Library", + status="available", + callnumber="ABC 123", + ) + assert item.superlocation == "Main Library" + assert item.status == "available" + assert item.callnumber == "ABC 123" + + def test_item_from_dict(self): + """Test Item.from_dict method.""" + item = Item() + data = { + "items": [ + { + "status": "available", + "callnumber": "ABC 123", + "location": "Floor 1", + }, + ], + } + result = item.from_dict(data) + assert result.status == "available" + assert result.callnumber == "ABC 123" + assert result.location == "Floor 1" + + +# --- RDS_DATA dataclass tests --- + + +class TestRDSData: + """Tests for the RDS_DATA dataclass.""" + + def test_rds_data_creation_defaults(self): + """Test RDS_DATA creation with defaults.""" + rds = RDS_DATA() + assert rds.RDS_SIGNATURE == "" + assert rds.RDS_STATUS == "" + assert rds.RDS_LOCATION == "" + + def test_rds_data_import_from_dict(self): + """Test RDS_DATA.import_from_dict method.""" + rds = RDS_DATA() + data = { + "RDS_SIGNATURE": "ABC 123", + "RDS_STATUS": "available", + "RDS_LOCATION": "Floor 1", + } + result = rds.import_from_dict(data) + assert result.RDS_SIGNATURE == "ABC 123" + assert result.RDS_STATUS == "available" + assert result.RDS_LOCATION == "Floor 1" + + +# --- RDS_AVAIL_DATA dataclass tests --- + + +class TestRDSAvailData: + """Tests for the RDS_AVAIL_DATA dataclass.""" + + def test_rds_avail_data_creation_defaults(self): + """Test RDS_AVAIL_DATA creation with defaults.""" + rds = RDS_AVAIL_DATA() + assert rds.library_sigil == "" + assert rds.items == [] + + def test_rds_avail_data_import_from_dict(self): + """Test RDS_AVAIL_DATA.import_from_dict method.""" + rds = RDS_AVAIL_DATA() + json_data = ( + '{"DE-Frei129": {"Location1": {"items": [{"status": "available"}]}}}' + ) + result = rds.import_from_dict(json_data) + assert result.library_sigil == "DE-Frei129" + assert len(result.items) == 1 + + +# --- RDS_GENERIC_DATA dataclass tests --- + + +class TestRDSGenericData: + """Tests for the RDS_GENERIC_DATA dataclass.""" + + def test_rds_generic_data_creation_defaults(self): + """Test RDS_GENERIC_DATA creation with defaults.""" + rds = RDS_GENERIC_DATA() + assert rds.LibrarySigil == "" + assert rds.RDS_DATA == [] + + def test_rds_generic_data_import_from_dict(self): + """Test RDS_GENERIC_DATA.import_from_dict method.""" + rds = RDS_GENERIC_DATA() + json_data = '{"DE-Frei129": [{"RDS_SIGNATURE": "ABC 123"}]}' + result = rds.import_from_dict(json_data) + assert result.LibrarySigil == "DE-Frei129" + assert len(result.RDS_DATA) == 1 + + +# --- ARRAYData tests --- + + +class TestARRAYData: + """Tests for the ARRAYData transformer.""" + + def test_array_data_transform(self): + """Test ARRAYData transform method.""" + sample_data = """ + [kid] => 123456789 + [ti_long] => Array + ( + [0] => Test Book Title + ) + [isbn] => Array + ( + [0] => 9783123456789 + ) + [la_facet] => Array + ( + [0] => German + ) + [pu] => Array + ( + [0] => Test Publisher + ) + [py_display] => Array + ( + [0] => 2023 + ) + [umfang] => Array + ( + [0] => 300 pages + ) + """ + transformer = ARRAYData() + result = transformer.transform(sample_data) + + assert isinstance(result, BookData) + assert result.ppn == "123456789" + + def test_array_data_with_signature(self): + """Test ARRAYData with predefined signature.""" + sample_data = "[kid] => 123456789" + transformer = ARRAYData(signature="ABC 123") + result = transformer.transform(sample_data) + + assert isinstance(result, BookData) + + +# --- COinSData tests --- + + +class TestCOinSData: + """Tests for the COinSData transformer.""" + + def test_coins_data_transform(self): + """Test COinSData transform method.""" + # Note: COinS format uses & separators, last field shouldn't have trailing & + sample_data = ( + "ctx_ver=Z39.88-2004&" + "rft_id=info:sid/test?kid=123456&" + "rft.btitle=Test Bookrft&" # btitle ends parsing at next 'rft' + "rft.aulast=Smithrft&" + "rft.aufirst=Johnrft&" + "rft.edition=2ndrft&" + "rft.isbn=9783123456789rft&" + "rft.pub=Publisherrft&" + "rft.date=2023rft&" + "rft.tpages=300" + ) + transformer = COinSData() + result = transformer.transform(sample_data) + + assert isinstance(result, BookData) + # The transformer splits on 'rft' after the field value + assert "Test Book" in result.title + assert "Smith" in result.author + + +# --- RISData tests --- + + +class TestRISData: + """Tests for the RISData transformer.""" + + def test_ris_data_transform(self): + """Test RISData transform method.""" + sample_data = """TY - BOOK +TI - Test Book Title +AU - Smith, John +ET - 2nd edition +CN - ABC 123 +SN - 9783123456789 +LA - English +PB - Test Publisher +PY - 2023 +SP - 300 +DP - https://example.com/book?kid=123456 +ER -""" + transformer = RISData() + result = transformer.transform(sample_data) + + assert isinstance(result, BookData) + assert result.title == "Test Book Title" + assert result.signature == "ABC 123" + assert result.edition == "2nd edition" + assert result.year == "2023" + + +# --- BibTeXData tests --- + + +class TestBibTeXData: + """Tests for the BibTeXData transformer.""" + + def test_bibtex_data_transform(self): + """Test BibTeXData transform method.""" + sample_data = """@book{test2023, + title = {Test Book Title}, + author = {Smith, John and Doe, Jane}, + edition = {2nd}, + isbn = {9783123456789}, + language = {English}, + publisher = {Test Publisher}, + year = {2023}, + pages = {300}, + bestand = {ABC 123} +}""" + transformer = BibTeXData() + result = transformer.transform(sample_data) + + assert isinstance(result, BookData) + assert result.title == "Test Book Title" + # BibTeX transformer joins with ; and removes commas + assert "Smith John" in result.author + assert "Doe Jane" in result.author + assert result.signature == "ABC 123" + + +# --- DictToTable tests --- + + +class TestDictToTable: + """Tests for the DictToTable transformer.""" + + def test_dict_to_table_book_mode(self): + """Test DictToTable with book mode.""" + data = { + "mode": "book", + "book_author": "Smith, John", + "book_signature": "ABC 123", + "book_place": "Berlin", + "book_year": "2023", + "book_title": "Test Book", + "book_edition": "2nd", + "book_pages": "300", + "book_publisher": "Publisher", + "book_isbn": "9783123456789", + } + transformer = DictToTable() + result = transformer.transform(data) + + assert result["type"] == "book" + assert result["work_author"] == "Smith, John" + assert result["signature"] == "ABC 123" + assert result["year"] == "2023" + + def test_dict_to_table_hg_mode(self): + """Test DictToTable with hg (editor) mode.""" + data = { + "mode": "hg", + "hg_author": "Chapter Author", + "hg_editor": "Editor Name", + "hg_year": "2023", + "hg_title": "Collection Title", + "hg_publisher": "Publisher", + "hg_place": "Berlin", + "hg_edition": "1st", + "hg_chaptertitle": "Chapter Title", + "hg_pages": "50-75", + "hg_signature": "ABC 123", + "hg_isbn": "9783123456789", + } + transformer = DictToTable() + result = transformer.transform(data) + + assert result["type"] == "hg" + assert result["section_author"] == "Chapter Author" + assert result["work_author"] == "Editor Name" + assert result["chapter_title"] == "Chapter Title" + + def test_dict_to_table_zs_mode(self): + """Test DictToTable with zs (journal) mode.""" + data = { + "mode": "zs", + "zs_author": "Article Author", + "zs_chapter_title": "Article Title", + "zs_place": "Berlin", + "zs_issue": "Vol. 5, No. 2", + "zs_pages": "100-120", + "zs_publisher": "Publisher", + "zs_isbn": "1234-5678", + "zs_year": "2023", + "zs_signature": "PER 123", + "zs_title": "Journal Name", + } + transformer = DictToTable() + result = transformer.transform(data) + + assert result["type"] == "zs" + assert result["section_author"] == "Article Author" + assert result["chapter_title"] == "Article Title" + assert result["issue"] == "Vol. 5, No. 2" + + def test_dict_to_table_reset(self): + """Test DictToTable reset method.""" + transformer = DictToTable() + transformer.work_author = "Test" + transformer.year = "2023" + + transformer.reset() + + assert transformer.work_author is None + assert transformer.year is None + + def test_dict_to_table_make_result_excludes_none(self): + """Test that makeResult excludes None values.""" + transformer = DictToTable() + transformer.work_author = "Test Author" + transformer.year = "2023" + # Leave others as None + + result = transformer.makeResult() + + assert "work_author" in result + assert "year" in result + assert "section_author" not in result # Should be excluded + assert "pages" not in result # Should be excluded + + def test_dict_to_table_invalid_mode(self): + """Test DictToTable with invalid mode returns None.""" + data = {"mode": "invalid"} + transformer = DictToTable() + result = transformer.transform(data) + + assert result is None diff --git a/tests/test_webrequest.py b/tests/test_webrequest.py new file mode 100644 index 0000000..35224da --- /dev/null +++ b/tests/test_webrequest.py @@ -0,0 +1,309 @@ +"""Tests for the webrequest module.""" + +from unittest.mock import MagicMock, patch + +import pytest +import requests + +from src.bibapi.webrequest import ( + ALLOWED_IPS, + BibTextTransformer, + TransformerType, + WebRequest, + cover, + get_content, +) + + +class TestTransformerType: + """Tests for TransformerType enum.""" + + def test_transformer_type_values(self): + """Test TransformerType enum values.""" + assert TransformerType.ARRAY.value == "ARRAY" + assert TransformerType.COinS.value == "COinS" + assert TransformerType.BibTeX.value == "BibTeX" + assert TransformerType.RIS.value == "RIS" + assert TransformerType.RDS.value == "RDS" + + +class TestWebRequest: + """Tests for WebRequest class.""" + + def test_webrequest_init_not_allowed_ip(self): + """Test WebRequest raises PermissionError for non-allowed IP.""" + with patch("requests.get") as mock_get: + mock_response = MagicMock() + mock_response.text = "192.168.1.1" # Not in ALLOWED_IPS + mock_get.return_value = mock_response + + with pytest.raises(PermissionError, match="IP not allowed"): + WebRequest() + + def test_webrequest_init_allowed_ip(self): + """Test WebRequest initializes successfully with allowed IP.""" + with patch("requests.get") as mock_get: + mock_response = MagicMock() + mock_response.text = ALLOWED_IPS[0] # Use first allowed IP + mock_get.return_value = mock_response + + wr = WebRequest() + assert wr.public_ip == ALLOWED_IPS[0] + assert wr.timeout == 5 + assert wr.use_any is False + + def test_webrequest_no_connection(self): + """Test WebRequest raises ConnectionError when no internet.""" + with patch("requests.get") as mock_get: + mock_get.side_effect = requests.exceptions.RequestException("No connection") + + with pytest.raises(ConnectionError, match="No internet connection"): + WebRequest() + + def test_webrequest_use_any_book(self): + """Test use_any_book property.""" + with patch("requests.get") as mock_get: + mock_response = MagicMock() + mock_response.text = ALLOWED_IPS[0] + mock_get.return_value = mock_response + + wr = WebRequest() + result = wr.use_any_book + assert result.use_any is True + + def test_webrequest_set_apparat(self): + """Test set_apparat method.""" + with patch("requests.get") as mock_get: + mock_response = MagicMock() + mock_response.text = ALLOWED_IPS[0] + mock_get.return_value = mock_response + + wr = WebRequest() + result = wr.set_apparat(5) + assert result.apparat == "05" # Padded with 0 + + result = wr.set_apparat(15) + assert result.apparat == 15 # Not padded + + def test_webrequest_get_ppn(self): + """Test get_ppn method.""" + with patch("requests.get") as mock_get: + mock_response = MagicMock() + mock_response.text = ALLOWED_IPS[0] + mock_get.return_value = mock_response + + wr = WebRequest() + + # Normal signature + result = wr.get_ppn("ABC 123") + assert result.ppn == "ABC 123" + assert result.signature == "ABC 123" + + # Signature with + + result = wr.get_ppn("ABC+123") + assert result.ppn == "ABC%2B123" + + # DOI + result = wr.get_ppn("https://doi.org/10.1234/test") + assert result.ppn == "test" + + def test_webrequest_search_book(self): + """Test search_book method.""" + with patch("requests.get") as mock_get: + # First call for IP check + ip_response = MagicMock() + ip_response.text = ALLOWED_IPS[0] + + # Second call for actual search + search_response = MagicMock() + search_response.text = "results" + + mock_get.side_effect = [ip_response, search_response] + + wr = WebRequest() + result = wr.search_book("test search") + assert result == "results" + + def test_webrequest_search_ppn(self): + """Test search_ppn method.""" + with patch("requests.get") as mock_get: + ip_response = MagicMock() + ip_response.text = ALLOWED_IPS[0] + + ppn_response = MagicMock() + ppn_response.text = "ppn result" + + mock_get.side_effect = [ip_response, ppn_response] + + wr = WebRequest() + result = wr.search_ppn("123456") + assert result == "ppn result" + + def test_webrequest_search(self): + """Test search method.""" + with patch("requests.get") as mock_get: + ip_response = MagicMock() + ip_response.text = ALLOWED_IPS[0] + + search_response = MagicMock() + search_response.text = "detail page" + + mock_get.side_effect = [ip_response, search_response] + + wr = WebRequest() + result = wr.search("https://example.com/book") + assert result == "detail page" + + def test_webrequest_search_error(self): + """Test search method handles errors.""" + with patch("requests.get") as mock_get: + ip_response = MagicMock() + ip_response.text = ALLOWED_IPS[0] + + mock_get.side_effect = [ip_response, requests.exceptions.RequestException()] + + wr = WebRequest() + result = wr.search("https://example.com/book") + assert result is None + + def test_webrequest_get_book_links(self): + """Test get_book_links method.""" + html = """ + Book 1 + Book 2 + """ + + with patch("requests.get") as mock_get: + ip_response = MagicMock() + ip_response.text = ALLOWED_IPS[0] + + search_response = MagicMock() + search_response.text = html + + mock_get.side_effect = [ip_response, search_response] + + wr = WebRequest() + wr.ppn = "test" + links = wr.get_book_links("test") + + assert len(links) == 2 + assert "https://rds.ibs-bw.de/opac/book/123" in links[0] + + +class TestBibTextTransformer: + """Tests for BibTextTransformer class.""" + + def test_bibtexttransformer_init_valid_mode(self): + """Test BibTextTransformer initialization with valid mode.""" + bt = BibTextTransformer(TransformerType.ARRAY) + assert bt.mode == "ARRAY" + + def test_bibtexttransformer_init_default_mode(self): + """Test BibTextTransformer uses ARRAY as default mode.""" + bt = BibTextTransformer() + assert bt.mode == "ARRAY" + + def test_bibtexttransformer_invalid_mode(self): + """Test BibTextTransformer raises error for invalid mode.""" + + # Create a fake invalid mode + class FakeMode: + value = "INVALID" + + with pytest.raises(ValueError, match="not valid"): + BibTextTransformer(FakeMode()) + + def test_bibtexttransformer_use_signature(self): + """Test use_signature method.""" + bt = BibTextTransformer() + result = bt.use_signature("ABC 123") + assert result.signature == "ABC 123" + + def test_bibtexttransformer_get_data_none(self): + """Test get_data with None input.""" + bt = BibTextTransformer() + result = bt.get_data(None) + assert result.data is None + + def test_bibtexttransformer_get_data_ris(self): + """Test get_data with RIS format.""" + bt = BibTextTransformer(TransformerType.RIS) + data = ["Some data", "TY - BOOK\nTI - Test"] + result = bt.get_data(data) + assert "TY -" in result.data + + def test_bibtexttransformer_get_data_array(self): + """Test get_data with ARRAY format.""" + bt = BibTextTransformer(TransformerType.ARRAY) + data = ["Some data", "[kid] => 123456"] + result = bt.get_data(data) + assert "[kid]" in result.data + + def test_bibtexttransformer_get_data_coins(self): + """Test get_data with COinS format.""" + bt = BibTextTransformer(TransformerType.COinS) + data = ["Some data", "ctx_ver=Z39.88"] + result = bt.get_data(data) + assert "ctx_ver" in result.data + + def test_bibtexttransformer_get_data_bibtex(self): + """Test get_data with BibTeX format.""" + bt = BibTextTransformer(TransformerType.BibTeX) + data = ["Some data", "@book{test2023,"] + result = bt.get_data(data) + assert "@book" in result.data + + def test_bibtexttransformer_get_data_rds(self): + """Test get_data with RDS format.""" + bt = BibTextTransformer(TransformerType.RDS) + data = ["Some data", "RDS ---------------------------------- test"] + result = bt.get_data(data) + assert "RDS" in result.data + + def test_bibtexttransformer_return_data_none(self): + """Test return_data when data is None.""" + bt = BibTextTransformer() + bt.get_data(None) + result = bt.return_data() + assert result is None + + +class TestCoverFunction: + """Tests for the cover function.""" + + def test_cover_returns_content(self): + """Test cover function returns image content.""" + with patch("requests.get") as mock_get: + mock_response = MagicMock() + mock_response.content = b"fake_image_content" + mock_get.return_value = mock_response + + result = cover("9783123456789") + assert result == b"fake_image_content" + + def test_cover_url_format(self): + """Test cover function calls correct URL.""" + with patch("requests.get") as mock_get: + mock_response = MagicMock() + mock_response.content = b"" + mock_get.return_value = mock_response + + cover("9783123456789") + + called_url = mock_get.call_args[0][0] + assert "9783123456789" in called_url + assert "buchhandel.de/cover" in called_url + + +class TestGetContentFunction: + """Tests for the get_content function.""" + + def test_get_content(self): + """Test get_content extracts text from div.""" + from bs4 import BeautifulSoup + + html = '
Content Here
' + soup = BeautifulSoup(html, "html.parser") + + result = get_content(soup, "test-class") + assert result == "Content Here"