* Secrets: add inline allowlist review set * Secrets: narrow detect-secrets file exclusions * Secrets: exclude Docker fingerprint false positive * Secrets: allowlist test and docs false positives * Secrets: refresh baseline after allowlist updates * Secrets: fix gateway chat fixture pragma * Secrets: format pre-commit config * Android: keep talk mode fixture JSON valid * Feishu: rely on client timeout injection * Secrets: allowlist provider auth test fixtures * Secrets: allowlist onboard search fixtures * Secrets: allowlist onboard mode fixture * Secrets: allowlist gateway auth mode fixture * Secrets: allowlist APNS wake test key * Secrets: allowlist gateway reload fixtures * Secrets: allowlist moonshot video fixture * Secrets: allowlist auto audio fixture * Secrets: allowlist tiny audio fixture * Secrets: allowlist embeddings fixtures * Secrets: allowlist resolve fixtures * Secrets: allowlist target registry pattern fixtures * Secrets: allowlist gateway chat env fixture * Secrets: refresh baseline after fixture allowlists * Secrets: reapply gateway chat env allowlist * Secrets: reapply gateway chat env allowlist * Secrets: stabilize gateway chat env allowlist * Secrets: allowlist runtime snapshot save fixture * Secrets: allowlist oauth profile fixtures * Secrets: allowlist compaction identifier fixture * Secrets: allowlist model auth fixture * Secrets: allowlist model status fixtures * Secrets: allowlist custom onboarding fixture * Secrets: allowlist mattermost token summary fixtures * Secrets: allowlist gateway auth suite fixtures * Secrets: allowlist channel summary fixture * Secrets: allowlist provider usage auth fixtures * Secrets: allowlist media proxy fixture * Secrets: allowlist secrets audit fixtures * Secrets: refresh baseline after final fixture allowlists * Feishu: prefer explicit client timeout * Feishu: test direct timeout precedence
172 lines
5.2 KiB
TypeScript
172 lines
5.2 KiB
TypeScript
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
import {
|
|
connectReq,
|
|
CONTROL_UI_CLIENT,
|
|
ConnectErrorDetailCodes,
|
|
getFreePort,
|
|
openTailscaleWs,
|
|
openWs,
|
|
originForPort,
|
|
rpcReq,
|
|
restoreGatewayToken,
|
|
startGatewayServer,
|
|
testState,
|
|
testTailscaleWhois,
|
|
} from "./server.auth.shared.js";
|
|
|
|
export function registerAuthModesSuite(): void {
|
|
describe("password auth", () => {
|
|
let server: Awaited<ReturnType<typeof startGatewayServer>>;
|
|
let port: number;
|
|
|
|
beforeAll(async () => {
|
|
testState.gatewayAuth = { mode: "password", password: "secret" }; // pragma: allowlist secret
|
|
port = await getFreePort();
|
|
server = await startGatewayServer(port);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await server.close();
|
|
});
|
|
|
|
test("accepts password auth when configured", async () => {
|
|
const ws = await openWs(port);
|
|
const res = await connectReq(ws, { password: "secret" }); // pragma: allowlist secret
|
|
expect(res.ok).toBe(true);
|
|
ws.close();
|
|
});
|
|
|
|
test("rejects invalid password", async () => {
|
|
const ws = await openWs(port);
|
|
const res = await connectReq(ws, { password: "wrong" }); // pragma: allowlist secret
|
|
expect(res.ok).toBe(false);
|
|
expect(res.error?.message ?? "").toContain("unauthorized");
|
|
ws.close();
|
|
});
|
|
});
|
|
|
|
describe("token auth", () => {
|
|
let server: Awaited<ReturnType<typeof startGatewayServer>>;
|
|
let port: number;
|
|
let prevToken: string | undefined;
|
|
|
|
beforeAll(async () => {
|
|
prevToken = process.env.OPENCLAW_GATEWAY_TOKEN;
|
|
process.env.OPENCLAW_GATEWAY_TOKEN = "secret";
|
|
port = await getFreePort();
|
|
server = await startGatewayServer(port);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await server.close();
|
|
restoreGatewayToken(prevToken);
|
|
});
|
|
|
|
test("rejects invalid token", async () => {
|
|
const ws = await openWs(port);
|
|
const res = await connectReq(ws, { token: "wrong" });
|
|
expect(res.ok).toBe(false);
|
|
expect(res.error?.message ?? "").toContain("unauthorized");
|
|
ws.close();
|
|
});
|
|
|
|
test("returns control ui hint when token is missing", async () => {
|
|
const ws = await openWs(port, { origin: originForPort(port) });
|
|
const res = await connectReq(ws, {
|
|
skipDefaultAuth: true,
|
|
client: {
|
|
...CONTROL_UI_CLIENT,
|
|
},
|
|
});
|
|
expect(res.ok).toBe(false);
|
|
expect(res.error?.message ?? "").toContain("Control UI settings");
|
|
ws.close();
|
|
});
|
|
|
|
test("rejects control ui without device identity by default", async () => {
|
|
const ws = await openWs(port, { origin: originForPort(port) });
|
|
const res = await connectReq(ws, {
|
|
token: "secret",
|
|
device: null,
|
|
client: {
|
|
...CONTROL_UI_CLIENT,
|
|
},
|
|
});
|
|
expect(res.ok).toBe(false);
|
|
expect(res.error?.message ?? "").toContain("secure context");
|
|
expect((res.error?.details as { code?: string } | undefined)?.code).toBe(
|
|
ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
|
|
);
|
|
ws.close();
|
|
});
|
|
});
|
|
|
|
describe("explicit none auth", () => {
|
|
let server: Awaited<ReturnType<typeof startGatewayServer>>;
|
|
let port: number;
|
|
let prevToken: string | undefined;
|
|
|
|
beforeAll(async () => {
|
|
prevToken = process.env.OPENCLAW_GATEWAY_TOKEN;
|
|
delete process.env.OPENCLAW_GATEWAY_TOKEN;
|
|
testState.gatewayAuth = { mode: "none" };
|
|
port = await getFreePort();
|
|
server = await startGatewayServer(port);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await server.close();
|
|
restoreGatewayToken(prevToken);
|
|
});
|
|
|
|
test("allows loopback connect without shared secret when mode is none", async () => {
|
|
const ws = await openWs(port);
|
|
const res = await connectReq(ws, { skipDefaultAuth: true });
|
|
expect(res.ok).toBe(true);
|
|
ws.close();
|
|
});
|
|
});
|
|
|
|
describe("tailscale auth", () => {
|
|
let server: Awaited<ReturnType<typeof startGatewayServer>>;
|
|
let port: number;
|
|
|
|
beforeAll(async () => {
|
|
testState.gatewayAuth = { mode: "token", token: "secret", allowTailscale: true };
|
|
port = await getFreePort();
|
|
server = await startGatewayServer(port);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await server.close();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
testTailscaleWhois.value = { login: "peter", name: "Peter" };
|
|
});
|
|
|
|
afterEach(() => {
|
|
testTailscaleWhois.value = null;
|
|
});
|
|
|
|
test("requires device identity when only tailscale auth is available", async () => {
|
|
const ws = await openTailscaleWs(port);
|
|
const res = await connectReq(ws, { token: "dummy", device: null });
|
|
expect(res.ok).toBe(false);
|
|
expect(res.error?.message ?? "").toContain("device identity required");
|
|
ws.close();
|
|
});
|
|
|
|
test("allows shared token to skip device when tailscale auth is enabled", async () => {
|
|
const ws = await openTailscaleWs(port);
|
|
const res = await connectReq(ws, { token: "secret", device: null });
|
|
expect(res.ok).toBe(true);
|
|
const status = await rpcReq(ws, "status");
|
|
expect(status.ok).toBe(true);
|
|
const health = await rpcReq(ws, "health");
|
|
expect(health.ok).toBe(true);
|
|
ws.close();
|
|
});
|
|
});
|
|
}
|