From 1e84b834d1ceb36bdc6a4d65cc9fb16ca8145d73 Mon Sep 17 00:00:00 2001 From: kiritowu Date: Thu, 7 Nov 2024 14:59:22 +0800 Subject: [PATCH] [feat] Ensure 100% test coverage --- poetry.lock | 223 ++++- pyproject.toml | 32 + tests/__init__.py | 0 .../catalogue/test_get_availability_info.py | 261 ++++++ .../test_get_most_checkouts_trends_titles.py | 230 +++++ tests/api/catalogue/test_get_new_titles.py | 277 ++++++ tests/api/catalogue/test_get_title_details.py | 229 +++++ tests/api/catalogue/test_get_titles.py | 216 +++++ tests/api/catalogue/test_search_titles.py | 227 +++++ tests/models/test_bad_request_error.py | 69 ++ tests/models/test_bib_format.py | 29 + tests/models/test_book_cover.py | 88 ++ tests/models/test_checkouts_title.py | 99 +++ tests/models/test_checkouts_trend.py | 138 +++ tests/models/test_course_code.py | 88 ++ tests/models/test_facet.py | 84 ++ tests/models/test_facet_data.py | 52 ++ .../test_get_availability_info_response_v2.py | 122 +++ .../test_get_title_details_response_v2.py | 740 ++++++++++++++++ tests/models/test_get_titles_response_v2.py | 111 +++ tests/models/test_internal_server_error.py | 69 ++ tests/models/test_item.py | 281 +++++++ tests/models/test_method_not_allowed_error.py | 74 ++ tests/models/test_new_arrival_title.py | 795 ++++++++++++++++++ tests/models/test_not_found_error.py | 69 ++ tests/models/test_not_implemented_error.py | 69 ++ ...t_search_most_checkouts_titles_response.py | 176 ++++ .../models/test_search_new_titles_response.py | 1 + .../test_search_new_titles_response_v2.py | 259 ++++++ .../models/test_search_titles_response_v2.py | 115 +++ .../models/test_service_unavailable_error.py | 49 ++ tests/models/test_status.py | 78 ++ tests/models/test_title.py | 297 +++++++ tests/models/test_title_record.py | 272 ++++++ tests/models/test_title_summary.py | 145 ++++ tests/models/test_too_many_requests_error.py | 69 ++ tests/models/test_unauthorized_error.py | 69 ++ tests/test_client.py | 286 +++++++ tests/test_errors.py | 28 + tests/test_types.py | 115 +++ 40 files changed, 6630 insertions(+), 1 deletion(-) create mode 100644 tests/__init__.py create mode 100644 tests/api/catalogue/test_get_availability_info.py create mode 100644 tests/api/catalogue/test_get_most_checkouts_trends_titles.py create mode 100644 tests/api/catalogue/test_get_new_titles.py create mode 100644 tests/api/catalogue/test_get_title_details.py create mode 100644 tests/api/catalogue/test_get_titles.py create mode 100644 tests/api/catalogue/test_search_titles.py create mode 100644 tests/models/test_bad_request_error.py create mode 100644 tests/models/test_bib_format.py create mode 100644 tests/models/test_book_cover.py create mode 100644 tests/models/test_checkouts_title.py create mode 100644 tests/models/test_checkouts_trend.py create mode 100644 tests/models/test_course_code.py create mode 100644 tests/models/test_facet.py create mode 100644 tests/models/test_facet_data.py create mode 100644 tests/models/test_get_availability_info_response_v2.py create mode 100644 tests/models/test_get_title_details_response_v2.py create mode 100644 tests/models/test_get_titles_response_v2.py create mode 100644 tests/models/test_internal_server_error.py create mode 100644 tests/models/test_item.py create mode 100644 tests/models/test_method_not_allowed_error.py create mode 100644 tests/models/test_new_arrival_title.py create mode 100644 tests/models/test_not_found_error.py create mode 100644 tests/models/test_not_implemented_error.py create mode 100644 tests/models/test_search_most_checkouts_titles_response.py create mode 100644 tests/models/test_search_new_titles_response.py create mode 100644 tests/models/test_search_new_titles_response_v2.py create mode 100644 tests/models/test_search_titles_response_v2.py create mode 100644 tests/models/test_service_unavailable_error.py create mode 100644 tests/models/test_status.py create mode 100644 tests/models/test_title.py create mode 100644 tests/models/test_title_record.py create mode 100644 tests/models/test_title_summary.py create mode 100644 tests/models/test_too_many_requests_error.py create mode 100644 tests/models/test_unauthorized_error.py create mode 100644 tests/test_client.py create mode 100644 tests/test_errors.py create mode 100644 tests/test_types.py diff --git a/poetry.lock b/poetry.lock index a2ed6e3..73c3ce1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -52,6 +52,104 @@ files = [ {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.6.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + [[package]] name = "exceptiongroup" version = "1.2.2" @@ -137,6 +235,118 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "8.3.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.24.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, + {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, +] + +[package.dependencies] +pytest = ">=8.2,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -188,6 +398,17 @@ files = [ doc = ["reno", "sphinx"] test = ["pytest", "tornado (>=4.5)", "typeguard"] +[[package]] +name = "tomli" +version = "2.0.2" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -202,4 +423,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "cf1f81e9a716ea2980a57f484a8663c896a4920b923c752bbe40494113d9cca0" +content-hash = "21d6d9a7a67d3c6e6a5e43e3f211fb7d7ed4b20196f86a0ee308418eb9aee661" diff --git a/pyproject.toml b/pyproject.toml index 9de62f7..ee19004 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,12 @@ attrs = ">=21.3.0" python-dateutil = "^2.8.0" tenacity = "^9.0.0" +[tool.poetry.dev-dependencies] +pytest = "^8.0.0" +pytest-asyncio = "^0.24.0" +pytest-cov = "^4.0.0" +pytest-mock = "^3.11.1" + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" @@ -30,3 +36,29 @@ select = ["F", "I", "UP"] [tool.pyright] venvPath="." venv="venv" + +# Pytest Configs +[tool.pytest.ini_options] +testpaths = "tests" +python_files = "test_*.py" +addopts = "--cov=nlb_catalogue_client --cov-report=term-missing" + +# Coverage configs +[tool.coverage.run] +source = ["nlb_catalogue_client"] +omit = [ + "*/tests/*", + "*/__init__.py" +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "pass", + "raise ImportError", + "except ImportError:", + "if TYPE_CHECKING:", +] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/api/catalogue/test_get_availability_info.py b/tests/api/catalogue/test_get_availability_info.py new file mode 100644 index 0000000..ff47acf --- /dev/null +++ b/tests/api/catalogue/test_get_availability_info.py @@ -0,0 +1,261 @@ +from http import HTTPStatus +from typing import Any, Dict + +import httpx +import pytest +from tenacity import wait_none + +from nlb_catalogue_client.api.catalogue import get_get_availability_info +from nlb_catalogue_client.client import AuthenticatedClient +from nlb_catalogue_client.errors import UnexpectedStatus +from nlb_catalogue_client.models.get_availability_info_response_v2 import GetAvailabilityInfoResponseV2 + + +@pytest.fixture() +def client() -> AuthenticatedClient: + return AuthenticatedClient(base_url="https://api.example.com", token="test_token", raise_on_unexpected_status=True) + + +@pytest.fixture() +def success_response() -> Dict[str, Any]: + return { + "setId": 1, + "totalRecords": 1, + "count": 1, + "hasMoreRecords": False, + "nextRecordsOffset": 0, + "items": [ + { + "media": {"code": "BK", "name": "BOOK"}, + "usageLevel": {"code": "PUB", "name": "Public"}, + "location": {"code": "ADLR", "name": "Adult Lending"}, + "transactionStatus": {"code": "A", "name": "Available"}, + "irn": 123456, + "itemId": "B1234567J", + "brn": 123456, + "volumeName": "2023 issue 1", + "callNumber": "123.123 ART", + "formattedCallNumber": "English 123.123 -[ART]", + "language": "English", + "suffix": "-[ART]", + "donor": "Donated by John Doe", + "price": 29.99, + "status": {"code": "A", "name": "Available"}, + "minAgeLimit": 13, + } + ], + } + + +@pytest.fixture() +def error_responses() -> Dict[int, Dict[str, Any]]: + return { + 400: {"error": "Bad Request", "message": "Invalid request parameters", "statusCode": 400}, + 401: {"error": "Unauthorized", "message": "Authentication token is invalid", "statusCode": 401}, + 404: {"error": "Not Found", "message": "Resource not found", "statusCode": 404}, + 405: {"error": "Method Not Allowed", "message": "HTTP method not allowed", "statusCode": 405}, + 429: {"error": "Too Many Requests", "message": "Rate limit exceeded", "statusCode": 429}, + 500: {"error": "Internal Server Error", "message": "An unexpected error occurred", "statusCode": 500}, + 503: {"error": "Service Unavailable", "message": "Service is temporarily unavailable", "statusCode": 503}, + } + + +@pytest.fixture() +def mock_response(success_response) -> httpx.Response: + return httpx.Response( + status_code=200, + json=success_response, + ) + + +class TestGetAvailabilityInfo: + def test_get_kwargs(self): + kwargs = get_get_availability_info._get_kwargs( + limit=10, + sort_fields="title", + set_id=1, + offset=5, + brn=123456, + isbn="9781234567890", + ) + + assert kwargs == { + "method": "get", + "url": "/GetAvailabilityInfo", + "params": { + "Limit": 10, + "SortFields": "title", + "SetId": 1, + "Offset": 5, + "BRN": 123456, + "ISBN": "9781234567890", + }, + } + + def test_get_kwargs_with_defaults(self): + kwargs = get_get_availability_info._get_kwargs() + + assert kwargs == { + "method": "get", + "url": "/GetAvailabilityInfo", + "params": { + "Limit": 20, + "SetId": 0, + "Offset": 0, + }, + } + + def test_sync_detailed_success(self, mocker, client: AuthenticatedClient, mock_response: httpx.Response): + mocker.patch("httpx.Client.request", return_value=mock_response) + + response = get_get_availability_info.sync_detailed(client=client, brn=123456) + + assert response.status_code == HTTPStatus.OK + assert isinstance(response.parsed, GetAvailabilityInfoResponseV2) + assert response.parsed.total_records == 1 + assert response.parsed.items + assert len(response.parsed.items) == 1 + assert response.parsed.items[0].brn == 123456 + + def test_sync_success(self, mocker, client: AuthenticatedClient, mock_response: httpx.Response): + mocker.patch("httpx.Client.request", return_value=mock_response) + + response = get_get_availability_info.sync(client=client, brn=123456) + + assert isinstance(response, GetAvailabilityInfoResponseV2) + assert response.total_records == 1 + assert response.items + assert len(response.items) == 1 + assert response.items[0].brn == 123456 + + @pytest.mark.asyncio + async def test_asyncio_detailed_success(self, mocker, client: AuthenticatedClient, mock_response: httpx.Response): + mocker.patch("httpx.AsyncClient.request", return_value=mock_response) + + response = await get_get_availability_info.asyncio_detailed(client=client, brn=123456) + + assert response.status_code == HTTPStatus.OK + assert isinstance(response.parsed, GetAvailabilityInfoResponseV2) + assert response.parsed.total_records == 1 + assert response.parsed.items + assert len(response.parsed.items) == 1 + assert response.parsed.items[0].brn == 123456 + + @pytest.mark.asyncio + async def test_asyncio_success(self, mocker, client: AuthenticatedClient, mock_response: httpx.Response): + mocker.patch("httpx.AsyncClient.request", return_value=mock_response) + + response = await get_get_availability_info.asyncio(client=client, brn=123456) + + assert isinstance(response, GetAvailabilityInfoResponseV2) + assert response.total_records == 1 + assert response.items + assert len(response.items) == 1 + assert response.items[0].brn == 123456 + + @pytest.mark.parametrize( + "status_code,error_type", + [ + (400, "BadRequestError"), + (401, "UnauthorizedError"), + (404, "NotFoundError"), + (405, "MethodNotAllowedError"), + (429, "TooManyRequestsError"), + (500, "InternalServerError"), + (503, "ServiceUnavailableError"), + ], + ) + def test_error_responses_sync( + self, + mocker, + client: AuthenticatedClient, + error_responses: Dict[int, Dict[str, Any]], + status_code: int, + error_type: str, + ): + error_mock_response = httpx.Response( + status_code=status_code, + json=error_responses[status_code], + ) + mocker.patch("httpx.Client.request", return_value=error_mock_response) + + # Tenacity to stop waiting + get_get_availability_info.sync_detailed.retry.wait = wait_none() + + response = get_get_availability_info.sync_detailed(client=client, brn=123456) + + assert response.status_code == status_code + assert error_type in str(type(response.parsed)) + assert response.parsed + assert not isinstance(response.parsed, GetAvailabilityInfoResponseV2) + assert response.parsed.error == error_responses[status_code]["error"] + assert response.parsed.message == error_responses[status_code]["message"] + assert response.parsed.status_code == status_code + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "status_code,error_type", + [ + (400, "BadRequestError"), + (401, "UnauthorizedError"), + (404, "NotFoundError"), + (405, "MethodNotAllowedError"), + (429, "TooManyRequestsError"), + (500, "InternalServerError"), + (503, "ServiceUnavailableError"), + ], + ) + async def test_error_responses_async( + self, + mocker, + client: AuthenticatedClient, + error_responses: Dict[int, Dict[str, Any]], + status_code: int, + error_type: str, + ): + error_mock_response = httpx.Response( + status_code=status_code, + json=error_responses[status_code], + ) + mocker.patch("httpx.AsyncClient.request", return_value=error_mock_response) + + # Tenacity to stop waiting + get_get_availability_info.asyncio_detailed.retry.wait = wait_none() + + response = await get_get_availability_info.asyncio_detailed(client=client, brn=123456) + + assert response.status_code == status_code + assert error_type in str(type(response.parsed)) + assert response.parsed + assert not isinstance(response.parsed, GetAvailabilityInfoResponseV2) + assert response.parsed.error == error_responses[status_code]["error"] + assert response.parsed.message == error_responses[status_code]["message"] + assert response.parsed.status_code == status_code + + def test_sync_detailed_unexpected_status_raises( + self, + mocker, + client: AuthenticatedClient, + ): + unexpected_response = httpx.Response( + status_code=418, # I'm a teapot - unexpected status + json={"error": "Unexpected", "message": "I'm a teapot", "statusCode": 418}, + ) + mocker.patch("httpx.Client.request", return_value=unexpected_response) + + with pytest.raises(UnexpectedStatus): + get_get_availability_info.sync_detailed(client=client, brn=123456) + + def test_sync_detailed_unexpected_status_return_none( + self, + mocker, + client: AuthenticatedClient, + ): + unexpected_response = httpx.Response( + status_code=418, # I'm a teapot - unexpected status + json={"error": "Unexpected", "message": "I'm a teapot", "statusCode": 418}, + ) + mocker.patch("httpx.Client.request", return_value=unexpected_response) + client.raise_on_unexpected_status = False + + assert get_get_availability_info.sync_detailed(client=client, brn=123456).parsed is None diff --git a/tests/api/catalogue/test_get_most_checkouts_trends_titles.py b/tests/api/catalogue/test_get_most_checkouts_trends_titles.py new file mode 100644 index 0000000..c807b60 --- /dev/null +++ b/tests/api/catalogue/test_get_most_checkouts_trends_titles.py @@ -0,0 +1,230 @@ +from http import HTTPStatus +from typing import Any, Dict + +import httpx +import pytest +from tenacity import wait_none + +from nlb_catalogue_client.api.catalogue import get_get_most_checkouts_trends_titles +from nlb_catalogue_client.client import AuthenticatedClient +from nlb_catalogue_client.errors import UnexpectedStatus +from nlb_catalogue_client.models.search_most_checkouts_titles_response import SearchMostCheckoutsTitlesResponse + + +@pytest.fixture() +def client() -> AuthenticatedClient: + return AuthenticatedClient(base_url="https://api.example.com", token="test_token", raise_on_unexpected_status=True) + + +@pytest.fixture() +def success_response() -> Dict[str, Any]: + return { + "checkoutsTrends": [ + { + "title": "Sample Book Title", + "author": "John Doe", + "isbn": "9781234567890", + "brn": 123456, + "checkoutCount": 42, + } + ] + } + + +@pytest.fixture() +def error_responses() -> Dict[int, Dict[str, Any]]: + return { + 400: {"error": "Bad Request", "message": "Invalid request parameters", "statusCode": 400}, + 401: {"error": "Unauthorized", "message": "Authentication token is invalid", "statusCode": 401}, + 404: {"error": "Not Found", "message": "Resource not found", "statusCode": 404}, + 405: {"error": "Method Not Allowed", "message": "HTTP method not allowed", "statusCode": 405}, + 429: {"error": "Too Many Requests", "message": "Rate limit exceeded", "statusCode": 429}, + 500: {"error": "Internal Server Error", "message": "An unexpected error occurred", "statusCode": 500}, + 503: {"error": "Service Unavailable", "message": "Service is temporarily unavailable", "statusCode": 503}, + } + + +@pytest.fixture() +def mock_response(success_response) -> httpx.Response: + return httpx.Response( + status_code=200, + json=success_response, + ) + + +class TestGetMostCheckoutsTrendsTitles: + def test_get_kwargs(self): + kwargs = get_get_most_checkouts_trends_titles._get_kwargs( + location_code="AMKPL", + duration="past30days", + ) + + assert kwargs == { + "method": "get", + "url": "/GetMostCheckoutsTrendsTitles", + "params": { + "LocationCode": "AMKPL", + "Duration": "past30days", + }, + } + + def test_get_kwargs_with_defaults(self): + kwargs = get_get_most_checkouts_trends_titles._get_kwargs( + location_code="AMKPL", + ) + + assert kwargs == { + "method": "get", + "url": "/GetMostCheckoutsTrendsTitles", + "params": { + "LocationCode": "AMKPL", + "Duration": "past30days", + }, + } + + def test_sync_detailed_success(self, mocker, client: AuthenticatedClient, mock_response: httpx.Response): + mocker.patch("httpx.Client.request", return_value=mock_response) + + response = get_get_most_checkouts_trends_titles.sync_detailed(client=client, location_code="AMKPL") + + assert response.status_code == HTTPStatus.OK + assert isinstance(response.parsed, SearchMostCheckoutsTitlesResponse) + assert response.parsed.checkouts_trends + assert len(response.parsed.checkouts_trends) == 1 + + def test_sync_success(self, mocker, client: AuthenticatedClient, mock_response: httpx.Response): + mocker.patch("httpx.Client.request", return_value=mock_response) + + response = get_get_most_checkouts_trends_titles.sync(client=client, location_code="AMKPL") + + assert isinstance(response, SearchMostCheckoutsTitlesResponse) + assert response.checkouts_trends + assert len(response.checkouts_trends) == 1 + + @pytest.mark.asyncio + async def test_asyncio_detailed_success(self, mocker, client: AuthenticatedClient, mock_response: httpx.Response): + mocker.patch("httpx.AsyncClient.request", return_value=mock_response) + + response = await get_get_most_checkouts_trends_titles.asyncio_detailed(client=client, location_code="AMKPL") + + assert response.status_code == HTTPStatus.OK + assert isinstance(response.parsed, SearchMostCheckoutsTitlesResponse) + assert response.parsed.checkouts_trends + assert len(response.parsed.checkouts_trends) == 1 + + @pytest.mark.asyncio + async def test_asyncio_success(self, mocker, client: AuthenticatedClient, mock_response: httpx.Response): + mocker.patch("httpx.AsyncClient.request", return_value=mock_response) + + response = await get_get_most_checkouts_trends_titles.asyncio(client=client, location_code="AMKPL") + + assert isinstance(response, SearchMostCheckoutsTitlesResponse) + assert response.checkouts_trends + assert len(response.checkouts_trends) == 1 + + @pytest.mark.parametrize( + "status_code,error_type", + [ + (400, "BadRequestError"), + (401, "UnauthorizedError"), + (404, "NotFoundError"), + (405, "MethodNotAllowedError"), + (429, "TooManyRequestsError"), + (500, "InternalServerError"), + (503, "ServiceUnavailableError"), + ], + ) + def test_error_responses_sync( + self, + mocker, + client: AuthenticatedClient, + error_responses: Dict[int, Dict[str, Any]], + status_code: int, + error_type: str, + ): + error_mock_response = httpx.Response( + status_code=status_code, + json=error_responses[status_code], + ) + mocker.patch("httpx.Client.request", return_value=error_mock_response) + + # Tenacity to stop waiting + get_get_most_checkouts_trends_titles.sync_detailed.retry.wait = wait_none() + + response = get_get_most_checkouts_trends_titles.sync_detailed(client=client, location_code="AMKPL") + + assert response.status_code == status_code + assert error_type in str(type(response.parsed)) + assert response.parsed + assert not isinstance(response.parsed, SearchMostCheckoutsTitlesResponse) + assert response.parsed.error == error_responses[status_code]["error"] + assert response.parsed.message == error_responses[status_code]["message"] + assert response.parsed.status_code == status_code + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "status_code,error_type", + [ + (400, "BadRequestError"), + (401, "UnauthorizedError"), + (404, "NotFoundError"), + (405, "MethodNotAllowedError"), + (429, "TooManyRequestsError"), + (500, "InternalServerError"), + (503, "ServiceUnavailableError"), + ], + ) + async def test_error_responses_async( + self, + mocker, + client: AuthenticatedClient, + error_responses: Dict[int, Dict[str, Any]], + status_code: int, + error_type: str, + ): + error_mock_response = httpx.Response( + status_code=status_code, + json=error_responses[status_code], + ) + mocker.patch("httpx.AsyncClient.request", return_value=error_mock_response) + + # Tenacity to stop waiting + get_get_most_checkouts_trends_titles.asyncio_detailed.retry.wait = wait_none() + + response = await get_get_most_checkouts_trends_titles.asyncio_detailed(client=client, location_code="AMKPL") + + assert response.status_code == status_code + assert error_type in str(type(response.parsed)) + assert response.parsed + assert not isinstance(response.parsed, SearchMostCheckoutsTitlesResponse) + assert response.parsed.error == error_responses[status_code]["error"] + assert response.parsed.message == error_responses[status_code]["message"] + assert response.parsed.status_code == status_code + + def test_sync_detailed_unexpected_status_raises( + self, + mocker, + client: AuthenticatedClient, + ): + unexpected_response = httpx.Response( + status_code=418, # I'm a teapot - unexpected status + json={"error": "Unexpected", "message": "I'm a teapot", "statusCode": 418}, + ) + mocker.patch("httpx.Client.request", return_value=unexpected_response) + + with pytest.raises(UnexpectedStatus): + get_get_most_checkouts_trends_titles.sync_detailed(client=client, location_code="AMKPL") + + def test_sync_detailed_unexpected_status_return_none( + self, + mocker, + client: AuthenticatedClient, + ): + unexpected_response = httpx.Response( + status_code=418, # I'm a teapot - unexpected status + json={"error": "Unexpected", "message": "I'm a teapot", "statusCode": 418}, + ) + mocker.patch("httpx.Client.request", return_value=unexpected_response) + client.raise_on_unexpected_status = False + + assert get_get_most_checkouts_trends_titles.sync_detailed(client=client, location_code="AMKPL").parsed is None diff --git a/tests/api/catalogue/test_get_new_titles.py b/tests/api/catalogue/test_get_new_titles.py new file mode 100644 index 0000000..1ee153b --- /dev/null +++ b/tests/api/catalogue/test_get_new_titles.py @@ -0,0 +1,277 @@ +from http import HTTPStatus +from typing import Any, Dict + +import httpx +import pytest +from tenacity import wait_none + +from nlb_catalogue_client.api.catalogue import get_get_new_titles +from nlb_catalogue_client.client import AuthenticatedClient +from nlb_catalogue_client.errors import UnexpectedStatus +from nlb_catalogue_client.models.search_new_titles_response_v2 import SearchNewTitlesResponseV2 + + +@pytest.fixture() +def client() -> AuthenticatedClient: + return AuthenticatedClient(base_url="https://api.example.com", token="test_token", raise_on_unexpected_status=True) + + +@pytest.fixture() +def success_response() -> Dict[str, Any]: + return { + "totalRecords": 100, + "count": 20, + "nextRecordsOffset": 20, + "hasMoreRecords": True, + "titles": [ + { + "format": {"code": "BK", "name": "BOOK"}, + "brn": 123456, + "digitalId": None, + "title": "Sample Book Title", + "nativeTitle": None, + "author": "John Doe", + "nativeAuthor": None, + "otherAuthors": ["Jane Smith"], + "nativeOtherAuthors": None, + "isbns": ["9781234567890"], + "language": ["English"], + "allowReservation": True, + "isRestricted": False, + "activeReservationsCount": 0, + } + ], + } + + +@pytest.fixture() +def error_responses() -> Dict[int, Dict[str, Any]]: + return { + 400: {"error": "Bad Request", "message": "Invalid request parameters", "statusCode": 400}, + 401: {"error": "Unauthorized", "message": "Authentication token is invalid", "statusCode": 401}, + 404: {"error": "Not Found", "message": "Resource not found", "statusCode": 404}, + 405: {"error": "Method Not Allowed", "message": "HTTP method not allowed", "statusCode": 405}, + 429: {"error": "Too Many Requests", "message": "Rate limit exceeded", "statusCode": 429}, + 500: {"error": "Internal Server Error", "message": "An unexpected error occurred", "statusCode": 500}, + 503: {"error": "Service Unavailable", "message": "Service is temporarily unavailable", "statusCode": 503}, + } + + +@pytest.fixture() +def mock_response(success_response) -> httpx.Response: + return httpx.Response( + status_code=200, + json=success_response, + ) + + +class TestGetNewTitles: + def test_get_kwargs(self): + kwargs = get_get_new_titles._get_kwargs( + date_range="Monthly", + limit=50, + sort_fields="title", + set_id=1, + offset=20, + material_types=["BK", "DVD"], + intended_audiences=["ADULT", "TEEN"], + languages=["ENG", "CHI"], + availability=True, + fiction=True, + locations=["ABC"], + ) + + assert kwargs == { + "method": "get", + "url": "/GetNewTitles", + "params": { + "DateRange": "Monthly", + "Limit": 50, + "SortFields": "title", + "SetId": 1, + "Offset": 20, + "MaterialTypes": ["BK", "DVD"], + "IntendedAudiences": ["ADULT", "TEEN"], + "Languages": ["ENG", "CHI"], + "Availability": True, + "Fiction": True, + "Locations": ["ABC"], + }, + } + + def test_get_kwargs_with_defaults(self): + kwargs = get_get_new_titles._get_kwargs() + + assert kwargs == { + "method": "get", + "url": "/GetNewTitles", + "params": { + "DateRange": "Weekly", + "Limit": 200, + "SetId": 0, + "Offset": 0, + }, + } + + def test_sync_detailed_success(self, mocker, client: AuthenticatedClient, mock_response: httpx.Response): + mocker.patch("httpx.Client.request", return_value=mock_response) + + response = get_get_new_titles.sync_detailed(client=client) + + assert response.status_code == HTTPStatus.OK + assert isinstance(response.parsed, SearchNewTitlesResponseV2) + assert response.parsed.total_records == 100 + assert response.parsed.count == 20 + assert response.parsed.next_records_offset == 20 + assert response.parsed.has_more_records is True + assert len(response.parsed.titles) == 1 + assert response.parsed.titles[0].brn == 123456 + + def test_sync_success(self, mocker, client: AuthenticatedClient, mock_response: httpx.Response): + mocker.patch("httpx.Client.request", return_value=mock_response) + + response = get_get_new_titles.sync(client=client) + + assert isinstance(response, SearchNewTitlesResponseV2) + assert response.total_records == 100 + assert response.count == 20 + assert response.next_records_offset == 20 + assert response.has_more_records is True + assert len(response.titles) == 1 + assert response.titles[0].brn == 123456 + + @pytest.mark.asyncio + async def test_asyncio_detailed_success(self, mocker, client: AuthenticatedClient, mock_response: httpx.Response): + mocker.patch("httpx.AsyncClient.request", return_value=mock_response) + + response = await get_get_new_titles.asyncio_detailed(client=client) + + assert response.status_code == HTTPStatus.OK + assert isinstance(response.parsed, SearchNewTitlesResponseV2) + assert response.parsed.total_records == 100 + assert response.parsed.count == 20 + assert response.parsed.next_records_offset == 20 + assert response.parsed.has_more_records is True + assert len(response.parsed.titles) == 1 + assert response.parsed.titles[0].brn == 123456 + + @pytest.mark.asyncio + async def test_asyncio_success(self, mocker, client: AuthenticatedClient, mock_response: httpx.Response): + mocker.patch("httpx.AsyncClient.request", return_value=mock_response) + + response = await get_get_new_titles.asyncio(client=client) + + assert isinstance(response, SearchNewTitlesResponseV2) + assert response.total_records == 100 + assert response.count == 20 + assert response.next_records_offset == 20 + assert response.has_more_records is True + assert len(response.titles) == 1 + assert response.titles[0].brn == 123456 + + @pytest.mark.parametrize( + "status_code,error_type", + [ + (400, "BadRequestError"), + (401, "UnauthorizedError"), + (404, "NotFoundError"), + (405, "MethodNotAllowedError"), + (429, "TooManyRequestsError"), + (500, "InternalServerError"), + (503, "ServiceUnavailableError"), + ], + ) + def test_error_responses_sync( + self, + mocker, + client: AuthenticatedClient, + error_responses: Dict[int, Dict[str, Any]], + status_code: int, + error_type: str, + ): + error_mock_response = httpx.Response( + status_code=status_code, + json=error_responses[status_code], + ) + mocker.patch("httpx.Client.request", return_value=error_mock_response) + + # Tenacity to stop waiting + get_get_new_titles.sync_detailed.retry.wait = wait_none() + + response = get_get_new_titles.sync_detailed(client=client) + + assert response.status_code == status_code + assert error_type in str(type(response.parsed)) + assert response.parsed + assert not isinstance(response.parsed, SearchNewTitlesResponseV2) + assert response.parsed.error == error_responses[status_code]["error"] + assert response.parsed.message == error_responses[status_code]["message"] + assert response.parsed.status_code == status_code + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "status_code,error_type", + [ + (400, "BadRequestError"), + (401, "UnauthorizedError"), + (404, "NotFoundError"), + (405, "MethodNotAllowedError"), + (429, "TooManyRequestsError"), + (500, "InternalServerError"), + (503, "ServiceUnavailableError"), + ], + ) + async def test_error_responses_async( + self, + mocker, + client: AuthenticatedClient, + error_responses: Dict[int, Dict[str, Any]], + status_code: int, + error_type: str, + ): + error_mock_response = httpx.Response( + status_code=status_code, + json=error_responses[status_code], + ) + mocker.patch("httpx.AsyncClient.request", return_value=error_mock_response) + + # Tenacity to stop waiting + get_get_new_titles.asyncio_detailed.retry.wait = wait_none() + + response = await get_get_new_titles.asyncio_detailed(client=client) + + assert response.status_code == status_code + assert error_type in str(type(response.parsed)) + assert response.parsed + assert not isinstance(response.parsed, SearchNewTitlesResponseV2) + assert response.parsed.error == error_responses[status_code]["error"] + assert response.parsed.message == error_responses[status_code]["message"] + assert response.parsed.status_code == status_code + + def test_sync_detailed_unexpected_status_raises( + self, + mocker, + client: AuthenticatedClient, + ): + unexpected_response = httpx.Response( + status_code=418, # I'm a teapot - unexpected status + json={"error": "Unexpected", "message": "I'm a teapot", "statusCode": 418}, + ) + mocker.patch("httpx.Client.request", return_value=unexpected_response) + + with pytest.raises(UnexpectedStatus): + get_get_new_titles.sync_detailed(client=client) + + def test_sync_detailed_unexpected_status_return_none( + self, + mocker, + client: AuthenticatedClient, + ): + unexpected_response = httpx.Response( + status_code=418, # I'm a teapot - unexpected status + json={"error": "Unexpected", "message": "I'm a teapot", "statusCode": 418}, + ) + mocker.patch("httpx.Client.request", return_value=unexpected_response) + client.raise_on_unexpected_status = False + + assert get_get_new_titles.sync_detailed(client=client).parsed is None diff --git a/tests/api/catalogue/test_get_title_details.py b/tests/api/catalogue/test_get_title_details.py new file mode 100644 index 0000000..27360ac --- /dev/null +++ b/tests/api/catalogue/test_get_title_details.py @@ -0,0 +1,229 @@ +from http import HTTPStatus +from typing import Any, Dict + +import httpx +import pytest +from tenacity import wait_none + +from nlb_catalogue_client.api.catalogue import get_get_title_details +from nlb_catalogue_client.client import AuthenticatedClient +from nlb_catalogue_client.errors import UnexpectedStatus +from nlb_catalogue_client.models.get_title_details_response_v2 import GetTitleDetailsResponseV2 + + +@pytest.fixture() +def client() -> AuthenticatedClient: + return AuthenticatedClient(base_url="https://api.example.com", token="test_token", raise_on_unexpected_status=True) + + +@pytest.fixture() +def success_response() -> Dict[str, Any]: + return { + "format": {"code": "BK", "name": "BOOK"}, + "brn": 123456, + "title": "Sample Book Title", + "author": "John Doe", + "isbns": ["9781234567890"], + "publisher": ["Sample Publisher"], + "publish_date": "2023", + "language": ["English"], + "subjects": ["Fiction", "Literature"], + "physical_description": ["300 pages ; 21 cm"], + "allow_reservation": True, + "is_restricted": False, + "active_reservations_count": 0, + "availability": True, + "serial": False, + } + + +@pytest.fixture() +def error_responses() -> Dict[int, Dict[str, Any]]: + return { + 400: {"error": "Bad Request", "message": "Invalid request parameters", "statusCode": 400}, + 401: {"error": "Unauthorized", "message": "Authentication token is invalid", "statusCode": 401}, + 404: {"error": "Not Found", "message": "Resource not found", "statusCode": 404}, + 405: {"error": "Method Not Allowed", "message": "HTTP method not allowed", "statusCode": 405}, + 429: {"error": "Too Many Requests", "message": "Rate limit exceeded", "statusCode": 429}, + 500: {"error": "Internal Server Error", "message": "An unexpected error occurred", "statusCode": 500}, + 503: {"error": "Service Unavailable", "message": "Service is temporarily unavailable", "statusCode": 503}, + } + + +@pytest.fixture() +def mock_response(success_response) -> httpx.Response: + return httpx.Response( + status_code=200, + json=success_response, + ) + + +class TestGetTitleDetails: + def test_get_kwargs_with_brn(self): + kwargs = get_get_title_details._get_kwargs(brn=123456) + + assert kwargs == { + "method": "get", + "url": "/GetTitleDetails", + "params": {"BRN": 123456}, + } + + def test_get_kwargs_with_isbn(self): + kwargs = get_get_title_details._get_kwargs(isbn="9781234567890") + + assert kwargs == { + "method": "get", + "url": "/GetTitleDetails", + "params": {"ISBN": "9781234567890"}, + } + + def test_get_kwargs_with_both_params(self): + kwargs = get_get_title_details._get_kwargs(brn=123456, isbn="9781234567890") + + assert kwargs == { + "method": "get", + "url": "/GetTitleDetails", + "params": {"BRN": 123456, "ISBN": "9781234567890"}, + } + + def test_sync_detailed_success(self, mocker, client: AuthenticatedClient, mock_response: httpx.Response): + mocker.patch("httpx.Client.request", return_value=mock_response) + + response = get_get_title_details.sync_detailed(client=client, brn=123456) + + assert response.status_code == HTTPStatus.OK + assert isinstance(response.parsed, GetTitleDetailsResponseV2) + assert response.parsed.brn == 123456 + assert response.parsed.title == "Sample Book Title" + assert response.parsed.author == "John Doe" + assert response.parsed.isbns == ["9781234567890"] + + def test_sync_success(self, mocker, client: AuthenticatedClient, mock_response: httpx.Response): + mocker.patch("httpx.Client.request", return_value=mock_response) + + response = get_get_title_details.sync(client=client, brn=123456) + + assert isinstance(response, GetTitleDetailsResponseV2) + assert response.brn == 123456 + assert response.title == "Sample Book Title" + assert response.author == "John Doe" + assert response.isbns == ["9781234567890"] + + @pytest.mark.asyncio + async def test_asyncio_success(self, mocker, client: AuthenticatedClient, mock_response: httpx.Response): + mocker.patch("httpx.AsyncClient.request", return_value=mock_response) + + response = await get_get_title_details.asyncio(client=client, brn=123456) + + assert isinstance(response, GetTitleDetailsResponseV2) + assert response.brn == 123456 + assert response.title == "Sample Book Title" + assert response.author == "John Doe" + assert response.isbns == ["9781234567890"] + + @pytest.mark.parametrize( + "status_code,error_type", + [ + (400, "BadRequestError"), + (401, "UnauthorizedError"), + (404, "NotFoundError"), + (405, "MethodNotAllowedError"), + (429, "TooManyRequestsError"), + (500, "InternalServerError"), + (503, "ServiceUnavailableError"), + ], + ) + def test_error_responses_sync( + self, + mocker, + client: AuthenticatedClient, + error_responses: Dict[int, Dict[str, Any]], + status_code: int, + error_type: str, + ): + error_mock_response = httpx.Response( + status_code=status_code, + json=error_responses[status_code], + ) + mocker.patch("httpx.Client.request", return_value=error_mock_response) + + # Tenacity to stop waiting + get_get_title_details.sync_detailed.retry.wait = wait_none() + + response = get_get_title_details.sync_detailed(client=client, brn=123456) + + assert response.status_code == status_code + assert error_type in str(type(response.parsed)) + assert response.parsed + assert not isinstance(response.parsed, GetTitleDetailsResponseV2) + assert response.parsed.error == error_responses[status_code]["error"] + assert response.parsed.message == error_responses[status_code]["message"] + assert response.parsed.status_code == status_code + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "status_code,error_type", + [ + (400, "BadRequestError"), + (401, "UnauthorizedError"), + (404, "NotFoundError"), + (405, "MethodNotAllowedError"), + (429, "TooManyRequestsError"), + (500, "InternalServerError"), + (503, "ServiceUnavailableError"), + ], + ) + async def test_error_responses_async( + self, + mocker, + client: AuthenticatedClient, + error_responses: Dict[int, Dict[str, Any]], + status_code: int, + error_type: str, + ): + error_mock_response = httpx.Response( + status_code=status_code, + json=error_responses[status_code], + ) + mocker.patch("httpx.AsyncClient.request", return_value=error_mock_response) + + # Tenacity to stop waiting + get_get_title_details.asyncio_detailed.retry.wait = wait_none() + + response = await get_get_title_details.asyncio_detailed(client=client, brn=123456) + + assert response.status_code == status_code + assert error_type in str(type(response.parsed)) + assert response.parsed + assert not isinstance(response.parsed, GetTitleDetailsResponseV2) + assert response.parsed.error == error_responses[status_code]["error"] + assert response.parsed.message == error_responses[status_code]["message"] + assert response.parsed.status_code == status_code + + def test_sync_detailed_unexpected_status_raises( + self, + mocker, + client: AuthenticatedClient, + ): + unexpected_response = httpx.Response( + status_code=418, # I'm a teapot - unexpected status + json={"error": "Unexpected", "message": "I'm a teapot", "statusCode": 418}, + ) + mocker.patch("httpx.Client.request", return_value=unexpected_response) + + with pytest.raises(UnexpectedStatus): + get_get_title_details.sync_detailed(client=client, brn=123456) + + def test_sync_detailed_unexpected_status_return_none( + self, + mocker, + client: AuthenticatedClient, + ): + unexpected_response = httpx.Response( + status_code=418, # I'm a teapot - unexpected status + json={"error": "Unexpected", "message": "I'm a teapot", "statusCode": 418}, + ) + mocker.patch("httpx.Client.request", return_value=unexpected_response) + client.raise_on_unexpected_status = False + + assert get_get_title_details.sync_detailed(client=client, brn=123456).parsed is None diff --git a/tests/api/catalogue/test_get_titles.py b/tests/api/catalogue/test_get_titles.py new file mode 100644 index 0000000..b02d805 --- /dev/null +++ b/tests/api/catalogue/test_get_titles.py @@ -0,0 +1,216 @@ +from http import HTTPStatus +from typing import Any, Dict + +import httpx +import pytest +from tenacity import wait_none + +from nlb_catalogue_client.api.catalogue import get_get_titles +from nlb_catalogue_client.client import AuthenticatedClient +from nlb_catalogue_client.errors import UnexpectedStatus +from nlb_catalogue_client.models.get_titles_response_v2 import GetTitlesResponseV2 + + +@pytest.fixture() +def client() -> AuthenticatedClient: + return AuthenticatedClient(base_url="https://api.example.com", token="test_token", raise_on_unexpected_status=True) + + +@pytest.fixture() +def success_response() -> Dict[str, Any]: + return { + "totalRecords": 100, + "count": 20, + "hasMoreRecords": True, + "nextRecordsOffset": 20, + "setId": 12345, + "titles": [ + { + "format": {"code": "BK", "name": "BOOK"}, + "brn": 123456, + "title": "Sample Book Title", + "author": "John Doe", + "isbns": ["9781234567890"], + "publisher": ["Sample Publisher"], + "publish_date": "2023", + "language": ["English"], + "subjects": ["Fiction", "Literature"], + } + ], + } + + +@pytest.fixture() +def error_responses() -> Dict[int, Dict[str, Any]]: + return { + 400: {"error": "Bad Request", "message": "Invalid request parameters", "statusCode": 400}, + 401: {"error": "Unauthorized", "message": "Authentication token is invalid", "statusCode": 401}, + 404: {"error": "Not Found", "message": "Resource not found", "statusCode": 404}, + 405: {"error": "Method Not Allowed", "message": "HTTP method not allowed", "statusCode": 405}, + 429: {"error": "Too Many Requests", "message": "Rate limit exceeded", "statusCode": 429}, + 500: {"error": "Internal Server Error", "message": "An unexpected error occurred", "statusCode": 500}, + 503: {"error": "Service Unavailable", "message": "Service is temporarily unavailable", "statusCode": 503}, + } + + +class TestGetTitles: + def test_get_kwargs_with_keywords(self): + kwargs = get_get_titles._get_kwargs(keywords="python programming") + + assert kwargs == { + "method": "get", + "url": "/GetTitles", + "params": {"Keywords": "python programming", "Limit": 20, "SetId": 0, "Offset": 0}, + } + + def test_get_kwargs_with_all_params(self): + kwargs = get_get_titles._get_kwargs( + keywords="python", + title="Python Programming", + author="John Smith", + subject="Computer Science", + isbn="9781234567890", + limit=50, + sort_fields="title", + set_id=1, + offset=20, + ) + + assert kwargs == { + "method": "get", + "url": "/GetTitles", + "params": { + "Keywords": "python", + "Title": "Python Programming", + "Author": "John Smith", + "Subject": "Computer Science", + "ISBN": "9781234567890", + "Limit": 50, + "SortFields": "title", + "SetId": 1, + "Offset": 20, + }, + } + + def test_sync_success(self, mocker, client: AuthenticatedClient, success_response: Dict[str, Any]): + mock_response = httpx.Response( + status_code=200, + json=success_response, + ) + mocker.patch("httpx.Client.request", return_value=mock_response) + + response = get_get_titles.sync(client=client, keywords="python") + + assert isinstance(response, GetTitlesResponseV2) + assert response.total_records == 100 + assert response.count == 20 + assert response.has_more_records is True + assert response.next_records_offset == 20 + assert response.set_id == 12345 + assert response.titles + assert len(response.titles) == 1 + + def test_sync_detailed_success(self, mocker, client: AuthenticatedClient, success_response: Dict[str, Any]): + mock_response = httpx.Response( + status_code=200, + json=success_response, + ) + mocker.patch("httpx.Client.request", return_value=mock_response) + + response = get_get_titles.sync_detailed(client=client, keywords="python") + + assert response.status_code == HTTPStatus.OK + assert isinstance(response.parsed, GetTitlesResponseV2) + assert response.parsed.total_records == 100 + assert response.parsed.count == 20 + assert response.parsed.has_more_records is True + assert response.parsed.next_records_offset == 20 + assert response.parsed.set_id == 12345 + assert response.parsed.titles + assert len(response.parsed.titles) == 1 + + @pytest.mark.asyncio + async def test_asyncio_success(self, mocker, client: AuthenticatedClient, success_response: Dict[str, Any]): + mock_response = httpx.Response( + status_code=200, + json=success_response, + ) + mocker.patch("httpx.AsyncClient.request", return_value=mock_response) + + response = await get_get_titles.asyncio(client=client, keywords="python") + + assert isinstance(response, GetTitlesResponseV2) + assert response.total_records == 100 + assert response.count == 20 + assert response.has_more_records is True + assert response.next_records_offset == 20 + assert response.set_id == 12345 + assert response.titles + assert len(response.titles) == 1 + + @pytest.mark.parametrize( + "status_code,error_type", + [ + (400, "BadRequestError"), + (401, "UnauthorizedError"), + (404, "NotFoundError"), + (405, "MethodNotAllowedError"), + (429, "TooManyRequestsError"), + (500, "InternalServerError"), + (503, "ServiceUnavailableError"), + ], + ) + def test_error_responses_sync( + self, + mocker, + client: AuthenticatedClient, + error_responses: Dict[int, Dict[str, Any]], + status_code: int, + error_type: str, + ): + error_mock_response = httpx.Response( + status_code=status_code, + json=error_responses[status_code], + ) + mocker.patch("httpx.Client.request", return_value=error_mock_response) + + # Tenacity to stop waiting + get_get_titles.sync_detailed.retry.wait = wait_none() + + response = get_get_titles.sync_detailed(client=client, keywords="python") + + assert response.status_code == status_code + assert error_type in str(type(response.parsed)) + assert response.parsed + assert not isinstance(response.parsed, GetTitlesResponseV2) + assert response.parsed.error == error_responses[status_code]["error"] + assert response.parsed.message == error_responses[status_code]["message"] + assert response.parsed.status_code == status_code + + def test_sync_detailed_unexpected_status_raises( + self, + mocker, + client: AuthenticatedClient, + ): + unexpected_response = httpx.Response( + status_code=418, # I'm a teapot - unexpected status + json={"error": "Unexpected", "message": "I'm a teapot", "statusCode": 418}, + ) + mocker.patch("httpx.Client.request", return_value=unexpected_response) + + with pytest.raises(UnexpectedStatus): + get_get_titles.sync_detailed(client=client, keywords="python") + + def test_sync_detailed_unexpected_status_return_none( + self, + mocker, + client: AuthenticatedClient, + ): + unexpected_response = httpx.Response( + status_code=418, # I'm a teapot - unexpected status + json={"error": "Unexpected", "message": "I'm a teapot", "statusCode": 418}, + ) + mocker.patch("httpx.Client.request", return_value=unexpected_response) + client.raise_on_unexpected_status = False + + assert get_get_titles.sync_detailed(client=client, keywords="python").parsed is None diff --git a/tests/api/catalogue/test_search_titles.py b/tests/api/catalogue/test_search_titles.py new file mode 100644 index 0000000..b7f671c --- /dev/null +++ b/tests/api/catalogue/test_search_titles.py @@ -0,0 +1,227 @@ +from http import HTTPStatus +from typing import Any, Dict + +import httpx +import pytest +from tenacity import wait_none + +from nlb_catalogue_client.api.catalogue import get_search_titles +from nlb_catalogue_client.client import AuthenticatedClient +from nlb_catalogue_client.errors import UnexpectedStatus +from nlb_catalogue_client.models.search_titles_response_v2 import SearchTitlesResponseV2 + + +@pytest.fixture() +def client() -> AuthenticatedClient: + return AuthenticatedClient(base_url="https://api.example.com", token="test_token", raise_on_unexpected_status=True) + + +@pytest.fixture() +def success_response() -> Dict[str, Any]: + return { + "totalRecords": 100, + "count": 20, + "hasMoreRecords": True, + "nextRecordsOffset": 20, + "titles": [ + { + "format": {"code": "BK", "name": "BOOK"}, + "brn": 123456, + "title": "Sample Book Title", + "author": "John Doe", + "isbns": ["9781234567890"], + "publisher": ["Sample Publisher"], + "publish_date": "2023", + "language": ["English"], + "subjects": ["Fiction", "Literature"], + } + ], + "facets": [{"name": "Format", "values": [{"value": "Book", "count": 80}, {"value": "eBook", "count": 20}]}], + } + + +@pytest.fixture() +def error_responses() -> Dict[int, Dict[str, Any]]: + return { + 400: {"error": "Bad Request", "message": "Invalid request parameters", "statusCode": 400}, + 401: {"error": "Unauthorized", "message": "Authentication token is invalid", "statusCode": 401}, + 404: {"error": "Not Found", "message": "Resource not found", "statusCode": 404}, + 405: {"error": "Method Not Allowed", "message": "HTTP method not allowed", "statusCode": 405}, + 429: {"error": "Too Many Requests", "message": "Rate limit exceeded", "statusCode": 429}, + 500: {"error": "Internal Server Error", "message": "An unexpected error occurred", "statusCode": 500}, + 501: {"error": "Not Implemented", "message": "Feature not implemented", "statusCode": 501}, + 503: {"error": "Service Unavailable", "message": "Service is temporarily unavailable", "statusCode": 503}, + } + + +class TestSearchTitles: + def test_get_kwargs_with_required_params(self): + kwargs = get_search_titles._get_kwargs(keywords="python programming") + + assert kwargs == { + "method": "get", + "url": "/SearchTitles", + "params": {"Keywords": "python programming", "Limit": 20, "Offset": 0}, + } + + def test_get_kwargs_with_all_params(self): + kwargs = get_search_titles._get_kwargs( + keywords="python", + source="overdrive", + limit=50, + sort_fields="title", + offset=20, + material_types=["BK", "DVD"], + intended_audiences=["ADULT", "TEEN"], + date_from=20230101, + date_to=20231231, + locations=["AMKPL", "BIPL"], + languages=["ENG", "CHI"], + availability=True, + fiction=True, + ) + + assert kwargs == { + "method": "get", + "url": "/SearchTitles", + "params": { + "Keywords": "python", + "Source": "overdrive", + "Limit": 50, + "SortFields": "title", + "Offset": 20, + "MaterialTypes": ["BK", "DVD"], + "IntendedAudiences": ["ADULT", "TEEN"], + "DateFrom": 20230101, + "DateTo": 20231231, + "Locations": ["AMKPL", "BIPL"], + "Languages": ["ENG", "CHI"], + "Availability": True, + "Fiction": True, + }, + } + + def test_sync_detailed_success(self, mocker, client: AuthenticatedClient, success_response: Dict[str, Any]): + mock_response = httpx.Response( + status_code=200, + json=success_response, + ) + mocker.patch("httpx.Client.request", return_value=mock_response) + + response = get_search_titles.sync_detailed(client=client, keywords="python") + + assert response.status_code == HTTPStatus.OK + assert isinstance(response.parsed, SearchTitlesResponseV2) + assert response.parsed.total_records == 100 + assert response.parsed.count == 20 + assert response.parsed.has_more_records is True + assert response.parsed.next_records_offset == 20 + assert response.parsed.titles + assert len(response.parsed.titles) == 1 + assert response.parsed.facets + assert len(response.parsed.facets) == 1 + + def test_sync_success(self, mocker, client: AuthenticatedClient, success_response: Dict[str, Any]): + mock_response = httpx.Response( + status_code=200, + json=success_response, + ) + mocker.patch("httpx.Client.request", return_value=mock_response) + + response = get_search_titles.sync(client=client, keywords="python") + + assert isinstance(response, SearchTitlesResponseV2) + assert response.total_records == 100 + assert response.count == 20 + assert response.has_more_records is True + assert response.next_records_offset == 20 + assert response.titles + assert len(response.titles) == 1 + assert response.facets + assert len(response.facets) == 1 + + @pytest.mark.asyncio + async def test_asyncio_success(self, mocker, client: AuthenticatedClient, success_response: Dict[str, Any]): + mock_response = httpx.Response( + status_code=200, + json=success_response, + ) + mocker.patch("httpx.AsyncClient.request", return_value=mock_response) + + response = await get_search_titles.asyncio(client=client, keywords="python") + + assert isinstance(response, SearchTitlesResponseV2) + assert response.total_records == 100 + assert response.count == 20 + assert response.has_more_records is True + assert response.next_records_offset == 20 + assert len(response.titles) == 1 + assert len(response.facets) == 1 + + @pytest.mark.parametrize( + "status_code,error_type", + [ + (400, "BadRequestError"), + (401, "UnauthorizedError"), + (404, "NotFoundError"), + (405, "MethodNotAllowedError"), + (429, "TooManyRequestsError"), + (500, "InternalServerError"), + (501, "NotImplementedError_"), + (503, "ServiceUnavailableError"), + ], + ) + def test_error_responses_sync( + self, + mocker, + client: AuthenticatedClient, + error_responses: Dict[int, Dict[str, Any]], + status_code: int, + error_type: str, + ): + error_mock_response = httpx.Response( + status_code=status_code, + json=error_responses[status_code], + ) + mocker.patch("httpx.Client.request", return_value=error_mock_response) + + # Tenacity to stop waiting + get_search_titles.sync_detailed.retry.wait = wait_none() + + response = get_search_titles.sync_detailed(client=client, keywords="python") + + assert response.status_code == status_code + assert error_type in str(type(response.parsed)) + assert response.parsed + assert not isinstance(response.parsed, SearchTitlesResponseV2) + assert response.parsed.error == error_responses[status_code]["error"] + assert response.parsed.message == error_responses[status_code]["message"] + assert response.parsed.status_code == status_code + + def test_sync_detailed_unexpected_status_raises( + self, + mocker, + client: AuthenticatedClient, + ): + unexpected_response = httpx.Response( + status_code=418, # I'm a teapot - unexpected status + json={"error": "Unexpected", "message": "I'm a teapot", "statusCode": 418}, + ) + mocker.patch("httpx.Client.request", return_value=unexpected_response) + + with pytest.raises(UnexpectedStatus): + get_search_titles.sync_detailed(client=client, keywords="python") + + def test_sync_detailed_unexpected_status_return_none( + self, + mocker, + client: AuthenticatedClient, + ): + unexpected_response = httpx.Response( + status_code=418, # I'm a teapot - unexpected status + json={"error": "Unexpected", "message": "I'm a teapot", "statusCode": 418}, + ) + mocker.patch("httpx.Client.request", return_value=unexpected_response) + client.raise_on_unexpected_status = False + + assert get_search_titles.sync_detailed(client=client, keywords="python").parsed is None diff --git a/tests/models/test_bad_request_error.py b/tests/models/test_bad_request_error.py new file mode 100644 index 0000000..6f2dc9d --- /dev/null +++ b/tests/models/test_bad_request_error.py @@ -0,0 +1,69 @@ +import pytest + +from nlb_catalogue_client.models.bad_request_error import BadRequestError +from nlb_catalogue_client.types import UNSET + + +@pytest.fixture() +def error_with_status() -> tuple[BadRequestError, dict]: + return BadRequestError(error="Bad Request", message="Field is missing", status_code=400), { + "error": "Bad Request", + "message": "Field is missing", + "statusCode": 400, + } + + +@pytest.fixture() +def error_without_status() -> tuple[BadRequestError, dict]: + return BadRequestError(error="Bad Request", message="Field is missing"), { + "error": "Bad Request", + "message": "Field is missing", + } + + +class TestBadRequestError: + @pytest.mark.parametrize( + "error,message,status_code,expected_status", + [ + ("Bad Request", "Field is missing", 400, 400), + ("Bad Request", "Field is missing", None, None), + ("Bad Request", "Field is missing", UNSET, UNSET), + ], + ) + def test_basic_initialization(self, error, message, status_code, expected_status): + error_obj = BadRequestError(error=error, message=message, status_code=status_code) + assert error_obj.error == error + assert error_obj.message == message + assert error_obj.status_code == expected_status + + def test_to_dict_with_status(self, error_with_status): + assert error_with_status[0].to_dict() == error_with_status[1] + + def test_to_dict_without_status(self, error_without_status): + assert error_without_status[0].to_dict() == error_without_status[1] + + def test_from_dict_with_status(self, error_with_status): + assert BadRequestError.from_dict(error_with_status[1]) == error_with_status[0] + + def test_from_dict_without_status(self, error_without_status): + assert BadRequestError.from_dict(error_without_status[1]) == error_without_status[0] + + @pytest.mark.parametrize( + "input_data", + [ + {"error": "Bad Request", "message": "Field is missing", "statusCode": 400}, + {"error": "Bad Request", "message": "Field is missing"}, + ], + ) + def test_dict_roundtrip(self, input_data): + """Test that to_dict and from_dict are inverse operations""" + original = BadRequestError.from_dict(input_data) + dict_form = original.to_dict() + recreated = BadRequestError.from_dict(dict_form) + assert original == recreated + + def test_from_dict_preserves_original(self, error_with_status): + """Test that from_dict doesn't modify the input dictionary""" + input_dict = error_with_status[1].copy() + BadRequestError.from_dict(input_dict) + assert input_dict == error_with_status[1] diff --git a/tests/models/test_bib_format.py b/tests/models/test_bib_format.py new file mode 100644 index 0000000..96e84de --- /dev/null +++ b/tests/models/test_bib_format.py @@ -0,0 +1,29 @@ +import pytest + +from nlb_catalogue_client.models.bib_format import BibFormat + + +@pytest.fixture() +def bib_format_full() -> tuple[BibFormat, dict]: + return BibFormat(code="BK", name="BOOKS"), {"code": "BK", "name": "BOOKS"} + + +class TestBibFormat: + @pytest.mark.parametrize( + "code,name", + [ + ("BK", "BOOKS"), + ("SR", "SERIALS"), + ("", ""), + ], + ) + def test_basic_initialization(self, code, name): + bib_format = BibFormat(code=code, name=name) + assert bib_format.code == code + assert bib_format.name == name + + def test_to_dict_full(self, bib_format_full): + assert bib_format_full[0].to_dict() == bib_format_full[1] + + def test_from_dict_full(self, bib_format_full): + assert BibFormat.from_dict(bib_format_full[1]) == bib_format_full[0] diff --git a/tests/models/test_book_cover.py b/tests/models/test_book_cover.py new file mode 100644 index 0000000..ac3f58c --- /dev/null +++ b/tests/models/test_book_cover.py @@ -0,0 +1,88 @@ +import pytest + +from nlb_catalogue_client.models.book_cover import BookCover +from nlb_catalogue_client.types import UNSET + + +@pytest.fixture() +def book_cover_full() -> tuple[BookCover, dict]: + return BookCover( + small="https://example.com/small.jpg", + medium="https://example.com/medium.jpg", + large="https://example.com/large.jpg", + ), { + "small": "https://example.com/small.jpg", + "medium": "https://example.com/medium.jpg", + "large": "https://example.com/large.jpg", + } + + +@pytest.fixture() +def book_cover_required_only(): + return BookCover(), {} + + +@pytest.fixture() +def book_cover_with_none(): + return BookCover(small=None, medium=None, large=None), {"small": None, "medium": None, "large": None} + + +class TestBookCover: + @pytest.mark.parametrize( + "small,medium,large", + [ + ( + "https://example.com/small.jpg", + "https://example.com/medium.jpg", + "https://example.com/large.jpg", + ), + (None, None, None), + (UNSET, UNSET, UNSET), + ], + ) + def test_basic_initialization(self, small, medium, large): + book_cover = BookCover(small=small, medium=medium, large=large) + assert book_cover.small == small + assert book_cover.medium == medium + assert book_cover.large == large + + def test_to_dict_full(self, book_cover_full): + assert book_cover_full[0].to_dict() == book_cover_full[1] + + def test_to_dict_required_only(self, book_cover_required_only): + assert book_cover_required_only[0].to_dict() == book_cover_required_only[1] + + def test_to_dict_with_none(self, book_cover_with_none): + assert book_cover_with_none[0].to_dict() == book_cover_with_none[1] + + def test_from_dict_full(self, book_cover_full): + assert BookCover.from_dict(book_cover_full[1]) == book_cover_full[0] + + def test_from_dict_required_only(self, book_cover_required_only): + assert BookCover.from_dict(book_cover_required_only[1]) == book_cover_required_only[0] + + def test_from_dict_with_none(self, book_cover_with_none): + assert BookCover.from_dict(book_cover_with_none[1]) == book_cover_with_none[0] + + @pytest.mark.parametrize( + "input_data,expected_small,expected_medium,expected_large", + [ + ( + {"small": "https://example.com/small.jpg"}, + "https://example.com/small.jpg", + UNSET, + UNSET, + ), + ( + {"small": UNSET, "medium": UNSET, "large": UNSET}, + UNSET, + UNSET, + UNSET, + ), + ], + ) + def test_from_dict_edge_cases(self, input_data, expected_small, expected_medium, expected_large): + book_cover = BookCover.from_dict(input_data) + assert book_cover.small == expected_small + assert book_cover.medium == expected_medium + assert book_cover.large == expected_large diff --git a/tests/models/test_checkouts_title.py b/tests/models/test_checkouts_title.py new file mode 100644 index 0000000..3b20684 --- /dev/null +++ b/tests/models/test_checkouts_title.py @@ -0,0 +1,99 @@ +import pytest + +from nlb_catalogue_client.models.checkouts_title import CheckoutsTitle +from nlb_catalogue_client.types import UNSET + + +@pytest.fixture() +def checkouts_title_full() -> tuple[CheckoutsTitle, dict]: + return CheckoutsTitle( + title="Test Book", + native_title="测试书", + author="John Doe", + native_author="约翰", + isbns=["1234567890"], + checkouts_count=5, + ), { + "title": "Test Book", + "nativeTitle": "测试书", + "author": "John Doe", + "nativeAuthor": "约翰", + "isbns": ["1234567890"], + "checkoutsCount": 5, + } + + +@pytest.fixture() +def checkouts_title_required_only(): + return CheckoutsTitle(), {} + + +@pytest.fixture() +def checkouts_title_with_none(): + return CheckoutsTitle( + title=None, native_title=None, author=None, native_author=None, isbns=None, checkouts_count=0 + ), {"title": None, "nativeTitle": None, "author": None, "nativeAuthor": None, "isbns": None, "checkoutsCount": 0} + + +class TestCheckoutsTitle: + @pytest.mark.parametrize( + "title,native_title,author,native_author,isbns,checkouts_count", + [ + ("Test Book", "测试书", "John Doe", "约翰", ["1234567890"], 5), + (None, None, None, None, None, 0), + (UNSET, UNSET, UNSET, UNSET, UNSET, UNSET), + ], + ) + def test_basic_initialization(self, title, native_title, author, native_author, isbns, checkouts_count): + checkout = CheckoutsTitle( + title=title, + native_title=native_title, + author=author, + native_author=native_author, + isbns=isbns, + checkouts_count=checkouts_count, + ) + assert checkout.title == title + assert checkout.native_title == native_title + assert checkout.author == author + assert checkout.native_author == native_author + assert checkout.isbns == isbns + assert checkout.checkouts_count == checkouts_count + + def test_to_dict_full(self, checkouts_title_full): + assert checkouts_title_full[0].to_dict() == checkouts_title_full[1] + + def test_to_dict_required_only(self, checkouts_title_required_only): + assert checkouts_title_required_only[0].to_dict() == checkouts_title_required_only[1] + + def test_to_dict_with_none(self, checkouts_title_with_none): + assert checkouts_title_with_none[0].to_dict() == checkouts_title_with_none[1] + + def test_from_dict_full(self, checkouts_title_full): + assert CheckoutsTitle.from_dict(checkouts_title_full[1]) == checkouts_title_full[0] + + def test_from_dict_required_only(self, checkouts_title_required_only): + assert CheckoutsTitle.from_dict(checkouts_title_required_only[1]) == checkouts_title_required_only[0] + + def test_from_dict_with_none(self, checkouts_title_with_none): + assert CheckoutsTitle.from_dict(checkouts_title_with_none[1]) == checkouts_title_with_none[0] + + @pytest.mark.parametrize( + "input_data,expected_title,expected_isbns", + [ + ( + {"title": "Test Book", "checkoutsCount": 5}, + "Test Book", + UNSET, + ), + ( + {"title": None, "isbns": 123}, + None, + 123, + ), + ], + ) + def test_from_dict_edge_cases(self, input_data, expected_title, expected_isbns): + checkout = CheckoutsTitle.from_dict(input_data) + assert checkout.title == expected_title + assert checkout.isbns == expected_isbns diff --git a/tests/models/test_checkouts_trend.py b/tests/models/test_checkouts_trend.py new file mode 100644 index 0000000..d8b23a7 --- /dev/null +++ b/tests/models/test_checkouts_trend.py @@ -0,0 +1,138 @@ +import pytest + +from nlb_catalogue_client.models.checkouts_title import CheckoutsTitle +from nlb_catalogue_client.models.checkouts_trend import CheckoutsTrend +from nlb_catalogue_client.types import UNSET + + +@pytest.fixture() +def sample_checkouts_title(): + return CheckoutsTitle( + title="Test Book", + native_title="测试书", + author="John Doe", + native_author="约翰", + isbns=["1234567890"], + checkouts_count=5, + ) + + +@pytest.fixture() +def checkouts_trend_full(sample_checkouts_title) -> tuple[CheckoutsTrend, dict]: + return CheckoutsTrend( + language="English", + age_level="A", + fiction=True, + singapore_collection=False, + checkouts_titles=[sample_checkouts_title], + ), { + "language": "English", + "ageLevel": "A", + "fiction": True, + "singaporeCollection": False, + "checkoutsTitles": [sample_checkouts_title.to_dict()], + } + + +@pytest.fixture() +def checkouts_trend_with_none() -> tuple[CheckoutsTrend, dict]: + return CheckoutsTrend(language=None, age_level=None, checkouts_titles=None), { + "language": None, + "ageLevel": None, + "checkoutsTitles": None, + } + + +class TestCheckoutsTrend: + @pytest.mark.parametrize( + "language,age_level,fiction,singapore_collection,checkouts_titles,expected_titles_len", + [ + ("English", "A", True, False, [CheckoutsTitle(title="Test Book")], 1), + (None, None, True, False, None, None), + (UNSET, UNSET, UNSET, UNSET, UNSET, None), + ], + ) + def test_basic_initialization( + self, language, age_level, fiction, singapore_collection, checkouts_titles, expected_titles_len + ): + trend = CheckoutsTrend( + language=language, + age_level=age_level, + fiction=fiction, + singapore_collection=singapore_collection, + checkouts_titles=checkouts_titles, + ) + assert trend.language == language + assert trend.age_level == age_level + assert trend.fiction == fiction + assert trend.singapore_collection == singapore_collection + assert trend.checkouts_titles == checkouts_titles + if expected_titles_len: + assert isinstance(trend.checkouts_titles, list) + assert len(trend.checkouts_titles) == expected_titles_len + + def test_to_dict_full(self, checkouts_trend_full): + assert checkouts_trend_full[0].to_dict() == checkouts_trend_full[1] + + def test_to_dict_with_none(self, checkouts_trend_with_none): + assert checkouts_trend_with_none[0].to_dict() == checkouts_trend_with_none[1] + + def test_to_dict_with_unset(self): + trend = CheckoutsTrend() + assert trend.to_dict() == {} + + def test_from_dict_full(self, checkouts_trend_full): + assert CheckoutsTrend.from_dict(checkouts_trend_full[1]) == checkouts_trend_full[0] + + def test_from_dict_with_none(self, checkouts_trend_with_none): + assert CheckoutsTrend.from_dict(checkouts_trend_with_none[1]) == checkouts_trend_with_none[0] + + @pytest.mark.parametrize( + "input_data,expected_language,expected_age_level,expected_fiction,expected_singapore_collection,expected_checkouts_titles", + [ + ({"language": "English", "fiction": True}, "English", UNSET, True, UNSET, UNSET), + ( + { + "language": "English", + "ageLevel": "A", + "fiction": True, + "singaporeCollection": False, + "checkoutsTitles": "invalid_value", + }, + "English", + "A", + True, + False, + "invalid_value", + ), + ( + { + "language": UNSET, + "ageLevel": UNSET, + "fiction": UNSET, + "singaporeCollection": UNSET, + "checkoutsTitles": UNSET, + }, + UNSET, + UNSET, + UNSET, + UNSET, + UNSET, + ), + ], + ) + def test_from_dict_edge_cases( + self, + input_data, + expected_language, + expected_age_level, + expected_fiction, + expected_singapore_collection, + expected_checkouts_titles, + ): + trend = CheckoutsTrend.from_dict(input_data) + assert trend.language == expected_language + assert trend.age_level == expected_age_level + assert trend.fiction == expected_fiction + assert trend.singapore_collection == expected_singapore_collection + assert trend.checkouts_titles == expected_checkouts_titles diff --git a/tests/models/test_course_code.py b/tests/models/test_course_code.py new file mode 100644 index 0000000..9214b56 --- /dev/null +++ b/tests/models/test_course_code.py @@ -0,0 +1,88 @@ +import pytest + +from nlb_catalogue_client.models.course_code import CourseCode +from nlb_catalogue_client.types import UNSET + + +@pytest.fixture() +def course_code_full() -> tuple[CourseCode, dict]: + return CourseCode(code="N1001", cluster_name="Lifestyle Design", category_name="Culture & Society"), { + "code": "N1001", + "clusterName": "Lifestyle Design", + "categoryName": "Culture & Society", + } + + +@pytest.fixture() +def course_code_required_only() -> tuple[CourseCode, dict]: + return CourseCode(code="N1001", cluster_name="Lifestyle Design"), { + "code": "N1001", + "clusterName": "Lifestyle Design", + } + + +@pytest.fixture() +def course_code_with_none() -> tuple[CourseCode, dict]: + return CourseCode(code="N1001", cluster_name="Lifestyle Design", category_name=None), { + "code": "N1001", + "clusterName": "Lifestyle Design", + "categoryName": None, + } + + +class TestCourseCode: + @pytest.mark.parametrize( + "code,cluster_name,category_name", + [ + ("N1001", "Lifestyle Design", "Culture & Society"), + ("N1002", "Digital Design", None), + ("N1003", "Web Design", UNSET), + ], + ) + def test_basic_initialization(self, code, cluster_name, category_name): + course = CourseCode(code=code, cluster_name=cluster_name, category_name=category_name) + assert course.code == code + assert course.cluster_name == cluster_name + assert course.category_name == category_name + + def test_to_dict_full(self, course_code_full): + assert course_code_full[0].to_dict() == course_code_full[1] + + def test_to_dict_required_only(self, course_code_required_only): + assert course_code_required_only[0].to_dict() == course_code_required_only[1] + + def test_to_dict_with_none(self, course_code_with_none): + assert course_code_with_none[0].to_dict() == course_code_with_none[1] + + def test_from_dict_full(self, course_code_full): + assert CourseCode.from_dict(course_code_full[1]) == course_code_full[0] + + def test_from_dict_required_only(self, course_code_required_only): + assert CourseCode.from_dict(course_code_required_only[1]) == course_code_required_only[0] + + def test_from_dict_with_none(self, course_code_with_none): + assert CourseCode.from_dict(course_code_with_none[1]) == course_code_with_none[0] + + @pytest.mark.parametrize( + "input_data,expected_code,expected_cluster_name,expected_category_name", + [ + ({"code": "N1001", "clusterName": "Lifestyle Design"}, "N1001", "Lifestyle Design", UNSET), + ( + {"code": "N1001", "clusterName": "Lifestyle Design", "categoryName": None}, + "N1001", + "Lifestyle Design", + None, + ), + ( + {"code": "N1001", "clusterName": "Lifestyle Design", "categoryName": UNSET}, + "N1001", + "Lifestyle Design", + UNSET, + ), + ], + ) + def test_from_dict_edge_cases(self, input_data, expected_code, expected_cluster_name, expected_category_name): + course = CourseCode.from_dict(input_data) + assert course.code == expected_code + assert course.cluster_name == expected_cluster_name + assert course.category_name == expected_category_name diff --git a/tests/models/test_facet.py b/tests/models/test_facet.py new file mode 100644 index 0000000..9ab9577 --- /dev/null +++ b/tests/models/test_facet.py @@ -0,0 +1,84 @@ +import pytest + +from nlb_catalogue_client.models.facet import Facet +from nlb_catalogue_client.models.facet_data import FacetData +from nlb_catalogue_client.types import UNSET + + +@pytest.fixture() +def facet_data_instance(): + return FacetData(id="audioBook", data="Audio Book", count=100) + + +@pytest.fixture() +def facet_full(facet_data_instance: FacetData) -> tuple[Facet, dict]: + return Facet(id="materialType", name="Material Type", values=[facet_data_instance]), { + "id": "materialType", + "name": "Material Type", + "values": [facet_data_instance.to_dict()], + } + + +@pytest.fixture() +def facet_required_only(): + return Facet(), {} + + +@pytest.fixture() +def facet_with_none(): + return Facet(id=None, name=None, values=None), {"id": None, "name": None, "values": None} + + +class TestFacet: + @pytest.mark.parametrize( + "id,name,values", + [ + ( + "materialType", + "Material Type", + [FacetData(id="book", data="Book", count=100)], + ), + (None, None, None), + (UNSET, UNSET, UNSET), + ], + ) + def test_basic_initialization(self, id, name, values): + facet = Facet(id=id, name=name, values=values) + assert facet.id == id + assert facet.name == name + assert facet.values == values + + def test_to_dict_full(self, facet_full): + assert facet_full[0].to_dict() == facet_full[1] + + def test_to_dict_required_only(self, facet_required_only): + assert facet_required_only[0].to_dict() == facet_required_only[1] + + def test_to_dict_with_none(self, facet_with_none): + assert facet_with_none[0].to_dict() == facet_with_none[1] + + def test_from_dict_full(self, facet_full): + assert Facet.from_dict(facet_full[1]) == facet_full[0] + + def test_from_dict_required_only(self, facet_required_only): + assert Facet.from_dict(facet_required_only[1]) == facet_required_only[0] + + def test_from_dict_with_none(self, facet_with_none): + assert Facet.from_dict(facet_with_none[1]) == facet_with_none[0] + + @pytest.mark.parametrize( + "input_data,expected_id,expected_name,expected_values", + [ + ( + {"id": "materialType", "name": "Material Type", "values": "invalid_value"}, + "materialType", + "Material Type", + "invalid_value", + ), + ], + ) + def test_from_dict_edge_cases(self, input_data, expected_id, expected_name, expected_values): + facet = Facet.from_dict(input_data) + assert facet.id == expected_id + assert facet.name == expected_name + assert facet.values == expected_values diff --git a/tests/models/test_facet_data.py b/tests/models/test_facet_data.py new file mode 100644 index 0000000..76ad85d --- /dev/null +++ b/tests/models/test_facet_data.py @@ -0,0 +1,52 @@ +import pytest + +from nlb_catalogue_client.models.facet_data import FacetData +from nlb_catalogue_client.types import UNSET + + +@pytest.fixture() +def facet_data() -> tuple[FacetData, dict]: + return FacetData(id="audioBook", data="Audio Book", count=17), { + "id": "audioBook", + "data": "Audio Book", + "count": 17, + } + + +@pytest.fixture() +def facet_data_required_only(): + return FacetData(), {} + + +@pytest.fixture() +def facet_data_with_none(): + return FacetData(id=None, data=None), {"id": None, "data": None} + + +class TestFacetData: + @pytest.mark.parametrize( + "id,data,count", [("audioBook", "Audio Book", 17), (None, None, None), (UNSET, UNSET, UNSET)] + ) + def test_basic_initialization(self, id, data, count): + facet = FacetData(id=id, data=data, count=count) + assert facet.id == id + assert facet.data == data + assert facet.count == count + + def test_to_dict_full(self, facet_data): + assert facet_data[0].to_dict() == facet_data[1] + + def test_to_dict_required_only(self, facet_data_required_only): + assert facet_data_required_only[0].to_dict() == facet_data_required_only[1] + + def test_to_dict_with_none(self, facet_data_with_none): + assert facet_data_with_none[0].to_dict() == facet_data_with_none[1] + + def test_from_dict_full(self, facet_data): + assert FacetData.from_dict(facet_data[1]) == facet_data[0] + + def test_from_dict_required_only(self, facet_data_required_only): + assert FacetData.from_dict(facet_data_required_only[1]) == facet_data_required_only[0] + + def test_from_dict_with_none(self, facet_data_with_none): + assert FacetData.from_dict(facet_data_with_none[1]) == facet_data_with_none[0] diff --git a/tests/models/test_get_availability_info_response_v2.py b/tests/models/test_get_availability_info_response_v2.py new file mode 100644 index 0000000..42ab624 --- /dev/null +++ b/tests/models/test_get_availability_info_response_v2.py @@ -0,0 +1,122 @@ +from nlb_catalogue_client.models.get_availability_info_response_v2 import GetAvailabilityInfoResponseV2 +from nlb_catalogue_client.models.item import Item +from nlb_catalogue_client.models.location import Location +from nlb_catalogue_client.models.media import Media +from nlb_catalogue_client.models.transaction_status import TransactionStatus +from nlb_catalogue_client.models.usage_level import UsageLevel +from nlb_catalogue_client.types import UNSET + + +class TestGetAvailabilityInfoResponseV2: + def setup_method(self): + """Setup common test objects""" + self.media = Media(code="BK", name="BOOKS") + self.usage_level = UsageLevel(code="A", name="Adult") + self.location = Location(code="AMKL", name="Ang Mo Kio Library") + self.transaction_status = TransactionStatus(code="A", name="Available") + self.item = Item( + media=self.media, + usage_level=self.usage_level, + location=self.location, + transaction_status=self.transaction_status, + ) + + def test_basic_initialization(self): + """Test initialization with all fields""" + response = GetAvailabilityInfoResponseV2( + set_id=123, total_records=100, count=20, has_more_records=True, next_records_offset=20, items=[self.item] + ) + + assert response.set_id == 123 + assert response.total_records == 100 + assert response.count == 20 + assert response.has_more_records is True + assert response.next_records_offset == 20 + assert response.items == [self.item] + + def test_initialization_with_required_only(self): + """Test initialization with no fields (all optional)""" + response = GetAvailabilityInfoResponseV2() + + assert response.set_id is UNSET + assert response.total_records is UNSET + assert response.count is UNSET + assert response.has_more_records is False # Default value + assert response.next_records_offset is UNSET + assert response.items is UNSET + + def test_to_dict_full(self): + """Test converting to dictionary with all fields""" + response = GetAvailabilityInfoResponseV2( + set_id=123, total_records=100, count=20, has_more_records=True, next_records_offset=20, items=[self.item] + ) + + expected = { + "setId": 123, + "totalRecords": 100, + "count": 20, + "hasMoreRecords": True, + "nextRecordsOffset": 20, + "items": [ + { + "media": {"code": "BK", "name": "BOOKS"}, + "usageLevel": {"code": "A", "name": "Adult"}, + "location": {"code": "AMKL", "name": "Ang Mo Kio Library"}, + "transactionStatus": {"code": "A", "name": "Available"}, + } + ], + } + + assert response.to_dict() == expected + + def test_to_dict_required_only(self): + """Test converting to dictionary with no fields set""" + response = GetAvailabilityInfoResponseV2() + + # Only hasMoreRecords should be included as it has a default value + expected = {"hasMoreRecords": False} + + assert response.to_dict() == expected + + def test_from_dict_full(self): + """Test creating object from dictionary with all fields""" + data = { + "setId": 123, + "totalRecords": 100, + "count": 20, + "hasMoreRecords": True, + "nextRecordsOffset": 20, + "items": [ + { + "media": {"code": "BK", "name": "BOOKS"}, + "usageLevel": {"code": "A", "name": "Adult"}, + "location": {"code": "AMKL", "name": "Ang Mo Kio Library"}, + "transactionStatus": {"code": "A", "name": "Available"}, + } + ], + } + + response = GetAvailabilityInfoResponseV2.from_dict(data) + + assert response.set_id == 123 + assert response.total_records == 100 + assert response.count == 20 + assert response.has_more_records is True + assert response.next_records_offset == 20 + assert response.items + assert len(response.items) == 1 + assert isinstance(response.items[0], Item) + assert response.items[0].media.code == "BK" + + def test_from_dict_required_only(self): + """Test creating object from empty dictionary""" + data = {} + + response = GetAvailabilityInfoResponseV2.from_dict(data) + + assert response.set_id is UNSET + assert response.total_records is UNSET + assert response.count is UNSET + assert response.has_more_records is UNSET + assert response.next_records_offset is UNSET + assert response.items == [] diff --git a/tests/models/test_get_title_details_response_v2.py b/tests/models/test_get_title_details_response_v2.py new file mode 100644 index 0000000..18cc9ae --- /dev/null +++ b/tests/models/test_get_title_details_response_v2.py @@ -0,0 +1,740 @@ +from nlb_catalogue_client.models.bib_format import BibFormat +from nlb_catalogue_client.models.get_title_details_response_v2 import GetTitleDetailsResponseV2 +from nlb_catalogue_client.types import UNSET + + +class TestGetTitleDetailsResponseV2: + def setup_method(self): + """Setup common test objects""" + self.format_ = BibFormat(code="BK", name="BOOKS") + + def test_basic_initialization(self): + """Test initialization with all fields""" + response = GetTitleDetailsResponseV2( + format_=self.format_, + brn=99999999, + digital_id="overdrive123", + other_titles=["Alternative Title 1"], + native_other_titles=["其他标题 1"], + variant_titles=["Variant 1"], + native_variant_titles=["变体 1"], + other_authors=["Jane Smith"], + native_other_authors=["简·史密斯"], + isbns=["9709999999"], + issns=["9999-9999"], + edition=["First Edition"], + native_edition=["第一版"], + publisher=["Sample Publisher"], + native_publisher=["示例出版社"], + publish_date="2023", + subjects=["Fiction"], + physical_description=["300 pages"], + native_physical_description=["300页"], + summary=["A story"], + native_summary=["故事"], + contents=["Chapter 1"], + native_contents=["第一章"], + thesis=["PhD Thesis"], + native_thesis=["博士论文"], + notes=["Special Edition"], + native_notes=["特别版"], + allow_reservation=True, + is_restricted=False, + active_reservations_count=3, + audience=["General", "Adult"], + audience_imda=["NC16", "PG"], + language=["English", "Chinese"], + serial=False, + volume_note=["Volume 1"], + native_volume_note=["第一卷"], + frequency=["Monthly"], + native_frequency=["每月"], + credits_=["Editor: John"], + native_credits=["编辑:约翰"], + performers=["Actor: Jane"], + native_performers=["演员:简"], + availability=True, + source="Library", + volumes=["2023 issue 1"], + title="Sample Book", + native_title="样本书", + series_title=["Series 1"], + native_series_title=["系列 1"], + author="John Doe", + native_author="约翰·多伊", + ) + + assert response.format_ == self.format_ + assert response.brn == 99999999 + assert response.digital_id == "overdrive123" + assert response.other_titles == ["Alternative Title 1"] + assert response.native_other_titles == ["其他标题 1"] + assert response.variant_titles == ["Variant 1"] + assert response.native_variant_titles == ["变体 1"] + assert response.other_authors == ["Jane Smith"] + assert response.native_other_authors == ["简·史密斯"] + assert response.isbns == ["9709999999"] + assert response.issns == ["9999-9999"] + assert response.edition == ["First Edition"] + assert response.native_edition == ["第一版"] + assert response.publisher == ["Sample Publisher"] + assert response.native_publisher == ["示例出版社"] + assert response.publish_date == "2023" + assert response.subjects == ["Fiction"] + assert response.physical_description == ["300 pages"] + assert response.native_physical_description == ["300页"] + assert response.summary == ["A story"] + assert response.native_summary == ["故事"] + assert response.contents == ["Chapter 1"] + assert response.native_contents == ["第一章"] + assert response.thesis == ["PhD Thesis"] + assert response.native_thesis == ["博士论文"] + assert response.notes == ["Special Edition"] + assert response.native_notes == ["特别版"] + assert response.allow_reservation is True + assert response.is_restricted is False + assert response.active_reservations_count == 3 + assert response.audience == ["General", "Adult"] + assert response.audience_imda == ["NC16", "PG"] + assert response.language == ["English", "Chinese"] + assert response.serial is False + assert response.volume_note == ["Volume 1"] + assert response.native_volume_note == ["第一卷"] + assert response.frequency == ["Monthly"] + assert response.native_frequency == ["每月"] + assert response.credits_ == ["Editor: John"] + assert response.native_credits == ["编辑:约翰"] + assert response.performers == ["Actor: Jane"] + assert response.native_performers == ["演员:简"] + assert response.availability is True + assert response.source == "Library" + assert response.volumes == ["2023 issue 1"] + assert response.title == "Sample Book" + assert response.native_title == "样本书" + assert response.series_title == ["Series 1"] + assert response.native_series_title == ["系列 1"] + assert response.author == "John Doe" + assert response.native_author == "约翰·多伊" + + def test_initialization_with_required_only(self): + """Test initialization with required fields only""" + response = GetTitleDetailsResponseV2(format_=self.format_) + + assert response.format_ == self.format_ + assert response.brn is UNSET + assert response.digital_id is UNSET + assert response.other_titles is UNSET + assert response.native_other_titles is UNSET + assert response.variant_titles is UNSET + assert response.native_variant_titles is UNSET + assert response.other_authors is UNSET + assert response.native_other_authors is UNSET + assert response.isbns is UNSET + assert response.issns is UNSET + assert response.edition is UNSET + assert response.native_edition is UNSET + assert response.publisher is UNSET + assert response.native_publisher is UNSET + assert response.publish_date is UNSET + assert response.subjects is UNSET + assert response.physical_description is UNSET + assert response.native_physical_description is UNSET + assert response.summary is UNSET + assert response.native_summary is UNSET + assert response.contents is UNSET + assert response.native_contents is UNSET + assert response.thesis is UNSET + assert response.native_thesis is UNSET + assert response.notes is UNSET + assert response.native_notes is UNSET + assert response.allow_reservation is True # Default value + assert response.is_restricted is False # Default value + assert response.active_reservations_count is UNSET + assert response.audience is UNSET + assert response.audience_imda is UNSET + assert response.language is UNSET + assert response.serial is False # Default value + assert response.volume_note is UNSET + assert response.native_volume_note is UNSET + assert response.frequency is UNSET + assert response.native_frequency is UNSET + assert response.credits_ is UNSET + assert response.native_credits is UNSET + assert response.performers is UNSET + assert response.native_performers is UNSET + assert response.availability is UNSET + assert response.source is UNSET + assert response.volumes is UNSET + assert response.title is UNSET + assert response.native_title is UNSET + assert response.series_title is UNSET + assert response.native_series_title is UNSET + assert response.author is UNSET + assert response.native_author is UNSET + + def test_to_dict_full(self): + """Test converting to dictionary with all fields""" + response = GetTitleDetailsResponseV2( + format_=self.format_, + brn=99999999, + digital_id="overdrive123", + other_titles=["Alternative Title 1"], + native_other_titles=["其他标题 1"], + variant_titles=["Variant 1"], + native_variant_titles=["变体 1"], + other_authors=["Jane Smith"], + native_other_authors=["简·史密斯"], + isbns=["9709999999"], + issns=["9999-9999"], + edition=["First Edition"], + native_edition=["第一版"], + publisher=["Sample Publisher"], + native_publisher=["示例出版社"], + publish_date="2023", + subjects=["Fiction"], + physical_description=["300 pages"], + native_physical_description=["300页"], + summary=["A story"], + native_summary=["故事"], + contents=["Chapter 1"], + native_contents=["第一章"], + thesis=["PhD Thesis"], + native_thesis=["博士论文"], + notes=["Special Edition"], + native_notes=["特别版"], + allow_reservation=True, + is_restricted=False, + active_reservations_count=3, + audience=["General", "Adult"], + audience_imda=["NC16", "PG"], + language=["English", "Chinese"], + serial=False, + volume_note=["Volume 1"], + native_volume_note=["第一卷"], + frequency=["Monthly"], + native_frequency=["每月"], + credits_=["Editor: John"], + native_credits=["编辑:约翰"], + performers=["Actor: Jane"], + native_performers=["演员:简"], + availability=True, + source="Library", + volumes=["2023 issue 1"], + title="Sample Book", + native_title="样本书", + series_title=["Series 1"], + native_series_title=["系列 1"], + author="John Doe", + native_author="约翰·多伊", + ) + + expected = { + "format": {"code": "BK", "name": "BOOKS"}, + "brn": 99999999, + "digitalId": "overdrive123", + "otherTitles": ["Alternative Title 1"], + "nativeOtherTitles": ["其他标题 1"], + "variantTitles": ["Variant 1"], + "nativeVariantTitles": ["变体 1"], + "otherAuthors": ["Jane Smith"], + "nativeOtherAuthors": ["简·史密斯"], + "isbns": ["9709999999"], + "issns": ["9999-9999"], + "edition": ["First Edition"], + "nativeEdition": ["第一版"], + "publisher": ["Sample Publisher"], + "nativePublisher": ["示例出版社"], + "publishDate": "2023", + "subjects": ["Fiction"], + "physicalDescription": ["300 pages"], + "nativePhysicalDescription": ["300页"], + "summary": ["A story"], + "nativeSummary": ["故事"], + "contents": ["Chapter 1"], + "nativeContents": ["第一章"], + "thesis": ["PhD Thesis"], + "nativeThesis": ["博士论文"], + "notes": ["Special Edition"], + "nativeNotes": ["特别版"], + "allowReservation": True, + "isRestricted": False, + "activeReservationsCount": 3, + "audience": ["General", "Adult"], + "audienceImda": ["NC16", "PG"], + "language": ["English", "Chinese"], + "serial": False, + "volumeNote": ["Volume 1"], + "nativeVolumeNote": ["第一卷"], + "frequency": ["Monthly"], + "nativeFrequency": ["每月"], + "credits": ["Editor: John"], + "nativeCredits": ["编辑:约翰"], + "performers": ["Actor: Jane"], + "nativePerformers": ["演员:简"], + "availability": True, + "source": "Library", + "volumes": ["2023 issue 1"], + "title": "Sample Book", + "nativeTitle": "样本书", + "seriesTitle": ["Series 1"], + "nativeSeriesTitle": ["系列 1"], + "author": "John Doe", + "nativeAuthor": "约翰·多伊", + } + + assert response.to_dict() == expected + + def test_to_dict_required_only(self): + """Test converting to dictionary with required fields only""" + response = GetTitleDetailsResponseV2(format_=self.format_) + + expected = { + "format": {"code": "BK", "name": "BOOKS"}, + "allowReservation": True, + "isRestricted": False, + "serial": False, + } + + assert response.to_dict() == expected + + def test_to_dict_with_none(self): + """Test converting to dictionary with None values""" + response = GetTitleDetailsResponseV2( + format_=self.format_, + digital_id=None, + other_titles=None, + native_other_titles=None, + variant_titles=None, + native_variant_titles=None, + other_authors=None, + native_other_authors=None, + issns=None, + edition=None, + native_edition=None, + native_publisher=None, + native_physical_description=None, + summary=None, + native_summary=None, + contents=None, + native_contents=None, + thesis=None, + native_thesis=None, + native_notes=None, + audience=None, + audience_imda=None, + volume_note=None, + native_volume_note=None, + frequency=None, + native_frequency=None, + credits_=None, + native_credits=None, + performers=None, + native_performers=None, + source=None, + volumes=None, + native_title=None, + series_title=None, + native_series_title=None, + native_author=None, + ) + + expected = { + "format": {"code": "BK", "name": "BOOKS"}, + "digitalId": None, + "otherTitles": None, + "nativeOtherTitles": None, + "variantTitles": None, + "nativeVariantTitles": None, + "otherAuthors": None, + "nativeOtherAuthors": None, + "issns": None, + "edition": None, + "nativeEdition": None, + "nativePublisher": None, + "nativePhysicalDescription": None, + "summary": None, + "nativeSummary": None, + "contents": None, + "nativeContents": None, + "thesis": None, + "nativeThesis": None, + "nativeNotes": None, + "audience": None, + "audienceImda": None, + "volumeNote": None, + "nativeVolumeNote": None, + "frequency": None, + "nativeFrequency": None, + "credits": None, + "nativeCredits": None, + "performers": None, + "nativePerformers": None, + "source": None, + "volumes": None, + "nativeTitle": None, + "seriesTitle": None, + "nativeSeriesTitle": None, + "nativeAuthor": None, + "allowReservation": True, + "isRestricted": False, + "serial": False, + } + + assert response.to_dict() == expected + + def test_from_dict_full(self): + """Test creating object from dictionary with all fields""" + data = { + "format": {"code": "BK", "name": "BOOKS"}, + "brn": 99999999, + "digitalId": "overdrive123", + "otherTitles": ["Alternative Title 1"], + "nativeOtherTitles": ["其他标题 1"], + "variantTitles": ["Variant 1"], + "nativeVariantTitles": ["变体 1"], + "otherAuthors": ["Jane Smith"], + "nativeOtherAuthors": ["简·史密斯"], + "isbns": ["9709999999"], + "issns": ["9999-9999"], + "edition": ["First Edition"], + "nativeEdition": ["第一版"], + "publisher": ["Sample Publisher"], + "nativePublisher": ["示例出版社"], + "publishDate": "2023", + "subjects": ["Fiction"], + "physicalDescription": ["300 pages"], + "nativePhysicalDescription": ["300页"], + "summary": ["A story"], + "nativeSummary": ["故事"], + "contents": ["Chapter 1"], + "nativeContents": ["第一章"], + "thesis": ["PhD Thesis"], + "nativeThesis": ["博士论文"], + "notes": ["Special Edition"], + "nativeNotes": ["特别版"], + "allowReservation": True, + "isRestricted": False, + "activeReservationsCount": 3, + "audience": ["General", "Adult"], + "audienceImda": ["NC16", "PG"], + "language": ["English", "Chinese"], + "serial": False, + "volumeNote": ["Volume 1"], + "nativeVolumeNote": ["第一卷"], + "frequency": ["Monthly"], + "nativeFrequency": ["每月"], + "credits": ["Editor: John"], + "nativeCredits": ["编辑:约翰"], + "performers": ["Actor: Jane"], + "nativePerformers": ["演员:简"], + "availability": True, + "source": "Library", + "volumes": ["2023 issue 1"], + "title": "Sample Book", + "nativeTitle": "样本书", + "seriesTitle": ["Series 1"], + "nativeSeriesTitle": ["系列 1"], + "author": "John Doe", + "nativeAuthor": "约翰·多伊", + } + + response = GetTitleDetailsResponseV2.from_dict(data) + + assert response.format_.code == "BK" + assert response.format_.name == "BOOKS" + assert response.brn == 99999999 + assert response.digital_id == "overdrive123" + assert response.other_titles == ["Alternative Title 1"] + assert response.native_other_titles == ["其他标题 1"] + assert response.variant_titles == ["Variant 1"] + assert response.native_variant_titles == ["变体 1"] + assert response.other_authors == ["Jane Smith"] + assert response.native_other_authors == ["简·史密斯"] + assert response.isbns == ["9709999999"] + assert response.issns == ["9999-9999"] + assert response.edition == ["First Edition"] + assert response.native_edition == ["第一版"] + assert response.publisher == ["Sample Publisher"] + assert response.native_publisher == ["示例出版社"] + assert response.publish_date == "2023" + assert response.subjects == ["Fiction"] + assert response.physical_description == ["300 pages"] + assert response.native_physical_description == ["300页"] + assert response.summary == ["A story"] + assert response.native_summary == ["故事"] + assert response.contents == ["Chapter 1"] + assert response.native_contents == ["第一章"] + assert response.thesis == ["PhD Thesis"] + assert response.native_thesis == ["博士论文"] + assert response.notes == ["Special Edition"] + assert response.native_notes == ["特别版"] + assert response.allow_reservation is True + assert response.is_restricted is False + assert response.active_reservations_count == 3 + assert response.audience == ["General", "Adult"] + assert response.audience_imda == ["NC16", "PG"] + assert response.language == ["English", "Chinese"] + assert response.serial is False + assert response.volume_note == ["Volume 1"] + assert response.native_volume_note == ["第一卷"] + assert response.frequency == ["Monthly"] + assert response.native_frequency == ["每月"] + assert response.credits_ == ["Editor: John"] + assert response.native_credits == ["编辑:约翰"] + assert response.performers == ["Actor: Jane"] + assert response.native_performers == ["演员:简"] + assert response.availability is True + assert response.source == "Library" + assert response.volumes == ["2023 issue 1"] + assert response.title == "Sample Book" + assert response.native_title == "样本书" + assert response.series_title == ["Series 1"] + assert response.native_series_title == ["系列 1"] + assert response.author == "John Doe" + assert response.native_author == "约翰·多伊" + + def test_from_dict_required_only(self): + """Test creating object from dictionary with required fields only""" + data = { + "format": {"code": "BK", "name": "BOOKS"}, + } + + response = GetTitleDetailsResponseV2.from_dict(data) + + assert response.format_.code == "BK" + assert response.format_.name == "BOOKS" + assert response.brn is UNSET + assert response.digital_id is UNSET + assert response.other_titles is UNSET + assert response.native_other_titles is UNSET + assert response.variant_titles is UNSET + assert response.native_variant_titles is UNSET + assert response.other_authors is UNSET + assert response.native_other_authors is UNSET + assert response.isbns is UNSET + assert response.issns is UNSET + assert response.edition is UNSET + assert response.native_edition is UNSET + assert response.publisher is UNSET + assert response.native_publisher is UNSET + assert response.publish_date is UNSET + assert response.subjects is UNSET + assert response.physical_description is UNSET + assert response.native_physical_description is UNSET + assert response.summary is UNSET + assert response.native_summary is UNSET + assert response.contents is UNSET + assert response.native_contents is UNSET + assert response.thesis is UNSET + assert response.native_thesis is UNSET + assert response.notes is UNSET + assert response.native_notes is UNSET + assert response.allow_reservation is UNSET + assert response.is_restricted is UNSET + assert response.active_reservations_count is UNSET + assert response.audience is UNSET + assert response.audience_imda is UNSET + assert response.language is UNSET + assert response.serial is UNSET + assert response.volume_note is UNSET + assert response.native_volume_note is UNSET + assert response.frequency is UNSET + assert response.native_frequency is UNSET + assert response.credits_ is UNSET + assert response.native_credits is UNSET + assert response.performers is UNSET + assert response.native_performers is UNSET + assert response.availability is UNSET + assert response.source is UNSET + assert response.volumes is UNSET + assert response.title is UNSET + assert response.native_title is UNSET + assert response.series_title is UNSET + assert response.native_series_title is UNSET + assert response.author is UNSET + assert response.native_author is UNSET + + def test_from_dict_with_none(self): + """Test creating object from dictionary with None values""" + data = { + "format": {"code": "BK", "name": "BOOKS"}, + "digitalId": None, + "otherTitles": None, + "nativeOtherTitles": None, + "variantTitles": None, + "nativeVariantTitles": None, + "otherAuthors": None, + "nativeOtherAuthors": None, + "issns": None, + "edition": None, + "nativeEdition": None, + "nativePublisher": None, + "nativePhysicalDescription": None, + "summary": None, + "nativeSummary": None, + "contents": None, + "nativeContents": None, + "thesis": None, + "nativeThesis": None, + "nativeNotes": None, + "audience": None, + "audienceImda": None, + "volumeNote": None, + "nativeVolumeNote": None, + "frequency": None, + "nativeFrequency": None, + "credits": None, + "nativeCredits": None, + "performers": None, + "nativePerformers": None, + "source": None, + "volumes": None, + "nativeTitle": None, + "seriesTitle": None, + "nativeSeriesTitle": None, + "nativeAuthor": None, + } + + response = GetTitleDetailsResponseV2.from_dict(data) + + assert response.format_.code == "BK" + assert response.format_.name == "BOOKS" + assert response.digital_id is None + assert response.other_titles is None + assert response.native_other_titles is None + assert response.variant_titles is None + assert response.native_variant_titles is None + assert response.other_authors is None + assert response.native_other_authors is None + assert response.issns is None + assert response.edition is None + assert response.native_edition is None + assert response.native_publisher is None + assert response.native_physical_description is None + assert response.summary is None + assert response.native_summary is None + assert response.contents is None + assert response.native_contents is None + assert response.thesis is None + assert response.native_thesis is None + assert response.native_notes is None + assert response.audience is None + assert response.audience_imda is None + assert response.volume_note is None + assert response.native_volume_note is None + assert response.frequency is None + assert response.native_frequency is None + assert response.credits_ is None + assert response.native_credits is None + assert response.performers is None + assert response.native_performers is None + assert response.source is None + assert response.volumes is None + assert response.native_title is None + assert response.series_title is None + assert response.native_series_title is None + assert response.native_author is None + + def test_from_dict_with_invalid_values(self): + """Test creating object from dictionary with invalid field types""" + data = { + "format": {"code": "BK", "name": "BOOKS"}, + "otherTitles": "Not a list", + "nativeOtherTitles": "Not a list", + "variantTitles": "Not a list", + "nativeVariantTitles": "Not a list", + "otherAuthors": "Not a list", + "nativeOtherAuthors": "Not a list", + "isbns": "Not a list", + "issns": "Not a list", + "edition": "Not a list", + "nativeEdition": "Not a list", + "publisher": "Not a list", + "nativePublisher": "Not a list", + "subjects": "Not a list", + "physicalDescription": "Not a list", + "nativePhysicalDescription": "Not a list", + "summary": "Not a list", + "nativeSummary": "Not a list", + "contents": "Not a list", + "nativeContents": "Not a list", + "thesis": "Not a list", + "nativeThesis": "Not a list", + "notes": "Not a list", + "nativeNotes": "Not a list", + "audience": "Not a list", + "audienceImda": "Not a list", + "language": "Not a list", + "volumeNote": "Not a list", + "nativeVolumeNote": "Not a list", + "frequency": "Not a list", + "nativeFrequency": "Not a list", + "credits": "Not a list", + "nativeCredits": "Not a list", + "performers": "Not a list", + "nativePerformers": "Not a list", + "volumes": "Not a list", + "seriesTitle": "Not a list", + "nativeSeriesTitle": "Not a list", + "brn": "Not an integer", + "activeReservationsCount": "Not an integer", + "allowReservation": "Not a boolean", + "isRestricted": "Not a boolean", + "serial": "Not a boolean", + "availability": "Not a boolean", + } + + response = GetTitleDetailsResponseV2.from_dict(data) + + # Verify invalid list fields are returned as-is + assert response.other_titles == "Not a list" + assert response.native_other_titles == "Not a list" + assert response.variant_titles == "Not a list" + assert response.native_variant_titles == "Not a list" + assert response.other_authors == "Not a list" + assert response.native_other_authors == "Not a list" + assert response.isbns == "Not a list" + assert response.issns == "Not a list" + assert response.edition == "Not a list" + assert response.native_edition == "Not a list" + assert response.publisher == "Not a list" + assert response.native_publisher == "Not a list" + assert response.subjects == "Not a list" + assert response.physical_description == "Not a list" + assert response.native_physical_description == "Not a list" + assert response.summary == "Not a list" + assert response.native_summary == "Not a list" + assert response.contents == "Not a list" + assert response.native_contents == "Not a list" + assert response.thesis == "Not a list" + assert response.native_thesis == "Not a list" + assert response.notes == "Not a list" + assert response.native_notes == "Not a list" + assert response.audience == "Not a list" + assert response.audience_imda == "Not a list" + assert response.language == "Not a list" + assert response.volume_note == "Not a list" + assert response.native_volume_note == "Not a list" + assert response.frequency == "Not a list" + assert response.native_frequency == "Not a list" + assert response.credits_ == "Not a list" + assert response.native_credits == "Not a list" + assert response.performers == "Not a list" + assert response.native_performers == "Not a list" + assert response.volumes == "Not a list" + assert response.series_title == "Not a list" + assert response.native_series_title == "Not a list" + + # Verify invalid numeric fields are returned as-is + assert response.brn == "Not an integer" + assert response.active_reservations_count == "Not an integer" + + # Verify invalid boolean fields are returned as-is + assert response.allow_reservation == "Not a boolean" + assert response.is_restricted == "Not a boolean" + assert response.serial == "Not a boolean" + assert response.availability == "Not a boolean" + + # Verify required format field is still properly parsed + assert response.format_.code == "BK" + assert response.format_.name == "BOOKS" diff --git a/tests/models/test_get_titles_response_v2.py b/tests/models/test_get_titles_response_v2.py new file mode 100644 index 0000000..3bd3c5c --- /dev/null +++ b/tests/models/test_get_titles_response_v2.py @@ -0,0 +1,111 @@ +from nlb_catalogue_client.models.bib_format import BibFormat +from nlb_catalogue_client.models.get_titles_response_v2 import GetTitlesResponseV2 +from nlb_catalogue_client.models.title import Title +from nlb_catalogue_client.types import UNSET + + +class TestGetTitlesResponseV2: + def setup_method(self): + """Setup common test objects""" + self.format_ = BibFormat(code="BK", name="BOOKS") + self.title = Title( + format_=self.format_, + title="Sample Book", + author="John Doe", + brn=99999999, + ) + + def test_basic_initialization(self): + """Test initialization with all fields""" + response = GetTitlesResponseV2( + set_id=123, + total_records=100, + count=20, + has_more_records=True, + next_records_offset=20, + titles=[self.title], + ) + + assert response.set_id == 123 + assert response.total_records == 100 + assert response.count == 20 + assert response.has_more_records is True + assert response.next_records_offset == 20 + assert response.titles == [self.title] + + def test_initialization_with_required_only(self): + """Test initialization with no fields (all optional)""" + response = GetTitlesResponseV2() + + assert response.set_id is UNSET + assert response.total_records is UNSET + assert response.count is UNSET + assert response.has_more_records is False # Default value + assert response.next_records_offset is UNSET + assert response.titles is UNSET + + def test_to_dict_full(self): + """Test converting to dictionary with all fields""" + response = GetTitlesResponseV2( + set_id=123, + total_records=100, + count=20, + has_more_records=True, + next_records_offset=20, + titles=[self.title], + ) + + expected = { + "setId": 123, + "totalRecords": 100, + "count": 20, + "hasMoreRecords": True, + "nextRecordsOffset": 20, + "titles": [ + { + "format": {"code": "BK", "name": "BOOKS"}, + "title": "Sample Book", + "author": "John Doe", + "brn": 99999999, + "allowReservation": True, + "isRestricted": False, + "serial": False, + } + ], + } + + assert response.to_dict() == expected + + def test_to_dict_required_only(self): + """Test converting to dictionary with no fields set""" + response = GetTitlesResponseV2() + + # Only hasMoreRecords should be included as it has a default value + expected = {"hasMoreRecords": False} + + assert response.to_dict() == expected + + def test_from_dict_full(self): + """Test creating object from dictionary with all fields""" + data = { + "setId": 123, + "totalRecords": 100, + "count": 20, + "hasMoreRecords": True, + "nextRecordsOffset": 20, + "titles": [ + { + "format": {"code": "BK", "name": "BOOKS"}, + "title": "Sample Book", + "author": "John Doe", + "brn": 99999999, + "allowReservation": True, + "isRestricted": False, + "serial": False, + } + ], + } + + response = GetTitlesResponseV2.from_dict(data) + + assert response.set_id == 123 diff --git a/tests/models/test_internal_server_error.py b/tests/models/test_internal_server_error.py new file mode 100644 index 0000000..fc65011 --- /dev/null +++ b/tests/models/test_internal_server_error.py @@ -0,0 +1,69 @@ +import pytest + +from nlb_catalogue_client.models.internal_server_error import InternalServerError +from nlb_catalogue_client.types import UNSET + + +@pytest.fixture() +def error_with_status() -> tuple[InternalServerError, dict]: + return InternalServerError(error="Internal Server Error", message="Internal server error", status_code=500), { + "error": "Internal Server Error", + "message": "Internal server error", + "statusCode": 500, + } + + +@pytest.fixture() +def error_without_status() -> tuple[InternalServerError, dict]: + return InternalServerError(error="Internal Server Error", message="Internal server error"), { + "error": "Internal Server Error", + "message": "Internal server error", + } + + +class TestInternalServerError: + @pytest.mark.parametrize( + "error,message,status_code,expected_status", + [ + ("Internal Server Error", "Internal server error", 500, 500), + ("Internal Server Error", "Internal server error", None, None), + ("Internal Server Error", "Internal server error", UNSET, UNSET), + ], + ) + def test_basic_initialization(self, error, message, status_code, expected_status): + error_obj = InternalServerError(error=error, message=message, status_code=status_code) + assert error_obj.error == error + assert error_obj.message == message + assert error_obj.status_code == expected_status + + def test_to_dict_with_status(self, error_with_status): + assert error_with_status[0].to_dict() == error_with_status[1] + + def test_to_dict_without_status(self, error_without_status): + assert error_without_status[0].to_dict() == error_without_status[1] + + def test_from_dict_with_status(self, error_with_status): + assert InternalServerError.from_dict(error_with_status[1]) == error_with_status[0] + + def test_from_dict_without_status(self, error_without_status): + assert InternalServerError.from_dict(error_without_status[1]) == error_without_status[0] + + @pytest.mark.parametrize( + "input_data", + [ + {"error": "Internal Server Error", "message": "Internal server error", "statusCode": 500}, + {"error": "Internal Server Error", "message": "Internal server error"}, + ], + ) + def test_dict_roundtrip(self, input_data): + """Test that to_dict and from_dict are inverse operations""" + original = InternalServerError.from_dict(input_data) + dict_form = original.to_dict() + recreated = InternalServerError.from_dict(dict_form) + assert original == recreated + + def test_from_dict_preserves_original(self, error_with_status): + """Test that from_dict doesn't modify the input dictionary""" + input_dict = error_with_status[1].copy() + InternalServerError.from_dict(input_dict) + assert input_dict == error_with_status[1] diff --git a/tests/models/test_item.py b/tests/models/test_item.py new file mode 100644 index 0000000..3fcd74c --- /dev/null +++ b/tests/models/test_item.py @@ -0,0 +1,281 @@ +from datetime import datetime + +from nlb_catalogue_client.models.course_code import CourseCode +from nlb_catalogue_client.models.item import Item +from nlb_catalogue_client.models.location import Location +from nlb_catalogue_client.models.media import Media +from nlb_catalogue_client.models.status import Status +from nlb_catalogue_client.models.transaction_status import TransactionStatus +from nlb_catalogue_client.models.usage_level import UsageLevel +from nlb_catalogue_client.types import UNSET, Unset + + +class TestItem: + def setup_method(self): + """Setup common test objects""" + self.media = Media(code="DV18", name="Digital Video for M18") + self.usage_level = UsageLevel(code="001", name="Junior Picture Adult Lending") + self.location = Location(code="AMKPL", name="Ang Mo Kio Public Library") + self.course_code = CourseCode(code="N1001", cluster_name="Lifestyle Design", category_name="Culture & Society") + self.transaction_status = TransactionStatus( + code="I", + name="In Transit", + date=datetime(2019, 7, 21, 14, 32, 45), + in_transit_from=Location(code="AMKPL", name="Ang Mo Kio Public Library"), + in_transit_to=Location(code="BBPL", name="Bukit Batok Public Library"), + ) + self.status = Status(code="A", name="Available") + + def test_basic_initialization(self): + item = Item( + media=self.media, + usage_level=self.usage_level, + location=self.location, + transaction_status=self.transaction_status, + irn=99999999, + item_id="BxxxxxxxJ", + brn=99999999, + volume_name="2023 issue 1", + call_number="123.123 ART", + formatted_call_number="English 123.123 -[ART]", + course_code=self.course_code, + language="English", + suffix="-[ART]", + donor="Donated by abc", + price=9999.99, + status=self.status, + min_age_limit=13, + ) + + assert item.media == self.media + assert item.usage_level == self.usage_level + assert item.location == self.location + assert item.transaction_status == self.transaction_status + assert item.irn == 99999999 + assert item.item_id == "BxxxxxxxJ" + assert item.brn == 99999999 + assert item.volume_name == "2023 issue 1" + assert item.call_number == "123.123 ART" + assert item.formatted_call_number == "English 123.123 -[ART]" + assert item.course_code == self.course_code + assert item.language == "English" + assert item.suffix == "-[ART]" + assert item.donor == "Donated by abc" + assert item.price == 9999.99 + assert item.status == self.status + assert item.min_age_limit == 13 + + def test_initialization_required_only(self): + item = Item( + media=self.media, + usage_level=self.usage_level, + location=self.location, + transaction_status=self.transaction_status, + ) + + assert item.media == self.media + assert item.usage_level == self.usage_level + assert item.location == self.location + assert item.transaction_status == self.transaction_status + assert item.irn is UNSET + assert item.item_id is UNSET + assert item.volume_name is UNSET + assert item.course_code is UNSET + assert item.status is UNSET + + def test_to_dict_full(self): + item = Item( + media=self.media, + usage_level=self.usage_level, + location=self.location, + transaction_status=self.transaction_status, + irn=99999999, + item_id="BxxxxxxxJ", + brn=99999999, + volume_name="2023 issue 1", + call_number="123.123 ART", + formatted_call_number="English 123.123 -[ART]", + course_code=self.course_code, + language="English", + suffix="-[ART]", + donor="Donated by abc", + price=9999.99, + status=self.status, + min_age_limit=13, + ) + + result = item.to_dict() + assert result["media"] == self.media.to_dict() + assert result["usageLevel"] == self.usage_level.to_dict() + assert result["location"] == self.location.to_dict() + assert result["transactionStatus"] == self.transaction_status.to_dict() + assert result["irn"] == 99999999 + assert result["itemId"] == "BxxxxxxxJ" + assert result["brn"] == 99999999 + assert result["volumeName"] == "2023 issue 1" + assert result["callNumber"] == "123.123 ART" + assert result["formattedCallNumber"] == "English 123.123 -[ART]" + assert result["courseCode"] == self.course_code.to_dict() + assert result["language"] == "English" + assert result["donor"] == "Donated by abc" + assert result["price"] == 9999.99 + assert result["status"] == self.status.to_dict() + assert result["minAgeLimit"] == 13 + + def test_to_dict_required_only(self): + item = Item( + media=self.media, + usage_level=self.usage_level, + location=self.location, + transaction_status=self.transaction_status, + ) + + expected = { + "media": {"code": "DV18", "name": "Digital Video for M18"}, + "usageLevel": {"code": "001", "name": "Junior Picture Adult Lending"}, + "location": {"code": "AMKPL", "name": "Ang Mo Kio Public Library"}, + "transactionStatus": { + "code": "I", + "name": "In Transit", + "date": "2019-07-21T14:32:45", + "inTransitFrom": {"code": "AMKPL", "name": "Ang Mo Kio Public Library"}, + "inTransitTo": {"code": "BBPL", "name": "Bukit Batok Public Library"}, + }, + } + + result = item.to_dict() + assert result == expected + + def test_from_dict_full(self): + data = { + "media": {"code": "DV18", "name": "Digital Video for M18"}, + "usageLevel": {"code": "001", "name": "Junior Picture Adult Lending"}, + "location": {"code": "AMKPL", "name": "Ang Mo Kio Public Library"}, + "transactionStatus": { + "code": "I", + "name": "In Transit", + "date": "2019-07-21T14:32:45", + "inTransitFrom": {"code": "AMKPL", "name": "Ang Mo Kio Public Library"}, + "inTransitTo": {"code": "BBPL", "name": "Bukit Batok Public Library"}, + }, + "irn": 99999999, + "itemId": "BxxxxxxxJ", + "brn": 99999999, + "volumeName": "2023 issue 1", + "callNumber": "123.123 ART", + "formattedCallNumber": "English 123.123 -[ART]", + "courseCode": {"code": "N1001", "clusterName": "Lifestyle Design", "categoryName": "Culture & Society"}, + "language": "English", + "suffix": "-[ART]", + "donor": "Donated by abc", + "price": 9999.99, + "status": {"code": "A", "name": "Available"}, + "minAgeLimit": 13, + } + + item = Item.from_dict(data) + + # Test required fields + assert item.media.code == "DV18" + assert item.media.name == "Digital Video for M18" + assert item.usage_level.code == "001" + assert item.usage_level.name == "Junior Picture Adult Lending" + assert item.location.code == "AMKPL" + assert item.location.name == "Ang Mo Kio Public Library" + assert item.transaction_status.code == "I" + assert item.transaction_status.name == "In Transit" + + # Test optional fields + assert item.irn == 99999999 + assert item.item_id == "BxxxxxxxJ" + assert item.brn == 99999999 + assert item.volume_name == "2023 issue 1" + assert item.call_number == "123.123 ART" + assert item.formatted_call_number == "English 123.123 -[ART]" + assert not isinstance(item.course_code, Unset) + assert item.course_code.code == "N1001" + assert item.course_code.cluster_name == "Lifestyle Design" + assert item.course_code.category_name == "Culture & Society" + assert item.language == "English" + assert item.suffix == "-[ART]" + assert item.donor == "Donated by abc" + assert item.price == 9999.99 + assert not isinstance(item.status, Unset) + assert item.status.code == "A" + assert item.status.name == "Available" + assert item.min_age_limit == 13 + + def test_from_dict_required_only(self): + data = { + "media": {"code": "DV18", "name": "Digital Video for M18"}, + "usageLevel": {"code": "001", "name": "Junior Picture Adult Lending"}, + "location": {"code": "AMKPL", "name": "Ang Mo Kio Public Library"}, + "transactionStatus": { + "code": "I", + "name": "In Transit", + "date": "2019-07-21T14:32:45", + "inTransitFrom": {"code": "AMKPL", "name": "Ang Mo Kio Public Library"}, + "inTransitTo": {"code": "BBPL", "name": "Bukit Batok Public Library"}, + }, + } + + item = Item.from_dict(data) + + # Test required fields are present + assert item.media.code == "DV18" + assert item.media.name == "Digital Video for M18" + assert item.usage_level.code == "001" + assert item.usage_level.name == "Junior Picture Adult Lending" + assert item.location.code == "AMKPL" + assert item.location.name == "Ang Mo Kio Public Library" + assert item.transaction_status.code == "I" + assert item.transaction_status.name == "In Transit" + + # Test optional fields are UNSET + assert item.irn is UNSET + assert item.item_id is UNSET + assert item.brn is UNSET + assert item.volume_name is UNSET + assert item.call_number is UNSET + assert item.formatted_call_number is UNSET + assert item.course_code is UNSET + assert item.language is UNSET + assert item.suffix is UNSET + assert item.donor is UNSET + assert item.price is UNSET + assert item.status is UNSET + assert item.min_age_limit is UNSET + + def test_from_dict_with_none(self): + data = { + "media": {"code": "DV18", "name": "Digital Video for M18"}, + "usageLevel": {"code": "001", "name": "Junior Picture Adult Lending"}, + "location": {"code": "AMKPL", "name": "Ang Mo Kio Public Library"}, + "transactionStatus": {"code": "I", "name": "In Transit"}, + "volumeName": None, + "suffix": None, + "donor": None, + "price": None, + } + + item = Item.from_dict(data) + + # Test required fields + assert item.media.code == "DV18" + assert item.media.name == "Digital Video for M18" + assert item.usage_level.code == "001" + assert item.usage_level.name == "Junior Picture Adult Lending" + assert item.location.code == "AMKPL" + assert item.location.name == "Ang Mo Kio Public Library" + assert item.transaction_status.code == "I" + assert item.transaction_status.name == "In Transit" + + # Test fields with None values + assert item.volume_name is None + assert item.suffix is None + assert item.donor is None + assert item.price is None + + # Test remaining fields are UNSET + assert item.course_code is UNSET + assert item.status is UNSET diff --git a/tests/models/test_method_not_allowed_error.py b/tests/models/test_method_not_allowed_error.py new file mode 100644 index 0000000..c3b1b05 --- /dev/null +++ b/tests/models/test_method_not_allowed_error.py @@ -0,0 +1,74 @@ +import pytest + +from nlb_catalogue_client.models.method_not_allowed_error import MethodNotAllowedError +from nlb_catalogue_client.types import UNSET + + +@pytest.fixture() +def error_with_status() -> tuple[MethodNotAllowedError, dict]: + return MethodNotAllowedError( + error="Method Not Allowed", message="The requested resource does not support http method 'PUT'", status_code=405 + ), { + "error": "Method Not Allowed", + "message": "The requested resource does not support http method 'PUT'", + "statusCode": 405, + } + + +@pytest.fixture() +def error_without_status() -> tuple[MethodNotAllowedError, dict]: + return MethodNotAllowedError( + error="Method Not Allowed", message="The requested resource does not support http method 'PUT'" + ), {"error": "Method Not Allowed", "message": "The requested resource does not support http method 'PUT'"} + + +class TestMethodNotAllowedError: + @pytest.mark.parametrize( + "error,message,status_code,expected_status", + [ + ("Method Not Allowed", "The requested resource does not support http method 'PUT'", 405, 405), + ("Method Not Allowed", "The requested resource does not support http method 'PUT'", None, None), + ("Method Not Allowed", "The requested resource does not support http method 'PUT'", UNSET, UNSET), + ], + ) + def test_basic_initialization(self, error, message, status_code, expected_status): + error_obj = MethodNotAllowedError(error=error, message=message, status_code=status_code) + assert error_obj.error == error + assert error_obj.message == message + assert error_obj.status_code == expected_status + + def test_to_dict_with_status(self, error_with_status): + assert error_with_status[0].to_dict() == error_with_status[1] + + def test_to_dict_without_status(self, error_without_status): + assert error_without_status[0].to_dict() == error_without_status[1] + + def test_from_dict_with_status(self, error_with_status): + assert MethodNotAllowedError.from_dict(error_with_status[1]) == error_with_status[0] + + def test_from_dict_without_status(self, error_without_status): + assert MethodNotAllowedError.from_dict(error_without_status[1]) == error_without_status[0] + + @pytest.mark.parametrize( + "input_data", + [ + { + "error": "Method Not Allowed", + "message": "The requested resource does not support http method 'PUT'", + "statusCode": 405, + }, + {"error": "Method Not Allowed", "message": "The requested resource does not support http method 'PUT'"}, + ], + ) + def test_dict_roundtrip(self, input_data): + """Test that to_dict and from_dict are inverse operations""" + original = MethodNotAllowedError.from_dict(input_data) + dict_form = original.to_dict() + recreated = MethodNotAllowedError.from_dict(dict_form) + assert original == recreated + + def test_from_dict_preserves_original(self, error_with_status): + """Test that from_dict doesn't modify the input dictionary""" + input_dict = error_with_status[1].copy() + MethodNotAllowedError.from_dict(input_dict) + assert input_dict == error_with_status[1] diff --git a/tests/models/test_new_arrival_title.py b/tests/models/test_new_arrival_title.py new file mode 100644 index 0000000..0bc8a7e --- /dev/null +++ b/tests/models/test_new_arrival_title.py @@ -0,0 +1,795 @@ + +from nlb_catalogue_client.models.bib_format import BibFormat +from nlb_catalogue_client.models.new_arrival_title import NewArrivalTitle +from nlb_catalogue_client.types import UNSET + + +class TestNewArrivalTitle: + def setup_method(self): + """Setup common test objects""" + self.format_ = BibFormat(code="BK", name="BOOKS") + + def test_basic_initialization(self): + title = NewArrivalTitle( + format_=self.format_, + title="Sample Book", + native_title="样本书", + series_title=["Series 1", "Series 2"], + native_series_title=["系列 1", "系列 2"], + author="John Doe", + native_author="约翰·多伊", + other_authors=["Jane Smith", "Bob Wilson"], + native_other_authors=["简·史密斯", "鲍勃·威尔逊"], + language=["English", "Chinese"], + audience=["General", "Adult"], + audience_imda=["NC16", "PG"], + brn=99999999, + digital_id="overdrive123", + other_titles=["Alternative Title 1", "Alternative Title 2"], + native_other_titles=["其他标题 1", "其他标题 2"], + variant_titles=["Variant 1", "Variant 2"], + native_variant_titles=["变体 1", "变体 2"], + isbns=["9709999999"], + issns=["9999-9999"], + edition=["First Edition"], + native_edition=["第一版"], + publisher=["Sample Publisher"], + native_publisher=["示例出版社"], + publish_date="2023", + subjects=["Fiction", "Adventure"], + physical_description=["300 pages"], + native_physical_description=["300页"], + summary=["A fascinating story"], + native_summary=["一个引人入胜的故事"], + contents=["Chapter 1", "Chapter 2"], + native_contents=["第一章", "第二章"], + thesis=["PhD Thesis"], + native_thesis=["博士论文"], + notes=["Special Edition"], + native_notes=["特别版"], + active_reservations_count=3, + volume_note=["Volume 1"], + native_volume_note=["第一卷"], + frequency=["Monthly"], + native_frequency=["每月"], + credits_=["Editor: John Smith"], + native_credits=["编辑:约翰·史密斯"], + performers=["Actor: Jane Doe"], + native_performers=["演员:简·多伊"], + availability=True, + source="Library", + volumes=["2023 issue 1"], + ) + + assert title.format_ == self.format_ + assert title.title == "Sample Book" + assert title.native_title == "样本书" + assert title.series_title == ["Series 1", "Series 2"] + assert title.native_series_title == ["系列 1", "系列 2"] + assert title.author == "John Doe" + assert title.native_author == "约翰·多伊" + assert title.other_authors == ["Jane Smith", "Bob Wilson"] + assert title.native_other_authors == ["简·史密斯", "鲍勃·威尔逊"] + assert title.language == ["English", "Chinese"] + assert title.audience == ["General", "Adult"] + assert title.audience_imda == ["NC16", "PG"] + assert title.brn == 99999999 + assert title.digital_id == "overdrive123" + assert title.other_titles == ["Alternative Title 1", "Alternative Title 2"] + assert title.native_other_titles == ["其他标题 1", "其他标题 2"] + assert title.variant_titles == ["Variant 1", "Variant 2"] + assert title.native_variant_titles == ["变体 1", "变体 2"] + assert title.isbns == ["9709999999"] + assert title.issns == ["9999-9999"] + assert title.edition == ["First Edition"] + assert title.native_edition == ["第一版"] + assert title.publisher == ["Sample Publisher"] + assert title.native_publisher == ["示例出版社"] + assert title.publish_date == "2023" + assert title.subjects == ["Fiction", "Adventure"] + assert title.physical_description == ["300 pages"] + assert title.native_physical_description == ["300页"] + assert title.summary == ["A fascinating story"] + assert title.native_summary == ["一个引人入胜的故事"] + assert title.contents == ["Chapter 1", "Chapter 2"] + assert title.native_contents == ["第一章", "第二章"] + assert title.thesis == ["PhD Thesis"] + assert title.native_thesis == ["博士论文"] + assert title.notes == ["Special Edition"] + assert title.native_notes == ["特别版"] + assert title.active_reservations_count == 3 + assert title.volume_note == ["Volume 1"] + assert title.native_volume_note == ["第一卷"] + assert title.frequency == ["Monthly"] + assert title.native_frequency == ["每月"] + assert title.credits_ == ["Editor: John Smith"] + assert title.native_credits == ["编辑:约翰·史密斯"] + assert title.performers == ["Actor: Jane Doe"] + assert title.native_performers == ["演员:简·多伊"] + assert title.availability is True + assert title.source == "Library" + assert title.volumes == ["2023 issue 1"] + + def test_initialization_with_required_only(self): + title = NewArrivalTitle(format_=self.format_) + assert title.format_ == self.format_ + assert title.title is UNSET + assert title.native_title is UNSET + assert title.series_title is UNSET + assert title.native_series_title is UNSET + assert title.author is UNSET + assert title.native_author is UNSET + assert title.other_authors is UNSET + assert title.native_other_authors is UNSET + assert title.language is UNSET + assert title.audience is UNSET + assert title.audience_imda is UNSET + + def test_with_none_values(self): + """Test initialization with None values""" + title = NewArrivalTitle( + format_=self.format_, + title="Sample Book", + native_title=None, + series_title=None, + native_series_title=None, + native_author=None, + other_authors=None, + native_other_authors=None, + audience=None, + audience_imda=None, + digital_id=None, + other_titles=None, + native_other_titles=None, + variant_titles=None, + native_variant_titles=None, + issns=None, + edition=None, + native_edition=None, + native_publisher=None, + native_physical_description=None, + summary=None, + native_summary=None, + contents=None, + native_contents=None, + thesis=None, + native_thesis=None, + native_notes=None, + volume_note=None, + native_volume_note=None, + frequency=None, + native_frequency=None, + credits_=None, + native_credits=None, + performers=None, + native_performers=None, + source=None, + volumes=None, + ) + assert title.format_ == self.format_ + assert title.title == "Sample Book" + assert title.native_title is None + assert title.series_title is None + assert title.native_series_title is None + assert title.native_author is None + assert title.other_authors is None + assert title.native_other_authors is None + assert title.audience is None + assert title.audience_imda is None + assert title.digital_id is None + assert title.other_titles is None + assert title.native_other_titles is None + assert title.variant_titles is None + assert title.native_variant_titles is None + assert title.issns is None + assert title.edition is None + assert title.native_edition is None + assert title.native_publisher is None + assert title.native_physical_description is None + assert title.summary is None + assert title.native_summary is None + assert title.contents is None + assert title.native_contents is None + assert title.thesis is None + assert title.native_thesis is None + assert title.native_notes is None + assert title.volume_note is None + assert title.native_volume_note is None + assert title.frequency is None + assert title.native_frequency is None + assert title.credits_ is None + assert title.native_credits is None + assert title.performers is None + assert title.native_performers is None + assert title.source is None + assert title.volumes is None + + def test_to_dict_full(self): + title = NewArrivalTitle( + format_=self.format_, + title="Sample Book", + native_title="样本书", + series_title=["Series 1", "Series 2"], + native_series_title=["系列 1", "系列 2"], + author="John Doe", + native_author="约翰·多伊", + other_authors=["Jane Smith", "Bob Wilson"], + native_other_authors=["简·史密斯", "鲍勃·威尔逊"], + language=["English", "Chinese"], + audience=["General", "Adult"], + audience_imda=["NC16", "PG"], + brn=99999999, + digital_id="overdrive123", + other_titles=["Alternative Title 1", "Alternative Title 2"], + native_other_titles=["其他标题 1", "其他标题 2"], + variant_titles=["Variant 1", "Variant 2"], + native_variant_titles=["变体 1", "变体 2"], + isbns=["9709999999"], + issns=["9999-9999"], + edition=["First Edition"], + native_edition=["第一版"], + publisher=["Sample Publisher"], + native_publisher=["示例出版社"], + publish_date="2023", + subjects=["Fiction", "Adventure"], + physical_description=["300 pages"], + native_physical_description=["300页"], + summary=["A fascinating story"], + native_summary=["一个引人入胜的故事"], + contents=["Chapter 1", "Chapter 2"], + native_contents=["第一章", "第二章"], + thesis=["PhD Thesis"], + native_thesis=["博士论文"], + notes=["Special Edition"], + native_notes=["特别版"], + active_reservations_count=3, + volume_note=["Volume 1"], + native_volume_note=["第一卷"], + frequency=["Monthly"], + native_frequency=["每月"], + credits_=["Editor: John Smith"], + native_credits=["编辑:约翰·史密斯"], + performers=["Actor: Jane Doe"], + native_performers=["演员:简·多伊"], + availability=True, + source="Library", + volumes=["2023 issue 1"], + ) + + expected = { + "format": {"code": "BK", "name": "BOOKS"}, + "title": "Sample Book", + "nativeTitle": "样本书", + "seriesTitle": ["Series 1", "Series 2"], + "nativeSeriesTitle": ["系列 1", "系列 2"], + "author": "John Doe", + "nativeAuthor": "约翰·多伊", + "otherAuthors": ["Jane Smith", "Bob Wilson"], + "nativeOtherAuthors": ["简·史密斯", "鲍勃·威尔逊"], + "language": ["English", "Chinese"], + "audience": ["General", "Adult"], + "audienceImda": ["NC16", "PG"], + "brn": 99999999, + "digitalId": "overdrive123", + "otherTitles": ["Alternative Title 1", "Alternative Title 2"], + "nativeOtherTitles": ["其他标题 1", "其他标题 2"], + "variantTitles": ["Variant 1", "Variant 2"], + "nativeVariantTitles": ["变体 1", "变体 2"], + "isbns": ["9709999999"], + "issns": ["9999-9999"], + "edition": ["First Edition"], + "nativeEdition": ["第一版"], + "publisher": ["Sample Publisher"], + "nativePublisher": ["示例出版社"], + "publishDate": "2023", + "subjects": ["Fiction", "Adventure"], + "physicalDescription": ["300 pages"], + "nativePhysicalDescription": ["300页"], + "summary": ["A fascinating story"], + "nativeSummary": ["一个引人入胜的故事"], + "contents": ["Chapter 1", "Chapter 2"], + "nativeContents": ["第一章", "第二章"], + "thesis": ["PhD Thesis"], + "nativeThesis": ["博士论文"], + "notes": ["Special Edition"], + "nativeNotes": ["特别版"], + "activeReservationsCount": 3, + "volumeNote": ["Volume 1"], + "nativeVolumeNote": ["第一卷"], + "frequency": ["Monthly"], + "nativeFrequency": ["每月"], + "credits": ["Editor: John Smith"], + "nativeCredits": ["编辑:约翰·史密斯"], + "performers": ["Actor: Jane Doe"], + "nativePerformers": ["演员:简·多伊"], + "availability": True, + "source": "Library", + "volumes": ["2023 issue 1"], + "allowReservation": True, + "isRestricted": False, + "serial": False, + } + assert title.to_dict() == expected + + def test_to_dict_required(self): + """Test converting to dictionary with only required fields""" + title = NewArrivalTitle(format_=self.format_) + + expected = { + "format": {"code": "BK", "name": "BOOKS"}, + "allowReservation": True, + "isRestricted": False, + "serial": False, + } + assert title.to_dict() == expected + + def test_to_dict_with_none(self): + """Test converting to dictionary with None values""" + title = NewArrivalTitle( + format_=self.format_, + title="Sample Book", + native_title=None, + series_title=None, + native_series_title=None, + native_author=None, + other_authors=None, + native_other_authors=None, + audience=None, + audience_imda=None, + digital_id=None, + other_titles=None, + native_other_titles=None, + variant_titles=None, + native_variant_titles=None, + issns=None, + edition=None, + native_edition=None, + native_publisher=None, + native_physical_description=None, + summary=None, + native_summary=None, + contents=None, + native_contents=None, + thesis=None, + native_thesis=None, + native_notes=None, + volume_note=None, + native_volume_note=None, + frequency=None, + native_frequency=None, + credits_=None, + native_credits=None, + performers=None, + native_performers=None, + source=None, + volumes=None, + ) + + expected = { + "format": {"code": "BK", "name": "BOOKS"}, + "title": "Sample Book", + "nativeTitle": None, + "seriesTitle": None, + "nativeSeriesTitle": None, + "nativeAuthor": None, + "otherAuthors": None, + "nativeOtherAuthors": None, + "audience": None, + "audienceImda": None, + "digitalId": None, + "otherTitles": None, + "nativeOtherTitles": None, + "variantTitles": None, + "nativeVariantTitles": None, + "issns": None, + "edition": None, + "nativeEdition": None, + "nativePublisher": None, + "nativePhysicalDescription": None, + "summary": None, + "nativeSummary": None, + "contents": None, + "nativeContents": None, + "thesis": None, + "nativeThesis": None, + "nativeNotes": None, + "volumeNote": None, + "nativeVolumeNote": None, + "frequency": None, + "nativeFrequency": None, + "credits": None, + "nativeCredits": None, + "performers": None, + "nativePerformers": None, + "source": None, + "volumes": None, + "allowReservation": True, + "isRestricted": False, + "serial": False, + } + assert title.to_dict() == expected + + def test_from_dict_full(self): + data = { + "format": {"code": "BK", "name": "BOOKS"}, + "title": "Sample Book", + "nativeTitle": "样本书", + "seriesTitle": ["Series 1", "Series 2"], + "nativeSeriesTitle": ["系列 1", "系列 2"], + "author": "John Doe", + "nativeAuthor": "约翰·多伊", + "otherAuthors": ["Jane Smith", "Bob Wilson"], + "nativeOtherAuthors": ["简·史密斯", "鲍勃·威尔逊"], + "language": ["English", "Chinese"], + "audience": ["General", "Adult"], + "audienceImda": ["NC16", "PG"], + "brn": 99999999, + "digitalId": "overdrive123", + "otherTitles": ["Alternative Title 1", "Alternative Title 2"], + "nativeOtherTitles": ["其他标题 1", "其他标题 2"], + "variantTitles": ["Variant 1", "Variant 2"], + "nativeVariantTitles": ["变体 1", "变体 2"], + "isbns": ["9709999999"], + "issns": ["9999-9999"], + "edition": ["First Edition"], + "nativeEdition": ["第一版"], + "publisher": ["Sample Publisher"], + "nativePublisher": ["示例出版社"], + "publishDate": "2023", + "subjects": ["Fiction", "Adventure"], + "physicalDescription": ["300 pages"], + "nativePhysicalDescription": ["300页"], + "summary": ["A fascinating story"], + "nativeSummary": ["一个引人入胜的故事"], + "contents": ["Chapter 1", "Chapter 2"], + "nativeContents": ["第一章", "第二章"], + "thesis": ["PhD Thesis"], + "nativeThesis": ["博士论文"], + "notes": ["Special Edition"], + "nativeNotes": ["特别版"], + "activeReservationsCount": 3, + "volumeNote": ["Volume 1"], + "nativeVolumeNote": ["第一卷"], + "frequency": ["Monthly"], + "nativeFrequency": ["每月"], + "credits": ["Editor: John Smith"], + "nativeCredits": ["编辑:约翰·史密斯"], + "performers": ["Actor: Jane Doe"], + "nativePerformers": ["演员:简·多伊"], + "availability": True, + "source": "Library", + "volumes": ["2023 issue 1"], + "allowReservation": True, + "isRestricted": False, + "serial": False, + } + + title = NewArrivalTitle.from_dict(data) + assert title.format_.code == "BK" + assert title.format_.name == "BOOKS" + assert title.title == "Sample Book" + assert title.native_title == "样本书" + assert title.series_title == ["Series 1", "Series 2"] + assert title.native_series_title == ["系列 1", "系列 2"] + assert title.author == "John Doe" + assert title.native_author == "约翰·多伊" + assert title.other_authors == ["Jane Smith", "Bob Wilson"] + assert title.native_other_authors == ["简·史密斯", "鲍勃·威尔逊"] + assert title.language == ["English", "Chinese"] + assert title.audience == ["General", "Adult"] + assert title.audience_imda == ["NC16", "PG"] + assert title.brn == 99999999 + assert title.digital_id == "overdrive123" + assert title.other_titles == ["Alternative Title 1", "Alternative Title 2"] + assert title.native_other_titles == ["其他标题 1", "其他标题 2"] + assert title.variant_titles == ["Variant 1", "Variant 2"] + assert title.native_variant_titles == ["变体 1", "变体 2"] + assert title.isbns == ["9709999999"] + assert title.issns == ["9999-9999"] + assert title.edition == ["First Edition"] + assert title.native_edition == ["第一版"] + assert title.publisher == ["Sample Publisher"] + assert title.native_publisher == ["示例出版社"] + assert title.publish_date == "2023" + assert title.subjects == ["Fiction", "Adventure"] + assert title.physical_description == ["300 pages"] + assert title.native_physical_description == ["300页"] + assert title.summary == ["A fascinating story"] + assert title.native_summary == ["一个引人入胜的故事"] + assert title.contents == ["Chapter 1", "Chapter 2"] + assert title.native_contents == ["第一章", "第二章"] + assert title.thesis == ["PhD Thesis"] + assert title.native_thesis == ["博士论文"] + assert title.notes == ["Special Edition"] + assert title.native_notes == ["特别版"] + assert title.active_reservations_count == 3 + assert title.volume_note == ["Volume 1"] + assert title.native_volume_note == ["第一卷"] + assert title.frequency == ["Monthly"] + assert title.native_frequency == ["每月"] + assert title.credits_ == ["Editor: John Smith"] + assert title.native_credits == ["编辑:约翰·史密斯"] + assert title.performers == ["Actor: Jane Doe"] + assert title.native_performers == ["演员:简·多伊"] + assert title.availability is True + assert title.source == "Library" + assert title.volumes == ["2023 issue 1"] + + def test_from_dict_required_only(self): + """Test creating object from dictionary with only required fields""" + data = {"format": {"code": "BK", "name": "BOOKS"}} + + title = NewArrivalTitle.from_dict(data) + + # Verify required field is properly set + assert title.format_.code == "BK" + assert title.format_.name == "BOOKS" + + # Verify all optional fields are UNSET + assert title.brn is UNSET + assert title.digital_id is UNSET + assert title.other_titles is UNSET + assert title.native_other_titles is UNSET + assert title.variant_titles is UNSET + assert title.native_variant_titles is UNSET + assert title.other_authors is UNSET + assert title.native_other_authors is UNSET + assert title.isbns is UNSET + assert title.issns is UNSET + assert title.edition is UNSET + assert title.native_edition is UNSET + assert title.publisher is UNSET + assert title.native_publisher is UNSET + assert title.publish_date is UNSET + assert title.subjects is UNSET + assert title.physical_description is UNSET + assert title.native_physical_description is UNSET + assert title.summary is UNSET + assert title.native_summary is UNSET + assert title.contents is UNSET + assert title.native_contents is UNSET + assert title.thesis is UNSET + assert title.native_thesis is UNSET + assert title.notes is UNSET + assert title.native_notes is UNSET + assert title.active_reservations_count is UNSET + assert title.audience is UNSET + assert title.audience_imda is UNSET + assert title.language is UNSET + assert title.volume_note is UNSET + assert title.native_volume_note is UNSET + assert title.frequency is UNSET + assert title.native_frequency is UNSET + assert title.credits_ is UNSET + assert title.native_credits is UNSET + assert title.performers is UNSET + assert title.native_performers is UNSET + + def test_from_dict_with_none(self): + """Test creating object from dictionary with None values""" + data = { + "format": {"code": "BK", "name": "BOOKS"}, + "title": "Sample Book", + "nativeTitle": None, + "seriesTitle": None, + "nativeSeriesTitle": None, + "author": None, + "nativeAuthor": None, + "otherAuthors": None, + "nativeOtherAuthors": None, + "language": None, + "audience": None, + "audienceImda": None, + "brn": None, + "digitalId": None, + "otherTitles": None, + "nativeOtherTitles": None, + "variantTitles": None, + "nativeVariantTitles": None, + "isbns": None, + "issns": None, + "edition": None, + "nativeEdition": None, + "publisher": None, + "nativePublisher": None, + "publishDate": None, + "subjects": None, + "physicalDescription": None, + "nativePhysicalDescription": None, + "summary": None, + "nativeSummary": None, + "contents": None, + "nativeContents": None, + "thesis": None, + "nativeThesis": None, + "notes": None, + "nativeNotes": None, + "activeReservationsCount": None, + "volumeNote": None, + "nativeVolumeNote": None, + "frequency": None, + "nativeFrequency": None, + "credits": None, + "nativeCredits": None, + "performers": None, + "nativePerformers": None, + "availability": None, + "source": None, + "volumes": None, + "allowReservation": True, + "isRestricted": False, + "serial": False, + } + + title = NewArrivalTitle.from_dict(data) + + # Verify required fields and non-None value + assert title.format_.code == "BK" + assert title.format_.name == "BOOKS" + assert title.title == "Sample Book" + + # Verify all None values are properly deserialized + assert title.native_title is None + assert title.series_title is None + assert title.native_series_title is None + assert title.author is None + assert title.native_author is None + assert title.other_authors is None + assert title.native_other_authors is None + assert title.language is None + assert title.audience is None + assert title.audience_imda is None + assert title.brn is None + assert title.digital_id is None + assert title.other_titles is None + assert title.native_other_titles is None + assert title.variant_titles is None + assert title.native_variant_titles is None + assert title.isbns is None + assert title.issns is None + assert title.edition is None + assert title.native_edition is None + assert title.publisher is None + assert title.native_publisher is None + assert title.publish_date is None + assert title.subjects is None + assert title.physical_description is None + assert title.native_physical_description is None + assert title.summary is None + assert title.native_summary is None + assert title.contents is None + assert title.native_contents is None + assert title.thesis is None + assert title.native_thesis is None + assert title.notes is None + assert title.native_notes is None + assert title.active_reservations_count is None + assert title.volume_note is None + assert title.native_volume_note is None + assert title.frequency is None + assert title.native_frequency is None + assert title.credits_ is None + assert title.native_credits is None + assert title.performers is None + assert title.native_performers is None + assert title.availability is None + assert title.source is None + assert title.volumes is None + + # Verify default boolean values + assert title.allow_reservation is True + assert title.is_restricted is False + assert title.serial is False + + def test_from_dict_with_invalid_list_fields(self): + """Test creating object from dictionary with invalid list field types and verify TypeError handling""" + data = { + "format": {"code": "BK", "name": "BOOKS"}, + "title": "Sample Book", + # Test all list fields with invalid types + "seriesTitle": "Not a list", + "nativeSeriesTitle": 123, + "otherAuthors": {"key": "value"}, + "nativeOtherAuthors": True, + "otherTitles": 456, + "nativeOtherTitles": "Not a list", + "variantTitles": {"key": "value"}, + "nativeVariantTitles": 789, + "isbns": "Not a list", + "issns": {"key": "value"}, + "edition": True, + "nativeEdition": 101, + "publisher": "Not a list", + "nativePublisher": {"key": "value"}, + "subjects": True, + "physicalDescription": 102, + "nativePhysicalDescription": "Not a list", + "summary": {"key": "value"}, + "nativeSummary": 103, + "contents": "Not a list", + "nativeContents": {"key": "value"}, + "thesis": True, + "nativeThesis": 104, + "notes": "Not a list", + "nativeNotes": {"key": "value"}, + "volumeNote": True, + "nativeVolumeNote": 105, + "frequency": "Not a list", + "nativeFrequency": {"key": "value"}, + "credits": True, + "nativeCredits": 106, + "performers": "Not a list", + "nativePerformers": {"key": "value"}, + "volumes": True, + "audience": 107, + "audienceImda": "Not a list", + "language": {"key": "value"}, + } + + # Test normal initialization with invalid list fields + title = NewArrivalTitle.from_dict(data) + + # Verify required fields are still properly set + assert title.format_.code == "BK" + assert title.format_.name == "BOOKS" + assert title.title == "Sample Book" + + # Verify all invalid list fields are returned as-is + assert title.series_title == "Not a list" + assert title.native_series_title == 123 + assert title.other_authors == {"key": "value"} + assert title.native_other_authors is True + assert title.other_titles == 456 + assert title.native_other_titles == "Not a list" + assert title.variant_titles == {"key": "value"} + assert title.native_variant_titles == 789 + assert title.isbns == "Not a list" + assert title.issns == {"key": "value"} + assert title.edition is True + assert title.native_edition == 101 + assert title.publisher == "Not a list" + assert title.native_publisher == {"key": "value"} + assert title.subjects is True + assert title.physical_description == 102 + assert title.native_physical_description == "Not a list" + assert title.summary == {"key": "value"} + assert title.native_summary == 103 + assert title.contents == "Not a list" + assert title.native_contents == {"key": "value"} + assert title.thesis is True + assert title.native_thesis == 104 + assert title.notes == "Not a list" + assert title.native_notes == {"key": "value"} + assert title.volume_note is True + assert title.native_volume_note == 105 + assert title.frequency == "Not a list" + assert title.native_frequency == {"key": "value"} + assert title.credits_ is True + assert title.native_credits == 106 + assert title.performers == "Not a list" + assert title.native_performers == {"key": "value"} + assert title.volumes is True + assert title.audience == 107 + assert title.audience_imda == "Not a list" + assert title.language == {"key": "value"} + + # Test invalid types for non-list fields + invalid_data = { + "format": {"code": "BK", "name": "BOOKS"}, + "brn": "not an integer", + "availability": "not a boolean", + "allowReservation": "not a boolean", + "isRestricted": "not a boolean", + "serial": "not a boolean", + "activeReservationsCount": "not an integer", + } + + title = NewArrivalTitle.from_dict(invalid_data) + + # Verify non-list fields with invalid types are stored as-is + assert title.brn == "not an integer" + assert title.availability == "not a boolean" + assert title.allow_reservation == "not a boolean" + assert title.is_restricted == "not a boolean" + assert title.serial == "not a boolean" + assert title.active_reservations_count == "not an integer" diff --git a/tests/models/test_not_found_error.py b/tests/models/test_not_found_error.py new file mode 100644 index 0000000..203ef93 --- /dev/null +++ b/tests/models/test_not_found_error.py @@ -0,0 +1,69 @@ +import pytest + +from nlb_catalogue_client.models.not_found_error import NotFoundError +from nlb_catalogue_client.types import UNSET + + +@pytest.fixture() +def error_with_status() -> tuple[NotFoundError, dict]: + return NotFoundError(error="Not Found", message="Record(s) not found", status_code=404), { + "error": "Not Found", + "message": "Record(s) not found", + "statusCode": 404, + } + + +@pytest.fixture() +def error_without_status() -> tuple[NotFoundError, dict]: + return NotFoundError(error="Not Found", message="Record(s) not found"), { + "error": "Not Found", + "message": "Record(s) not found", + } + + +class TestNotFoundError: + @pytest.mark.parametrize( + "error,message,status_code,expected_status", + [ + ("Not Found", "Record(s) not found", 404, 404), + ("Not Found", "Record(s) not found", None, None), + ("Not Found", "Record(s) not found", UNSET, UNSET), + ], + ) + def test_basic_initialization(self, error, message, status_code, expected_status): + error_obj = NotFoundError(error=error, message=message, status_code=status_code) + assert error_obj.error == error + assert error_obj.message == message + assert error_obj.status_code == expected_status + + def test_to_dict_with_status(self, error_with_status): + assert error_with_status[0].to_dict() == error_with_status[1] + + def test_to_dict_without_status(self, error_without_status): + assert error_without_status[0].to_dict() == error_without_status[1] + + def test_from_dict_with_status(self, error_with_status): + assert NotFoundError.from_dict(error_with_status[1]) == error_with_status[0] + + def test_from_dict_without_status(self, error_without_status): + assert NotFoundError.from_dict(error_without_status[1]) == error_without_status[0] + + @pytest.mark.parametrize( + "input_data", + [ + {"error": "Not Found", "message": "Record(s) not found", "statusCode": 404}, + {"error": "Not Found", "message": "Record(s) not found"}, + ], + ) + def test_dict_roundtrip(self, input_data): + """Test that to_dict and from_dict are inverse operations""" + original = NotFoundError.from_dict(input_data) + dict_form = original.to_dict() + recreated = NotFoundError.from_dict(dict_form) + assert original == recreated + + def test_from_dict_preserves_original(self, error_with_status): + """Test that from_dict doesn't modify the input dictionary""" + input_dict = error_with_status[1].copy() + NotFoundError.from_dict(input_dict) + assert input_dict == error_with_status[1] diff --git a/tests/models/test_not_implemented_error.py b/tests/models/test_not_implemented_error.py new file mode 100644 index 0000000..730dee4 --- /dev/null +++ b/tests/models/test_not_implemented_error.py @@ -0,0 +1,69 @@ +import pytest + +from nlb_catalogue_client.models.not_implemented_error import NotImplementedError_ +from nlb_catalogue_client.types import UNSET + + +@pytest.fixture() +def error_with_status() -> tuple[NotImplementedError_, dict]: + return NotImplementedError_(error="Not Implemented", message="Not Implemented", status_code=501), { + "error": "Not Implemented", + "message": "Not Implemented", + "statusCode": 501, + } + + +@pytest.fixture() +def error_without_status() -> tuple[NotImplementedError_, dict]: + return NotImplementedError_(error="Not Implemented", message="Not Implemented"), { + "error": "Not Implemented", + "message": "Not Implemented", + } + + +class TestNotImplementedError: + @pytest.mark.parametrize( + "error,message,status_code,expected_status", + [ + ("Not Implemented", "Not Implemented", 501, 501), + ("Not Implemented", "Not Implemented", None, None), + ("Not Implemented", "Not Implemented", UNSET, UNSET), + ], + ) + def test_basic_initialization(self, error, message, status_code, expected_status): + error_obj = NotImplementedError_(error=error, message=message, status_code=status_code) + assert error_obj.error == error + assert error_obj.message == message + assert error_obj.status_code == expected_status + + def test_to_dict_with_status(self, error_with_status): + assert error_with_status[0].to_dict() == error_with_status[1] + + def test_to_dict_without_status(self, error_without_status): + assert error_without_status[0].to_dict() == error_without_status[1] + + def test_from_dict_with_status(self, error_with_status): + assert NotImplementedError_.from_dict(error_with_status[1]) == error_with_status[0] + + def test_from_dict_without_status(self, error_without_status): + assert NotImplementedError_.from_dict(error_without_status[1]) == error_without_status[0] + + @pytest.mark.parametrize( + "input_data", + [ + {"error": "Not Implemented", "message": "Not Implemented", "statusCode": 501}, + {"error": "Not Implemented", "message": "Not Implemented"}, + ], + ) + def test_dict_roundtrip(self, input_data): + """Test that to_dict and from_dict are inverse operations""" + original = NotImplementedError_.from_dict(input_data) + dict_form = original.to_dict() + recreated = NotImplementedError_.from_dict(dict_form) + assert original == recreated + + def test_from_dict_preserves_original(self, error_with_status): + """Test that from_dict doesn't modify the input dictionary""" + input_dict = error_with_status[1].copy() + NotImplementedError_.from_dict(input_dict) + assert input_dict == error_with_status[1] diff --git a/tests/models/test_search_most_checkouts_titles_response.py b/tests/models/test_search_most_checkouts_titles_response.py new file mode 100644 index 0000000..9e3313d --- /dev/null +++ b/tests/models/test_search_most_checkouts_titles_response.py @@ -0,0 +1,176 @@ +from nlb_catalogue_client.models.checkouts_title import CheckoutsTitle +from nlb_catalogue_client.models.checkouts_trend import CheckoutsTrend +from nlb_catalogue_client.models.search_most_checkouts_titles_response import SearchMostCheckoutsTitlesResponse +from nlb_catalogue_client.types import UNSET + + +class TestSearchMostCheckoutsTitlesResponse: + def setup_method(self): + """Setup common test objects""" + self.checkouts_title = CheckoutsTitle( + title="Sample Book", + native_title="样本书", + author="John Doe", + native_author="约翰·多伊", + isbns=["9709999999"], + checkouts_count=100, + ) + + self.checkouts_trend = CheckoutsTrend( + language="English", + age_level="A", + fiction=True, + singapore_collection=False, + checkouts_titles=[self.checkouts_title], + ) + + def test_basic_initialization(self): + """Test initialization with all fields""" + response = SearchMostCheckoutsTitlesResponse( + checkouts_trends=[self.checkouts_trend], + ) + + assert response.checkouts_trends == [self.checkouts_trend] + + def test_initialization_with_unset(self): + """Test initialization with UNSET values""" + response = SearchMostCheckoutsTitlesResponse() + + assert response.checkouts_trends is UNSET + + def test_to_dict_full(self): + """Test converting to dictionary with all fields""" + response = SearchMostCheckoutsTitlesResponse( + checkouts_trends=[self.checkouts_trend], + ) + + expected = { + "checkoutsTrends": [ + { + "language": "English", + "ageLevel": "A", + "fiction": True, + "singaporeCollection": False, + "checkoutsTitles": [ + { + "title": "Sample Book", + "nativeTitle": "样本书", + "author": "John Doe", + "nativeAuthor": "约翰·多伊", + "isbns": ["9709999999"], + "checkoutsCount": 100, + } + ], + } + ] + } + + assert response.to_dict() == expected + + def test_to_dict_unset(self): + """Test converting to dictionary with no fields set""" + response = SearchMostCheckoutsTitlesResponse() + + expected = {} + + assert response.to_dict() == expected + + def test_from_dict_full(self): + """Test creating object from dictionary with all fields""" + data = { + "checkoutsTrends": [ + { + "language": "English", + "ageLevel": "A", + "fiction": True, + "singaporeCollection": False, + "checkoutsTitles": [ + { + "title": "Sample Book", + "nativeTitle": "样本书", + "author": "John Doe", + "nativeAuthor": "约翰·多伊", + "isbns": ["9709999999"], + "checkoutsCount": 100, + } + ], + } + ] + } + + response = SearchMostCheckoutsTitlesResponse.from_dict(data) + + assert len(response.checkouts_trends) == 1 + trend = response.checkouts_trends[0] + assert trend.language == "English" + assert trend.age_level == "A" + assert trend.fiction is True + assert trend.singapore_collection is False + assert len(trend.checkouts_titles) == 1 + title = trend.checkouts_titles[0] + assert title.title == "Sample Book" + assert title.native_title == "样本书" + assert title.author == "John Doe" + assert title.native_author == "约翰·多伊" + assert title.isbns == ["9709999999"] + assert title.checkouts_count == 100 + + def test_from_dict_empty(self): + """Test creating object from empty dictionary""" + data = {} + + response = SearchMostCheckoutsTitlesResponse.from_dict(data) + + assert response.checkouts_trends == [] + + def test_from_dict_with_none(self): + """Test creating object from dictionary with None values""" + data = { + "checkoutsTrends": [ + { + "language": None, + "ageLevel": None, + "checkoutsTitles": None, + } + ] + } + + response = SearchMostCheckoutsTitlesResponse.from_dict(data) + trend = response.checkouts_trends[0] + + assert trend.language is None + assert trend.age_level is None + assert trend.checkouts_titles is None + + def test_from_dict_with_invalid_values(self): + """Test creating object from dictionary with invalid field types""" + data = { + "checkoutsTrends": [ + { + "language": 123, # Should be string + "ageLevel": True, # Should be string + "fiction": "Not a boolean", # Should be boolean + "singaporeCollection": "Not a boolean", # Should be boolean + "checkoutsTitles": [ + { + "title": 123, # Should be string + "isbns": "Not a list", # Should be list + "checkoutsCount": "Not an integer", # Should be integer + } + ], + } + ] + } + + response = SearchMostCheckoutsTitlesResponse.from_dict(data) + trend = response.checkouts_trends[0] + title = trend.checkouts_titles[0] + + # Verify invalid values are returned as-is + assert trend.language == 123 + assert trend.age_level is True + assert trend.fiction == "Not a boolean" + assert trend.singapore_collection == "Not a boolean" + assert title.title == 123 + assert title.isbns == "Not a list" + assert title.checkouts_count == "Not an integer" diff --git a/tests/models/test_search_new_titles_response.py b/tests/models/test_search_new_titles_response.py new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/tests/models/test_search_new_titles_response.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/models/test_search_new_titles_response_v2.py b/tests/models/test_search_new_titles_response_v2.py new file mode 100644 index 0000000..c38b68e --- /dev/null +++ b/tests/models/test_search_new_titles_response_v2.py @@ -0,0 +1,259 @@ +from nlb_catalogue_client.models.bib_format import BibFormat +from nlb_catalogue_client.models.new_arrival_title import NewArrivalTitle +from nlb_catalogue_client.models.search_new_titles_response_v2 import SearchNewTitlesResponseV2 +from nlb_catalogue_client.types import UNSET + + +class TestSearchNewTitlesResponseV2: + def setup_method(self): + """Setup common test objects""" + self.format_ = BibFormat(code="BK", name="BOOKS") + self.new_arrival_title = NewArrivalTitle( + format_=self.format_, + brn=99999999, + digital_id="overdrive123", + other_titles=["Alternative Title 1"], + native_other_titles=["其他标题 1"], + variant_titles=["Variant 1"], + native_variant_titles=["变体 1"], + other_authors=["Jane Smith"], + native_other_authors=["简·史密斯"], + isbns=["9709999999"], + issns=["9999-9999"], + edition=["First Edition"], + native_edition=["第一版"], + publisher=["Sample Publisher"], + native_publisher=["示例出版社"], + publish_date="2023", + subjects=["Fiction"], + physical_description=["300 pages"], + native_physical_description=["300页"], + summary=["A story"], + native_summary=["故事"], + contents=["Chapter 1"], + native_contents=["第一章"], + thesis=["PhD Thesis"], + native_thesis=["博士论文"], + notes=["Special Edition"], + native_notes=["特别版"], + allow_reservation=True, + is_restricted=False, + active_reservations_count=3, + audience=["General", "Adult"], + audience_imda=["NC16", "PG"], + language=["English", "Chinese"], + serial=False, + volume_note=["Volume 1"], + native_volume_note=["第一卷"], + frequency=["Monthly"], + native_frequency=["每月"], + credits_=["Editor: John"], + native_credits=["编辑:约翰"], + performers=["Actor: Jane"], + native_performers=["演员:简"], + availability=True, + source="Library", + volumes=["2023 issue 1"], + title="Sample Book", + native_title="样本书", + series_title=["Series 1"], + native_series_title=["系列 1"], + author="John Doe", + native_author="约翰·多伊", + ) + + def test_basic_initialization(self): + """Test initialization with all fields""" + response = SearchNewTitlesResponseV2( + total_records=100, + count=1, + has_more_records=True, + next_records_offset=10, + titles=[self.new_arrival_title], + ) + + assert response.total_records == 100 + assert response.count == 1 + assert response.has_more_records is True + assert response.next_records_offset == 10 + assert response.titles == [self.new_arrival_title] + + def test_initialization_with_unset(self): + """Test initialization with UNSET values""" + response = SearchNewTitlesResponseV2() + + assert response.total_records is UNSET + assert response.count is UNSET + assert response.has_more_records is False + assert response.next_records_offset is UNSET + assert response.titles is UNSET + + def test_to_dict_full(self): + """Test converting to dictionary with all fields""" + response = SearchNewTitlesResponseV2( + total_records=100, + count=1, + has_more_records=True, + next_records_offset=10, + titles=[self.new_arrival_title], + ) + + expected = { + "totalRecords": 100, + "count": 1, + "hasMoreRecords": True, + "nextRecordsOffset": 10, + "titles": [ + { + "format": {"code": "BK", "name": "BOOKS"}, + "brn": 99999999, + "digitalId": "overdrive123", + "otherTitles": ["Alternative Title 1"], + "nativeOtherTitles": ["其他标题 1"], + "variantTitles": ["Variant 1"], + "nativeVariantTitles": ["变体 1"], + "otherAuthors": ["Jane Smith"], + "nativeOtherAuthors": ["简·史密斯"], + "isbns": ["9709999999"], + "issns": ["9999-9999"], + "edition": ["First Edition"], + "nativeEdition": ["第一版"], + "publisher": ["Sample Publisher"], + "nativePublisher": ["示例出版社"], + "publishDate": "2023", + "subjects": ["Fiction"], + "physicalDescription": ["300 pages"], + "nativePhysicalDescription": ["300页"], + "summary": ["A story"], + "nativeSummary": ["故事"], + "contents": ["Chapter 1"], + "nativeContents": ["第一章"], + "thesis": ["PhD Thesis"], + "nativeThesis": ["博士论文"], + "notes": ["Special Edition"], + "nativeNotes": ["特别版"], + "allowReservation": True, + "isRestricted": False, + "activeReservationsCount": 3, + "audience": ["General", "Adult"], + "audienceImda": ["NC16", "PG"], + "language": ["English", "Chinese"], + "serial": False, + "volumeNote": ["Volume 1"], + "nativeVolumeNote": ["第一卷"], + "frequency": ["Monthly"], + "nativeFrequency": ["每月"], + "credits": ["Editor: John"], + "nativeCredits": ["编辑:约翰"], + "performers": ["Actor: Jane"], + "nativePerformers": ["演员:简"], + "availability": True, + "source": "Library", + "volumes": ["2023 issue 1"], + "title": "Sample Book", + "nativeTitle": "样本书", + "seriesTitle": ["Series 1"], + "nativeSeriesTitle": ["系列 1"], + "author": "John Doe", + "nativeAuthor": "约翰·多伊", + } + ], + } + + assert response.to_dict() == expected + + def test_from_dict_full(self): + """Test creating object from dictionary with all fields""" + data = { + "totalRecords": 100, + "count": 1, + "hasMoreRecords": True, + "nextRecordsOffset": 10, + "titles": [ + { + "format": {"code": "BK", "name": "BOOKS"}, + "brn": 99999999, + "digitalId": "overdrive123", + "otherTitles": ["Alternative Title 1"], + "nativeOtherTitles": ["其他标题 1"], + "variantTitles": ["Variant 1"], + "nativeVariantTitles": ["变体 1"], + "otherAuthors": ["Jane Smith"], + "nativeOtherAuthors": ["简·史密斯"], + "isbns": ["9709999999"], + "issns": ["9999-9999"], + "edition": ["First Edition"], + "nativeEdition": ["第一版"], + "publisher": ["Sample Publisher"], + "nativePublisher": ["示例出版社"], + "publishDate": "2023", + "subjects": ["Fiction"], + "physicalDescription": ["300 pages"], + "nativePhysicalDescription": ["300页"], + "summary": ["A story"], + "nativeSummary": ["故事"], + "contents": ["Chapter 1"], + "nativeContents": ["第一章"], + "thesis": ["PhD Thesis"], + "nativeThesis": ["博士论文"], + "notes": ["Special Edition"], + "nativeNotes": ["特别版"], + "allowReservation": True, + "isRestricted": False, + "activeReservationsCount": 3, + "audience": ["General", "Adult"], + "audienceImda": ["NC16", "PG"], + "language": ["English", "Chinese"], + "serial": False, + "volumeNote": ["Volume 1"], + "nativeVolumeNote": ["第一卷"], + "frequency": ["Monthly"], + "nativeFrequency": ["每月"], + "credits": ["Editor: John"], + "nativeCredits": ["编辑:约翰"], + "performers": ["Actor: Jane"], + "nativePerformers": ["演员:简"], + "availability": True, + "source": "Library", + "volumes": ["2023 issue 1"], + "title": "Sample Book", + "nativeTitle": "样本书", + "seriesTitle": ["Series 1"], + "nativeSeriesTitle": ["系列 1"], + "author": "John Doe", + "nativeAuthor": "约翰·多伊", + } + ], + } + + response = SearchNewTitlesResponseV2.from_dict(data) + + assert response.total_records == 100 + assert response.count == 1 + assert response.has_more_records is True + assert response.next_records_offset == 10 + assert response.titles + assert len(response.titles) == 1 + title = response.titles[0] + assert isinstance(title, NewArrivalTitle) + assert title.format_.code == "BK" + assert title.format_.name == "BOOKS" + assert title.brn == 99999999 + + def test_from_dict_with_none(self): + """Test creating object from dictionary with None values""" + data = { + "totalRecords": None, + "count": None, + "hasMoreRecords": None, + "nextRecordsOffset": None, + "titles": None, + } + + response = SearchNewTitlesResponseV2.from_dict(data) + + assert response.total_records is None + assert response.count is None + assert response.has_more_records is None + assert response.next_records_offset is None + assert response.titles == [] diff --git a/tests/models/test_search_titles_response_v2.py b/tests/models/test_search_titles_response_v2.py new file mode 100644 index 0000000..adf9a3e --- /dev/null +++ b/tests/models/test_search_titles_response_v2.py @@ -0,0 +1,115 @@ +from nlb_catalogue_client.models.bib_format import BibFormat +from nlb_catalogue_client.models.facet import Facet +from nlb_catalogue_client.models.facet_data import FacetData +from nlb_catalogue_client.models.search_titles_response_v2 import SearchTitlesResponseV2 +from nlb_catalogue_client.models.title_record import TitleRecord +from nlb_catalogue_client.models.title_summary import TitleSummary +from nlb_catalogue_client.types import UNSET + + +class TestSearchTitlesResponseV2: + def setup_method(self): + """Setup common test objects""" + self.format_ = BibFormat(code="BK", name="BOOKS") + + self.title_record = TitleRecord( + format_=self.format_, + ) + self.facet_data = FacetData(id="audioBook", data="Audio Book", count=17) + self.title_summary = TitleSummary( + title="Sample Book", + native_title="样本书", + series_title=["Series 1"], + native_series_title=["系列 1"], + author="John Doe", + native_author="约翰·多伊", + cover_url=UNSET, + records=[self.title_record], + ) + self.facet = Facet(id="audioBook", name="Audio Book", values=[self.facet_data]) + + def test_basic_initialization(self): + """Test initialization with all fields""" + response = SearchTitlesResponseV2( + total_records=100, + count=1, + next_records_offset=10, + has_more_records=True, + titles=[self.title_summary], + facets=[self.facet], + ) + + assert response.total_records == 100 + assert response.count == 1 + assert response.next_records_offset == 10 + assert response.has_more_records is True + assert response.titles == [self.title_summary] + assert response.facets == [self.facet] + + def test_initialization_with_unset(self): + """Test initialization with UNSET values""" + response = SearchTitlesResponseV2() + + assert response.total_records is UNSET + assert response.count is UNSET + assert response.next_records_offset is UNSET + assert response.has_more_records is False + assert response.titles == UNSET + assert response.facets == UNSET + + def test_to_dict_full(self): + """Test converting to dictionary with all fields""" + response = SearchTitlesResponseV2( + total_records=100, + count=1, + next_records_offset=10, + has_more_records=True, + titles=[self.title_summary], + facets=[self.facet], + ) + + expected = { + "totalRecords": 100, + "count": 1, + "nextRecordsOffset": 10, + "hasMoreRecords": True, + "titles": [self.title_summary.to_dict()], + "facets": [self.facet.to_dict()], + } + + assert response.to_dict() == expected + + def test_to_dict_empty(self): + """Test converting to dictionary with no fields set""" + response = SearchTitlesResponseV2() + + assert response.to_dict() == {"hasMoreRecords": False} + + def test_from_dict(self): + """Test creating object from dictionary""" + data = { + "totalRecords": 100, + "count": 1, + "nextRecordsOffset": 10, + "hasMoreRecords": True, + "titles": [self.title_summary.to_dict()], + "facets": [self.facet.to_dict()], + } + response = SearchTitlesResponseV2.from_dict(data) + assert response.total_records == 100 + assert response.count == 1 + assert response.next_records_offset == 10 + assert response.has_more_records is True + assert response.titles == [self.title_summary] + assert response.facets == [self.facet] + + def test_from_dict_with_unset(self): + """Test creating object from dictionary with unset fields""" + data = {} + response = SearchTitlesResponseV2.from_dict(data) + assert response.has_more_records is UNSET + assert response.total_records is UNSET + assert response.count is UNSET + assert response.next_records_offset is UNSET + assert response.titles == [] + assert response.facets == [] diff --git a/tests/models/test_service_unavailable_error.py b/tests/models/test_service_unavailable_error.py new file mode 100644 index 0000000..48c219c --- /dev/null +++ b/tests/models/test_service_unavailable_error.py @@ -0,0 +1,49 @@ +import pytest + +from nlb_catalogue_client.models.service_unavailable_error import ServiceUnavailableError +from nlb_catalogue_client.types import UNSET + + +@pytest.fixture() +def error_with_status() -> tuple[ServiceUnavailableError, dict]: + return ServiceUnavailableError(error="Service unavailable", message="Service unavailable", status_code=503), { + "error": "Service unavailable", + "message": "Service unavailable", + "statusCode": 503, + } + + +@pytest.fixture() +def error_without_status() -> tuple[ServiceUnavailableError, dict]: + return ServiceUnavailableError(error="Service unavailable", message="Service unavailable"), { + "error": "Service unavailable", + "message": "Service unavailable", + } + + +class TestServiceUnavailableError: + @pytest.mark.parametrize( + "error,message,status_code,expected_status", + [ + ("Service unavailable", "Service unavailable", 503, 503), + ("Service unavailable", "Service unavailable", None, None), + ("Service unavailable", "Service unavailable", UNSET, UNSET), + ], + ) + def test_basic_initialization(self, error, message, status_code, expected_status): + error_obj = ServiceUnavailableError(error=error, message=message, status_code=status_code) + assert error_obj.error == error + assert error_obj.message == message + assert error_obj.status_code == expected_status + + def test_to_dict_with_status(self, error_with_status): + assert error_with_status[0].to_dict() == error_with_status[1] + + def test_to_dict_without_status(self, error_without_status): + assert error_without_status[0].to_dict() == error_without_status[1] + + def test_from_dict_with_status(self, error_with_status): + assert ServiceUnavailableError.from_dict(error_with_status[1]) == error_with_status[0] + + def test_from_dict_without_status(self, error_without_status): + assert ServiceUnavailableError.from_dict(error_without_status[1]) == error_without_status[0] diff --git a/tests/models/test_status.py b/tests/models/test_status.py new file mode 100644 index 0000000..1208035 --- /dev/null +++ b/tests/models/test_status.py @@ -0,0 +1,78 @@ +from datetime import date + +from nlb_catalogue_client.models.status import Status +from nlb_catalogue_client.types import UNSET + + +class TestStatus: + def test_basic_initialization(self): + status = Status(name="In Transit", code="I", set_date=date(2019, 7, 21)) + assert status.name == "In Transit" + assert status.code == "I" + assert status.set_date == date(2019, 7, 21) + + def test_initialization_with_required_only(self): + status = Status(name="In Transit") + assert status.name == "In Transit" + assert status.code is UNSET + assert status.set_date is UNSET + + def test_with_none_values(self): + status = Status(name="In Transit", code=None, set_date=None) + assert status.name == "In Transit" + assert status.code is None + assert status.set_date is None + + def test_to_dict_full(self): + status = Status(name="In Transit", code="I", set_date=date(2019, 7, 21)) + expected = {"name": "In Transit", "code": "I", "setDate": "2019-07-21"} + assert status.to_dict() == expected + + def test_to_dict_required_only(self): + status = Status(name="In Transit") + expected = {"name": "In Transit"} + assert status.to_dict() == expected + + def test_to_dict_with_none(self): + status = Status(name="In Transit", code=None, set_date=None) + expected = {"name": "In Transit", "code": None, "setDate": None} + assert status.to_dict() == expected + + def test_from_dict_full(self): + data = {"name": "In Transit", "code": "I", "setDate": "2019-07-21"} + status = Status.from_dict(data) + assert status.name == "In Transit" + assert status.code == "I" + assert status.set_date == date(2019, 7, 21) + + def test_from_dict_required_only(self): + data = {"name": "In Transit"} + status = Status.from_dict(data) + assert status.name == "In Transit" + assert status.code is UNSET + assert status.set_date is UNSET + + def test_from_dict_with_none(self): + data = {"name": "In Transit", "code": None, "setDate": None} + status = Status.from_dict(data) + assert status.name == "In Transit" + assert status.code is None + assert status.set_date is None + + def test_from_dict_with_unset(self): + data = {"name": "In Transit", "code": UNSET, "setDate": UNSET} + status = Status.from_dict(data) + assert status.name == "In Transit" + assert status.code is UNSET + assert status.set_date is UNSET + + def test_from_dict_with_invalid_set_date(self): + data = { + "name": "In Transit", + "code": "I", + "setDate": 0, # Invalid date format + } + status = Status.from_dict(data) + assert status.name == "In Transit" + assert status.code == "I" + assert status.set_date == 0 # Should return the original invalid value diff --git a/tests/models/test_title.py b/tests/models/test_title.py new file mode 100644 index 0000000..c1bbbe2 --- /dev/null +++ b/tests/models/test_title.py @@ -0,0 +1,297 @@ +import pytest + +from nlb_catalogue_client.models.bib_format import BibFormat +from nlb_catalogue_client.models.title import Title +from nlb_catalogue_client.types import UNSET + + +@pytest.fixture() +def bib_format() -> BibFormat: + return BibFormat(code="BK", name="BOOKS") + + +@pytest.fixture() +def title_full(bib_format) -> tuple[Title, dict]: + return Title( + format_=bib_format, + title="Sample Book", + native_title="样本书", + series_title=["Series 1", "Series 2"], + native_series_title=["系列 1", "系列 2"], + author="John Doe", + native_author="约翰·多伊", + other_authors=["Jane Smith", "Bob Wilson"], + native_other_authors=["简·史密斯", "鲍勃·威尔逊"], + language=["English", "Chinese"], + audience=["General", "Adult"], + audience_imda=["NC16", "PG"], + brn=99999999, + digital_id="overdrive123", + other_titles=["Alternative Title 1", "Alternative Title 2"], + native_other_titles=["其他标题 1", "其他标题 2"], + variant_titles=["Variant 1", "Variant 2"], + native_variant_titles=["变体 1", "变体 2"], + isbns=["9709999999"], + issns=["9999-9999"], + edition=["First Edition"], + native_edition=["第一版"], + publisher=["Sample Publisher"], + native_publisher=["示例出版社"], + publish_date="2023", + subjects=["Fiction", "Adventure"], + physical_description=["300 pages"], + native_physical_description=["300页"], + summary=["A fascinating story"], + native_summary=["一个引人入胜的故事"], + contents=["Chapter 1", "Chapter 2"], + native_contents=["第一章", "第二章"], + thesis=["PhD Thesis"], + native_thesis=["博士论文"], + notes=["Special Edition"], + native_notes=["特别版"], + active_reservations_count=3, + volume_note=["Volume 1"], + native_volume_note=["第一卷"], + frequency=["Monthly"], + native_frequency=["每月"], + credits_=["Editor: John Smith"], + native_credits=["编辑:约翰·史密斯"], + performers=["Actor: Jane Doe"], + native_performers=["演员:简·多伊"], + availability=True, + source="Library", + volumes=["2023 issue 1"], + ), { + "format": {"code": "BK", "name": "BOOKS"}, + "title": "Sample Book", + "nativeTitle": "样本书", + "seriesTitle": ["Series 1", "Series 2"], + "nativeSeriesTitle": ["系列 1", "系列 2"], + "author": "John Doe", + "nativeAuthor": "约翰·多伊", + "otherAuthors": ["Jane Smith", "Bob Wilson"], + "nativeOtherAuthors": ["简·史密斯", "鲍勃·威尔逊"], + "language": ["English", "Chinese"], + "audience": ["General", "Adult"], + "audienceImda": ["NC16", "PG"], + "brn": 99999999, + "digitalId": "overdrive123", + "otherTitles": ["Alternative Title 1", "Alternative Title 2"], + "nativeOtherTitles": ["其他标题 1", "其他标题 2"], + "variantTitles": ["Variant 1", "Variant 2"], + "nativeVariantTitles": ["变体 1", "变体 2"], + "isbns": ["9709999999"], + "issns": ["9999-9999"], + "edition": ["First Edition"], + "nativeEdition": ["第一版"], + "publisher": ["Sample Publisher"], + "nativePublisher": ["示例出版社"], + "publishDate": "2023", + "subjects": ["Fiction", "Adventure"], + "physicalDescription": ["300 pages"], + "nativePhysicalDescription": ["300页"], + "summary": ["A fascinating story"], + "nativeSummary": ["一个引人入胜的故事"], + "contents": ["Chapter 1", "Chapter 2"], + "nativeContents": ["第一章", "第二章"], + "thesis": ["PhD Thesis"], + "nativeThesis": ["博士论文"], + "notes": ["Special Edition"], + "nativeNotes": ["特别版"], + "activeReservationsCount": 3, + "volumeNote": ["Volume 1"], + "nativeVolumeNote": ["第一卷"], + "frequency": ["Monthly"], + "nativeFrequency": ["每月"], + "credits": ["Editor: John Smith"], + "nativeCredits": ["编辑:约翰·史密斯"], + "performers": ["Actor: Jane Doe"], + "nativePerformers": ["演员:简·多伊"], + "availability": True, + "source": "Library", + "volumes": ["2023 issue 1"], + "allowReservation": True, + "isRestricted": False, + "serial": False, + } + + +@pytest.fixture() +def title_required_only(bib_format) -> tuple[Title, dict]: + return Title(format_=bib_format), { + "format": {"code": "BK", "name": "BOOKS"}, + "allowReservation": True, + "isRestricted": False, + "serial": False, + } + + +@pytest.fixture() +def title_with_none(bib_format) -> tuple[Title, dict]: + return Title( + format_=bib_format, + title="Sample Book", + native_title=None, + series_title=None, + native_series_title=None, + native_author=None, + other_authors=None, + native_other_authors=None, + audience=None, + audience_imda=None, + digital_id=None, + other_titles=None, + native_other_titles=None, + variant_titles=None, + native_variant_titles=None, + issns=None, + edition=None, + native_edition=None, + native_publisher=None, + native_physical_description=None, + summary=None, + native_summary=None, + contents=None, + native_contents=None, + thesis=None, + native_thesis=None, + native_notes=None, + volume_note=None, + native_volume_note=None, + frequency=None, + native_frequency=None, + credits_=None, + native_credits=None, + performers=None, + native_performers=None, + source=None, + volumes=None, + ), { + "format": {"code": "BK", "name": "BOOKS"}, + "title": "Sample Book", + "nativeTitle": None, + "seriesTitle": None, + "nativeSeriesTitle": None, + "nativeAuthor": None, + "otherAuthors": None, + "nativeOtherAuthors": None, + "audience": None, + "audienceImda": None, + "digitalId": None, + "otherTitles": None, + "nativeOtherTitles": None, + "variantTitles": None, + "nativeVariantTitles": None, + "issns": None, + "edition": None, + "nativeEdition": None, + "nativePublisher": None, + "nativePhysicalDescription": None, + "summary": None, + "nativeSummary": None, + "contents": None, + "nativeContents": None, + "thesis": None, + "nativeThesis": None, + "nativeNotes": None, + "volumeNote": None, + "nativeVolumeNote": None, + "frequency": None, + "nativeFrequency": None, + "credits": None, + "nativeCredits": None, + "performers": None, + "nativePerformers": None, + "source": None, + "volumes": None, + "allowReservation": True, + "isRestricted": False, + "serial": False, + } + + +class TestTitle: + def test_to_dict_full(self, title_full): + assert title_full[0].to_dict() == title_full[1] + + def test_to_dict_required(self, title_required_only): + assert title_required_only[0].to_dict() == title_required_only[1] + + def test_to_dict_with_none(self, title_with_none): + """Test that to_dict correctly handles None values""" + assert title_with_none[0].to_dict() == title_with_none[1] + + def test_from_dict_full(self, title_full): + assert Title.from_dict(title_full[1]) == title_full[0] + + def test_from_dict_required_only(self, title_required_only): + title = Title.from_dict(title_required_only[1]) + assert title == title_required_only[0] + # Verify all optional fields are UNSET + assert title.title is UNSET + assert title.native_title is UNSET + # ... (verify other UNSET fields) + + def test_from_dict_with_none(self, title_with_none): + title = Title.from_dict(title_with_none[1]) + assert title == title_with_none[0] + + @pytest.mark.parametrize( + "field_name,invalid_value", + [ + ("seriesTitle", "Not a list"), + ("nativeSeriesTitle", 123), + ("otherAuthors", {"key": "value"}), + ("nativeOtherAuthors", True), + ("language", "Single string"), + ("audience", 42), + ("audienceImda", False), + ("otherTitles", "Not a list"), + ("nativeOtherTitles", 123), + ("variantTitles", {"key": "value"}), + ("nativeVariantTitles", True), + ("isbns", "Single ISBN"), + ("issns", 42), + ("edition", False), + ("nativeEdition", "Not a list"), + ("publisher", 123), + ("nativePublisher", {"key": "value"}), + ("subjects", True), + ("physicalDescription", "Not a list"), + ("nativePhysicalDescription", 123), + ("summary", {"key": "value"}), + ("nativeSummary", True), + ("contents", "Not a list"), + ("nativeContents", 42), + ("thesis", False), + ("nativeThesis", "Not a list"), + ("notes", 123), + ("nativeNotes", {"key": "value"}), + ("volumeNote", True), + ("nativeVolumeNote", "Not a list"), + ("frequency", 42), + ("nativeFrequency", False), + ("credits", "Not a list"), + ("nativeCredits", 123), + ("performers", {"key": "value"}), + ("nativePerformers", True), + ("volumes", "Not a list"), + ], + ) + def test_from_dict_with_invalid_list_fields(self, bib_format, field_name, invalid_value): + data = {"format": {"code": "BK", "name": "BOOKS"}, "title": "Sample Book", field_name: invalid_value} + title = Title.from_dict(data) + if field_name == "credits": + assert title.credits_ == invalid_value + return + assert getattr(title, self._to_python_name(field_name)) == invalid_value + + @staticmethod + def _to_python_name(camel_case: str) -> str: + """Convert camelCase to snake_case""" + import re + + name = re.sub("([A-Z]+)", r"_\1", camel_case).lower() + if name.startswith("_"): + name = name[1:] + return name diff --git a/tests/models/test_title_record.py b/tests/models/test_title_record.py new file mode 100644 index 0000000..78b1ef8 --- /dev/null +++ b/tests/models/test_title_record.py @@ -0,0 +1,272 @@ +import pytest + +from nlb_catalogue_client.models.bib_format import BibFormat +from nlb_catalogue_client.models.title_record import TitleRecord +from nlb_catalogue_client.types import UNSET + + +@pytest.fixture() +def bib_format() -> BibFormat: + return BibFormat(code="BK", name="BOOKS") + + +@pytest.fixture() +def title_record_full(bib_format) -> tuple[TitleRecord, dict]: + return TitleRecord( + format_=bib_format, + brn=99999999, + digital_id="overdrive123", + other_titles=["Alternative Title 1"], + native_other_titles=["其他标题 1"], + variant_titles=["Variant 1"], + native_variant_titles=["变体 1"], + other_authors=["Jane Smith"], + native_other_authors=["简·史密斯"], + isbns=["9709999999"], + issns=["9999-9999"], + edition=["First Edition"], + native_edition=["第一版"], + publisher=["Sample Publisher"], + native_publisher=["示例出版社"], + publish_date="2023", + subjects=["Fiction"], + physical_description=["300 pages"], + native_physical_description=["300页"], + summary=["A story"], + native_summary=["故事"], + contents=["Chapter 1"], + native_contents=["第一章"], + thesis=["PhD Thesis"], + native_thesis=["博士论文"], + notes=["Special Edition"], + native_notes=["特别版"], + active_reservations_count=3, + volume_note=["Volume 1"], + native_volume_note=["第一卷"], + frequency=["Monthly"], + native_frequency=["每月"], + credits_=["Editor: John"], + native_credits=["编辑:约翰"], + performers=["Actor: Jane"], + native_performers=["演员:简"], + availability=True, + source="Library", + volumes=["2023 issue 1"], + audience=["General", "Adult"], + audience_imda=["NC16", "PG"], + language=["English", "Chinese"], + ), { + "format": {"code": "BK", "name": "BOOKS"}, + "brn": 99999999, + "digitalId": "overdrive123", + "otherTitles": ["Alternative Title 1"], + "nativeOtherTitles": ["其他标题 1"], + "variantTitles": ["Variant 1"], + "nativeVariantTitles": ["变体 1"], + "otherAuthors": ["Jane Smith"], + "nativeOtherAuthors": ["简·史密斯"], + "isbns": ["9709999999"], + "issns": ["9999-9999"], + "edition": ["First Edition"], + "nativeEdition": ["第一版"], + "publisher": ["Sample Publisher"], + "nativePublisher": ["示例出版社"], + "publishDate": "2023", + "subjects": ["Fiction"], + "physicalDescription": ["300 pages"], + "nativePhysicalDescription": ["300页"], + "summary": ["A story"], + "nativeSummary": ["故事"], + "contents": ["Chapter 1"], + "nativeContents": ["第一章"], + "thesis": ["PhD Thesis"], + "nativeThesis": ["博士论文"], + "notes": ["Special Edition"], + "nativeNotes": ["特别版"], + "activeReservationsCount": 3, + "volumeNote": ["Volume 1"], + "nativeVolumeNote": ["第一卷"], + "frequency": ["Monthly"], + "nativeFrequency": ["每月"], + "credits": ["Editor: John"], + "nativeCredits": ["编辑:约翰"], + "performers": ["Actor: Jane"], + "nativePerformers": ["演员:简"], + "availability": True, + "source": "Library", + "volumes": ["2023 issue 1"], + "allowReservation": True, + "isRestricted": False, + "serial": False, + "audience": ["General", "Adult"], + "audienceImda": ["NC16", "PG"], + "language": ["English", "Chinese"], + } + + +@pytest.fixture() +def title_record_required_only(bib_format) -> tuple[TitleRecord, dict]: + return TitleRecord(format_=bib_format), { + "format": {"code": "BK", "name": "BOOKS"}, + "allowReservation": True, + "isRestricted": False, + "serial": False, + } + + +@pytest.fixture() +def title_record_with_none(bib_format) -> tuple[TitleRecord, dict]: + return TitleRecord( + format_=bib_format, + digital_id=None, + other_titles=None, + native_other_titles=None, + variant_titles=None, + native_variant_titles=None, + other_authors=None, + native_other_authors=None, + issns=None, + edition=None, + native_edition=None, + native_publisher=None, + native_physical_description=None, + summary=None, + native_summary=None, + contents=None, + native_contents=None, + thesis=None, + native_thesis=None, + native_notes=None, + volume_note=None, + native_volume_note=None, + frequency=None, + native_frequency=None, + credits_=None, + native_credits=None, + performers=None, + native_performers=None, + source=None, + volumes=None, + audience=None, + audience_imda=None, + ), { + "format": {"code": "BK", "name": "BOOKS"}, + "digitalId": None, + "otherTitles": None, + "nativeOtherTitles": None, + "variantTitles": None, + "nativeVariantTitles": None, + "otherAuthors": None, + "nativeOtherAuthors": None, + "issns": None, + "edition": None, + "nativeEdition": None, + "nativePublisher": None, + "nativePhysicalDescription": None, + "summary": None, + "nativeSummary": None, + "contents": None, + "nativeContents": None, + "thesis": None, + "nativeThesis": None, + "nativeNotes": None, + "volumeNote": None, + "nativeVolumeNote": None, + "frequency": None, + "nativeFrequency": None, + "credits": None, + "nativeCredits": None, + "performers": None, + "nativePerformers": None, + "source": None, + "volumes": None, + "allowReservation": True, + "isRestricted": False, + "serial": False, + "audience": None, + "audienceImda": None, + } + + +class TestTitleRecord: + def test_to_dict_full(self, title_record_full): + assert title_record_full[0].to_dict() == title_record_full[1] + + def test_to_dict_required_only(self, title_record_required_only): + assert title_record_required_only[0].to_dict() == title_record_required_only[1] + + def test_to_dict_with_none(self, title_record_with_none): + assert title_record_with_none[0].to_dict() == title_record_with_none[1] + + def test_from_dict_full(self, title_record_full): + assert TitleRecord.from_dict(title_record_full[1]) == title_record_full[0] + + def test_from_dict_required_only(self, title_record_required_only): + title_record = TitleRecord.from_dict(title_record_required_only[1]) + assert title_record == title_record_required_only[0] + # Verify all optional fields are UNSET + assert title_record.brn is UNSET + assert title_record.digital_id is UNSET + # ... (verify other UNSET fields) + + def test_from_dict_with_none(self, title_record_with_none): + title_record = TitleRecord.from_dict(title_record_with_none[1]) + assert title_record == title_record_with_none[0] + + @pytest.mark.parametrize( + "field_name,invalid_value", + [ + ("otherTitles", "Not a list"), + ("nativeOtherTitles", 123), + ("variantTitles", {"key": "value"}), + ("nativeVariantTitles", True), + ("otherAuthors", "Not a list"), + ("nativeOtherAuthors", 456), + ("isbns", {"key": "value"}), + ("issns", True), + ("edition", "Not a list"), + ("nativeEdition", 789), + ("publisher", {"key": "value"}), + ("nativePublisher", True), + ("subjects", "Not a list"), + ("physicalDescription", 101), + ("nativePhysicalDescription", {"key": "value"}), + ("summary", True), + ("nativeSummary", "Not a list"), + ("contents", 102), + ("nativeContents", {"key": "value"}), + ("thesis", True), + ("nativeThesis", "Not a list"), + ("notes", 103), + ("nativeNotes", {"key": "value"}), + ("volumeNote", True), + ("nativeVolumeNote", "Not a list"), + ("frequency", 104), + ("nativeFrequency", {"key": "value"}), + ("credits", True), + ("nativeCredits", "Not a list"), + ("performers", 105), + ("nativePerformers", {"key": "value"}), + ("volumes", True), + ("audience", {"key": "value"}), + ("audienceImda", "Not a list"), + ("language", {"key": "value"}), + ], + ) + def test_from_dict_with_invalid_list_fields(self, bib_format, field_name, invalid_value): + data = {"format": {"code": "BK", "name": "BOOKS"}, field_name: invalid_value} + title_record = TitleRecord.from_dict(data) + if field_name == "credits": + assert title_record.credits_ == invalid_value + return + assert getattr(title_record, self._to_python_name(field_name)) == invalid_value + + @staticmethod + def _to_python_name(camel_case: str) -> str: + """Convert camelCase to snake_case""" + import re + + name = re.sub("([A-Z]+)", r"_\1", camel_case).lower() + if name.startswith("_"): + name = name[1:] + return name diff --git a/tests/models/test_title_summary.py b/tests/models/test_title_summary.py new file mode 100644 index 0000000..af3e4a3 --- /dev/null +++ b/tests/models/test_title_summary.py @@ -0,0 +1,145 @@ +import pytest + +from nlb_catalogue_client.models.bib_format import BibFormat +from nlb_catalogue_client.models.book_cover import BookCover +from nlb_catalogue_client.models.title_record import TitleRecord +from nlb_catalogue_client.models.title_summary import TitleSummary +from nlb_catalogue_client.types import UNSET + + +@pytest.fixture() +def bib_format() -> BibFormat: + return BibFormat(code="BK", name="BOOKS") + + +@pytest.fixture() +def book_cover() -> BookCover: + return BookCover(small="small.jpg", medium="medium.jpg", large="large.jpg") + + +@pytest.fixture() +def title_record(bib_format) -> TitleRecord: + return TitleRecord(format_=bib_format) + + +@pytest.fixture() +def title_summary_full(book_cover, title_record) -> tuple[TitleSummary, dict]: + return TitleSummary( + title="Sample Book / John Doe", + native_title="样本书 / 约翰·多伊", + series_title=["Series 1", "Series 2"], + native_series_title=["系列 1", "系列 2"], + author="John Doe", + native_author="约翰·多伊", + cover_url=book_cover, + records=[title_record], + ), { + "title": "Sample Book / John Doe", + "nativeTitle": "样本书 / 约翰·多伊", + "seriesTitle": ["Series 1", "Series 2"], + "nativeSeriesTitle": ["系列 1", "系列 2"], + "author": "John Doe", + "nativeAuthor": "约翰·多伊", + "coverUrl": {"small": "small.jpg", "medium": "medium.jpg", "large": "large.jpg"}, + "records": [ + { + "format": {"code": "BK", "name": "BOOKS"}, + "allowReservation": True, + "isRestricted": False, + "serial": False, + } + ], + } + + +@pytest.fixture() +def title_summary_required_only() -> tuple[TitleSummary, dict]: + return TitleSummary(), {} + + +@pytest.fixture() +def title_summary_with_none() -> tuple[TitleSummary, dict]: + return TitleSummary( + title=None, + native_title=None, + series_title=None, + native_series_title=None, + author=None, + native_author=None, + cover_url=UNSET, + records=UNSET, + ), { + "title": None, + "nativeTitle": None, + "seriesTitle": None, + "nativeSeriesTitle": None, + "author": None, + "nativeAuthor": None, + } + + +class TestTitleSummary: + def test_to_dict_full(self, title_summary_full): + assert title_summary_full[0].to_dict() == title_summary_full[1] + + def test_to_dict_required_only(self, title_summary_required_only): + assert title_summary_required_only[0].to_dict() == title_summary_required_only[1] + + def test_to_dict_with_none(self, title_summary_with_none): + assert title_summary_with_none[0].to_dict() == title_summary_with_none[1] + + def test_from_dict_full(self, title_summary_full): + title_summary = TitleSummary.from_dict(title_summary_full[1]) + + assert title_summary.title == title_summary_full[0].title + assert title_summary.native_title == title_summary_full[0].native_title + assert title_summary.series_title == title_summary_full[0].series_title + assert title_summary.native_series_title == title_summary_full[0].native_series_title + assert title_summary.author == title_summary_full[0].author + assert title_summary.native_author == title_summary_full[0].native_author + assert isinstance(title_summary.cover_url, BookCover) + assert title_summary.cover_url.to_dict() == title_summary_full[0].cover_url.to_dict() + assert len(title_summary.records) == 1 + assert isinstance(title_summary.records[0], TitleRecord) + assert title_summary.records[0].format_.code == title_summary_full[0].records[0].format_.code + + def test_from_dict_required_only(self, title_summary_required_only): + title_summary = TitleSummary.from_dict(title_summary_required_only[1]) + + assert title_summary.title is UNSET + assert title_summary.native_title is UNSET + assert title_summary.series_title is UNSET + assert title_summary.native_series_title is UNSET + assert title_summary.author is UNSET + assert title_summary.native_author is UNSET + assert title_summary.cover_url is UNSET + assert title_summary.records == [] + + def test_from_dict_with_none(self, title_summary_with_none): + title_summary = TitleSummary.from_dict(title_summary_with_none[1]) + + assert title_summary.title is None + assert title_summary.native_title is None + assert title_summary.series_title is None + assert title_summary.native_series_title is None + assert title_summary.author is None + assert title_summary.native_author is None + assert title_summary.cover_url is UNSET + assert title_summary.records == [] + + @pytest.mark.parametrize( + "field_name,invalid_value", + [ + ("seriesTitle", "Not a list"), + ("nativeSeriesTitle", "Not a list"), + ], + ) + def test_from_dict_with_invalid_fields(self, field_name, invalid_value): + data = {field_name: invalid_value} + title_summary = TitleSummary.from_dict(data) + + field_map = { + "seriesTitle": "series_title", + "nativeSeriesTitle": "native_series_title", + } + assert getattr(title_summary, field_map[field_name]) == invalid_value diff --git a/tests/models/test_too_many_requests_error.py b/tests/models/test_too_many_requests_error.py new file mode 100644 index 0000000..307d78e --- /dev/null +++ b/tests/models/test_too_many_requests_error.py @@ -0,0 +1,69 @@ +import pytest + +from nlb_catalogue_client.models.too_many_requests_error import TooManyRequestsError +from nlb_catalogue_client.types import UNSET + + +@pytest.fixture() +def error_with_status() -> tuple[TooManyRequestsError, dict]: + return TooManyRequestsError(error="Too Many Requests", message="API calls quota exceeded", status_code=429), { + "error": "Too Many Requests", + "message": "API calls quota exceeded", + "statusCode": 429, + } + + +@pytest.fixture() +def error_without_status() -> tuple[TooManyRequestsError, dict]: + return TooManyRequestsError(error="Too Many Requests", message="API calls quota exceeded"), { + "error": "Too Many Requests", + "message": "API calls quota exceeded", + } + + +class TestTooManyRequestsError: + @pytest.mark.parametrize( + "error,message,status_code,expected_status", + [ + ("Too Many Requests", "API calls quota exceeded", 429, 429), + ("Too Many Requests", "API calls quota exceeded", None, None), + ("Too Many Requests", "API calls quota exceeded", UNSET, UNSET), + ], + ) + def test_basic_initialization(self, error, message, status_code, expected_status): + error_obj = TooManyRequestsError(error=error, message=message, status_code=status_code) + assert error_obj.error == error + assert error_obj.message == message + assert error_obj.status_code == expected_status + + def test_to_dict_with_status(self, error_with_status): + assert error_with_status[0].to_dict() == error_with_status[1] + + def test_to_dict_without_status(self, error_without_status): + assert error_without_status[0].to_dict() == error_without_status[1] + + def test_from_dict_with_status(self, error_with_status): + assert TooManyRequestsError.from_dict(error_with_status[1]) == error_with_status[0] + + def test_from_dict_without_status(self, error_without_status): + assert TooManyRequestsError.from_dict(error_without_status[1]) == error_without_status[0] + + @pytest.mark.parametrize( + "input_data", + [ + {"error": "Too Many Requests", "message": "API calls quota exceeded", "statusCode": 429}, + {"error": "Too Many Requests", "message": "API calls quota exceeded"}, + ], + ) + def test_dict_roundtrip(self, input_data): + """Test that to_dict and from_dict are inverse operations""" + original = TooManyRequestsError.from_dict(input_data) + dict_form = original.to_dict() + recreated = TooManyRequestsError.from_dict(dict_form) + assert original == recreated + + def test_from_dict_preserves_original(self, error_with_status): + """Test that from_dict doesn't modify the input dictionary""" + input_dict = error_with_status[1].copy() + TooManyRequestsError.from_dict(input_dict) + assert input_dict == error_with_status[1] diff --git a/tests/models/test_unauthorized_error.py b/tests/models/test_unauthorized_error.py new file mode 100644 index 0000000..273f309 --- /dev/null +++ b/tests/models/test_unauthorized_error.py @@ -0,0 +1,69 @@ +import pytest + +from nlb_catalogue_client.models.unauthorized_error import UnauthorizedError +from nlb_catalogue_client.types import UNSET + + +@pytest.fixture() +def error_with_status() -> tuple[UnauthorizedError, dict]: + return UnauthorizedError(error="Unauthorized", message="Invalid API Key", status_code=401), { + "error": "Unauthorized", + "message": "Invalid API Key", + "statusCode": 401, + } + + +@pytest.fixture() +def error_without_status() -> tuple[UnauthorizedError, dict]: + return UnauthorizedError(error="Unauthorized", message="Invalid API Key"), { + "error": "Unauthorized", + "message": "Invalid API Key", + } + + +class TestUnauthorizedError: + @pytest.mark.parametrize( + "error,message,status_code,expected_status", + [ + ("Unauthorized", "Invalid API Key", 401, 401), + ("Unauthorized", "Invalid API Key", None, None), + ("Unauthorized", "Invalid API Key", UNSET, UNSET), + ], + ) + def test_basic_initialization(self, error, message, status_code, expected_status): + error_obj = UnauthorizedError(error=error, message=message, status_code=status_code) + assert error_obj.error == error + assert error_obj.message == message + assert error_obj.status_code == expected_status + + def test_to_dict_with_status(self, error_with_status): + assert error_with_status[0].to_dict() == error_with_status[1] + + def test_to_dict_without_status(self, error_without_status): + assert error_without_status[0].to_dict() == error_without_status[1] + + def test_from_dict_with_status(self, error_with_status): + assert UnauthorizedError.from_dict(error_with_status[1]) == error_with_status[0] + + def test_from_dict_without_status(self, error_without_status): + assert UnauthorizedError.from_dict(error_without_status[1]) == error_without_status[0] + + @pytest.mark.parametrize( + "input_data", + [ + {"error": "Unauthorized", "message": "Invalid API Key", "statusCode": 401}, + {"error": "Unauthorized", "message": "Invalid API Key"}, + ], + ) + def test_dict_roundtrip(self, input_data): + """Test that to_dict and from_dict are inverse operations""" + original = UnauthorizedError.from_dict(input_data) + dict_form = original.to_dict() + recreated = UnauthorizedError.from_dict(dict_form) + assert original == recreated + + def test_from_dict_preserves_original(self, error_with_status): + """Test that from_dict doesn't modify the input dictionary""" + input_dict = error_with_status[1].copy() + UnauthorizedError.from_dict(input_dict) + assert input_dict == error_with_status[1] diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..1cd0cdc --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,286 @@ +import httpx +import pytest + +from nlb_catalogue_client.client import AuthenticatedClient, Client + + +@pytest.fixture +def base_url(): + return "https://api.example.com" + + +@pytest.fixture +def test_client(base_url): + return Client(base_url=base_url) + + +@pytest.fixture +def test_auth_client(base_url): + return AuthenticatedClient(base_url=base_url, token="test_token") + + +class TestClient: + def test_client_initialization(self, base_url): + client = Client(base_url=base_url) + assert client._base_url == base_url + assert client._cookies == {} + assert client._headers == {} + assert client._timeout is None + assert client._verify_ssl is True + assert client._follow_redirects is False + assert client._httpx_args == dict() + + def test_client_is_none_when_initialized(self, base_url): + # HTTPX Client are not initialized when Client is declared + client = Client(base_url=base_url) + assert client._client is None + assert client._async_client is None + + def test_with_headers(self, test_client): + new_headers = {"X-Test": "test"} + updated_client = test_client.with_headers(new_headers) + assert updated_client._headers == new_headers + + def test_with_headers_sync(self, test_client): + new_headers = {"X-Test": "test"} + with test_client as test_client: + updated_client = test_client.with_headers(new_headers) + assert updated_client._headers == new_headers + + @pytest.mark.asyncio + async def test_with_headers_async(self, test_client): + new_headers = {"X-Test": "test"} + async with test_client as test_client: + updated_client = test_client.with_headers(new_headers) + assert updated_client._headers == new_headers + + def test_with_cookies(self, test_client): + new_cookies = {"session": "test123"} + updated_client = test_client.with_cookies(new_cookies) + assert updated_client._cookies == new_cookies + + def test_with_cookies_sync(self, test_client): + new_cookies = {"session": "test123"} + with test_client as test_client: + updated_client = test_client.with_cookies(new_cookies) + assert updated_client._cookies == new_cookies + + @pytest.mark.asyncio + async def test_with_cookies_async(self, test_client): + new_cookies = {"session": "test123"} + async with test_client as test_client: + updated_client = test_client.with_cookies(new_cookies) + assert updated_client._cookies == new_cookies + + def test_with_timeout(self, test_client): + timeout = httpx.Timeout(10.0) + updated_client = test_client.with_timeout(timeout) + assert updated_client._timeout == timeout + + def test_with_timeout_sync(self, test_client): + timeout = httpx.Timeout(10.0) + with test_client as test_client: + updated_client = test_client.with_timeout(timeout) + assert updated_client._timeout == timeout + + @pytest.mark.asyncio + async def test_with_timeout_async(self, test_client): + timeout = httpx.Timeout(10.0) + async with test_client as test_client: + updated_client = test_client.with_timeout(timeout) + assert updated_client._timeout == timeout + + def test_get_httpx_client(self, test_client): + httpx_client = test_client.get_httpx_client() + assert isinstance(httpx_client, httpx.Client) + assert httpx_client.base_url == test_client._base_url + + @pytest.mark.asyncio + async def test_get_async_httpx_client(self, test_client): + async_client = test_client.get_async_httpx_client() + assert isinstance(async_client, httpx.AsyncClient) + assert async_client.base_url == test_client._base_url + + def test_context_manager(self, test_client): + with test_client as client: + assert isinstance(client, Client) + assert client._client is not None + + def test_set_httpx_client(self, test_client): + custom_client = httpx.Client(base_url="https://custom.example.com") + updated_client = test_client.set_httpx_client(custom_client) + assert updated_client._client == custom_client + + def test_set_async_httpx_client(self, test_client): + custom_client = httpx.AsyncClient(base_url="https://custom.example.com") + updated_client = test_client.set_async_httpx_client(custom_client) + assert updated_client._async_client == custom_client + + @pytest.mark.asyncio + async def test_async_context_manager(self, test_client): + async with test_client as client: + assert isinstance(client, Client) + assert client._async_client is not None + + def test_client_with_custom_settings(self, base_url): + custom_headers = {"X-Custom": "value"} + custom_cookies = {"session": "123"} + custom_timeout = httpx.Timeout(30.0) + + client = Client( + base_url=base_url, + headers=custom_headers, + cookies=custom_cookies, + timeout=custom_timeout, + verify_ssl=False, + follow_redirects=True, + httpx_args={"http2": True}, + ) + + assert client._headers == custom_headers + assert client._cookies == custom_cookies + assert client._timeout == custom_timeout + assert client._verify_ssl is False + assert client._follow_redirects is True + assert client._httpx_args == {"http2": True} + + +class TestAuthenticatedClient: + def test_auth_client_initialization(self, base_url): + client = AuthenticatedClient(base_url=base_url, token="test_token") + assert client._base_url == base_url + assert client.token == "test_token" + assert client.prefix == "Bearer" + assert client.auth_header_name == "Authorization" + assert client._cookies == {} + assert client._headers == {} + assert client._timeout is None + assert client._verify_ssl is True + assert client._follow_redirects is False + assert client._httpx_args == dict() + + def test_auth_header_in_client(self, test_auth_client): + httpx_client = test_auth_client.get_httpx_client() + assert httpx_client.headers["Authorization"] == "Bearer test_token" + + def test_custom_auth_prefix(self, base_url): + client = AuthenticatedClient(base_url=base_url, token="test_token", prefix="Basic") + httpx_client = client.get_httpx_client() + assert httpx_client.headers["Authorization"] == "Basic test_token" + + def test_with_headers_auth_client(self, test_auth_client): + new_headers = {"X-Test": "test"} + updated_client = test_auth_client.with_headers(new_headers) + assert updated_client._headers == { + **new_headers, + } + + def test_with_headers_auth_client_sync(self, test_auth_client): + new_headers = {"X-Test": "test"} + with test_auth_client as test_auth_client: + updated_client = test_auth_client.with_headers(new_headers) + assert updated_client._headers.get("X-Test") == "test" + + @pytest.mark.asyncio + async def test_with_headers_auth_client_async(self, test_auth_client): + new_headers = {"X-Test": "test"} + async with test_auth_client as test_auth_client: + updated_client = test_auth_client.with_headers(new_headers) + assert updated_client._headers.get("X-Test") == "test" + + def test_with_cookies(self, test_auth_client): + new_cookies = {"session": "test123"} + updated_client = test_auth_client.with_cookies(new_cookies) + assert updated_client._cookies == new_cookies + + def test_with_cookies_sync(self, test_auth_client): + new_cookies = {"session": "test123"} + with test_auth_client as test_client: + updated_client = test_client.with_cookies(new_cookies) + assert updated_client._cookies == new_cookies + + @pytest.mark.asyncio + async def test_with_cookies_async(self, test_auth_client): + new_cookies = {"session": "test123"} + async with test_auth_client as test_client: + updated_client = test_client.with_cookies(new_cookies) + assert updated_client._cookies == new_cookies + + def test_with_timeout(self, test_auth_client): + timeout = httpx.Timeout(10.0) + updated_client = test_auth_client.with_timeout(timeout) + assert updated_client._timeout == timeout + + def test_with_timeout_sync(self, test_auth_client): + timeout = httpx.Timeout(10.0) + with test_auth_client as test_client: + updated_client = test_client.with_timeout(timeout) + assert updated_client._timeout == timeout + + @pytest.mark.asyncio + async def test_with_timeout_async(self, test_auth_client): + timeout = httpx.Timeout(10.0) + async with test_auth_client as test_client: + updated_client = test_client.with_timeout(timeout) + assert updated_client._timeout == timeout + + @pytest.mark.asyncio + async def test_async_auth_client(self, test_auth_client): + async_client = test_auth_client.get_async_httpx_client() + assert isinstance(async_client, httpx.AsyncClient) + assert async_client.headers["Authorization"] == "Bearer test_token" + + def test_auth_context_manager(self, test_auth_client): + with test_auth_client as client: + assert isinstance(client, AuthenticatedClient) + assert client._client is not None + assert client._client.headers["Authorization"] == "Bearer test_token" + + def test_auth_client_without_prefix(self, base_url): + client = AuthenticatedClient( + base_url=base_url, + token="test_token", + prefix="", # Empty prefix + ) + httpx_client = client.get_httpx_client() + assert httpx_client.headers["Authorization"] == "test_token" + + def test_set_httpx_client_auth(self, test_auth_client): + custom_client = httpx.Client(base_url="https://custom.example.com") + updated_client = test_auth_client.set_httpx_client(custom_client) + assert updated_client._client == custom_client + + def test_set_async_httpx_client_auth(self, test_auth_client): + custom_client = httpx.AsyncClient(base_url="https://custom.example.com") + updated_client = test_auth_client.set_async_httpx_client(custom_client) + assert updated_client._async_client == custom_client + + @pytest.mark.asyncio + async def test_async_context_manager_auth(self, test_auth_client): + async with test_auth_client as client: + assert isinstance(client, AuthenticatedClient) + assert client._async_client is not None + assert client._async_client.headers["Authorization"] == "Bearer test_token" + + def test_auth_client_with_custom_settings(self, base_url): + custom_headers = {"X-Custom": "value"} + custom_cookies = {"session": "123"} + custom_timeout = httpx.Timeout(30.0) + + client = AuthenticatedClient( + base_url=base_url, + token="test_token", + headers=custom_headers, + cookies=custom_cookies, + timeout=custom_timeout, + verify_ssl=False, + follow_redirects=True, + httpx_args={"http2": True}, + ) + + assert client._headers == custom_headers + assert client._cookies == custom_cookies + assert client._timeout == custom_timeout + assert client._verify_ssl is False + assert client._follow_redirects is True + assert client._httpx_args == {"http2": True} diff --git a/tests/test_errors.py b/tests/test_errors.py new file mode 100644 index 0000000..e5c0bcd --- /dev/null +++ b/tests/test_errors.py @@ -0,0 +1,28 @@ +from nlb_catalogue_client.errors import UnexpectedStatus + + +class TestUnexpectedStatus: + def test_basic_initialization(self): + """Test basic initialization with simple values""" + error = UnexpectedStatus(404, b"Not Found") + assert error.status_code == 404 + assert error.content == b"Not Found" + + def test_error_message_format(self): + """Test the formatted error message""" + error = UnexpectedStatus(500, b"Server Error") + expected_message = "Unexpected status code: 500\n\nResponse content:\nServer Error" + assert str(error) == expected_message + + def test_with_empty_content(self): + """Test initialization with empty content""" + error = UnexpectedStatus(400, b"") + assert error.status_code == 400 + assert error.content == b"" + assert str(error) == "Unexpected status code: 400\n\nResponse content:\n" + + def test_inheritance(self): + """Test proper inheritance from Exception""" + error = UnexpectedStatus(404, b"Not Found") + assert isinstance(error, Exception) + assert isinstance(error, UnexpectedStatus) diff --git a/tests/test_types.py b/tests/test_types.py new file mode 100644 index 0000000..acad6bc --- /dev/null +++ b/tests/test_types.py @@ -0,0 +1,115 @@ +import io +from http import HTTPStatus + +import pytest + +from nlb_catalogue_client.types import UNSET, File, Response + + +class TestUnset: + def test_unset_bool(self): + """Test that UNSET evaluates to False""" + assert bool(UNSET) is False + assert not UNSET + + def test_identity(self): + """Test that UNSET is UNSET""" + assert UNSET is UNSET + + +class TestFile: + @pytest.fixture + def sample_binary_data(self): + return b"Sample file content" + + @pytest.fixture + def sample_file_obj(self, sample_binary_data): + return io.BytesIO(sample_binary_data) + + def test_file_basic_initialization(self, sample_file_obj): + """Test basic File initialization""" + file = File(payload=sample_file_obj) + assert file.payload == sample_file_obj + assert file.file_name is None + assert file.mime_type is None + + def test_file_full_initialization(self, sample_file_obj): + """Test File initialization with all parameters""" + file = File(payload=sample_file_obj, file_name="test.txt", mime_type="text/plain") + assert file.payload == sample_file_obj + assert file.file_name == "test.txt" + assert file.mime_type == "text/plain" + + def test_file_to_tuple(self, sample_file_obj): + """Test conversion to tuple format""" + file = File(payload=sample_file_obj, file_name="test.txt", mime_type="text/plain") + tuple_result = file.to_tuple() + assert tuple_result == ("test.txt", sample_file_obj, "text/plain") + + +class TestResponse: + def test_response_initialization(self): + """Test basic Response initialization""" + response = Response( + status_code=HTTPStatus.OK, + content=b"Test content", + headers={"Content-Type": "text/plain"}, + parsed="Parsed content", + ) + assert response.status_code == HTTPStatus.OK + assert response.content == b"Test content" + assert response.headers == {"Content-Type": "text/plain"} + assert response.parsed == "Parsed content" + + def test_response_with_none_parsed(self): + """Test Response with None parsed content""" + response = Response(status_code=HTTPStatus.NO_CONTENT, content=b"", headers={}, parsed=None) + assert response.status_code == HTTPStatus.NO_CONTENT + assert response.content == b"" + assert response.headers == {} + assert response.parsed is None + + def test_response_with_different_status_codes(self): + """Test Response with various HTTP status codes""" + status_codes = [ + HTTPStatus.OK, + HTTPStatus.CREATED, + HTTPStatus.ACCEPTED, + HTTPStatus.NO_CONTENT, + HTTPStatus.BAD_REQUEST, + HTTPStatus.NOT_FOUND, + ] + for status_code in status_codes: + response = Response(status_code=status_code, content=b"", headers={}, parsed=None) + assert response.status_code == status_code + + def test_response_with_different_content_types(self): + """Test Response with different content types in headers""" + content_types = ["text/plain", "application/json", "application/xml", "text/html"] + for content_type in content_types: + response = Response( + status_code=HTTPStatus.OK, content=b"Test content", headers={"Content-Type": content_type}, parsed=None + ) + assert response.headers["Content-Type"] == content_type + + def test_response_with_binary_content(self): + """Test Response with binary content""" + binary_content = b"\x00\x01\x02\x03" + response = Response( + status_code=HTTPStatus.OK, + content=binary_content, + headers={"Content-Type": "application/octet-stream"}, + parsed=None, + ) + assert response.content == binary_content + + def test_response_with_multiple_headers(self): + """Test Response with multiple headers""" + headers = { + "Content-Type": "application/json", + "Content-Length": "100", + "Authorization": "Bearer token", + "X-Custom-Header": "custom-value", + } + response = Response(status_code=HTTPStatus.OK, content=b"{}", headers=headers, parsed=None) + assert response.headers == headers