style: format backend with black
Also drops the session-sliding comment in login_required.
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
"""Point the app at a dedicated test database before any test module imports it."""
|
||||
|
||||
import os
|
||||
|
||||
import psycopg2
|
||||
from psycopg2 import sql
|
||||
|
||||
ADMIN_URL = os.environ.get("TEST_ADMIN_DATABASE_URL", "postgresql://postgres:[email protected]:5432/postgres")
|
||||
ADMIN_URL = os.environ.get(
|
||||
"TEST_ADMIN_DATABASE_URL", "postgresql://postgres:[email protected]:5432/postgres"
|
||||
)
|
||||
TEST_DATABASE = "books_test"
|
||||
|
||||
|
||||
@@ -13,9 +16,13 @@ def _ensure_test_database() -> None:
|
||||
conn.autocommit = True # CREATE DATABASE cannot run inside a transaction
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT 1 FROM pg_database WHERE datname = %s", (TEST_DATABASE,))
|
||||
cur.execute(
|
||||
"SELECT 1 FROM pg_database WHERE datname = %s", (TEST_DATABASE,)
|
||||
)
|
||||
if cur.fetchone() is None:
|
||||
cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(TEST_DATABASE)))
|
||||
cur.execute(
|
||||
sql.SQL("CREATE DATABASE {}").format(sql.Identifier(TEST_DATABASE))
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@@ -34,7 +34,12 @@ class ApiTestCase(unittest.TestCase):
|
||||
return (client or self.client).open(path, **kwargs)
|
||||
|
||||
def register(self, username="alice", password="correct horse battery", client=None):
|
||||
response = self.call("POST", "/api/auth/register", {"username": username, "password": password}, client)
|
||||
response = self.call(
|
||||
"POST",
|
||||
"/api/auth/register",
|
||||
{"username": username, "password": password},
|
||||
client,
|
||||
)
|
||||
self.assertEqual(response.status_code, 201, response.get_json())
|
||||
return response
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ class AppTests(ApiTestCase):
|
||||
db_execute(SCHEMA.read_text())
|
||||
db_execute(SCHEMA.read_text())
|
||||
self.assertEqual(db_execute("SELECT count(*) FROM genres")[0][0], 16)
|
||||
self.assertEqual(db_execute("SELECT last_value FROM genres_id_seq")[0][0], before)
|
||||
self.assertEqual(
|
||||
db_execute("SELECT last_value FROM genres_id_seq")[0][0], before
|
||||
)
|
||||
|
||||
def test_unknown_route_returns_json_404(self):
|
||||
response = self.call("GET", "/api/nope")
|
||||
@@ -24,7 +26,9 @@ class AppTests(ApiTestCase):
|
||||
self.assertIn("error", response.get_json())
|
||||
|
||||
def test_mutation_without_json_content_type_is_rejected(self):
|
||||
response = self.client.post("/api/health", data="x", content_type="text/plain", base_url=HTTPS)
|
||||
response = self.client.post(
|
||||
"/api/health", data="x", content_type="text/plain", base_url=HTTPS
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(
|
||||
response.get_json(),
|
||||
@@ -32,6 +36,10 @@ class AppTests(ApiTestCase):
|
||||
)
|
||||
|
||||
def test_oversized_body_is_rejected_before_parsing(self):
|
||||
response = self.call("POST", "/api/auth/login", {"username": "a", "password": "x" * (2 * 1024 * 1024)})
|
||||
response = self.call(
|
||||
"POST",
|
||||
"/api/auth/login",
|
||||
{"username": "a", "password": "x" * (2 * 1024 * 1024)},
|
||||
)
|
||||
self.assertEqual(response.status_code, 413)
|
||||
self.assertIn("error", response.get_json())
|
||||
|
||||
+67
-14
@@ -8,7 +8,11 @@ from tests.support import ApiTestCase, db_execute
|
||||
|
||||
class AuthTests(ApiTestCase):
|
||||
def session_token(self, client=None) -> str:
|
||||
return (client or self.client).get_cookie("sid", domain="localhost", path="/api").value
|
||||
return (
|
||||
(client or self.client)
|
||||
.get_cookie("sid", domain="localhost", path="/api")
|
||||
.value
|
||||
)
|
||||
|
||||
def test_register_logs_in_and_me_returns_user(self):
|
||||
response = self.register("Alice")
|
||||
@@ -25,11 +29,20 @@ class AuthTests(ApiTestCase):
|
||||
def test_session_token_is_stored_hashed(self):
|
||||
self.register()
|
||||
digest = hashlib.sha256(self.session_token().encode()).digest()
|
||||
self.assertEqual(db_execute("SELECT count(*) FROM sessions WHERE token_hash = %s", (digest,))[0][0], 1)
|
||||
self.assertEqual(
|
||||
db_execute(
|
||||
"SELECT count(*) FROM sessions WHERE token_hash = %s", (digest,)
|
||||
)[0][0],
|
||||
1,
|
||||
)
|
||||
|
||||
def test_duplicate_username_is_case_insensitive(self):
|
||||
self.register("Alice")
|
||||
response = self.call("POST", "/api/auth/register", {"username": "alice", "password": "another long password"})
|
||||
response = self.call(
|
||||
"POST",
|
||||
"/api/auth/register",
|
||||
{"username": "alice", "password": "another long password"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 409)
|
||||
self.assertEqual(response.get_json()["field"], "username")
|
||||
|
||||
@@ -40,7 +53,14 @@ class AuthTests(ApiTestCase):
|
||||
({"username": "alice", "password": "short"}, "password"),
|
||||
({"username": "alice", "password": "x" * 1025}, "password"),
|
||||
({"username": "alice", "password": "\ud800" * 12}, "password"),
|
||||
({"username": "alice", "password": "long enough password", "is_admin": True}, "is_admin"),
|
||||
(
|
||||
{
|
||||
"username": "alice",
|
||||
"password": "long enough password",
|
||||
"is_admin": True,
|
||||
},
|
||||
"is_admin",
|
||||
),
|
||||
]
|
||||
for payload, field in cases:
|
||||
with self.subTest(payload=payload):
|
||||
@@ -51,33 +71,54 @@ class AuthTests(ApiTestCase):
|
||||
def test_login_is_case_insensitive_on_username(self):
|
||||
self.register("Alice")
|
||||
self.client = app.test_client() # fresh cookie jar: signed out
|
||||
response = self.call("POST", "/api/auth/login", {"username": "ALICE", "password": "correct horse battery"})
|
||||
response = self.call(
|
||||
"POST",
|
||||
"/api/auth/login",
|
||||
{"username": "ALICE", "password": "correct horse battery"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.get_json(), {"id": 1, "username": "Alice"})
|
||||
|
||||
def test_wrong_password_and_unknown_user_are_indistinguishable(self):
|
||||
self.register()
|
||||
wrong = self.call("POST", "/api/auth/login", {"username": "alice", "password": "not the password"})
|
||||
unknown = self.call("POST", "/api/auth/login", {"username": "nobody", "password": "not the password"})
|
||||
wrong = self.call(
|
||||
"POST",
|
||||
"/api/auth/login",
|
||||
{"username": "alice", "password": "not the password"},
|
||||
)
|
||||
unknown = self.call(
|
||||
"POST",
|
||||
"/api/auth/login",
|
||||
{"username": "nobody", "password": "not the password"},
|
||||
)
|
||||
self.assertEqual(wrong.status_code, 401)
|
||||
self.assertEqual(unknown.status_code, 401)
|
||||
self.assertEqual(wrong.get_json(), unknown.get_json())
|
||||
self.assertEqual(wrong.get_json(), {"error": "Invalid username or password"})
|
||||
|
||||
def test_login_with_malformed_username_is_401_not_500(self):
|
||||
response = self.call("POST", "/api/auth/login", {"username": "a\x00b", "password": "whatever password"})
|
||||
response = self.call(
|
||||
"POST",
|
||||
"/api/auth/login",
|
||||
{"username": "a\x00b", "password": "whatever password"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_me_without_session_is_401(self):
|
||||
response = self.call("GET", "/api/auth/me")
|
||||
self.assertEqual(response.status_code, 401)
|
||||
self.assertEqual(response.get_json(), {"error": "Not signed in or session expired; log in again"})
|
||||
self.assertEqual(
|
||||
response.get_json(),
|
||||
{"error": "Not signed in or session expired; log in again"},
|
||||
)
|
||||
|
||||
def test_logout_revokes_token_server_side(self):
|
||||
self.register()
|
||||
token = self.session_token()
|
||||
self.assertEqual(self.call("POST", "/api/auth/logout").status_code, 204)
|
||||
self.client.set_cookie("sid", token, domain="localhost", path="/api") # replay the stolen cookie
|
||||
self.client.set_cookie(
|
||||
"sid", token, domain="localhost", path="/api"
|
||||
) # replay the stolen cookie
|
||||
self.assertEqual(self.call("GET", "/api/auth/me").status_code, 401)
|
||||
|
||||
def test_idle_session_expires(self):
|
||||
@@ -87,14 +128,18 @@ class AuthTests(ApiTestCase):
|
||||
|
||||
def test_absolute_cap_applies_even_inside_idle_window(self):
|
||||
self.register()
|
||||
db_execute("UPDATE sessions SET created_at = now() - interval '12 hours 1 second'")
|
||||
db_execute(
|
||||
"UPDATE sessions SET created_at = now() - interval '12 hours 1 second'"
|
||||
)
|
||||
self.assertEqual(self.call("GET", "/api/auth/me").status_code, 401)
|
||||
|
||||
def test_activity_slides_idle_expiry(self):
|
||||
self.register()
|
||||
db_execute("UPDATE sessions SET expires_at = now() + interval '1 minute'")
|
||||
self.assertEqual(self.call("GET", "/api/auth/me").status_code, 200)
|
||||
remaining = db_execute("SELECT expires_at - now() > interval '29 minutes' FROM sessions")[0][0]
|
||||
remaining = db_execute(
|
||||
"SELECT expires_at - now() > interval '29 minutes' FROM sessions"
|
||||
)[0][0]
|
||||
self.assertTrue(remaining)
|
||||
|
||||
def test_old_hash_is_upgraded_on_login(self):
|
||||
@@ -102,6 +147,14 @@ class AuthTests(ApiTestCase):
|
||||
pepper = os.environ["PASSWORD_PEPPER"].encode()
|
||||
old = hash_password("correct horse battery", pepper, iterations=1000)
|
||||
db_execute("UPDATE users SET password_hash = %s", (old,))
|
||||
response = self.call("POST", "/api/auth/login", {"username": "alice", "password": "correct horse battery"})
|
||||
response = self.call(
|
||||
"POST",
|
||||
"/api/auth/login",
|
||||
{"username": "alice", "password": "correct horse battery"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(db_execute("SELECT password_hash FROM users")[0][0].startswith("pbkdf2_sha256$600000$"))
|
||||
self.assertTrue(
|
||||
db_execute("SELECT password_hash FROM users")[0][0].startswith(
|
||||
"pbkdf2_sha256$600000$"
|
||||
)
|
||||
)
|
||||
|
||||
+48
-13
@@ -9,8 +9,14 @@ class BookTests(ApiTestCase):
|
||||
def test_create_returns_full_shape(self):
|
||||
book = self.create_book()
|
||||
self.assertEqual(book["title"], "Dune")
|
||||
self.assertEqual(book["genre"], {"id": self.genre_id("Science Fiction"), "name": "Science Fiction"})
|
||||
self.assertEqual((book["current_page"], book["total_pages"], book["status"]), (0, 412, "not_started"))
|
||||
self.assertEqual(
|
||||
book["genre"],
|
||||
{"id": self.genre_id("Science Fiction"), "name": "Science Fiction"},
|
||||
)
|
||||
self.assertEqual(
|
||||
(book["current_page"], book["total_pages"], book["status"]),
|
||||
(0, 412, "not_started"),
|
||||
)
|
||||
self.assertIn("T", book["created_at"]) # ISO 8601
|
||||
|
||||
def test_create_trims_text(self):
|
||||
@@ -19,25 +25,44 @@ class BookTests(ApiTestCase):
|
||||
|
||||
def test_progress_updates_status(self):
|
||||
book = self.create_book()
|
||||
reading = self.call("PATCH", f"/api/books/{book['id']}", {"current_page": 100}).get_json()
|
||||
reading = self.call(
|
||||
"PATCH", f"/api/books/{book['id']}", {"current_page": 100}
|
||||
).get_json()
|
||||
self.assertEqual((reading["current_page"], reading["status"]), (100, "reading"))
|
||||
finished = self.call("PATCH", f"/api/books/{book['id']}", {"current_page": 412}).get_json()
|
||||
finished = self.call(
|
||||
"PATCH", f"/api/books/{book['id']}", {"current_page": 412}
|
||||
).get_json()
|
||||
self.assertEqual(finished["status"], "finished")
|
||||
self.assertEqual(self.call("GET", f"/api/books/{book['id']}").get_json()["current_page"], 412)
|
||||
self.assertEqual(
|
||||
self.call("GET", f"/api/books/{book['id']}").get_json()["current_page"], 412
|
||||
)
|
||||
|
||||
def test_edit_details(self):
|
||||
book = self.create_book()
|
||||
response = self.call(
|
||||
"PATCH", f"/api/books/{book['id']}",
|
||||
{"title": "Dune Messiah", "genre_id": self.genre_id("Fantasy"), "total_pages": 256},
|
||||
"PATCH",
|
||||
f"/api/books/{book['id']}",
|
||||
{
|
||||
"title": "Dune Messiah",
|
||||
"genre_id": self.genre_id("Fantasy"),
|
||||
"total_pages": 256,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
updated = response.get_json()
|
||||
self.assertEqual((updated["title"], updated["genre"]["name"], updated["total_pages"]), ("Dune Messiah", "Fantasy", 256))
|
||||
self.assertEqual(
|
||||
(updated["title"], updated["genre"]["name"], updated["total_pages"]),
|
||||
("Dune Messiah", "Fantasy", 256),
|
||||
)
|
||||
self.assertGreater(updated["updated_at"], book["updated_at"])
|
||||
|
||||
def test_create_validation(self):
|
||||
valid = {"title": "Dune", "author": "Frank Herbert", "genre_id": self.genre_id("Fiction"), "total_pages": 412}
|
||||
valid = {
|
||||
"title": "Dune",
|
||||
"author": "Frank Herbert",
|
||||
"genre_id": self.genre_id("Fiction"),
|
||||
"total_pages": 412,
|
||||
}
|
||||
cases = [
|
||||
({**valid, "title": " "}, "title"),
|
||||
({k: v for k, v in valid.items() if k != "author"}, "author"),
|
||||
@@ -69,13 +94,19 @@ class BookTests(ApiTestCase):
|
||||
|
||||
def test_empty_patch_is_rejected(self):
|
||||
book = self.create_book()
|
||||
self.assertEqual(self.call("PATCH", f"/api/books/{book['id']}", {}).status_code, 400)
|
||||
self.assertEqual(
|
||||
self.call("PATCH", f"/api/books/{book['id']}", {}).status_code, 400
|
||||
)
|
||||
|
||||
def test_delete(self):
|
||||
book = self.create_book()
|
||||
self.assertEqual(self.call("DELETE", f"/api/books/{book['id']}").status_code, 204)
|
||||
self.assertEqual(
|
||||
self.call("DELETE", f"/api/books/{book['id']}").status_code, 204
|
||||
)
|
||||
self.assertEqual(self.call("GET", f"/api/books/{book['id']}").status_code, 404)
|
||||
self.assertEqual(self.call("DELETE", f"/api/books/{book['id']}").status_code, 404)
|
||||
self.assertEqual(
|
||||
self.call("DELETE", f"/api/books/{book['id']}").status_code, 404
|
||||
)
|
||||
|
||||
def test_out_of_range_id_is_404(self):
|
||||
self.assertEqual(self.call("GET", "/api/books/99999999999").status_code, 404)
|
||||
@@ -105,7 +136,11 @@ class BookIsolationTests(ApiTestCase):
|
||||
book = self.create_book()
|
||||
bob = self.other_user("bob")
|
||||
path = f"/api/books/{book['id']}"
|
||||
for method, payload in (("GET", None), ("PATCH", {"title": "Hacked"}), ("DELETE", None)):
|
||||
for method, payload in (
|
||||
("GET", None),
|
||||
("PATCH", {"title": "Hacked"}),
|
||||
("DELETE", None),
|
||||
):
|
||||
with self.subTest(method=method):
|
||||
response = self.call(method, path, payload, client=bob)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@@ -29,19 +29,30 @@ class NoteTests(ApiTestCase):
|
||||
|
||||
def test_delete(self):
|
||||
note = self.add("Temporary").get_json()
|
||||
self.assertEqual(self.call("DELETE", f"/api/notes/{note['id']}").status_code, 204)
|
||||
self.assertEqual(
|
||||
self.call("DELETE", f"/api/notes/{note['id']}").status_code, 204
|
||||
)
|
||||
self.assertEqual(self.call("GET", self.notes_path).get_json(), [])
|
||||
self.assertEqual(self.call("DELETE", f"/api/notes/{note['id']}").status_code, 404)
|
||||
self.assertEqual(
|
||||
self.call("DELETE", f"/api/notes/{note['id']}").status_code, 404
|
||||
)
|
||||
|
||||
def test_validation(self):
|
||||
for body, status in ((" ", 400), ("x" * 10_001, 400), ("a\x00b", 400)):
|
||||
with self.subTest(length=len(body)):
|
||||
self.assertEqual(self.add(body).status_code, status)
|
||||
self.assertEqual(self.call("POST", self.notes_path, {"body": "ok", "book_id": 2}).status_code, 400)
|
||||
self.assertEqual(
|
||||
self.call(
|
||||
"POST", self.notes_path, {"body": "ok", "book_id": 2}
|
||||
).status_code,
|
||||
400,
|
||||
)
|
||||
|
||||
def test_missing_book_and_out_of_range_ids_are_404(self):
|
||||
self.assertEqual(self.call("GET", "/api/books/999/notes").status_code, 404)
|
||||
self.assertEqual(self.call("PATCH", "/api/notes/99999999999", {"body": "x"}).status_code, 404)
|
||||
self.assertEqual(
|
||||
self.call("PATCH", "/api/notes/99999999999", {"body": "x"}).status_code, 404
|
||||
)
|
||||
|
||||
def test_deleting_book_deletes_its_notes(self):
|
||||
self.add("Will be gone")
|
||||
@@ -53,7 +64,9 @@ class NoteIsolationTests(ApiTestCase):
|
||||
def test_other_user_cannot_read_add_edit_or_delete_notes(self):
|
||||
self.register("alice")
|
||||
book = self.create_book()
|
||||
note = self.call("POST", f"/api/books/{book['id']}/notes", {"body": "Private"}).get_json()
|
||||
note = self.call(
|
||||
"POST", f"/api/books/{book['id']}/notes", {"body": "Private"}
|
||||
).get_json()
|
||||
bob = self.other_user("bob")
|
||||
attempts = (
|
||||
("GET", f"/api/books/{book['id']}/notes", None),
|
||||
@@ -63,6 +76,11 @@ class NoteIsolationTests(ApiTestCase):
|
||||
)
|
||||
for method, path, payload in attempts:
|
||||
with self.subTest(method=method, path=path):
|
||||
self.assertEqual(self.call(method, path, payload, client=bob).status_code, 404)
|
||||
bodies = [n["body"] for n in self.call("GET", f"/api/books/{book['id']}/notes").get_json()]
|
||||
self.assertEqual(
|
||||
self.call(method, path, payload, client=bob).status_code, 404
|
||||
)
|
||||
bodies = [
|
||||
n["body"]
|
||||
for n in self.call("GET", f"/api/books/{book['id']}/notes").get_json()
|
||||
]
|
||||
self.assertEqual(bodies, ["Private"])
|
||||
|
||||
@@ -14,7 +14,9 @@ KNOWN_ANSWER = "pbkdf2_sha256$1000$AAAAAAAAAAAAAAAAAAAAAA==$9ZLLEusnEPU3Km8h+vnd
|
||||
|
||||
class PasswordHashTests(unittest.TestCase):
|
||||
def test_known_answer(self):
|
||||
stored = hash_password("correct horse battery staple", PEPPER, salt=bytes(16), iterations=1000)
|
||||
stored = hash_password(
|
||||
"correct horse battery staple", PEPPER, salt=bytes(16), iterations=1000
|
||||
)
|
||||
self.assertEqual(stored, KNOWN_ANSWER)
|
||||
|
||||
def test_round_trip_uses_current_iterations_and_random_salt(self):
|
||||
@@ -28,7 +30,9 @@ class PasswordHashTests(unittest.TestCase):
|
||||
self.assertFalse(verify_password("wrong password!!", KNOWN_ANSWER, PEPPER))
|
||||
|
||||
def test_wrong_pepper_fails(self):
|
||||
self.assertFalse(verify_password("correct horse battery staple", KNOWN_ANSWER, b"q" * 32))
|
||||
self.assertFalse(
|
||||
verify_password("correct horse battery staple", KNOWN_ANSWER, b"q" * 32)
|
||||
)
|
||||
|
||||
def test_needs_rehash_below_current_iterations(self):
|
||||
self.assertTrue(needs_rehash(KNOWN_ANSWER))
|
||||
|
||||
@@ -8,7 +8,9 @@ class SearchTests(ApiTestCase):
|
||||
self.scifi = self.genre_id("Science Fiction")
|
||||
self.fantasy = self.genre_id("Fantasy")
|
||||
self.create_book(title="Dune", author="Frank Herbert", genre_id=self.scifi)
|
||||
self.create_book(title="The Hobbit", author="J.R.R. Tolkien", genre_id=self.fantasy)
|
||||
self.create_book(
|
||||
title="The Hobbit", author="J.R.R. Tolkien", genre_id=self.fantasy
|
||||
)
|
||||
self.create_book(title="100% Pure", author="Jane_Doe", genre_id=self.fantasy)
|
||||
self.create_book(title="1000 Pages", author="Back\\slash", genre_id=self.scifi)
|
||||
|
||||
@@ -22,20 +24,28 @@ class SearchTests(ApiTestCase):
|
||||
self.assertEqual(self.titles("q=tolkien"), ["The Hobbit"])
|
||||
|
||||
def test_wildcards_match_literally(self):
|
||||
self.assertEqual(self.titles("q=100%25"), ["100% Pure"]) # %25 is "%"
|
||||
self.assertEqual(self.titles("q=e_D"), ["100% Pure"]) # literal "_" in Jane_Doe
|
||||
self.assertEqual(self.titles("q=k_s"), []) # unescaped "_" would match "Back\\slash"
|
||||
self.assertEqual(self.titles("q=k%5Cs"), ["1000 Pages"]) # %5C is "\"
|
||||
self.assertEqual(self.titles("q=100%25"), ["100% Pure"]) # %25 is "%"
|
||||
self.assertEqual(self.titles("q=e_D"), ["100% Pure"]) # literal "_" in Jane_Doe
|
||||
self.assertEqual(
|
||||
self.titles("q=k_s"), []
|
||||
) # unescaped "_" would match "Back\\slash"
|
||||
self.assertEqual(self.titles("q=k%5Cs"), ["1000 Pages"]) # %5C is "\"
|
||||
|
||||
def test_genre_filter_and_combination(self):
|
||||
self.assertEqual(self.titles(f"genre_id={self.fantasy}"), ["100% Pure", "The Hobbit"])
|
||||
self.assertEqual(
|
||||
self.titles(f"genre_id={self.fantasy}"), ["100% Pure", "The Hobbit"]
|
||||
)
|
||||
self.assertEqual(self.titles(f"q=100&genre_id={self.scifi}"), ["1000 Pages"])
|
||||
|
||||
def test_blank_query_returns_everything(self):
|
||||
self.assertEqual(len(self.titles("q=%20%20")), 4)
|
||||
|
||||
def test_invalid_parameters(self):
|
||||
for query, field in (("genre_id=abc", "genre_id"), ("genre_id=99999", "genre_id"), ("q=a%00b", "q")):
|
||||
for query, field in (
|
||||
("genre_id=abc", "genre_id"),
|
||||
("genre_id=99999", "genre_id"),
|
||||
("q=a%00b", "q"),
|
||||
):
|
||||
with self.subTest(query=query):
|
||||
response = self.call("GET", f"/api/books?{query}")
|
||||
self.assertEqual(response.status_code, 400)
|
||||
@@ -43,4 +53,6 @@ class SearchTests(ApiTestCase):
|
||||
|
||||
def test_search_never_returns_other_users_books(self):
|
||||
bob = self.other_user("bob")
|
||||
self.assertEqual(self.call("GET", "/api/books?q=dune", client=bob).get_json(), [])
|
||||
self.assertEqual(
|
||||
self.call("GET", "/api/books?q=dune", client=bob).get_json(), []
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user