Merge remote-tracking branch 'origin/v2/main' into v2/1549-oauth-storage-no-zustand
# Conflicts: # core/auth/store.ts
This commit is contained in:
commit
edc7dbb1c1
3 changed files with 785 additions and 14 deletions
|
|
@ -17,8 +17,12 @@ const defaultFetch: typeof fetch = (...args) => globalThis.fetch(...args);
|
|||
* Shared web OAuth store: `RemoteOAuthStorage` → `GET/POST /api/storage/oauth`
|
||||
* → `~/.mcp-inspector/storage/oauth.json` (same file as CLI/TUI).
|
||||
*
|
||||
* Memoized by `{ baseUrl, authToken }` so connect, EMA IdP session, and
|
||||
* per-server clear all mutate the same in-memory view.
|
||||
* Caches a single instance keyed by `{ baseUrl, authToken }`: repeat calls with
|
||||
* the same key return that instance, so connect, EMA IdP session, and per-server
|
||||
* clear all mutate the same in-memory view. A call with a different key replaces
|
||||
* the cached instance (cache of one). That is sufficient because the web app
|
||||
* uses a stable origin + page-lifetime API token, so the key never changes
|
||||
* within a session.
|
||||
*/
|
||||
export function getRemoteOAuthStorage(
|
||||
options: WebRemoteOAuthStorageOptions,
|
||||
|
|
|
|||
|
|
@ -132,12 +132,12 @@ describe("RemoteOAuthStorage (unit, mocked fetch)", () => {
|
|||
version: 0,
|
||||
};
|
||||
let resolveFetch!: (value: Response) => void;
|
||||
const delayedFetch = vi.fn(
|
||||
const delayedFetch = vi.fn<typeof fetch>(
|
||||
() =>
|
||||
new Promise<Response>((resolve) => {
|
||||
resolveFetch = resolve;
|
||||
}),
|
||||
) as unknown as typeof fetch;
|
||||
);
|
||||
|
||||
const delayedStorage = new RemoteOAuthStorage({
|
||||
baseUrl: "http://remote.example",
|
||||
|
|
@ -152,9 +152,9 @@ describe("RemoteOAuthStorage (unit, mocked fetch)", () => {
|
|||
});
|
||||
|
||||
it("load() rejects when remote GET fails instead of hanging", async () => {
|
||||
const failingFetch = vi.fn(
|
||||
const failingFetch = vi.fn<typeof fetch>(
|
||||
async () => new Response("server error", { status: 500 }),
|
||||
) as unknown as typeof fetch;
|
||||
);
|
||||
|
||||
const failingStorage = new RemoteOAuthStorage({
|
||||
baseUrl: "http://remote.example",
|
||||
|
|
|
|||
|
|
@ -67,6 +67,58 @@ describe("useEmaIdpLoginState", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("logout swallows a clear failure without an unhandled rejection and keeps state", async () => {
|
||||
const exp = Math.floor(Date.now() / 1000) + 3600;
|
||||
const payload = btoa(JSON.stringify({ exp }))
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
vi.mocked(storage.getIdpSession).mockResolvedValue({
|
||||
idToken: `h.${payload}.s`,
|
||||
});
|
||||
vi.mocked(storage.clearIdpSession).mockRejectedValue(
|
||||
new Error("storage backend unreachable"),
|
||||
);
|
||||
const unhandled = vi.fn();
|
||||
const onUnhandled = (event: PromiseRejectionEvent) => {
|
||||
event.preventDefault();
|
||||
unhandled();
|
||||
};
|
||||
window.addEventListener("unhandledrejection", onUnhandled);
|
||||
|
||||
try {
|
||||
const { result } = renderHook(() =>
|
||||
useEmaIdpLoginState(storage, "https://idp.test", true),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loginState).toBe("logged_in");
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.logout();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(storage.clearIdpSession).toHaveBeenCalledWith(
|
||||
"https://idp.test",
|
||||
);
|
||||
});
|
||||
// Give the rejected promise a turn to settle so any unhandled rejection
|
||||
// would have fired.
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
// Clear failed, so the session is still present: state stays "logged_in"
|
||||
// and no unhandled rejection escaped.
|
||||
expect(result.current.loginState).toBe("logged_in");
|
||||
expect(unhandled).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
window.removeEventListener("unhandledrejection", onUnhandled);
|
||||
}
|
||||
});
|
||||
|
||||
it("reports 'expired' for an expired token with no refresh token", async () => {
|
||||
const exp = Math.floor(Date.now() / 1000) - 3600;
|
||||
const payload = btoa(JSON.stringify({ exp }))
|
||||
|
|
|
|||
Loading…
Reference in a new issue