Vendor backend and frontend into the monorepo
The submodule collapse was only half applied: .gitmodules was deleted but backend/ and frontend/ were still recorded as gitlinks, so none of their files were tracked. Replace the gitlinks with the real trees. Also untrack .env (it carried placeholder secrets) in favour of a tracked .env.example, drop the committed __pycache__, and narrow the blanket *.png ignore that would have swallowed design assets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3cb16958ba
commit
1916f7f778
@@ -0,0 +1,191 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from pwdlib.hashers.bcrypt import BcryptHasher
|
||||
from sqlmodel import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.security import get_password_hash, verify_password
|
||||
from app.crud import create_user
|
||||
from app.models import User, UserCreate
|
||||
from app.utils import generate_password_reset_token
|
||||
from tests.utils.user import user_authentication_headers
|
||||
from tests.utils.utils import random_email, random_lower_string
|
||||
|
||||
|
||||
def test_get_access_token(client: TestClient) -> None:
|
||||
login_data = {
|
||||
"username": settings.FIRST_SUPERUSER,
|
||||
"password": settings.FIRST_SUPERUSER_PASSWORD,
|
||||
}
|
||||
r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data)
|
||||
tokens = r.json()
|
||||
assert r.status_code == 200
|
||||
assert "access_token" in tokens
|
||||
assert tokens["access_token"]
|
||||
|
||||
|
||||
def test_get_access_token_incorrect_password(client: TestClient) -> None:
|
||||
login_data = {
|
||||
"username": settings.FIRST_SUPERUSER,
|
||||
"password": "incorrect",
|
||||
}
|
||||
r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_use_access_token(
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
r = client.post(
|
||||
f"{settings.API_V1_STR}/login/test-token",
|
||||
headers=superuser_token_headers,
|
||||
)
|
||||
result = r.json()
|
||||
assert r.status_code == 200
|
||||
assert "email" in result
|
||||
|
||||
|
||||
def test_recovery_password(
|
||||
client: TestClient, normal_user_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
with (
|
||||
patch("app.core.config.settings.SMTP_HOST", "smtp.example.com"),
|
||||
patch("app.core.config.settings.SMTP_USER", "admin@example.com"),
|
||||
):
|
||||
email = "test@example.com"
|
||||
r = client.post(
|
||||
f"{settings.API_V1_STR}/password-recovery/{email}",
|
||||
headers=normal_user_token_headers,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json() == {
|
||||
"message": "If that email is registered, we sent a password recovery link"
|
||||
}
|
||||
|
||||
|
||||
def test_recovery_password_user_not_exits(
|
||||
client: TestClient, normal_user_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
email = "jVgQr@example.com"
|
||||
r = client.post(
|
||||
f"{settings.API_V1_STR}/password-recovery/{email}",
|
||||
headers=normal_user_token_headers,
|
||||
)
|
||||
# Should return 200 with generic message to prevent email enumeration attacks
|
||||
assert r.status_code == 200
|
||||
assert r.json() == {
|
||||
"message": "If that email is registered, we sent a password recovery link"
|
||||
}
|
||||
|
||||
|
||||
def test_reset_password(client: TestClient, db: Session) -> None:
|
||||
email = random_email()
|
||||
password = random_lower_string()
|
||||
new_password = random_lower_string()
|
||||
|
||||
user_create = UserCreate(
|
||||
email=email,
|
||||
full_name="Test User",
|
||||
password=password,
|
||||
is_active=True,
|
||||
is_superuser=False,
|
||||
)
|
||||
user = create_user(session=db, user_create=user_create)
|
||||
token = generate_password_reset_token(email=email)
|
||||
headers = user_authentication_headers(client=client, email=email, password=password)
|
||||
data = {"new_password": new_password, "token": token}
|
||||
|
||||
r = client.post(
|
||||
f"{settings.API_V1_STR}/reset-password/",
|
||||
headers=headers,
|
||||
json=data,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json() == {"message": "Password updated successfully"}
|
||||
|
||||
db.refresh(user)
|
||||
verified, _ = verify_password(new_password, user.hashed_password)
|
||||
assert verified
|
||||
|
||||
|
||||
def test_reset_password_invalid_token(
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
data = {"new_password": "changethis", "token": "invalid"}
|
||||
r = client.post(
|
||||
f"{settings.API_V1_STR}/reset-password/",
|
||||
headers=superuser_token_headers,
|
||||
json=data,
|
||||
)
|
||||
response = r.json()
|
||||
|
||||
assert "detail" in response
|
||||
assert r.status_code == 400
|
||||
assert response["detail"] == "Invalid token"
|
||||
|
||||
|
||||
def test_login_with_bcrypt_password_upgrades_to_argon2(
|
||||
client: TestClient, db: Session
|
||||
) -> None:
|
||||
"""Test that logging in with a bcrypt password hash upgrades it to argon2."""
|
||||
email = random_email()
|
||||
password = random_lower_string()
|
||||
|
||||
# Create a bcrypt hash directly (simulating legacy password)
|
||||
bcrypt_hasher = BcryptHasher()
|
||||
bcrypt_hash = bcrypt_hasher.hash(password)
|
||||
assert bcrypt_hash.startswith("$2") # bcrypt hashes start with $2
|
||||
|
||||
user = User(email=email, hashed_password=bcrypt_hash, is_active=True)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
assert user.hashed_password.startswith("$2")
|
||||
|
||||
login_data = {"username": email, "password": password}
|
||||
r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data)
|
||||
assert r.status_code == 200
|
||||
tokens = r.json()
|
||||
assert "access_token" in tokens
|
||||
|
||||
db.refresh(user)
|
||||
|
||||
# Verify the hash was upgraded to argon2
|
||||
assert user.hashed_password.startswith("$argon2")
|
||||
|
||||
verified, updated_hash = verify_password(password, user.hashed_password)
|
||||
assert verified
|
||||
# Should not need another update since it's already argon2
|
||||
assert updated_hash is None
|
||||
|
||||
|
||||
def test_login_with_argon2_password_keeps_hash(client: TestClient, db: Session) -> None:
|
||||
"""Test that logging in with an argon2 password hash does not update it."""
|
||||
email = random_email()
|
||||
password = random_lower_string()
|
||||
|
||||
# Create an argon2 hash (current default)
|
||||
argon2_hash = get_password_hash(password)
|
||||
assert argon2_hash.startswith("$argon2")
|
||||
|
||||
# Create user with argon2 hash
|
||||
user = User(email=email, hashed_password=argon2_hash, is_active=True)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
original_hash = user.hashed_password
|
||||
|
||||
login_data = {"username": email, "password": password}
|
||||
r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data)
|
||||
assert r.status_code == 200
|
||||
tokens = r.json()
|
||||
assert "access_token" in tokens
|
||||
|
||||
db.refresh(user)
|
||||
|
||||
assert user.hashed_password == original_hash
|
||||
assert user.hashed_password.startswith("$argon2")
|
||||
Reference in New Issue
Block a user