refactor(auth): enhance business login session validation and add tests
This commit is contained in:
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||||||
|
import type { IAuthStorage } from "@/data/storage/auth";
|
||||||
|
|
||||||
export async function hasBusinessLoginToken(): Promise<boolean> {
|
export async function hasBusinessLoginToken(): Promise<boolean> {
|
||||||
const result = await AuthStorage.getInstance().getLoginToken();
|
return hasCompleteBusinessAuthSession(AuthStorage.getInstance());
|
||||||
return result.success && Boolean(result.data);
|
}
|
||||||
|
|
||||||
|
export async function hasCompleteBusinessAuthSession(
|
||||||
|
storage: IAuthStorage,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const loginTokenResult = await storage.getLoginToken();
|
||||||
|
if (!loginTokenResult.success || !loginTokenResult.data) return false;
|
||||||
|
|
||||||
|
const refreshTokenResult = await storage.getRefreshToken();
|
||||||
|
return refreshTokenResult.success && Boolean(refreshTokenResult.data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { fetchFacebookUserData } from "@/data/services";
|
|||||||
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||||||
import { UserStorage } from "@/data/storage/user/user_storage";
|
import { UserStorage } from "@/data/storage/user/user_storage";
|
||||||
import { deviceIdentifier, Logger, Result } from "@/utils";
|
import { deviceIdentifier, Logger, Result } from "@/utils";
|
||||||
|
import { hasCompleteBusinessAuthSession } from "@/lib/auth/auth_session";
|
||||||
|
|
||||||
import { readGuestId } from "./auth-helpers";
|
import { readGuestId } from "./auth-helpers";
|
||||||
|
|
||||||
@@ -235,7 +236,9 @@ export const guestLoginActor = fromPromise<LoginStatus, void>(async () => {
|
|||||||
// 1. deviceIdentifier.getDeviceId() —— 无则生成 + 落盘
|
// 1. deviceIdentifier.getDeviceId() —— 无则生成 + 落盘
|
||||||
// (生产环境使用 fingerprintjs;非生产环境使用本地 UUID)
|
// (生产环境使用 fingerprintjs;非生产环境使用本地 UUID)
|
||||||
// (给后续 OAuth/email 登录准备 deviceId,不在 splash 首次访问就 auto-create 游客)
|
// (给后续 OAuth/email 登录准备 deviceId,不在 splash 首次访问就 auto-create 游客)
|
||||||
// 2. AuthStorage.getLoginToken() —— 有 → 读取 loginProvider,缺省回退 "email"
|
// 2. AuthStorage loginToken + refreshToken
|
||||||
|
// —— 两者都有 → 读取 loginProvider,缺省回退 "email"
|
||||||
|
// —— 任一缺失 → 不算业务登录态
|
||||||
// 3. AuthStorage.getFacebookId() —— 有 → facebookIdLogin → "facebook"
|
// 3. AuthStorage.getFacebookId() —— 有 → facebookIdLogin → "facebook"
|
||||||
// 4. AuthStorage.getGuestToken() —— 有 → "guest"
|
// 4. AuthStorage.getGuestToken() —— 有 → "guest"
|
||||||
// 5. 都没有 → "notLoggedIn"(让用户显式选 OAuth / Email / Guest 入口)
|
// 5. 都没有 → "notLoggedIn"(让用户显式选 OAuth / Email / Guest 入口)
|
||||||
@@ -245,9 +248,8 @@ export const checkAuthStatusActor = fromPromise<LoginStatus, void>(async () => {
|
|||||||
await deviceIdentifier.getDeviceId();
|
await deviceIdentifier.getDeviceId();
|
||||||
const storage = AuthStorage.getInstance();
|
const storage = AuthStorage.getInstance();
|
||||||
|
|
||||||
// 2. 查 loginToken
|
// 2. 查业务登录 token:loginToken 和 refreshToken 必须同时存在。
|
||||||
const loginTokenR = await storage.getLoginToken();
|
if (await hasCompleteBusinessAuthSession(storage)) {
|
||||||
if (loginTokenR.success && loginTokenR.data) {
|
|
||||||
const providerR = await storage.getLoginProvider();
|
const providerR = await storage.getLoginProvider();
|
||||||
if (providerR.success && isBusinessLoginProvider(providerR.data)) {
|
if (providerR.success && isBusinessLoginProvider(providerR.data)) {
|
||||||
return providerR.data;
|
return providerR.data;
|
||||||
|
|||||||
Reference in New Issue
Block a user