Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions clients/web/src/hooks/useOAuthRecovery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,54 @@ describe("useOAuthRecovery", () => {
expect(h.api().connectionInfoOAuth).toBeUndefined();
});

it("clears the details when the state read rejects", async () => {
const client = fakeClient({
getOAuthState: vi.fn().mockRejectedValue(new Error("backend down")),
});
const props: HarnessProps = {
servers: [entry("a")],
activeServerId: "a",
client,
};
const h = harness(props);
await waitFor(() => expect(client.getOAuthState).toHaveBeenCalled());
expect(h.api().connectionInfoOAuth).toBeUndefined();

// The rejection is terminated, not floated: a later resolving read still
// populates the panel, which an unhandled rejection would have prevented
// by failing the run.
client.getOAuthState = vi
.fn()
.mockResolvedValue({ tokens: { access_token: "t" } });
await act(async () => {
client.emit("oauthComplete", {});
});
await waitFor(() => expect(h.api().connectionInfoOAuth).toBeDefined());
Comment thread
cliffhall marked this conversation as resolved.
Resolved
});

it("drops a rejected state read that lands after the session ended", async () => {
let fail: (reason: unknown) => void = () => {};
const client = fakeClient({
getOAuthState: vi.fn(
() =>
new Promise((_resolve, reject) => {
fail = reject;
}),
),
});
const props: HarnessProps = {
servers: [entry("a")],
activeServerId: "a",
client,
};
const h = harness(props);
h.rerender({ ...props, connectionStatus: "disconnected" });
await act(async () => {
fail(new Error("backend down"));
});
expect(h.api().connectionInfoOAuth).toBeUndefined();
Comment thread
cliffhall marked this conversation as resolved.
Resolved
});

it("drops a state read that lands after the session ended", async () => {
let settle: (value: unknown) => void = () => {};
const client = fakeClient({
Expand Down
23 changes: 17 additions & 6 deletions clients/web/src/hooks/useOAuthRecovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,12 +406,23 @@ export function useOAuthRecovery({
let cancelled = false;

const refresh = (): void => {
void inspectorClient.getOAuthState().then((state) => {
if (cancelled) return;
setConnectionInfoOAuthWhenConnected(
state ? oauthDetailsFromConnectionState(state) : undefined,
);
});
// void: a synchronous useEffect body cannot await. The chain is
// terminated below, so the rejection is handled rather than discarded.
void inspectorClient
.getOAuthState()
.then((state) => {
if (cancelled) return;
setConnectionInfoOAuthWhenConnected(
state ? oauthDetailsFromConnectionState(state) : undefined,
);
})
.catch(() => {
// The read failed (backend down, 401 on the API token, malformed
// stored state). Clear rather than keep the last successful read —
// a stale answer is indistinguishable from a fresh one in the panel.
if (cancelled) return;
setConnectionInfoOAuthWhenConnected(undefined);
Comment thread
cliffhall marked this conversation as resolved.
Resolved
});
};

const onAmbientAuthChallenge = (): void => {
Expand Down
Loading