import uuid import pytest from fastapi.testclient import TestClient from app.routers import auth as auth_router from tests.conftest import bearer ADMIN_EMAIL = "admin@example.com" ADMIN_PASSWORD = "admin-password-123" # --------------------------------------------------------------------------- # helpers # --------------------------------------------------------------------------- def _capture_email(monkeypatch) -> dict[str, str]: captured: dict[str, str] = {} async def capture(recipient: str, token: str) -> None: captured["token"] = token monkeypatch.setattr(auth_router, "send_confirmation_email", capture) return captured def _register(client: TestClient, email: str, password: str, username: str): return client.post( "/api/v1/auth/register", json={"email": email, "password": password, "username": username}, ) def _confirm(client: TestClient, token: str): return client.post("/api/v1/auth/confirm-email", json={"token": token}) def _login(client: TestClient, email: str, password: str): return client.post( "/api/v1/auth/login", json={"email": email, "password": password}, ) def _register_and_confirm( client: TestClient, monkeypatch, email: str, password: str, username: str, ) -> dict[str, str]: captured = _capture_email(monkeypatch) r = _register(client, email, password, username) assert r.status_code == 201 token = captured.get("token") assert token cr = _confirm(client, token) assert cr.status_code == 200 lr = _login(client, email, password) assert lr.status_code == 200 body = lr.json() return {"token": body["access_token"], "user_id": body["user"]["id"]} def _login_as_user(client: TestClient, email: str, password: str) -> str: r = _login(client, email, password) return r.json()["access_token"] # --------------------------------------------------------------------------- # register # --------------------------------------------------------------------------- def test_register_success(client: TestClient, monkeypatch) -> None: email = f"reg-{uuid.uuid4().hex[:8]}@example.com" captured = _capture_email(monkeypatch) r = _register(client, email, "regpassword", "注册测试") assert r.status_code == 201 assert r.json()["message"] == "注册成功,请查收确认邮件" assert "token" in captured def test_register_duplicate_email(client: TestClient, monkeypatch) -> None: email = f"dup-{uuid.uuid4().hex[:8]}@example.com" _register(client, email, "firstpass", "第一个用户") r = _register(client, email, "secondpass", "第二个用户") assert r.status_code == 409 assert "该邮箱已被注册" == r.json()["detail"] def test_register_duplicate_username(client: TestClient, monkeypatch) -> None: email_a = f"dupname-a-{uuid.uuid4().hex[:8]}@example.com" email_b = f"dupname-b-{uuid.uuid4().hex[:8]}@example.com" _register(client, email_a, "passA123", "同名用户") r = _register(client, email_b, "passB123", "同名用户") assert r.status_code == 409 assert "这个昵称已经被使用" == r.json()["detail"] def test_register_short_password(client: TestClient) -> None: r = _register(client, f"s-{uuid.uuid4().hex[:8]}@e.com", "1234567", "短密码") assert r.status_code == 422 def test_register_short_username(client: TestClient) -> None: r = _register(client, f"s-{uuid.uuid4().hex[:8]}@e.com", "pass12345", "A") assert r.status_code == 422 def test_register_invalid_email(client: TestClient) -> None: r = _register(client, "not-an-email", "pass12345", "坏邮箱") assert r.status_code == 422 # --------------------------------------------------------------------------- # confirm-email # --------------------------------------------------------------------------- def test_confirm_email_valid(client: TestClient, monkeypatch) -> None: email = f"confirm-{uuid.uuid4().hex[:8]}@example.com" captured = _capture_email(monkeypatch) r = _register(client, email, "confirmpass", "确认测试") assert r.status_code == 201 token = captured.get("token") assert token cr = _confirm(client, token) assert cr.status_code == 200 assert cr.json()["message"] == "邮箱确认成功" def test_confirm_email_invalid_token(client: TestClient) -> None: r = _confirm(client, "not-a-valid-token-at-all-never") assert r.status_code == 400 assert "无效" in r.json()["detail"] # --------------------------------------------------------------------------- # login # --------------------------------------------------------------------------- def test_login_success(client: TestClient, monkeypatch) -> None: email = f"login-{uuid.uuid4().hex[:8]}@example.com" info = _register_and_confirm(client, monkeypatch, email, "loginpass", "登录测试") assert info["token"] assert info["user_id"] def test_login_wrong_password(client: TestClient, monkeypatch) -> None: email = f"loginfail-{uuid.uuid4().hex[:8]}@example.com" _register_and_confirm(client, monkeypatch, email, "rightpass", "登录失败") r = _login(client, email, "wrongpass") assert r.status_code == 401 assert "邮箱或密码错误" == r.json()["detail"] def test_login_nonexistent_email(client: TestClient) -> None: r = _login(client, "nobody@example.com", "whatever") assert r.status_code == 401 def test_login_unverified_email(client: TestClient, monkeypatch) -> None: email = f"unverified-{uuid.uuid4().hex[:8]}@example.com" _capture_email(monkeypatch) _register(client, email, "unverified", "未确认") r = _login(client, email, "unverified") assert r.status_code == 403 assert "邮箱尚未确认" == r.json()["detail"] def test_login_disabled_user(client: TestClient, monkeypatch) -> None: email = f"disabled-{uuid.uuid4().hex[:8]}@example.com" info = _register_and_confirm(client, monkeypatch, email, "disabled", "禁用测试") admin_token = _login_as_user(client, ADMIN_EMAIL, ADMIN_PASSWORD) client.patch( f"/api/v1/admin/users/{info['user_id']}", headers=bearer(admin_token), json={"is_disabled": True}, ) r = _login(client, email, "disabled") assert r.status_code == 403 assert "账号已被禁用" == r.json()["detail"] client.patch( f"/api/v1/admin/users/{info['user_id']}", headers=bearer(admin_token), json={"is_disabled": False}, ) # --------------------------------------------------------------------------- # refresh # --------------------------------------------------------------------------- def test_refresh_success(client: TestClient, monkeypatch) -> None: email = f"refresh-{uuid.uuid4().hex[:8]}@example.com" _register_and_confirm(client, monkeypatch, email, "refreshpass", "刷新测试") r = client.post("/api/v1/auth/refresh") assert r.status_code == 200 assert "access_token" in r.json() def test_refresh_no_cookie(client: TestClient) -> None: session_client = TestClient(client.app) r = session_client.post("/api/v1/auth/refresh") assert r.status_code == 401 assert "缺少刷新令牌" == r.json()["detail"] def test_refresh_invalid_cookie(client: TestClient) -> None: r = client.post( "/api/v1/auth/refresh", cookies={"opencloud_refresh": "not-a-valid-refresh-token-at-all"}, ) assert r.status_code == 401 # --------------------------------------------------------------------------- # logout # --------------------------------------------------------------------------- def test_logout(client: TestClient) -> None: r = client.post("/api/v1/auth/logout") assert r.status_code == 200 assert r.json()["message"] == "已退出登录" # --------------------------------------------------------------------------- # forgot-password / reset-password # --------------------------------------------------------------------------- def test_forgot_password_existing_email(client: TestClient, monkeypatch) -> None: email = f"forgot-{uuid.uuid4().hex[:8]}@example.com" _register_and_confirm(client, monkeypatch, email, "forgotpass", "忘记密码") captured = {} monkeypatch.setattr(auth_router, "send_password_reset_email", captured.__setitem__) r = client.post("/api/v1/auth/forgot-password", json={"email": email}) assert r.status_code == 202 assert "邮件" in r.json()["message"] def test_forgot_password_nonexistent_email(client: TestClient) -> None: r = client.post("/api/v1/auth/forgot-password", json={"email": "ghost@example.com"}) assert r.status_code == 202 def test_reset_password_valid(client: TestClient, monkeypatch) -> None: email = f"reset-{uuid.uuid4().hex[:8]}@example.com" _register_and_confirm(client, monkeypatch, email, "oldpass12", "重置密码") captured = {} monkeypatch.setattr(auth_router, "send_password_reset_email", lambda r, t: captured.update(token=t)) client.post("/api/v1/auth/forgot-password", json={"email": email}) token = captured.get("token") assert token r = client.post( "/api/v1/auth/reset-password", json={"token": token, "password": "newpass12"}, ) assert r.status_code == 200 assert r.json()["message"] == "密码已重置,请重新登录" r = _login(client, email, "oldpass12") assert r.status_code == 401 r = _login(client, email, "newpass12") assert r.status_code == 200 def test_reset_password_invalid_token(client: TestClient) -> None: r = client.post( "/api/v1/auth/reset-password", json={"token": "not-a-real-reset-token-ever", "password": "newpass12"}, ) assert r.status_code == 400 assert "无效" in r.json()["detail"] # --------------------------------------------------------------------------- # change-password # --------------------------------------------------------------------------- def test_change_password(client: TestClient, monkeypatch) -> None: email = f"chpw-{uuid.uuid4().hex[:8]}@example.com" info = _register_and_confirm(client, monkeypatch, email, "oldpass12", "改密") r = client.patch( "/api/v1/auth/password", headers=bearer(info["token"]), json={"password": "updatedpass12"}, ) assert r.status_code == 200 assert "access_token" in r.json() r = _login(client, email, "oldpass12") assert r.status_code == 401 r = _login(client, email, "updatedpass12") assert r.status_code == 200 # --------------------------------------------------------------------------- # me # --------------------------------------------------------------------------- def test_me(client: TestClient, monkeypatch) -> None: email = f"me-{uuid.uuid4().hex[:8]}@example.com" info = _register_and_confirm(client, monkeypatch, email, "mepass12!", "我的信息") r = client.get("/api/v1/auth/me", headers=bearer(info["token"])) assert r.status_code == 200 body = r.json() assert body["user"]["email"] == email assert body["profile"]["username"] == "我的信息" def test_me_unauthenticated(client: TestClient) -> None: r = client.get("/api/v1/auth/me") assert r.status_code == 401