-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathauth.test.ts
More file actions
258 lines (213 loc) · 9.08 KB
/
auth.test.ts
File metadata and controls
258 lines (213 loc) · 9.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
import { describe, test, expect } from "bun:test";
import { resolve } from "path";
import * as util from "../lib/util";
import * as pkce from "../lib/pkce";
import * as authServer from "../lib/auth-server";
function runCli(args: string[], env: Record<string, string> = {}) {
const cliPath = resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
const bunBin = typeof process.execPath === "string" && process.execPath.length > 0 ? process.execPath : "bun";
const result = Bun.spawnSync([bunBin, cliPath, ...args], {
env: { ...process.env, ...env },
});
return {
status: result.exitCode,
stdout: new TextDecoder().decode(result.stdout),
stderr: new TextDecoder().decode(result.stderr),
};
}
describe("URL resolution", () => {
test("resolveBaseUrls returns correct production defaults", () => {
const result = util.resolveBaseUrls();
expect(result.apiBaseUrl).toBe("https://postgres.ai/api/general");
expect(result.uiBaseUrl).toBe("https://console.postgres.ai");
});
test("resolveBaseUrls strips trailing slashes", () => {
const result = util.resolveBaseUrls({
apiBaseUrl: "https://example.com/api/",
uiBaseUrl: "https://example.com/",
});
expect(result.apiBaseUrl).toBe("https://example.com/api");
expect(result.uiBaseUrl).toBe("https://example.com");
});
test("resolveBaseUrls respects environment variables", () => {
const originalApiUrl = process.env.PGAI_API_BASE_URL;
const originalUiUrl = process.env.PGAI_UI_BASE_URL;
try {
process.env.PGAI_API_BASE_URL = "https://custom-api.example.com/api/";
process.env.PGAI_UI_BASE_URL = "https://custom-ui.example.com/";
const result = util.resolveBaseUrls();
expect(result.apiBaseUrl).toBe("https://custom-api.example.com/api");
expect(result.uiBaseUrl).toBe("https://custom-ui.example.com");
} finally {
if (originalApiUrl === undefined) {
delete process.env.PGAI_API_BASE_URL;
} else {
process.env.PGAI_API_BASE_URL = originalApiUrl;
}
if (originalUiUrl === undefined) {
delete process.env.PGAI_UI_BASE_URL;
} else {
process.env.PGAI_UI_BASE_URL = originalUiUrl;
}
}
});
test("resolveBaseUrls prefers CLI options over env vars", () => {
const originalApiUrl = process.env.PGAI_API_BASE_URL;
try {
process.env.PGAI_API_BASE_URL = "https://env.example.com/api/";
const result = util.resolveBaseUrls({
apiBaseUrl: "https://cli-option.example.com/api/",
});
expect(result.apiBaseUrl).toBe("https://cli-option.example.com/api");
} finally {
if (originalApiUrl === undefined) {
delete process.env.PGAI_API_BASE_URL;
} else {
process.env.PGAI_API_BASE_URL = originalApiUrl;
}
}
});
test("resolveBaseUrls uses config baseUrl for API", () => {
const result = util.resolveBaseUrls({}, { baseUrl: "https://config.example.com/api/" });
expect(result.apiBaseUrl).toBe("https://config.example.com/api");
// UI should still use default since config doesn't have uiBaseUrl
expect(result.uiBaseUrl).toBe("https://console.postgres.ai");
});
test("normalizeBaseUrl throws on invalid URL", () => {
expect(() => util.normalizeBaseUrl("not-a-url")).toThrow(/Invalid base URL/);
});
test("normalizeBaseUrl accepts valid URLs", () => {
expect(util.normalizeBaseUrl("https://example.com")).toBe("https://example.com");
expect(util.normalizeBaseUrl("https://example.com/")).toBe("https://example.com");
expect(util.normalizeBaseUrl("https://example.com/api/")).toBe("https://example.com/api");
});
});
describe("PKCE module", () => {
test("generateCodeVerifier returns correct length string", () => {
const verifier = pkce.generateCodeVerifier();
expect(typeof verifier).toBe("string");
expect(verifier.length).toBeGreaterThanOrEqual(43);
expect(verifier.length).toBeLessThanOrEqual(128);
});
test("generateCodeChallenge returns base64url encoded SHA256", () => {
const verifier = pkce.generateCodeVerifier();
const challenge = pkce.generateCodeChallenge(verifier);
expect(typeof challenge).toBe("string");
expect(challenge.length).toBeGreaterThan(0);
// Base64url encoding should not contain + or / characters
expect(challenge).not.toMatch(/[+/]/);
});
test("generateState returns random string", () => {
const state1 = pkce.generateState();
const state2 = pkce.generateState();
expect(typeof state1).toBe("string");
expect(state1.length).toBeGreaterThan(0);
expect(state1).not.toBe(state2); // Should be random
});
test("generatePKCEParams returns all required parameters", () => {
const params = pkce.generatePKCEParams();
expect(params.codeVerifier).toBeTruthy();
expect(params.codeChallenge).toBeTruthy();
expect(params.codeChallengeMethod).toBe("S256");
expect(params.state).toBeTruthy();
});
});
describe("Auth callback server", () => {
test("createCallbackServer returns correct interface", () => {
const server = authServer.createCallbackServer(0, "test-state", 1000);
expect(server.server).toBeTruthy();
expect(server.server.stop).toBeInstanceOf(Function);
expect(server.promise).toBeInstanceOf(Promise);
expect(server.ready).toBeInstanceOf(Promise);
expect(server.getPort).toBeInstanceOf(Function);
// Clean up
server.server.stop();
});
test("createCallbackServer binds to a port", async () => {
const server = authServer.createCallbackServer(0, "test-state", 5000);
const port = await server.ready;
expect(typeof port).toBe("number");
expect(port).toBeGreaterThan(0);
// Clean up
server.server.stop();
});
test("createCallbackServer responds to callback requests", async () => {
const testState = "test-state-" + Math.random().toString(36).substring(7);
const server = authServer.createCallbackServer(0, testState, 5000);
const port = await server.ready;
// Simulate OAuth callback
const testCode = "test-auth-code";
const callbackUrl = `http://127.0.0.1:${port}/callback?code=${testCode}&state=${testState}`;
const fetchPromise = fetch(callbackUrl);
const result = await server.promise;
expect(result.code).toBe(testCode);
expect(result.state).toBe(testState);
// Check response
const response = await fetchPromise;
expect(response.status).toBe(200);
const text = await response.text();
expect(text).toMatch(/Authentication successful/);
});
test("createCallbackServer rejects on state mismatch", async () => {
const server = authServer.createCallbackServer(0, "expected-state", 5000);
const port = await server.ready;
const callbackUrl = `http://127.0.0.1:${port}/callback?code=test-code&state=wrong-state`;
const fetchPromise = fetch(callbackUrl);
await expect(server.promise).rejects.toThrow(/State mismatch/);
const response = await fetchPromise;
expect(response.status).toBe(400);
});
test("createCallbackServer handles OAuth errors", async () => {
const server = authServer.createCallbackServer(0, "test-state", 5000);
const port = await server.ready;
const callbackUrl = `http://127.0.0.1:${port}/callback?error=access_denied&error_description=User%20denied%20access`;
const fetchPromise = fetch(callbackUrl);
await expect(server.promise).rejects.toThrow(/OAuth error: access_denied/);
const response = await fetchPromise;
expect(response.status).toBe(400);
});
test("createCallbackServer times out", async () => {
const server = authServer.createCallbackServer(0, "test-state", 100); // 100ms timeout
await server.ready;
await expect(server.promise).rejects.toThrow(/timeout/i);
});
});
describe("CLI auth commands", () => {
test("cli: auth login --help shows all options", () => {
const r = runCli(["auth", "login", "--help"]);
expect(r.status).toBe(0);
expect(r.stdout).toMatch(/--set-key/);
expect(r.stdout).toMatch(/--debug/);
});
test("cli: auth show-key --help works", () => {
const r = runCli(["auth", "show-key", "--help"]);
expect(r.status).toBe(0);
expect(r.stdout).toMatch(/show.*key/i);
});
test("cli: auth remove-key --help works", () => {
const r = runCli(["auth", "remove-key", "--help"]);
expect(r.status).toBe(0);
expect(r.stdout).toMatch(/remove.*key/i);
});
});
describe("maskSecret utility", () => {
test("masks short secrets completely", () => {
expect(util.maskSecret("abc")).toBe("****");
expect(util.maskSecret("12345678")).toBe("****");
});
test("masks medium secrets with visible ends", () => {
const masked = util.maskSecret("1234567890123456");
// maskSecret shows first 4 chars, middle masked, last 4 chars for 16-char strings
expect(masked).toMatch(/^1234\*+3456$/);
});
test("masks long secrets appropriately", () => {
const secret = "abcdefghij1234567890klmnopqrstuvwxyz";
const masked = util.maskSecret(secret);
expect(masked.startsWith("abcdefghij12")).toBe(true);
expect(masked.endsWith("wxyz")).toBe(true);
expect(masked).toMatch(/\*+/);
});
test("handles empty string", () => {
expect(util.maskSecret("")).toBe("");
});
});