mirror of https://github.com/openclaw/openclaw.git
refactor: share gateway client auth retry helpers
This commit is contained in:
parent
60dc46ad10
commit
5f34391f75
|
|
@ -344,6 +344,20 @@ describe("GatewayClient connect auth payload", () => {
|
||||||
return parsed.params?.auth ?? {};
|
return parsed.params?.auth ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function connectRequestFrom(ws: MockWebSocket) {
|
||||||
|
const raw = ws.sent.find((frame) => frame.includes('"method":"connect"'));
|
||||||
|
expect(raw).toBeTruthy();
|
||||||
|
return JSON.parse(raw ?? "{}") as {
|
||||||
|
id?: string;
|
||||||
|
params?: {
|
||||||
|
auth?: {
|
||||||
|
token?: string;
|
||||||
|
deviceToken?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function emitConnectChallenge(ws: MockWebSocket, nonce = "nonce-1") {
|
function emitConnectChallenge(ws: MockWebSocket, nonce = "nonce-1") {
|
||||||
ws.emitMessage(
|
ws.emitMessage(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|
@ -354,6 +368,63 @@ describe("GatewayClient connect auth payload", () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startClientAndConnect(params: { client: GatewayClient; nonce?: string }) {
|
||||||
|
params.client.start();
|
||||||
|
const ws = getLatestWs();
|
||||||
|
ws.emitOpen();
|
||||||
|
emitConnectChallenge(ws, params.nonce);
|
||||||
|
return { ws, connect: connectRequestFrom(ws) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitConnectFailure(
|
||||||
|
ws: MockWebSocket,
|
||||||
|
connectId: string | undefined,
|
||||||
|
details: Record<string, unknown>,
|
||||||
|
) {
|
||||||
|
ws.emitMessage(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "res",
|
||||||
|
id: connectId,
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
code: "INVALID_REQUEST",
|
||||||
|
message: "unauthorized",
|
||||||
|
details,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectRetriedConnectAuth(params: {
|
||||||
|
firstWs: MockWebSocket;
|
||||||
|
connectId: string | undefined;
|
||||||
|
failureDetails: Record<string, unknown>;
|
||||||
|
}) {
|
||||||
|
emitConnectFailure(params.firstWs, params.connectId, params.failureDetails);
|
||||||
|
await vi.waitFor(() => expect(wsInstances.length).toBeGreaterThan(1), { timeout: 3_000 });
|
||||||
|
const ws = getLatestWs();
|
||||||
|
ws.emitOpen();
|
||||||
|
emitConnectChallenge(ws, "nonce-2");
|
||||||
|
return connectFrameFrom(ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectNoReconnectAfterConnectFailure(params: {
|
||||||
|
client: GatewayClient;
|
||||||
|
firstWs: MockWebSocket;
|
||||||
|
connectId: string | undefined;
|
||||||
|
failureDetails: Record<string, unknown>;
|
||||||
|
}) {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
try {
|
||||||
|
emitConnectFailure(params.firstWs, params.connectId, params.failureDetails);
|
||||||
|
await vi.advanceTimersByTimeAsync(30_000);
|
||||||
|
expect(wsInstances).toHaveLength(1);
|
||||||
|
} finally {
|
||||||
|
params.client.stop();
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
it("uses explicit shared token and does not inject stored device token", () => {
|
it("uses explicit shared token and does not inject stored device token", () => {
|
||||||
loadDeviceAuthTokenMock.mockReturnValue({ token: "stored-device-token" });
|
loadDeviceAuthTokenMock.mockReturnValue({ token: "stored-device-token" });
|
||||||
const client = new GatewayClient({
|
const client = new GatewayClient({
|
||||||
|
|
@ -457,37 +528,16 @@ describe("GatewayClient connect auth payload", () => {
|
||||||
token: "shared-token",
|
token: "shared-token",
|
||||||
});
|
});
|
||||||
|
|
||||||
client.start();
|
const { ws: ws1, connect: firstConnect } = startClientAndConnect({ client });
|
||||||
const ws1 = getLatestWs();
|
|
||||||
ws1.emitOpen();
|
|
||||||
emitConnectChallenge(ws1);
|
|
||||||
const firstConnectRaw = ws1.sent.find((frame) => frame.includes('"method":"connect"'));
|
|
||||||
expect(firstConnectRaw).toBeTruthy();
|
|
||||||
const firstConnect = JSON.parse(firstConnectRaw ?? "{}") as {
|
|
||||||
id?: string;
|
|
||||||
params?: { auth?: { token?: string; deviceToken?: string } };
|
|
||||||
};
|
|
||||||
expect(firstConnect.params?.auth?.token).toBe("shared-token");
|
expect(firstConnect.params?.auth?.token).toBe("shared-token");
|
||||||
expect(firstConnect.params?.auth?.deviceToken).toBeUndefined();
|
expect(firstConnect.params?.auth?.deviceToken).toBeUndefined();
|
||||||
|
|
||||||
ws1.emitMessage(
|
const retriedAuth = await expectRetriedConnectAuth({
|
||||||
JSON.stringify({
|
firstWs: ws1,
|
||||||
type: "res",
|
connectId: firstConnect.id,
|
||||||
id: firstConnect.id,
|
failureDetails: { code: "AUTH_TOKEN_MISMATCH", canRetryWithDeviceToken: true },
|
||||||
ok: false,
|
});
|
||||||
error: {
|
expect(retriedAuth).toMatchObject({
|
||||||
code: "INVALID_REQUEST",
|
|
||||||
message: "unauthorized",
|
|
||||||
details: { code: "AUTH_TOKEN_MISMATCH", canRetryWithDeviceToken: true },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await vi.waitFor(() => expect(wsInstances.length).toBeGreaterThan(1), { timeout: 3_000 });
|
|
||||||
const ws2 = getLatestWs();
|
|
||||||
ws2.emitOpen();
|
|
||||||
emitConnectChallenge(ws2, "nonce-2");
|
|
||||||
expect(connectFrameFrom(ws2)).toMatchObject({
|
|
||||||
token: "shared-token",
|
token: "shared-token",
|
||||||
deviceToken: "stored-device-token",
|
deviceToken: "stored-device-token",
|
||||||
});
|
});
|
||||||
|
|
@ -501,32 +551,13 @@ describe("GatewayClient connect auth payload", () => {
|
||||||
token: "shared-token",
|
token: "shared-token",
|
||||||
});
|
});
|
||||||
|
|
||||||
client.start();
|
const { ws: ws1, connect: firstConnect } = startClientAndConnect({ client });
|
||||||
const ws1 = getLatestWs();
|
const retriedAuth = await expectRetriedConnectAuth({
|
||||||
ws1.emitOpen();
|
firstWs: ws1,
|
||||||
emitConnectChallenge(ws1);
|
connectId: firstConnect.id,
|
||||||
const firstConnectRaw = ws1.sent.find((frame) => frame.includes('"method":"connect"'));
|
failureDetails: { code: "AUTH_UNAUTHORIZED", recommendedNextStep: "retry_with_device_token" },
|
||||||
expect(firstConnectRaw).toBeTruthy();
|
});
|
||||||
const firstConnect = JSON.parse(firstConnectRaw ?? "{}") as { id?: string };
|
expect(retriedAuth).toMatchObject({
|
||||||
|
|
||||||
ws1.emitMessage(
|
|
||||||
JSON.stringify({
|
|
||||||
type: "res",
|
|
||||||
id: firstConnect.id,
|
|
||||||
ok: false,
|
|
||||||
error: {
|
|
||||||
code: "INVALID_REQUEST",
|
|
||||||
message: "unauthorized",
|
|
||||||
details: { code: "AUTH_UNAUTHORIZED", recommendedNextStep: "retry_with_device_token" },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await vi.waitFor(() => expect(wsInstances.length).toBeGreaterThan(1), { timeout: 3_000 });
|
|
||||||
const ws2 = getLatestWs();
|
|
||||||
ws2.emitOpen();
|
|
||||||
emitConnectChallenge(ws2, "nonce-2");
|
|
||||||
expect(connectFrameFrom(ws2)).toMatchObject({
|
|
||||||
token: "shared-token",
|
token: "shared-token",
|
||||||
deviceToken: "stored-device-token",
|
deviceToken: "stored-device-token",
|
||||||
});
|
});
|
||||||
|
|
@ -534,71 +565,33 @@ describe("GatewayClient connect auth payload", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not auto-reconnect on AUTH_TOKEN_MISSING connect failures", async () => {
|
it("does not auto-reconnect on AUTH_TOKEN_MISSING connect failures", async () => {
|
||||||
vi.useFakeTimers();
|
|
||||||
const client = new GatewayClient({
|
const client = new GatewayClient({
|
||||||
url: "ws://127.0.0.1:18789",
|
url: "ws://127.0.0.1:18789",
|
||||||
token: "shared-token",
|
token: "shared-token",
|
||||||
});
|
});
|
||||||
|
|
||||||
client.start();
|
const { ws: ws1, connect: firstConnect } = startClientAndConnect({ client });
|
||||||
const ws1 = getLatestWs();
|
await expectNoReconnectAfterConnectFailure({
|
||||||
ws1.emitOpen();
|
client,
|
||||||
emitConnectChallenge(ws1);
|
firstWs: ws1,
|
||||||
const firstConnectRaw = ws1.sent.find((frame) => frame.includes('"method":"connect"'));
|
connectId: firstConnect.id,
|
||||||
expect(firstConnectRaw).toBeTruthy();
|
failureDetails: { code: "AUTH_TOKEN_MISSING" },
|
||||||
const firstConnect = JSON.parse(firstConnectRaw ?? "{}") as { id?: string };
|
});
|
||||||
|
|
||||||
ws1.emitMessage(
|
|
||||||
JSON.stringify({
|
|
||||||
type: "res",
|
|
||||||
id: firstConnect.id,
|
|
||||||
ok: false,
|
|
||||||
error: {
|
|
||||||
code: "INVALID_REQUEST",
|
|
||||||
message: "unauthorized",
|
|
||||||
details: { code: "AUTH_TOKEN_MISSING" },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(30_000);
|
|
||||||
expect(wsInstances).toHaveLength(1);
|
|
||||||
client.stop();
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not auto-reconnect on token mismatch when retry is not trusted", async () => {
|
it("does not auto-reconnect on token mismatch when retry is not trusted", async () => {
|
||||||
vi.useFakeTimers();
|
|
||||||
loadDeviceAuthTokenMock.mockReturnValue({ token: "stored-device-token" });
|
loadDeviceAuthTokenMock.mockReturnValue({ token: "stored-device-token" });
|
||||||
const client = new GatewayClient({
|
const client = new GatewayClient({
|
||||||
url: "wss://gateway.example.com:18789",
|
url: "wss://gateway.example.com:18789",
|
||||||
token: "shared-token",
|
token: "shared-token",
|
||||||
});
|
});
|
||||||
|
|
||||||
client.start();
|
const { ws: ws1, connect: firstConnect } = startClientAndConnect({ client });
|
||||||
const ws1 = getLatestWs();
|
await expectNoReconnectAfterConnectFailure({
|
||||||
ws1.emitOpen();
|
client,
|
||||||
emitConnectChallenge(ws1);
|
firstWs: ws1,
|
||||||
const firstConnectRaw = ws1.sent.find((frame) => frame.includes('"method":"connect"'));
|
connectId: firstConnect.id,
|
||||||
expect(firstConnectRaw).toBeTruthy();
|
failureDetails: { code: "AUTH_TOKEN_MISMATCH", canRetryWithDeviceToken: true },
|
||||||
const firstConnect = JSON.parse(firstConnectRaw ?? "{}") as { id?: string };
|
});
|
||||||
|
|
||||||
ws1.emitMessage(
|
|
||||||
JSON.stringify({
|
|
||||||
type: "res",
|
|
||||||
id: firstConnect.id,
|
|
||||||
ok: false,
|
|
||||||
error: {
|
|
||||||
code: "INVALID_REQUEST",
|
|
||||||
message: "unauthorized",
|
|
||||||
details: { code: "AUTH_TOKEN_MISMATCH", canRetryWithDeviceToken: true },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(30_000);
|
|
||||||
expect(wsInstances).toHaveLength(1);
|
|
||||||
client.stop();
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue