tests: add more tests
Some checks failed
/ typecheck (pull_request) Failing after 11s

This commit is contained in:
2025-12-09 09:17:13 +01:00
parent fda49d091c
commit 2a98718699
8 changed files with 1805 additions and 15 deletions

151
tests/conftest.py Normal file
View File

@@ -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 """<?xml version="1.0" encoding="UTF-8"?>
<marc:record xmlns:marc="http://www.loc.gov/MARC21/slim">
<marc:leader>00000nam a22000001i 4500</marc:leader>
<marc:controlfield tag="001">123456789</marc:controlfield>
<marc:controlfield tag="005">20230101120000.0</marc:controlfield>
<marc:datafield tag="020" ind1=" " ind2=" ">
<marc:subfield code="a">9783123456789</marc:subfield>
</marc:datafield>
<marc:datafield tag="041" ind1=" " ind2=" ">
<marc:subfield code="a">ger</marc:subfield>
</marc:datafield>
<marc:datafield tag="245" ind1="1" ind2="0">
<marc:subfield code="a">Test Book Title</marc:subfield>
<marc:subfield code="b">A Subtitle</marc:subfield>
</marc:datafield>
<marc:datafield tag="250" ind1=" " ind2=" ">
<marc:subfield code="a">2nd edition</marc:subfield>
</marc:datafield>
<marc:datafield tag="264" ind1=" " ind2="1">
<marc:subfield code="a">Berlin</marc:subfield>
<marc:subfield code="b">Test Publisher</marc:subfield>
<marc:subfield code="c">2023</marc:subfield>
</marc:datafield>
<marc:datafield tag="300" ind1=" " ind2=" ">
<marc:subfield code="a">456 pages</marc:subfield>
</marc:datafield>
<marc:datafield tag="338" ind1=" " ind2=" ">
<marc:subfield code="a">Band</marc:subfield>
</marc:datafield>
<marc:datafield tag="700" ind1="1" ind2=" ">
<marc:subfield code="a">Author, Test</marc:subfield>
</marc:datafield>
<marc:datafield tag="924" ind1=" " ind2=" ">
<marc:subfield code="9">Frei 129</marc:subfield>
<marc:subfield code="g">ABC 123</marc:subfield>
<marc:subfield code="b">DE-Frei129</marc:subfield>
</marc:datafield>
</marc:record>"""
@pytest.fixture
def sample_sru_response_xml() -> bytes:
"""Sample SRU searchRetrieveResponse XML for testing."""
return b"""<?xml version="1.0" encoding="UTF-8"?>
<zs:searchRetrieveResponse xmlns:zs="http://www.loc.gov/zing/srw/"
xmlns:marc="http://www.loc.gov/MARC21/slim">
<zs:version>1.1</zs:version>
<zs:numberOfRecords>1</zs:numberOfRecords>
<zs:records>
<zs:record>
<zs:recordSchema>marcxml</zs:recordSchema>
<zs:recordPacking>xml</zs:recordPacking>
<zs:recordData>
<marc:record>
<marc:leader>00000nam a22</marc:leader>
<marc:controlfield tag="001">123456789</marc:controlfield>
<marc:datafield tag="020" ind1=" " ind2=" ">
<marc:subfield code="a">9783123456789</marc:subfield>
</marc:datafield>
<marc:datafield tag="041" ind1=" " ind2=" ">
<marc:subfield code="a">ger</marc:subfield>
</marc:datafield>
<marc:datafield tag="245" ind1=" " ind2=" ">
<marc:subfield code="a">Test Book</marc:subfield>
</marc:datafield>
<marc:datafield tag="250" ind1=" " ind2=" ">
<marc:subfield code="a">1st edition</marc:subfield>
</marc:datafield>
<marc:datafield tag="264" ind1=" " ind2="1">
<marc:subfield code="b">Publisher</marc:subfield>
<marc:subfield code="c">2023</marc:subfield>
</marc:datafield>
<marc:datafield tag="300" ind1=" " ind2=" ">
<marc:subfield code="a">200 pages</marc:subfield>
</marc:datafield>
<marc:datafield tag="338" ind1=" " ind2=" ">
<marc:subfield code="a">Band</marc:subfield>
</marc:datafield>
<marc:datafield tag="700" ind1="1" ind2=" ">
<marc:subfield code="a">Author, Test</marc:subfield>
</marc:datafield>
<marc:datafield tag="924" ind1=" " ind2=" ">
<marc:subfield code="b">DE-Frei129</marc:subfield>
</marc:datafield>
</marc:record>
</zs:recordData>
<zs:recordPosition>1</zs:recordPosition>
</zs:record>
</zs:records>
<zs:echoedSearchRetrieveRequest>
<zs:version>1.1</zs:version>
<zs:query>pica.tit=Test</zs:query>
<zs:maximumRecords>100</zs:maximumRecords>
<zs:recordPacking>xml</zs:recordPacking>
<zs:recordSchema>marcxml</zs:recordSchema>
</zs:echoedSearchRetrieveRequest>
</zs:searchRetrieveResponse>"""
@pytest.fixture
def mock_catalogue_html() -> str:
"""Sample HTML response from catalogue search."""
return """<!DOCTYPE html>
<html>
<body>
<a class="title getFull" href="/opac/record/123">Book Title</a>
</body>
</html>"""
@pytest.fixture
def mock_catalogue_detail_html() -> str:
"""Sample HTML response from catalogue book detail page."""
return """<!DOCTYPE html>
<html>
<body>
<div class="headline text">Test Book Title</div>
<div class="col-xs-12 col-md-5 col-lg-4 rds-dl-head RDS_PPN"></div>
<div class="col-xs-12 col-md-7 col-lg-8 rds-dl-panel">123456789</div>
<div class="col-xs-12 col-md-5 col-lg-4 rds-dl-head RDS_EDITION"></div>
<div class="col-xs-12 col-md-7 col-lg-8 rds-dl-panel">2nd ed.</div>
<div class="col-xs-12 col-md-5 col-lg-4 rds-dl-head RDS_PERSON"></div>
<div class="col-xs-12 col-md-7 col-lg-8 rds-dl-panel">
<a href="#">Author One</a>
<a href="#">Author Two</a>
</div>
<div class="panel-body">
<div class="rds-dl RDS_SIGNATURE">
<div class="rds-dl-panel">ABC 123</div>
</div>
<div class="rds-dl RDS_STATUS">
<div class="rds-dl-panel">Available</div>
</div>
<div class="rds-dl RDS_LOCATION">
<div class="rds-dl-panel">Main Library</div>
</div>
</div>
<div class="RDS_ISBN"></div>
<div class="col-xs-12 col-md-7 col-lg-8 rds-dl-panel">9783123456789</div>
<div class="RDS_SCOPE"></div>
<div class="col-xs-12 col-md-7 col-lg-8 rds-dl-panel">300 pages</div>
</body>
</html>"""

View File

@@ -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 = "<html>search results</html>"
mocker.patch("requests.get", return_value=mock_response)
catalogue = Catalogue()
result = catalogue.search_book("test search")
assert result == "<html>search results</html>"
def test_search(self, mocker: MockerFixture):
"""Test search method."""
mocker.patch.object(Catalogue, "check_connection", return_value=True)
mock_response = MagicMock()
mock_response.text = "<html>detail page</html>"
mocker.patch("requests.get", return_value=mock_response)
catalogue = Catalogue()
result = catalogue.search("https://example.com/book/123")
assert result == "<html>detail page</html>"
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="<html></html>")
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="<html></html>")
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 = """<html>
<div class="col-xs-12 rds-dl RDS_LOCATION">
Semesterapparat-42
</div>
</html>"""
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 = """<html>
<div class="col-xs-12 rds-dl RDS_LOCATION">
Floor 1
Handbibliothek-Reference
</div>
</html>"""
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 = """<html>
<div class="col-xs-12 col-md-5 col-lg-4 rds-dl-head RDS_PERSON"></div>
<div class="col-xs-12 col-md-7 col-lg-8 rds-dl-panel">
<a href="#">Author One</a>
<a href="#">Author Two</a>
</div>
</html>"""
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 = """<html>
<div class="panel-body">
<div class="rds-dl RDS_SIGNATURE">
<div class="rds-dl-panel">ABC 123</div>
</div>
<div class="rds-dl RDS_STATUS">
<div class="rds-dl-panel">Available</div>
</div>
<div class="rds-dl RDS_LOCATION">
<div class="rds-dl-panel">Semesterapparat-1</div>
</div>
</div>
</html>"""
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

112
tests/test_init.py Normal file
View File

@@ -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"""<?xml version="1.0"?>
<zs:searchRetrieveResponse xmlns:zs="http://www.loc.gov/zing/srw/">
<zs:version>1.1</zs:version>
<zs:numberOfRecords>0</zs:numberOfRecords>
</zs:searchRetrieveResponse>"""
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()

View File

@@ -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"

View File

@@ -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")

View File

@@ -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"

375
tests/test_transformers.py Normal file
View File

@@ -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

309
tests/test_webrequest.py Normal file
View File

@@ -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 = "<html>results</html>"
mock_get.side_effect = [ip_response, search_response]
wr = WebRequest()
result = wr.search_book("test search")
assert result == "<html>results</html>"
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 = "<html>ppn result</html>"
mock_get.side_effect = [ip_response, ppn_response]
wr = WebRequest()
result = wr.search_ppn("123456")
assert result == "<html>ppn result</html>"
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 = "<html>detail page</html>"
mock_get.side_effect = [ip_response, search_response]
wr = WebRequest()
result = wr.search("https://example.com/book")
assert result == "<html>detail page</html>"
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 = """<html>
<a class="title getFull" href="/opac/book/123">Book 1</a>
<a class="title getFull" href="/opac/book/456">Book 2</a>
</html>"""
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 = '<html><div class="test-class"> Content Here </div></html>'
soup = BeautifulSoup(html, "html.parser")
result = get_content(soup, "test-class")
assert result == "Content Here"