Commits: 9
Write a test for password length validation
The test does not pass yet.
index 7ddafc7..72366da 100644
--- a/test_joker_registration_validation.py
+++ b/test_joker_registration_validation.py
@@ -1,4 +1,6 @@
=from main import JokerRegistration
+from pydantic import ValidationError
+import pytest
=
=
=def test_valid_registration():
@@ -8,3 +10,14 @@ def test_valid_registration():
= registration = JokerRegistration(**payload)
= assert registration.name == "Funny Guy"
= assert registration.password == "Jokes 4 you!"
+
+
+def test_password_too_short_registration():
+ """Password must be at least 8 characters long"""
+
+ payload = {"name": "Funny Guy", "password": "jokes"}
+
+ with pytest.raises(
+ ValidationError, match="Password must be at least 8 characters long"
+ ) as exception_info:
+ JokerRegistration(**payload)Implement password length validation
I had to modify the test, as it is quite difficult to modify built-in error message from Pydantic.
index 5874cff..3b9e704 100644
--- a/main.py
+++ b/main.py
@@ -16,7 +16,7 @@ import uvicorn
=from fastapi import FastAPI, HTTPException, Response, Depends
=from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
=from passlib.context import CryptContext
-from pydantic import BaseModel
+from pydantic import BaseModel, Field
=
=
=DB_PATH = Path(os.environ.get("jaas_db_path") or "./jokes.db").expanduser().resolve()
@@ -158,7 +158,7 @@ async def authenticate(form: Annotated[OAuth2PasswordRequestForm, Depends()]) ->
=# TODO: Validation: password and name length, not blank, etc.
=class JokerRegistration(BaseModel):
= name: str
- password: str
+ password: str = Field(min_length=8)
=
=
=@app.post("/jokers/", status_code=201)index 72366da..cda8f0d 100644
--- a/test_joker_registration_validation.py
+++ b/test_joker_registration_validation.py
@@ -18,6 +18,6 @@ def test_password_too_short_registration():
= payload = {"name": "Funny Guy", "password": "jokes"}
=
= with pytest.raises(
- ValidationError, match="Password must be at least 8 characters long"
+ ValidationError, match="at least 8 characters"
= ) as exception_info:
= JokerRegistration(**payload)Narrow the password length test conditions
Make sure the offending field is "password" and that it's too short.
index cda8f0d..44f23b2 100644
--- a/test_joker_registration_validation.py
+++ b/test_joker_registration_validation.py
@@ -21,3 +21,6 @@ def test_password_too_short_registration():
= ValidationError, match="at least 8 characters"
= ) as exception_info:
= JokerRegistration(**payload)
+
+ assert exception_info.value.errors()[0]["loc"] == ("password",)
+ assert exception_info.value.errors()[0]["type"] == "string_too_short"Write a TODO for a blank password validation
index 44f23b2..aa4e387 100644
--- a/test_joker_registration_validation.py
+++ b/test_joker_registration_validation.py
@@ -24,3 +24,6 @@ def test_password_too_short_registration():
=
= assert exception_info.value.errors()[0]["loc"] == ("password",)
= assert exception_info.value.errors()[0]["type"] == "string_too_short"
+
+
+# TODO: Password cannot be blankWrite a test for blank password validation
index aa4e387..767112a 100644
--- a/test_joker_registration_validation.py
+++ b/test_joker_registration_validation.py
@@ -26,4 +26,15 @@ def test_password_too_short_registration():
= assert exception_info.value.errors()[0]["type"] == "string_too_short"
=
=
-# TODO: Password cannot be blank
+def test_blank_password_registration():
+ """Password cannot be blank"""
+
+ payload = {"name": "Funny Guy", "password": "\t \t\t \n "}
+
+ with pytest.raises(
+ ValidationError, match="A password cannot be blank"
+ ) as exception_info:
+ JokerRegistration(**payload)
+
+ # assert exception_info.value.errors()[0]["loc"] == ("password",)
+ # assert exception_info.value.errors()[0]["type"] == "string_too_short"Use field_validator to check for blank passwords
index 3b9e704..0e41417 100644
--- a/main.py
+++ b/main.py
@@ -16,7 +16,7 @@ import uvicorn
=from fastapi import FastAPI, HTTPException, Response, Depends
=from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
=from passlib.context import CryptContext
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, Field, field_validator
=
=
=DB_PATH = Path(os.environ.get("jaas_db_path") or "./jokes.db").expanduser().resolve()
@@ -160,6 +160,12 @@ class JokerRegistration(BaseModel):
= name: str
= password: str = Field(min_length=8)
=
+ @field_validator("password")
+ @classmethod
+ def blank_password_validator(model, value: str):
+ assert not value.isspace(), "A password cannot be blank"
+ return value
+
=
=@app.post("/jokers/", status_code=201)
=def register_joker(registration: JokerRegistration) -> Joker:Test multiple blank passwords samples
Re-use the test function with parametrize mark. Both valid and invalid samples are provided, with second argument indicating if they are valid or not.
index 767112a..4236bd1 100644
--- a/test_joker_registration_validation.py
+++ b/test_joker_registration_validation.py
@@ -26,15 +26,25 @@ def test_password_too_short_registration():
= assert exception_info.value.errors()[0]["type"] == "string_too_short"
=
=
-def test_blank_password_registration():
+@pytest.mark.parametrize(
+ "input, good",
+ [
+ ("\t \t\t \n ", False),
+ ("\t \there\t \n ", True),
+ (" ", False),
+ (" * ", True),
+ ("a ", True),
+ ],
+)
+def test_blank_password_registration(input, good):
= """Password cannot be blank"""
=
- payload = {"name": "Funny Guy", "password": "\t \t\t \n "}
+ payload = {"name": "Funny Guy", "password": input}
=
- with pytest.raises(
- ValidationError, match="A password cannot be blank"
- ) as exception_info:
+ if good:
= JokerRegistration(**payload)
-
- # assert exception_info.value.errors()[0]["loc"] == ("password",)
- # assert exception_info.value.errors()[0]["type"] == "string_too_short"
+ else:
+ with pytest.raises(
+ ValidationError, match="A password cannot be blank"
+ ) as exception_info:
+ JokerRegistration(**payload)Implement first API test (GET /)
Using FastAPI TestClient.
Also write some TODOs for future tests.
index 9495fe0..9ab8b46 100644
--- a/flake.nix
+++ b/flake.nix
@@ -23,6 +23,7 @@
= ps.bcrypt
= ps.python-multipart
= ps.pytest
+ ps.httpx
= ]);
= in
=new file mode 100644
index 0000000..ebb1c61
--- /dev/null
+++ b/test_rest.py
@@ -0,0 +1,27 @@
+import pytest
+import main
+from fastapi.testclient import TestClient
+
+
+# TODO: Use dependency injection for DB connections so it can be mocked during tests
+#
+# Currently the database file is created in the CWD, and the tests are not
+# isolated. A unique temporary file should be created for every test.
+
+client = TestClient(main.app)
+
+
+def test_get_root():
+ response = client.get("/")
+ assert response.status_code == 200
+ body = response.json()
+ assert "id" in body
+ assert "text" in body
+ assert "author_id" in body
+ assert "author_name" in body
+ assert "laughs" in body
+
+
+# TODO: Test user registration
+
+# TODO: Test authentication. Use a fixture to register new users before authenticating.Write a user registration test (POST /jokers/)
It does pass the first time, but on subsequent trials it fails, because tests are not isolated. The same .db file is used every time (it also pollutes the filesystem with those files). Let's fix it!
index ebb1c61..42a9715 100644
--- a/test_rest.py
+++ b/test_rest.py
@@ -22,6 +22,14 @@ def test_get_root():
= assert "laughs" in body
=
=
-# TODO: Test user registration
+def test_post_jokers():
+ response = client.post(
+ "/jokers/", json={"name": "Bob", "password": "Bob is funny!"}
+ )
+ assert response.status_code == 201
+ body = response.json()
+ assert "id" in body
+ assert "name" in body
+
=
=# TODO: Test authentication. Use a fixture to register new users before authenticating.