| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- import { afterAll, beforeEach, describe, expect, test, vi } from "vitest";
- import { inArray } from "drizzle-orm";
- import { db } from "@/drizzle/db";
- import { keys, users } from "@/drizzle/schema";
- import {
- clearAuthCookie,
- getAuthCookie,
- getLoginRedirectTarget,
- getSession,
- setAuthCookie,
- validateKey,
- } from "@/lib/auth";
- /**
- * 说明:
- * - 本文件用于覆盖 auth.ts 的权限边界与 Cookie 行为
- * - 重点验证:allowReadOnlyAccess 白名单语义
- * - 以及 getSession/cookie 的读写一致性
- */
- let currentCookieValue: string | undefined;
- let currentAuthorizationValue: string | undefined;
- const cookieSet = vi.fn((name: string, value: string) => {
- if (name === "auth-token") currentCookieValue = value;
- });
- const cookieDelete = vi.fn((name: string) => {
- if (name === "auth-token") currentCookieValue = undefined;
- });
- vi.mock("next/headers", () => ({
- cookies: () => ({
- get: (name: string) => {
- if (name !== "auth-token") return undefined;
- return currentCookieValue ? { value: currentCookieValue } : undefined;
- },
- set: cookieSet,
- delete: cookieDelete,
- has: (name: string) => name === "auth-token" && Boolean(currentCookieValue),
- }),
- headers: () => ({
- get: (name: string) => {
- if (name.toLowerCase() !== "authorization") return null;
- return currentAuthorizationValue ?? null;
- },
- }),
- }));
- type TestUser = { id: number; name: string };
- type TestKey = { id: number; userId: number; key: string; canLoginWebUi: boolean };
- async function createTestUser(name: string): Promise<TestUser> {
- const [row] = await db
- .insert(users)
- .values({ name })
- .returning({ id: users.id, name: users.name });
- if (!row) throw new Error("创建测试用户失败:未返回插入结果");
- return row;
- }
- async function createTestKey(params: {
- userId: number;
- key: string;
- canLoginWebUi: boolean;
- }): Promise<TestKey> {
- const [row] = await db
- .insert(keys)
- .values({
- userId: params.userId,
- key: params.key,
- name: `key-${params.key}`,
- canLoginWebUi: params.canLoginWebUi,
- dailyResetMode: "rolling",
- dailyResetTime: "00:00",
- })
- .returning({
- id: keys.id,
- userId: keys.userId,
- key: keys.key,
- canLoginWebUi: keys.canLoginWebUi,
- });
- if (!row) throw new Error("创建测试 Key 失败:未返回插入结果");
- return row;
- }
- describe("auth.ts:validateKey / getSession(安全边界)", () => {
- const createdUserIds: number[] = [];
- const createdKeyIds: number[] = [];
- afterAll(async () => {
- const now = new Date();
- if (createdKeyIds.length > 0) {
- await db
- .update(keys)
- .set({ deletedAt: now, updatedAt: now })
- .where(inArray(keys.id, createdKeyIds));
- }
- if (createdUserIds.length > 0) {
- await db
- .update(users)
- .set({ deletedAt: now, updatedAt: now })
- .where(inArray(users.id, createdUserIds));
- }
- });
- beforeEach(() => {
- currentCookieValue = undefined;
- currentAuthorizationValue = undefined;
- cookieSet.mockClear();
- cookieDelete.mockClear();
- });
- test("admin token:应返回 admin session(无需 DB)", async () => {
- const adminToken = process.env.ADMIN_TOKEN;
- expect(adminToken).toBeTruthy();
- const session = await validateKey(adminToken as string);
- expect(session?.user.role).toBe("admin");
- expect(session?.key.canLoginWebUi).toBe(true);
- });
- test("不存在的 key:validateKey 应返回 null", async () => {
- const session = await validateKey(`non-existent-${Date.now()}`);
- expect(session).toBeNull();
- });
- test("canLoginWebUi=false 且 allowReadOnlyAccess=false:应拒绝", async () => {
- const unique = `auth-${Date.now()}-${Math.random().toString(16).slice(2)}`;
- const user = await createTestUser(`Test ${unique}`);
- createdUserIds.push(user.id);
- const key = await createTestKey({
- userId: user.id,
- key: `test-key-${unique}`,
- canLoginWebUi: false,
- });
- createdKeyIds.push(key.id);
- const session = await validateKey(key.key, { allowReadOnlyAccess: false });
- expect(session).toBeNull();
- });
- test("allowReadOnlyAccess=true:应允许只读 key 查询自己的数据", async () => {
- const unique = `auth-ro-${Date.now()}-${Math.random().toString(16).slice(2)}`;
- const user = await createTestUser(`Test ${unique}`);
- createdUserIds.push(user.id);
- const key = await createTestKey({
- userId: user.id,
- key: `test-ro-key-${unique}`,
- canLoginWebUi: false,
- });
- createdKeyIds.push(key.id);
- const session = await validateKey(key.key, { allowReadOnlyAccess: true });
- expect(session?.key.key).toBe(key.key);
- expect(session?.key.canLoginWebUi).toBe(false);
- });
- test("用户被软删除:validateKey 应返回 null", async () => {
- const unique = `auth-del-${Date.now()}-${Math.random().toString(16).slice(2)}`;
- const user = await createTestUser(`Test ${unique}`);
- createdUserIds.push(user.id);
- const key = await createTestKey({
- userId: user.id,
- key: `test-key-${unique}`,
- canLoginWebUi: true,
- });
- createdKeyIds.push(key.id);
- const now = new Date();
- await db
- .update(users)
- .set({ deletedAt: now, updatedAt: now })
- .where(inArray(users.id, [user.id]));
- const session = await validateKey(key.key, { allowReadOnlyAccess: true });
- expect(session).toBeNull();
- });
- test("getSession:无 Cookie 时返回 null;有 Cookie 时返回 session", async () => {
- const noCookie = await getSession({ allowReadOnlyAccess: true });
- expect(noCookie).toBeNull();
- const unique = `auth-sess-${Date.now()}-${Math.random().toString(16).slice(2)}`;
- const user = await createTestUser(`Test ${unique}`);
- createdUserIds.push(user.id);
- const key = await createTestKey({
- userId: user.id,
- key: `test-key-${unique}`,
- canLoginWebUi: false,
- });
- createdKeyIds.push(key.id);
- currentCookieValue = key.key;
- const session = await getSession({ allowReadOnlyAccess: true });
- expect(session?.key.key).toBe(key.key);
- });
- test("getSession:仅 Authorization: Bearer 时也应返回 session", async () => {
- const unique = `auth-bearer-${Date.now()}-${Math.random().toString(16).slice(2)}`;
- const user = await createTestUser(`Test ${unique}`);
- createdUserIds.push(user.id);
- const key = await createTestKey({
- userId: user.id,
- key: `test-key-${unique}`,
- canLoginWebUi: false,
- });
- createdKeyIds.push(key.id);
- currentAuthorizationValue = `Bearer ${key.key}`;
- const session = await getSession({ allowReadOnlyAccess: true });
- expect(session?.key.key).toBe(key.key);
- });
- });
- describe("auth.ts:Cookie 工具函数与跳转目标", () => {
- beforeEach(() => {
- currentCookieValue = undefined;
- currentAuthorizationValue = undefined;
- cookieSet.mockClear();
- cookieDelete.mockClear();
- });
- test("set/get/clear auth cookie:应读写一致", async () => {
- await setAuthCookie("abc");
- expect(cookieSet).toHaveBeenCalled();
- const value = await getAuthCookie();
- expect(value).toBe("abc");
- await clearAuthCookie();
- expect(cookieDelete).toHaveBeenCalledWith("auth-token");
- expect(await getAuthCookie()).toBeUndefined();
- });
- test("getLoginRedirectTarget:应根据 role 与 canLoginWebUi 决定跳转", () => {
- const adminTarget = getLoginRedirectTarget({
- user: { role: "admin" } as any,
- key: { canLoginWebUi: false } as any,
- });
- expect(adminTarget).toBe("/dashboard");
- const webUiTarget = getLoginRedirectTarget({
- user: { role: "user" } as any,
- key: { canLoginWebUi: true } as any,
- });
- expect(webUiTarget).toBe("/dashboard");
- const readonlyTarget = getLoginRedirectTarget({
- user: { role: "user" } as any,
- key: { canLoginWebUi: false } as any,
- });
- expect(readonlyTarget).toBe("/my-usage");
- });
- });
|