-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtest_books.py
More file actions
351 lines (286 loc) · 11.4 KB
/
test_books.py
File metadata and controls
351 lines (286 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
from collections.abc import Callable
from http import HTTPStatus
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session as OrmSession
from cms_backend.db.models import Book, Title
def test_get_books_empty(client: TestClient):
"""Test get books endpoint with no books"""
response = client.get("/v1/books")
assert response.status_code == HTTPStatus.OK
response_doc = response.json()
assert "meta" in response_doc
assert response_doc["meta"]["count"] == 0
assert response_doc["meta"]["skip"] == 0
assert response_doc["meta"]["limit"] == 20
assert response_doc["meta"]["page_size"] == 0
assert "items" in response_doc
assert response_doc["items"] == []
def test_get_books_with_data(
client: TestClient,
create_book: Callable[..., Book],
):
"""Test get books endpoint with books present"""
# Create 5 books
books: list[Book] = []
for i in range(5):
book = create_book(zim_metadata={"index": i, "name": f"book_{i}"})
books.append(book)
response = client.get("/v1/books")
assert response.status_code == HTTPStatus.OK
response_doc = response.json()
assert response_doc["meta"]["count"] == 5
assert response_doc["meta"]["skip"] == 0
assert response_doc["meta"]["limit"] == 20
assert response_doc["meta"]["page_size"] == 5
assert len(response_doc["items"]) == 5
# Verify the structure of returned books
for item in response_doc["items"]:
assert "id" in item
assert "title_id" in item
# Light schema should NOT include full data
assert "article_count" not in item
assert "media_count" not in item
assert "size" not in item
assert "zimcheck_result" not in item
assert "zim_metadata" not in item
assert "events" not in item
def test_get_books_pagination(
client: TestClient,
create_book: Callable[..., Book],
):
"""Test get books endpoint with pagination"""
# Create 10 books
for i in range(10):
create_book(zim_metadata={"index": i})
# Test first page
response = client.get("/v1/books?skip=0&limit=3")
assert response.status_code == HTTPStatus.OK
response_doc = response.json()
assert response_doc["meta"]["count"] == 10
assert response_doc["meta"]["skip"] == 0
assert response_doc["meta"]["limit"] == 3
assert response_doc["meta"]["page_size"] == 3
assert len(response_doc["items"]) == 3
# Test second page
response = client.get("/v1/books?skip=3&limit=3")
assert response.status_code == HTTPStatus.OK
response_doc = response.json()
assert response_doc["meta"]["count"] == 10
assert response_doc["meta"]["skip"] == 3
assert response_doc["meta"]["limit"] == 3
assert response_doc["meta"]["page_size"] == 3
assert len(response_doc["items"]) == 3
# Test last page (partial)
response = client.get("/v1/books?skip=9&limit=3")
assert response.status_code == HTTPStatus.OK
response_doc = response.json()
assert response_doc["meta"]["count"] == 10
assert response_doc["meta"]["skip"] == 9
assert response_doc["meta"]["limit"] == 3
assert response_doc["meta"]["page_size"] == 1
assert len(response_doc["items"]) == 1
def test_get_books_filter_by_has_title(
client: TestClient,
create_book: Callable[..., Book],
create_title: Callable[..., Title],
):
"""Test get books endpoint filtering by has_title"""
title = create_title()
# Create 3 books with title
for _ in range(3):
book = create_book(zim_metadata={"Name": title.name})
title.books.append(book)
# Create 2 books without title
for _ in range(2):
create_book(zim_metadata={"Name": "different_name"})
# Filter for books with title
response = client.get("/v1/books?has_title=true")
assert response.status_code == HTTPStatus.OK
response_doc = response.json()
assert response_doc["meta"]["count"] == 3
assert len(response_doc["items"]) == 3
for item in response_doc["items"]:
assert item["title_id"] is not None
# Filter for books without title
response = client.get("/v1/books?has_title=false")
assert response.status_code == HTTPStatus.OK
response_doc = response.json()
assert response_doc["meta"]["count"] == 2
assert len(response_doc["items"]) == 2
for item in response_doc["items"]:
assert item["title_id"] is None
def test_get_books_combined_filters(
client: TestClient,
create_book: Callable[..., Book],
create_title: Callable[..., Title],
):
"""Test get books endpoint with multiple filters combined"""
title = create_title()
# Create various books
# 1. With title
book1 = create_book(zim_metadata={"Name": title.name})
title.books.append(book1)
# 2. Without title
create_book(zim_metadata={"Name": "different"})
# 3. With title
book3 = create_book(zim_metadata={"Name": title.name})
title.books.append(book3)
# 4. Without title
create_book(zim_metadata={"Name": "another"})
# Filter for books with title
response = client.get("/v1/books?has_title=true")
assert response.status_code == HTTPStatus.OK
response_doc = response.json()
assert response_doc["meta"]["count"] == 2
# Filter for books without title
response = client.get("/v1/books?has_title=false")
assert response.status_code == HTTPStatus.OK
response_doc = response.json()
assert response_doc["meta"]["count"] == 2
@pytest.mark.parametrize(
"needs_attention,expected_count",
[
pytest.param(None, 5, id="no-filter"),
pytest.param(True, 4, id="needs-attention"),
pytest.param(False, 1, id="does-not-need-attention"),
],
)
def test_get_books_filter_by_needs_attention(
client: TestClient,
create_book: Callable[..., Book],
create_title: Callable[..., Title],
needs_attention: bool | None,
expected_count: int,
):
"""Test get books endpoint filtering by needs_attention"""
title = create_title()
book_with_title = create_book(zim_metadata={"Name": title.name})
title.books.append(book_with_title)
book_without_title = create_book(zim_metadata={"Name": "different_name"})
book_needs_processing = create_book(zim_metadata={"Name": title.name})
title.books.append(book_needs_processing)
book_needs_processing.needs_processing = True
book_has_error = create_book(zim_metadata={"Name": title.name})
title.books.append(book_has_error)
book_has_error.has_error = True
book_needs_file_operation = create_book(zim_metadata={"Name": title.name})
title.books.append(book_needs_file_operation)
book_needs_file_operation.needs_file_operation = True
url = (
"/v1/books"
if needs_attention is None
else f"/v1/books?needs_attention={str(needs_attention).lower()}"
)
response = client.get(url)
assert response.status_code == HTTPStatus.OK
response_doc = response.json()
assert response_doc["meta"]["count"] == expected_count
assert len(response_doc["items"]) == expected_count
if needs_attention is True:
returned_ids = {item["id"] for item in response_doc["items"]}
assert returned_ids == {
str(book_without_title.id),
str(book_needs_processing.id),
str(book_has_error.id),
str(book_needs_file_operation.id),
}
if needs_attention is False:
returned_ids = {item["id"] for item in response_doc["items"]}
assert returned_ids == {str(book_with_title.id)}
def test_get_books_filter_by_id(
client: TestClient,
create_book: Callable[..., Book],
):
"""Test get books endpoint passes id filter to database layer"""
from uuid import UUID
# Create books with specific UUIDs for partial matching
book1 = create_book(
_id=UUID("12345678-1234-5678-1234-567812345678"),
zim_metadata={"test": "book1"},
)
create_book(
_id=UUID("87654321-4321-8765-4321-876543218765"),
zim_metadata={"test": "book2"},
)
# Test that id parameter is passed through and filters correctly
response = client.get("/v1/books?id=1234-5678")
assert response.status_code == HTTPStatus.OK
response_doc = response.json()
assert response_doc["meta"]["count"] == 1
assert response_doc["items"][0]["id"] == str(book1.id)
def test_get_book_languages(
client: TestClient,
dbsession: OrmSession,
create_book: Callable[..., Book],
):
"""Test books languages endpoint returns sorted distinct production languages."""
prod_book = create_book(zim_metadata={"Language": "eng, fra"})
prod_book.location_kind = "prod"
other_prod_book = create_book(zim_metadata={"Language": " deu ,eng, ,spa "})
other_prod_book.location_kind = "prod"
create_book(zim_metadata={"Language": "hin"})
staging_book = create_book(zim_metadata={"Language": "ita"})
staging_book.location_kind = "staging"
blank_language_book = create_book(zim_metadata={"Language": " , "})
blank_language_book.location_kind = "prod"
missing_language_book = create_book(zim_metadata={"Name": "no-language"})
missing_language_book.location_kind = "prod"
dbsession.flush()
response = client.get("/v1/books/languages")
assert response.status_code == HTTPStatus.OK
assert response.json() == {"languages": ["deu", "eng", "fra", "spa"]}
def test_get_book_by_id(
client: TestClient,
book: Book,
):
"""Test get book by ID endpoint"""
response = client.get(f"/v1/books/{book.id}")
assert response.status_code == HTTPStatus.OK
response_doc = response.json()
# Verify all fields are present in full schema
assert "id" in response_doc
assert response_doc["id"] == str(book.id)
assert "title_id" in response_doc
assert "article_count" in response_doc
assert response_doc["article_count"] == book.article_count
assert "media_count" in response_doc
assert response_doc["media_count"] == book.media_count
assert "size" in response_doc
assert response_doc["size"] == book.size
assert "zimcheck_result_url" in response_doc
assert response_doc["zimcheck_result_url"] == book.zimcheck_result_url
assert "zim_metadata" in response_doc
assert response_doc["zim_metadata"] == book.zim_metadata
assert "events" in response_doc
assert response_doc["events"] == book.events
# Note: producer fields are no longer part of the Book model
def test_get_book_by_id_not_found(
client: TestClient,
book: Book, # noqa: ARG001 - needed for conftest
):
"""Test get book by ID endpoint when book doesn't exist"""
from uuid import uuid4
non_existent_id = uuid4()
response = client.get(f"/v1/books/{non_existent_id}")
assert response.status_code == HTTPStatus.NOT_FOUND
response_doc = response.json()
assert "success" in response_doc
assert response_doc["success"] is False
assert "message" in response_doc
@pytest.mark.parametrize(
"invalid_id",
[
pytest.param("not-a-uuid", id="invalid-format"),
pytest.param("1234", id="too-short"),
pytest.param("12345678-1234-5678-1234-56781234567g", id="invalid-char"),
],
)
def test_get_book_by_id_invalid_uuid(
client: TestClient,
book: Book, # noqa: ARG001 - needed for conftest
invalid_id: str,
):
"""Test get book by ID endpoint with invalid UUID format"""
response = client.get(f"/v1/books/{invalid_id}")
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY