2025-11-18 16:58:21 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Any, Dict
|
|
|
|
|
|
|
|
|
|
from flask import Flask, jsonify
|
|
|
|
|
|
|
|
|
|
from lawrisk.api import auth as auth_module
|
|
|
|
|
from lawrisk.api.auth import auth_bp
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_test_app() -> Flask:
|
|
|
|
|
base_path = Path(__file__).resolve().parents[1]
|
|
|
|
|
app = Flask(
|
|
|
|
|
__name__,
|
|
|
|
|
template_folder=str(base_path / "templates"),
|
|
|
|
|
static_folder=str(base_path / "static"),
|
|
|
|
|
)
|
|
|
|
|
app.secret_key = "test-secret"
|
|
|
|
|
app.register_blueprint(auth_bp)
|
|
|
|
|
app.config.update(TESTING=True)
|
|
|
|
|
|
|
|
|
|
@app.route("/protected-json")
|
|
|
|
|
@auth_module.login_required
|
|
|
|
|
def protected_json() -> Any:
|
|
|
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
|
|
|
|
@app.route("/protected-page")
|
|
|
|
|
@auth_module.login_required
|
|
|
|
|
def protected_page() -> Any:
|
|
|
|
|
return "secret"
|
|
|
|
|
|
|
|
|
|
return app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_login_success(monkeypatch) -> None:
|
|
|
|
|
app = build_test_app()
|
|
|
|
|
client = app.test_client()
|
|
|
|
|
|
|
|
|
|
fake_user: Dict[str, Any] = {
|
|
|
|
|
"id": "user-1",
|
|
|
|
|
"username": "admin",
|
|
|
|
|
"display_name": "Admin",
|
|
|
|
|
"role": "admin",
|
|
|
|
|
"grade": 100,
|
|
|
|
|
"is_active": True,
|
|
|
|
|
"password_hash": "hash",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(auth_module, "get_user_by_username", lambda _: fake_user)
|
|
|
|
|
monkeypatch.setattr(auth_module, "verify_password", lambda *_: True)
|
|
|
|
|
|
|
|
|
|
resp = client.post("/auth/login", json={"username": "admin", "password": "secret"})
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
data = resp.get_json()
|
|
|
|
|
assert data["user"]["role"] == "admin"
|
|
|
|
|
assert data["redirect"] == "/"
|
|
|
|
|
with client.session_transaction() as sess:
|
|
|
|
|
assert sess[auth_module.SESSION_USER_KEY]["username"] == "admin"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_login_invalid_credentials(monkeypatch) -> None:
|
|
|
|
|
app = build_test_app()
|
|
|
|
|
client = app.test_client()
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(auth_module, "get_user_by_username", lambda *_: None)
|
|
|
|
|
|
|
|
|
|
resp = client.post("/auth/login", json={"username": "ghost", "password": "bad"})
|
|
|
|
|
assert resp.status_code == 401
|
|
|
|
|
assert resp.get_json()["error"] == "invalid credentials"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_login_success_honors_next(monkeypatch) -> None:
|
|
|
|
|
app = build_test_app()
|
|
|
|
|
client = app.test_client()
|
|
|
|
|
|
|
|
|
|
fake_user: Dict[str, Any] = {
|
|
|
|
|
"id": "user-1",
|
|
|
|
|
"username": "admin",
|
|
|
|
|
"display_name": "Admin",
|
|
|
|
|
"role": "admin",
|
|
|
|
|
"grade": 100,
|
|
|
|
|
"is_active": True,
|
|
|
|
|
"password_hash": "hash",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(auth_module, "get_user_by_username", lambda _: fake_user)
|
|
|
|
|
monkeypatch.setattr(auth_module, "verify_password", lambda *_: True)
|
|
|
|
|
|
|
|
|
|
resp = client.post(
|
|
|
|
|
"/auth/login?next=/fs-ai-asistant/api/workflow/lawrisk/db_admin",
|
|
|
|
|
json={"username": "admin", "password": "secret"},
|
|
|
|
|
)
|
|
|
|
|
assert resp.status_code == 200
|
|
|
|
|
assert (
|
|
|
|
|
resp.get_json()["redirect"] == "/fs-ai-asistant/api/workflow/lawrisk/db_admin"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_me_endpoint_requires_session() -> None:
|
|
|
|
|
app = build_test_app()
|
|
|
|
|
client = app.test_client()
|
|
|
|
|
|
|
|
|
|
resp = client.get("/auth/me")
|
|
|
|
|
assert resp.status_code == 401
|
|
|
|
|
|
|
|
|
|
with client.session_transaction() as sess:
|
|
|
|
|
sess[auth_module.SESSION_USER_KEY] = {
|
|
|
|
|
"id": "user-2",
|
|
|
|
|
"username": "analyst",
|
|
|
|
|
"display_name": "Data Analyst",
|
|
|
|
|
"role": "reviewer",
|
|
|
|
|
"grade": 20,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp2 = client.get("/auth/me")
|
|
|
|
|
assert resp2.status_code == 200
|
|
|
|
|
assert resp2.get_json()["user"]["role"] == "reviewer"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_login_required_returns_json_error_for_api() -> None:
|
|
|
|
|
app = build_test_app()
|
|
|
|
|
client = app.test_client()
|
|
|
|
|
|
|
|
|
|
resp = client.get("/protected-json", headers={"Accept": "application/json"})
|
|
|
|
|
assert resp.status_code == 401
|
|
|
|
|
assert resp.get_json()["error"] == "authentication required"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_login_required_redirects_for_html() -> None:
|
|
|
|
|
app = build_test_app()
|
|
|
|
|
client = app.test_client()
|
|
|
|
|
|
|
|
|
|
resp = client.get("/protected-page", headers={"Accept": "text/html"})
|
|
|
|
|
assert resp.status_code == 302
|
2025-12-20 11:25:34 +08:00
|
|
|
assert "/fs-ai-asistant/api/workflow/lawrisk/login" in resp.headers["Location"]
|