style: format backend with black
Also drops the session-sliding comment in login_required.
This commit is contained in:
+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$"
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user