mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
AuthStore.initialize() cleared the stored token on any error from `api.getMe()`, which meant a transient failure — backend rolling restart, network blip, HMR-aborted fetch in local dev — would force a re-login. On 401 the token is already cleared upstream via ApiClient.onUnauthorized, so the store's catch block only needs to reset the in-memory user state. Check `err instanceof ApiError && err.status === 401` before clearing workspace context; leave the token in storage for every other error so the next initialize() can retry. Adds regression tests covering the 401 / 500 / network-failure / happy paths.
94 lines
3.1 KiB
TypeScript
94 lines
3.1 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import type { ApiClient } from "../api/client";
|
|
import { ApiError } from "../api/client";
|
|
import type { StorageAdapter, User } from "../types";
|
|
import { createAuthStore } from "./store";
|
|
|
|
const fakeUser: User = {
|
|
id: "u1",
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
avatar_url: null,
|
|
} as User;
|
|
|
|
function makeStorage(initial: Record<string, string> = {}): StorageAdapter & {
|
|
snapshot: () => Record<string, string>;
|
|
} {
|
|
const data = { ...initial };
|
|
return {
|
|
getItem: (k) => data[k] ?? null,
|
|
setItem: (k, v) => {
|
|
data[k] = v;
|
|
},
|
|
removeItem: (k) => {
|
|
delete data[k];
|
|
},
|
|
snapshot: () => ({ ...data }),
|
|
};
|
|
}
|
|
|
|
function makeApi(getMe: () => Promise<User>): ApiClient {
|
|
return {
|
|
setToken: vi.fn(),
|
|
getMe,
|
|
// Only the methods touched by store.initialize are needed. Cast to
|
|
// ApiClient for type compatibility — the store treats it opaquely.
|
|
} as unknown as ApiClient;
|
|
}
|
|
|
|
describe("authStore.initialize — token mode", () => {
|
|
it("keeps the stored token when getMe fails with a non-401 ApiError (e.g. 500)", async () => {
|
|
const storage = makeStorage({ multica_token: "t" });
|
|
const api = makeApi(() =>
|
|
Promise.reject(new ApiError("server error", 500, "Internal Server Error")),
|
|
);
|
|
const store = createAuthStore({ api, storage });
|
|
|
|
await store.getState().initialize();
|
|
|
|
expect(store.getState().user).toBeNull();
|
|
expect(store.getState().isLoading).toBe(false);
|
|
expect(storage.snapshot().multica_token).toBe("t");
|
|
});
|
|
|
|
it("keeps the stored token on a network failure (non-ApiError throw)", async () => {
|
|
const storage = makeStorage({ multica_token: "t" });
|
|
const api = makeApi(() => Promise.reject(new TypeError("fetch failed")));
|
|
const store = createAuthStore({ api, storage });
|
|
|
|
await store.getState().initialize();
|
|
|
|
expect(store.getState().user).toBeNull();
|
|
expect(storage.snapshot().multica_token).toBe("t");
|
|
});
|
|
|
|
it("on 401, leaves storage cleanup to ApiClient.onUnauthorized and resets state", async () => {
|
|
// Simulate the real path: ApiClient fires onUnauthorized on 401, which
|
|
// removes the token from storage. The store's catch block must not
|
|
// duplicate or short-circuit this — it should only reset in-memory
|
|
// auth state.
|
|
const storage = makeStorage({ multica_token: "t" });
|
|
const api = makeApi(() => {
|
|
storage.removeItem("multica_token"); // stand-in for onUnauthorized
|
|
return Promise.reject(new ApiError("unauthorized", 401, "Unauthorized"));
|
|
});
|
|
const store = createAuthStore({ api, storage });
|
|
|
|
await store.getState().initialize();
|
|
|
|
expect(store.getState().user).toBeNull();
|
|
expect(storage.snapshot().multica_token).toBeUndefined();
|
|
});
|
|
|
|
it("populates user when getMe succeeds", async () => {
|
|
const storage = makeStorage({ multica_token: "t" });
|
|
const api = makeApi(() => Promise.resolve(fakeUser));
|
|
const store = createAuthStore({ api, storage });
|
|
|
|
await store.getState().initialize();
|
|
|
|
expect(store.getState().user).toEqual(fakeUser);
|
|
expect(storage.snapshot().multica_token).toBe("t");
|
|
});
|
|
});
|