import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { parseSessionCookieValue } from "@/lib/auth/session"; import { createPlatformOrganization, createPlatformUser, updatePlatformUser } from "@/lib/server/account-store"; import { resetLocalAuthRateLimitForTests } from "@/lib/server/auth/local"; import type { PlatformUserRecord } from "@/lib/types"; import { POST } from "@/app/api/auth/password/route"; const SESSION_SECRET = "test-platform-session-secret-with-enough-entropy"; let runtimeDir = ""; let ordinaryUser: PlatformUserRecord; describe("platform password auth route", () => { beforeEach(async () => { runtimeDir = await mkdtemp(join(tmpdir(), "zhinian-auth-")); vi.stubEnv("ZHINIAN_DATA_DIR", runtimeDir); vi.stubEnv("ZHINIAN_AUTH_REQUIRED", "1"); vi.stubEnv("ZHINIAN_AUTH_SESSION_SECRET", SESSION_SECRET); vi.stubEnv("ZHINIAN_AUTH_DISABLED", ""); vi.stubEnv("ZHINIAN_DATA_BACKEND", "local"); resetLocalAuthRateLimitForTests(); const organization = await createPlatformOrganization("测试组织"); ordinaryUser = await createPlatformUser({ phone: "13800138000", displayName: "测试用户", password: "TestPass123", role: "user", organizationId: organization.id }); }); afterEach(async () => { resetLocalAuthRateLimitForTests(); vi.unstubAllEnvs(); await rm(runtimeDir, { force: true, recursive: true }); }); it("logs in every role with the platform phone/password session", async () => { const response = await POST(new Request("http://127.0.0.1/api/auth/password", { method: "POST", headers: { "x-forwarded-for": "192.0.2.10" }, body: JSON.stringify({ phone: "138 0013-8000", password: "TestPass123", next: "/assets" }) })); expect(response.status).toBe(200); await expect(response.json()).resolves.toMatchObject({ ok: true, redirectTo: "/assets", authMode: "user", user: { id: ordinaryUser.id, phone: "13800138000", role: "user", clientId: "platform" } }); expect(await parseSessionCookieValue( response.cookies.get("zhinian_session")?.value, SESSION_SECRET )).toMatchObject({ authMode: "user", sessionVersion: 1, user: { id: ordinaryUser.id, role: "user", clientId: "platform" } }); }); it("logs in a super administrator through the same endpoint", async () => { const admin = await createPlatformUser({ phone: "13900139000", displayName: "平台超级管理员", password: "AdminPass123", role: "super_admin" }); const response = await POST(new Request("http://127.0.0.1/api/auth/password", { method: "POST", body: JSON.stringify({ phone: admin.phone, password: "AdminPass123", authMode: "admin" }) })); expect(response.status).toBe(200); await expect(response.json()).resolves.toMatchObject({ authMode: "admin", user: { id: admin.id, role: "super_admin" } }); }); it("locks an account after five failed passwords", async () => { let lastResponse: Response | undefined; for (let attempt = 1; attempt <= 5; attempt += 1) { lastResponse = await POST(new Request("http://127.0.0.1/api/auth/password", { method: "POST", headers: { "x-forwarded-for": "192.0.2.11" }, body: JSON.stringify({ phone: ordinaryUser.phone, password: "wrong-pass" }) })); expect(lastResponse.status).toBe(attempt === 5 ? 423 : 401); } await expect(lastResponse?.json()).resolves.toMatchObject({ error: "登录失败次数过多,请 15 分钟后再试。" }); const lockedResponse = await POST(new Request("http://127.0.0.1/api/auth/password", { method: "POST", body: JSON.stringify({ phone: ordinaryUser.phone, password: "TestPass123" }) })); expect(lockedResponse.status).toBe(423); }); it("rejects disabled accounts and duplicate phone identities", async () => { await expect(createPlatformUser({ phone: ordinaryUser.phone, displayName: "重复账号", password: "AnotherPass123", role: "user", organizationId: ordinaryUser.organizationId })).rejects.toThrow("该手机号已创建账号。"); await updatePlatformUser(ordinaryUser.id, { status: "disabled" }); const response = await POST(new Request("http://127.0.0.1/api/auth/password", { method: "POST", body: JSON.stringify({ phone: ordinaryUser.phone, password: "TestPass123" }) })); expect(response.status).toBe(403); await expect(response.json()).resolves.toEqual({ error: "账号已停用,请联系管理员。" }); }); });