diff --git a/src/lib/auth/__tests__/auth_session.test.ts b/src/lib/auth/__tests__/auth_session.test.ts new file mode 100644 index 00000000..8188d6ac --- /dev/null +++ b/src/lib/auth/__tests__/auth_session.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it, vi } from "vitest"; + +import type { IAuthStorage } from "@/data/storage/auth"; +import { LoginStatus } from "@/data/dto/auth"; +import { Result } from "@/utils"; + +import { hasCompleteBusinessAuthSession } from "../auth_session"; + +function makeAuthStorageMock(input: { + loginToken?: string | null; + refreshToken?: string | null; + refreshTokenError?: Error; +}): IAuthStorage { + return { + getLoginToken: vi.fn(async () => Result.ok(input.loginToken ?? null)), + setLoginToken: vi.fn(async () => Result.ok(undefined)), + hasLoginToken: vi.fn(async () => Result.ok(Boolean(input.loginToken))), + clearLoginToken: vi.fn(async () => Result.ok(undefined)), + getLoginProvider: vi.fn(async () => Result.ok(LoginStatus.Email)), + setLoginProvider: vi.fn(async () => Result.ok(undefined)), + clearLoginProvider: vi.fn(async () => Result.ok(undefined)), + getGuestToken: vi.fn(async () => Result.ok(null)), + setGuestToken: vi.fn(async () => Result.ok(undefined)), + hasGuestToken: vi.fn(async () => Result.ok(false)), + clearGuestToken: vi.fn(async () => Result.ok(undefined)), + getDeviceId: vi.fn(async () => Result.ok(null)), + setDeviceId: vi.fn(async () => Result.ok(undefined)), + getRefreshToken: vi.fn(async () => + input.refreshTokenError + ? Result.err(input.refreshTokenError) + : Result.ok(input.refreshToken ?? null), + ), + setRefreshToken: vi.fn(async () => Result.ok(undefined)), + clearRefreshToken: vi.fn(async () => Result.ok(undefined)), + getFacebookId: vi.fn(async () => Result.ok(null)), + setFacebookId: vi.fn(async () => Result.ok(undefined)), + clearFacebookId: vi.fn(async () => Result.ok(undefined)), + clearAuthData: vi.fn(async () => Result.ok(undefined)), + }; +} + +describe("auth session helpers", () => { + it("treats a business session as complete when both tokens exist", async () => { + await expect( + hasCompleteBusinessAuthSession( + makeAuthStorageMock({ + loginToken: "login-token", + refreshToken: "refresh-token", + }), + ), + ).resolves.toBe(true); + }); + + it("does not treat login token alone as a complete business session", async () => { + await expect( + hasCompleteBusinessAuthSession( + makeAuthStorageMock({ + loginToken: "login-token", + refreshToken: null, + }), + ), + ).resolves.toBe(false); + }); + + it("does not restore a business session when refresh token cannot be read", async () => { + await expect( + hasCompleteBusinessAuthSession( + makeAuthStorageMock({ + loginToken: "login-token", + refreshTokenError: new Error("storage unavailable"), + }), + ), + ).resolves.toBe(false); + }); +}); diff --git a/src/lib/auth/auth_session.ts b/src/lib/auth/auth_session.ts index b2c8a9d1..4773a0f3 100644 --- a/src/lib/auth/auth_session.ts +++ b/src/lib/auth/auth_session.ts @@ -1,8 +1,18 @@ "use client"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; +import type { IAuthStorage } from "@/data/storage/auth"; export async function hasBusinessLoginToken(): Promise { - const result = await AuthStorage.getInstance().getLoginToken(); - return result.success && Boolean(result.data); + return hasCompleteBusinessAuthSession(AuthStorage.getInstance()); +} + +export async function hasCompleteBusinessAuthSession( + storage: IAuthStorage, +): Promise { + const loginTokenResult = await storage.getLoginToken(); + if (!loginTokenResult.success || !loginTokenResult.data) return false; + + const refreshTokenResult = await storage.getRefreshToken(); + return refreshTokenResult.success && Boolean(refreshTokenResult.data); } diff --git a/src/stores/auth/auth-actors.ts b/src/stores/auth/auth-actors.ts index 972509ec..ee2e209d 100644 --- a/src/stores/auth/auth-actors.ts +++ b/src/stores/auth/auth-actors.ts @@ -10,6 +10,7 @@ import { fetchFacebookUserData } from "@/data/services"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { UserStorage } from "@/data/storage/user/user_storage"; import { deviceIdentifier, Logger, Result } from "@/utils"; +import { hasCompleteBusinessAuthSession } from "@/lib/auth/auth_session"; import { readGuestId } from "./auth-helpers"; @@ -235,7 +236,9 @@ export const guestLoginActor = fromPromise(async () => { // 1. deviceIdentifier.getDeviceId() —— 无则生成 + 落盘 // (生产环境使用 fingerprintjs;非生产环境使用本地 UUID) // (给后续 OAuth/email 登录准备 deviceId,不在 splash 首次访问就 auto-create 游客) -// 2. AuthStorage.getLoginToken() —— 有 → 读取 loginProvider,缺省回退 "email" +// 2. AuthStorage loginToken + refreshToken +// —— 两者都有 → 读取 loginProvider,缺省回退 "email" +// —— 任一缺失 → 不算业务登录态 // 3. AuthStorage.getFacebookId() —— 有 → facebookIdLogin → "facebook" // 4. AuthStorage.getGuestToken() —— 有 → "guest" // 5. 都没有 → "notLoggedIn"(让用户显式选 OAuth / Email / Guest 入口) @@ -245,9 +248,8 @@ export const checkAuthStatusActor = fromPromise(async () => { await deviceIdentifier.getDeviceId(); const storage = AuthStorage.getInstance(); - // 2. 查 loginToken - const loginTokenR = await storage.getLoginToken(); - if (loginTokenR.success && loginTokenR.data) { + // 2. 查业务登录 token:loginToken 和 refreshToken 必须同时存在。 + if (await hasCompleteBusinessAuthSession(storage)) { const providerR = await storage.getLoginProvider(); if (providerR.success && isBusinessLoginProvider(providerR.data)) { return providerR.data;