File size: 4,840 Bytes
54286b5 | 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 | """
Unit Tests β Auth: hashing.py and jwt.py
No HTTP client or database required.
"""
from datetime import timedelta
from unittest.mock import MagicMock
import pytest
from fastapi import HTTPException
from backend.auth.hashing import hash_password, verify_password
from backend.auth.jwt import (
create_access_token,
create_refresh_token,
decode_token,
extract_token_from_request,
)
# ββ U-1 through U-4: Password Hashing ββββββββββββββββββββββββββββββββββββββββ
class TestHashPassword:
async def test_hash_returns_nonempty_string(self):
h = hash_password("mysecret")
assert isinstance(h, str) and len(h) > 0
async def test_hash_does_not_store_plaintext(self):
h = hash_password("mysecret")
assert "mysecret" not in h
async def test_two_hashes_of_same_password_differ(self):
# bcrypt uses random salt β different hashes for same input
h1 = hash_password("same")
h2 = hash_password("same")
assert h1 != h2
async def test_verify_correct_password(self):
h = hash_password("correct_horse_battery")
assert verify_password("correct_horse_battery", h) is True
async def test_verify_wrong_password(self):
h = hash_password("correct")
assert verify_password("wrong", h) is False
async def test_verify_empty_against_hash(self):
h = hash_password("nonempty")
assert verify_password("", h) is False
# ββ U-5 through U-9: JWT Operations ββββββββββββββββββββββββββββββββββββββββββ
class TestJWT:
async def test_create_access_token_returns_string(self):
token = create_access_token({"sub": "user-id-123"})
assert isinstance(token, str) and len(token) > 0
async def test_decode_access_token_returns_correct_sub(self):
token = create_access_token({"sub": "user-abc"})
payload = decode_token(token, expected_type="access")
assert payload["sub"] == "user-abc"
async def test_decode_token_has_type_access(self):
token = create_access_token({"sub": "x"})
payload = decode_token(token)
assert payload["type"] == "access"
async def test_expired_token_raises_401(self):
token = create_access_token({"sub": "x"}, expires_delta=timedelta(seconds=-1))
with pytest.raises(HTTPException) as exc_info:
decode_token(token)
assert exc_info.value.status_code == 401
async def test_refresh_token_type(self):
token = create_refresh_token({"sub": "user-r"})
payload = decode_token(token, expected_type="refresh")
assert payload["type"] == "refresh"
assert payload["sub"] == "user-r"
async def test_refresh_used_as_access_raises_401(self):
refresh = create_refresh_token({"sub": "user-r"})
with pytest.raises(HTTPException) as exc_info:
decode_token(refresh, expected_type="access")
assert exc_info.value.status_code == 401
async def test_tampered_token_raises_401(self):
import base64, json
token = create_access_token({"sub": "real-user"})
header, _, sig = token.split(".")
fake_payload = base64.urlsafe_b64encode(
json.dumps({"sub": "attacker", "type": "access"}).encode()
).rstrip(b"=").decode()
tampered = f"{header}.{fake_payload}.{sig}"
with pytest.raises(HTTPException) as exc_info:
decode_token(tampered)
assert exc_info.value.status_code == 401
# ββ U-10: extract_token_from_request βββββββββββββββββββββββββββββββββββββββββ
class TestExtractToken:
async def test_extracts_from_cookie(self):
req = MagicMock()
req.cookies = {"access_token": "cookie-token"}
req.headers = {}
result = extract_token_from_request(req)
assert result == "cookie-token"
async def test_extracts_from_bearer_header(self):
req = MagicMock()
req.cookies = {}
req.headers = {"Authorization": "Bearer header-token"}
result = extract_token_from_request(req)
assert result == "header-token"
async def test_returns_none_when_no_token(self):
req = MagicMock()
req.cookies = {}
req.headers = {}
result = extract_token_from_request(req)
assert result is None
async def test_cookie_takes_priority_over_header(self):
req = MagicMock()
req.cookies = {"access_token": "cookie-tok"}
req.headers = {"Authorization": "Bearer header-tok"}
result = extract_token_from_request(req)
assert result == "cookie-tok"
|