"""Tests for furtka.passwd — stdlib-only password hashing. The primary contract: hash/verify roundtrips cleanly, AND the verifier accepts the werkzeug hash format that 26.11 / 26.12 boxes wrote to ``users.json``. Losing that backward compat would lock out existing admins after a 26.13+ upgrade. """ from __future__ import annotations from furtka import passwd def test_hash_roundtrip(): h = passwd.hash_password("hunter2") assert passwd.verify_password("hunter2", h) assert not passwd.verify_password("wrong", h) def test_hash_is_salted(): # Two separate hashes of the same password must diverge. a = passwd.hash_password("same-pw") b = passwd.hash_password("same-pw") assert a != b assert passwd.verify_password("same-pw", a) assert passwd.verify_password("same-pw", b) def test_generated_hash_format(): # Shape is pbkdf2::$$ h = passwd.hash_password("x") parts = h.split("$", 2) assert len(parts) == 3 method, salt, digest = parts assert method.startswith("pbkdf2:sha256:") assert salt # digest is hex of pbkdf2_hmac sha256 → 64 hex chars assert len(digest) == 64 assert all(c in "0123456789abcdef" for c in digest) def test_verify_werkzeug_scrypt_hash(): """Known werkzeug scrypt hash generated by 26.11 / 26.12 boxes. Captured live off a .196 test VM after its auth bootstrap: username=daniel, password=test-admin-pw1 Hash format: scrypt:32768:8:1$$ If this regresses, every existing box that upgraded via 26.11 and set a password gets locked out on the next upgrade. """ known = ( "scrypt:32768:8:1$yWZUqJodowt9ieI1$" "2d1059b3564da7492b4aa3c2be7fff6fef06085e5e1bfd52e897948c58246b7a" "9603400355b7264f61c4436eba7bf8c947adec3d7a76be03b50efb4227e15a80" ) assert passwd.verify_password("test-admin-pw1", known) assert not passwd.verify_password("wrong-password", known) def test_verify_rejects_malformed_hashes(): # Empty / missing delimiters / unknown method / bad int — all False. assert not passwd.verify_password("x", "") assert not passwd.verify_password("x", "nothingspecial") assert not passwd.verify_password("x", "pbkdf2:sha256:600000") # no $salt$digest assert not passwd.verify_password("x", "pbkdf2$salt$digest") # missing hash + iter assert not passwd.verify_password("x", "bcrypt:12$salt$digest") # unsupported algo assert not passwd.verify_password("x", "pbkdf2:sha256:abc$salt$digest") # bad iter int def test_verify_rejects_nonstring_inputs(): # Defensive: users.json can be corrupted or have nulls. assert not passwd.verify_password(None, "pbkdf2:sha256:1000$salt$digest") # type: ignore[arg-type] assert not passwd.verify_password("x", None) # type: ignore[arg-type] assert not passwd.verify_password("x", 12345) # type: ignore[arg-type]