This commit is contained in:
151
tests/conftest.py
Normal file
151
tests/conftest.py
Normal 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>"""
|
||||
@@ -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
112
tests/test_init.py
Normal 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()
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
375
tests/test_transformers.py
Normal 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
309
tests/test_webrequest.py
Normal 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"
|
||||
Reference in New Issue
Block a user