postgresai 0.14.0-dev.54 → 0.14.0-dev.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -3
- package/bin/postgres-ai.ts +25 -8
- package/bunfig.toml +11 -3
- package/dist/bin/postgres-ai.js +26 -8
- package/lib/metrics-embedded.ts +1 -1
- package/package.json +3 -1
- package/test/auth.test.ts +258 -0
package/README.md
CHANGED
|
@@ -303,17 +303,24 @@ Normalization:
|
|
|
303
303
|
|
|
304
304
|
### Examples
|
|
305
305
|
|
|
306
|
-
|
|
306
|
+
For production (uses default URLs):
|
|
307
307
|
|
|
308
308
|
```bash
|
|
309
|
+
# Production auth - uses console.postgres.ai by default
|
|
310
|
+
postgresai auth --debug
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
For staging/development environments:
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
# Linux/macOS (bash/zsh)
|
|
309
317
|
export PGAI_API_BASE_URL=https://v2.postgres.ai/api/general/
|
|
310
318
|
export PGAI_UI_BASE_URL=https://console-dev.postgres.ai
|
|
311
319
|
postgresai auth --debug
|
|
312
320
|
```
|
|
313
321
|
|
|
314
|
-
Windows PowerShell:
|
|
315
|
-
|
|
316
322
|
```powershell
|
|
323
|
+
# Windows PowerShell
|
|
317
324
|
$env:PGAI_API_BASE_URL = "https://v2.postgres.ai/api/general/"
|
|
318
325
|
$env:PGAI_UI_BASE_URL = "https://console-dev.postgres.ai"
|
|
319
326
|
postgresai auth --debug
|
|
@@ -330,6 +337,27 @@ postgresai auth --debug \
|
|
|
330
337
|
Notes:
|
|
331
338
|
- If `PGAI_UI_BASE_URL` is not set, the default is `https://console.postgres.ai`.
|
|
332
339
|
|
|
340
|
+
## Development
|
|
341
|
+
|
|
342
|
+
### Testing
|
|
343
|
+
|
|
344
|
+
The CLI uses [Bun](https://bun.sh/) as the test runner with built-in coverage reporting.
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
# Run tests with coverage (default)
|
|
348
|
+
bun run test
|
|
349
|
+
|
|
350
|
+
# Run tests without coverage (faster iteration during development)
|
|
351
|
+
bun run test:fast
|
|
352
|
+
|
|
353
|
+
# Run tests with coverage and show report location
|
|
354
|
+
bun run test:coverage
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Coverage configuration is in `bunfig.toml`. Reports are generated in `coverage/` directory:
|
|
358
|
+
- `coverage/lcov-report/index.html` - HTML coverage report
|
|
359
|
+
- `coverage/lcov.info` - LCOV format for CI integration
|
|
360
|
+
|
|
333
361
|
## Requirements
|
|
334
362
|
|
|
335
363
|
- Node.js 18 or higher
|
package/bin/postgres-ai.ts
CHANGED
|
@@ -1185,17 +1185,33 @@ mon
|
|
|
1185
1185
|
|
|
1186
1186
|
// Update .env with custom tag if provided
|
|
1187
1187
|
const envFile = path.resolve(projectDir, ".env");
|
|
1188
|
-
const imageTag = opts.tag || pkg.version;
|
|
1189
1188
|
|
|
1190
|
-
// Build .env content
|
|
1191
|
-
|
|
1192
|
-
|
|
1189
|
+
// Build .env content, preserving important existing values
|
|
1190
|
+
// Read existing .env first to preserve CI/custom settings
|
|
1191
|
+
let existingTag: string | null = null;
|
|
1192
|
+
let existingRegistry: string | null = null;
|
|
1193
|
+
let existingPassword: string | null = null;
|
|
1194
|
+
|
|
1193
1195
|
if (fs.existsSync(envFile)) {
|
|
1194
1196
|
const existingEnv = fs.readFileSync(envFile, "utf8");
|
|
1197
|
+
// Extract existing values
|
|
1198
|
+
const tagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
|
|
1199
|
+
if (tagMatch) existingTag = tagMatch[1].trim();
|
|
1200
|
+
const registryMatch = existingEnv.match(/^PGAI_REGISTRY=(.+)$/m);
|
|
1201
|
+
if (registryMatch) existingRegistry = registryMatch[1].trim();
|
|
1195
1202
|
const pwdMatch = existingEnv.match(/^GF_SECURITY_ADMIN_PASSWORD=(.+)$/m);
|
|
1196
|
-
if (pwdMatch)
|
|
1197
|
-
|
|
1198
|
-
|
|
1203
|
+
if (pwdMatch) existingPassword = pwdMatch[1].trim();
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// Priority: CLI --tag flag > existing .env > package version
|
|
1207
|
+
const imageTag = opts.tag || existingTag || pkg.version;
|
|
1208
|
+
|
|
1209
|
+
const envLines: string[] = [`PGAI_TAG=${imageTag}`];
|
|
1210
|
+
if (existingRegistry) {
|
|
1211
|
+
envLines.push(`PGAI_REGISTRY=${existingRegistry}`);
|
|
1212
|
+
}
|
|
1213
|
+
if (existingPassword) {
|
|
1214
|
+
envLines.push(`GF_SECURITY_ADMIN_PASSWORD=${existingPassword}`);
|
|
1199
1215
|
}
|
|
1200
1216
|
fs.writeFileSync(envFile, envLines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 });
|
|
1201
1217
|
|
|
@@ -2102,7 +2118,8 @@ auth
|
|
|
2102
2118
|
}
|
|
2103
2119
|
|
|
2104
2120
|
// Step 3: Open browser
|
|
2105
|
-
|
|
2121
|
+
// Pass api_url so UI calls oauth_approve on the same backend where oauth_init created the session
|
|
2122
|
+
const authUrl = `${uiBaseUrl}/cli/auth?state=${encodeURIComponent(params.state)}&code_challenge=${encodeURIComponent(params.codeChallenge)}&code_challenge_method=S256&redirect_uri=${encodeURIComponent(redirectUri)}&api_url=${encodeURIComponent(apiBaseUrl)}`;
|
|
2106
2123
|
|
|
2107
2124
|
if (opts.debug) {
|
|
2108
2125
|
console.log(`Debug: Auth URL: ${authUrl}`);
|
package/bunfig.toml
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
# Integration tests that connect to databases need longer timeouts
|
|
7
7
|
timeout = 30000
|
|
8
8
|
|
|
9
|
-
# Coverage settings
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
# Coverage settings - enabled by default for test runs
|
|
10
|
+
coverage = true
|
|
11
|
+
coverageDir = "coverage"
|
|
12
|
+
|
|
13
|
+
# Skip coverage for test files and node_modules
|
|
14
|
+
coverageSkipTestFiles = true
|
|
15
|
+
|
|
16
|
+
# Reporter format for CI integration
|
|
17
|
+
# - text: console output with summary table
|
|
18
|
+
# - lcov: standard format for coverage tools
|
|
19
|
+
coverageReporter = ["text", "lcov"]
|
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -13064,7 +13064,7 @@ var {
|
|
|
13064
13064
|
// package.json
|
|
13065
13065
|
var package_default = {
|
|
13066
13066
|
name: "postgresai",
|
|
13067
|
-
version: "0.14.0-dev.
|
|
13067
|
+
version: "0.14.0-dev.55",
|
|
13068
13068
|
description: "postgres_ai CLI",
|
|
13069
13069
|
license: "Apache-2.0",
|
|
13070
13070
|
private: false,
|
|
@@ -13096,6 +13096,8 @@ var package_default = {
|
|
|
13096
13096
|
"start:node": "node ./dist/bin/postgres-ai.js --help",
|
|
13097
13097
|
dev: "bun run embed-metrics && bun --watch ./bin/postgres-ai.ts",
|
|
13098
13098
|
test: "bun run embed-metrics && bun test",
|
|
13099
|
+
"test:fast": "bun run embed-metrics && bun test --coverage=false",
|
|
13100
|
+
"test:coverage": "bun run embed-metrics && bun test --coverage && echo 'Coverage report: cli/coverage/lcov-report/index.html'",
|
|
13099
13101
|
typecheck: "bun run embed-metrics && bunx tsc --noEmit"
|
|
13100
13102
|
},
|
|
13101
13103
|
dependencies: {
|
|
@@ -15885,7 +15887,7 @@ var Result = import_lib.default.Result;
|
|
|
15885
15887
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
15886
15888
|
var defaults = import_lib.default.defaults;
|
|
15887
15889
|
// package.json
|
|
15888
|
-
var version = "0.14.0-dev.
|
|
15890
|
+
var version = "0.14.0-dev.55";
|
|
15889
15891
|
var package_default2 = {
|
|
15890
15892
|
name: "postgresai",
|
|
15891
15893
|
version,
|
|
@@ -15920,6 +15922,8 @@ var package_default2 = {
|
|
|
15920
15922
|
"start:node": "node ./dist/bin/postgres-ai.js --help",
|
|
15921
15923
|
dev: "bun run embed-metrics && bun --watch ./bin/postgres-ai.ts",
|
|
15922
15924
|
test: "bun run embed-metrics && bun test",
|
|
15925
|
+
"test:fast": "bun run embed-metrics && bun test --coverage=false",
|
|
15926
|
+
"test:coverage": "bun run embed-metrics && bun test --coverage && echo 'Coverage report: cli/coverage/lcov-report/index.html'",
|
|
15923
15927
|
typecheck: "bun run embed-metrics && bunx tsc --noEmit"
|
|
15924
15928
|
},
|
|
15925
15929
|
dependencies: {
|
|
@@ -26723,14 +26727,28 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
26723
26727
|
console.log(`Project directory: ${projectDir}
|
|
26724
26728
|
`);
|
|
26725
26729
|
const envFile = path5.resolve(projectDir, ".env");
|
|
26726
|
-
|
|
26727
|
-
|
|
26730
|
+
let existingTag = null;
|
|
26731
|
+
let existingRegistry = null;
|
|
26732
|
+
let existingPassword = null;
|
|
26728
26733
|
if (fs5.existsSync(envFile)) {
|
|
26729
26734
|
const existingEnv = fs5.readFileSync(envFile, "utf8");
|
|
26735
|
+
const tagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
|
|
26736
|
+
if (tagMatch)
|
|
26737
|
+
existingTag = tagMatch[1].trim();
|
|
26738
|
+
const registryMatch = existingEnv.match(/^PGAI_REGISTRY=(.+)$/m);
|
|
26739
|
+
if (registryMatch)
|
|
26740
|
+
existingRegistry = registryMatch[1].trim();
|
|
26730
26741
|
const pwdMatch = existingEnv.match(/^GF_SECURITY_ADMIN_PASSWORD=(.+)$/m);
|
|
26731
|
-
if (pwdMatch)
|
|
26732
|
-
|
|
26733
|
-
|
|
26742
|
+
if (pwdMatch)
|
|
26743
|
+
existingPassword = pwdMatch[1].trim();
|
|
26744
|
+
}
|
|
26745
|
+
const imageTag = opts.tag || existingTag || package_default.version;
|
|
26746
|
+
const envLines = [`PGAI_TAG=${imageTag}`];
|
|
26747
|
+
if (existingRegistry) {
|
|
26748
|
+
envLines.push(`PGAI_REGISTRY=${existingRegistry}`);
|
|
26749
|
+
}
|
|
26750
|
+
if (existingPassword) {
|
|
26751
|
+
envLines.push(`GF_SECURITY_ADMIN_PASSWORD=${existingPassword}`);
|
|
26734
26752
|
}
|
|
26735
26753
|
fs5.writeFileSync(envFile, envLines.join(`
|
|
26736
26754
|
`) + `
|
|
@@ -27535,7 +27553,7 @@ Please verify the --api-base-url parameter.`);
|
|
|
27535
27553
|
process.exit(1);
|
|
27536
27554
|
return;
|
|
27537
27555
|
}
|
|
27538
|
-
const authUrl = `${uiBaseUrl}/cli/auth?state=${encodeURIComponent(params.state)}&code_challenge=${encodeURIComponent(params.codeChallenge)}&code_challenge_method=S256&redirect_uri=${encodeURIComponent(redirectUri)}`;
|
|
27556
|
+
const authUrl = `${uiBaseUrl}/cli/auth?state=${encodeURIComponent(params.state)}&code_challenge=${encodeURIComponent(params.codeChallenge)}&code_challenge_method=S256&redirect_uri=${encodeURIComponent(redirectUri)}&api_url=${encodeURIComponent(apiBaseUrl)}`;
|
|
27539
27557
|
if (opts.debug) {
|
|
27540
27558
|
console.log(`Debug: Auth URL: ${authUrl}`);
|
|
27541
27559
|
}
|
package/lib/metrics-embedded.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postgresai",
|
|
3
|
-
"version": "0.14.0-dev.
|
|
3
|
+
"version": "0.14.0-dev.55",
|
|
4
4
|
"description": "postgres_ai CLI",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"private": false,
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
"start:node": "node ./dist/bin/postgres-ai.js --help",
|
|
33
33
|
"dev": "bun run embed-metrics && bun --watch ./bin/postgres-ai.ts",
|
|
34
34
|
"test": "bun run embed-metrics && bun test",
|
|
35
|
+
"test:fast": "bun run embed-metrics && bun test --coverage=false",
|
|
36
|
+
"test:coverage": "bun run embed-metrics && bun test --coverage && echo 'Coverage report: cli/coverage/lcov-report/index.html'",
|
|
35
37
|
"typecheck": "bun run embed-metrics && bunx tsc --noEmit"
|
|
36
38
|
},
|
|
37
39
|
"dependencies": {
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
|
|
4
|
+
import * as util from "../lib/util";
|
|
5
|
+
import * as pkce from "../lib/pkce";
|
|
6
|
+
import * as authServer from "../lib/auth-server";
|
|
7
|
+
|
|
8
|
+
function runCli(args: string[], env: Record<string, string> = {}) {
|
|
9
|
+
const cliPath = resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
|
|
10
|
+
const bunBin = typeof process.execPath === "string" && process.execPath.length > 0 ? process.execPath : "bun";
|
|
11
|
+
const result = Bun.spawnSync([bunBin, cliPath, ...args], {
|
|
12
|
+
env: { ...process.env, ...env },
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
status: result.exitCode,
|
|
16
|
+
stdout: new TextDecoder().decode(result.stdout),
|
|
17
|
+
stderr: new TextDecoder().decode(result.stderr),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("URL resolution", () => {
|
|
22
|
+
test("resolveBaseUrls returns correct production defaults", () => {
|
|
23
|
+
const result = util.resolveBaseUrls();
|
|
24
|
+
expect(result.apiBaseUrl).toBe("https://postgres.ai/api/general");
|
|
25
|
+
expect(result.uiBaseUrl).toBe("https://console.postgres.ai");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("resolveBaseUrls strips trailing slashes", () => {
|
|
29
|
+
const result = util.resolveBaseUrls({
|
|
30
|
+
apiBaseUrl: "https://example.com/api/",
|
|
31
|
+
uiBaseUrl: "https://example.com/",
|
|
32
|
+
});
|
|
33
|
+
expect(result.apiBaseUrl).toBe("https://example.com/api");
|
|
34
|
+
expect(result.uiBaseUrl).toBe("https://example.com");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("resolveBaseUrls respects environment variables", () => {
|
|
38
|
+
const originalApiUrl = process.env.PGAI_API_BASE_URL;
|
|
39
|
+
const originalUiUrl = process.env.PGAI_UI_BASE_URL;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
process.env.PGAI_API_BASE_URL = "https://custom-api.example.com/api/";
|
|
43
|
+
process.env.PGAI_UI_BASE_URL = "https://custom-ui.example.com/";
|
|
44
|
+
|
|
45
|
+
const result = util.resolveBaseUrls();
|
|
46
|
+
expect(result.apiBaseUrl).toBe("https://custom-api.example.com/api");
|
|
47
|
+
expect(result.uiBaseUrl).toBe("https://custom-ui.example.com");
|
|
48
|
+
} finally {
|
|
49
|
+
if (originalApiUrl === undefined) {
|
|
50
|
+
delete process.env.PGAI_API_BASE_URL;
|
|
51
|
+
} else {
|
|
52
|
+
process.env.PGAI_API_BASE_URL = originalApiUrl;
|
|
53
|
+
}
|
|
54
|
+
if (originalUiUrl === undefined) {
|
|
55
|
+
delete process.env.PGAI_UI_BASE_URL;
|
|
56
|
+
} else {
|
|
57
|
+
process.env.PGAI_UI_BASE_URL = originalUiUrl;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("resolveBaseUrls prefers CLI options over env vars", () => {
|
|
63
|
+
const originalApiUrl = process.env.PGAI_API_BASE_URL;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
process.env.PGAI_API_BASE_URL = "https://env.example.com/api/";
|
|
67
|
+
|
|
68
|
+
const result = util.resolveBaseUrls({
|
|
69
|
+
apiBaseUrl: "https://cli-option.example.com/api/",
|
|
70
|
+
});
|
|
71
|
+
expect(result.apiBaseUrl).toBe("https://cli-option.example.com/api");
|
|
72
|
+
} finally {
|
|
73
|
+
if (originalApiUrl === undefined) {
|
|
74
|
+
delete process.env.PGAI_API_BASE_URL;
|
|
75
|
+
} else {
|
|
76
|
+
process.env.PGAI_API_BASE_URL = originalApiUrl;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("resolveBaseUrls uses config baseUrl for API", () => {
|
|
82
|
+
const result = util.resolveBaseUrls({}, { baseUrl: "https://config.example.com/api/" });
|
|
83
|
+
expect(result.apiBaseUrl).toBe("https://config.example.com/api");
|
|
84
|
+
// UI should still use default since config doesn't have uiBaseUrl
|
|
85
|
+
expect(result.uiBaseUrl).toBe("https://console.postgres.ai");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("normalizeBaseUrl throws on invalid URL", () => {
|
|
89
|
+
expect(() => util.normalizeBaseUrl("not-a-url")).toThrow(/Invalid base URL/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("normalizeBaseUrl accepts valid URLs", () => {
|
|
93
|
+
expect(util.normalizeBaseUrl("https://example.com")).toBe("https://example.com");
|
|
94
|
+
expect(util.normalizeBaseUrl("https://example.com/")).toBe("https://example.com");
|
|
95
|
+
expect(util.normalizeBaseUrl("https://example.com/api/")).toBe("https://example.com/api");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("PKCE module", () => {
|
|
100
|
+
test("generateCodeVerifier returns correct length string", () => {
|
|
101
|
+
const verifier = pkce.generateCodeVerifier();
|
|
102
|
+
expect(typeof verifier).toBe("string");
|
|
103
|
+
expect(verifier.length).toBeGreaterThanOrEqual(43);
|
|
104
|
+
expect(verifier.length).toBeLessThanOrEqual(128);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("generateCodeChallenge returns base64url encoded SHA256", () => {
|
|
108
|
+
const verifier = pkce.generateCodeVerifier();
|
|
109
|
+
const challenge = pkce.generateCodeChallenge(verifier);
|
|
110
|
+
expect(typeof challenge).toBe("string");
|
|
111
|
+
expect(challenge.length).toBeGreaterThan(0);
|
|
112
|
+
// Base64url encoding should not contain + or / characters
|
|
113
|
+
expect(challenge).not.toMatch(/[+/]/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("generateState returns random string", () => {
|
|
117
|
+
const state1 = pkce.generateState();
|
|
118
|
+
const state2 = pkce.generateState();
|
|
119
|
+
expect(typeof state1).toBe("string");
|
|
120
|
+
expect(state1.length).toBeGreaterThan(0);
|
|
121
|
+
expect(state1).not.toBe(state2); // Should be random
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("generatePKCEParams returns all required parameters", () => {
|
|
125
|
+
const params = pkce.generatePKCEParams();
|
|
126
|
+
expect(params.codeVerifier).toBeTruthy();
|
|
127
|
+
expect(params.codeChallenge).toBeTruthy();
|
|
128
|
+
expect(params.codeChallengeMethod).toBe("S256");
|
|
129
|
+
expect(params.state).toBeTruthy();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("Auth callback server", () => {
|
|
134
|
+
test("createCallbackServer returns correct interface", () => {
|
|
135
|
+
const server = authServer.createCallbackServer(0, "test-state", 1000);
|
|
136
|
+
expect(server.server).toBeTruthy();
|
|
137
|
+
expect(server.server.stop).toBeInstanceOf(Function);
|
|
138
|
+
expect(server.promise).toBeInstanceOf(Promise);
|
|
139
|
+
expect(server.ready).toBeInstanceOf(Promise);
|
|
140
|
+
expect(server.getPort).toBeInstanceOf(Function);
|
|
141
|
+
|
|
142
|
+
// Clean up
|
|
143
|
+
server.server.stop();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("createCallbackServer binds to a port", async () => {
|
|
147
|
+
const server = authServer.createCallbackServer(0, "test-state", 5000);
|
|
148
|
+
const port = await server.ready;
|
|
149
|
+
expect(typeof port).toBe("number");
|
|
150
|
+
expect(port).toBeGreaterThan(0);
|
|
151
|
+
|
|
152
|
+
// Clean up
|
|
153
|
+
server.server.stop();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("createCallbackServer responds to callback requests", async () => {
|
|
157
|
+
const testState = "test-state-" + Math.random().toString(36).substring(7);
|
|
158
|
+
const server = authServer.createCallbackServer(0, testState, 5000);
|
|
159
|
+
const port = await server.ready;
|
|
160
|
+
|
|
161
|
+
// Simulate OAuth callback
|
|
162
|
+
const testCode = "test-auth-code";
|
|
163
|
+
const callbackUrl = `http://127.0.0.1:${port}/callback?code=${testCode}&state=${testState}`;
|
|
164
|
+
|
|
165
|
+
const fetchPromise = fetch(callbackUrl);
|
|
166
|
+
const result = await server.promise;
|
|
167
|
+
|
|
168
|
+
expect(result.code).toBe(testCode);
|
|
169
|
+
expect(result.state).toBe(testState);
|
|
170
|
+
|
|
171
|
+
// Check response
|
|
172
|
+
const response = await fetchPromise;
|
|
173
|
+
expect(response.status).toBe(200);
|
|
174
|
+
const text = await response.text();
|
|
175
|
+
expect(text).toMatch(/Authentication successful/);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("createCallbackServer rejects on state mismatch", async () => {
|
|
179
|
+
const server = authServer.createCallbackServer(0, "expected-state", 5000);
|
|
180
|
+
const port = await server.ready;
|
|
181
|
+
|
|
182
|
+
const callbackUrl = `http://127.0.0.1:${port}/callback?code=test-code&state=wrong-state`;
|
|
183
|
+
|
|
184
|
+
const fetchPromise = fetch(callbackUrl);
|
|
185
|
+
|
|
186
|
+
await expect(server.promise).rejects.toThrow(/State mismatch/);
|
|
187
|
+
|
|
188
|
+
const response = await fetchPromise;
|
|
189
|
+
expect(response.status).toBe(400);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("createCallbackServer handles OAuth errors", async () => {
|
|
193
|
+
const server = authServer.createCallbackServer(0, "test-state", 5000);
|
|
194
|
+
const port = await server.ready;
|
|
195
|
+
|
|
196
|
+
const callbackUrl = `http://127.0.0.1:${port}/callback?error=access_denied&error_description=User%20denied%20access`;
|
|
197
|
+
|
|
198
|
+
const fetchPromise = fetch(callbackUrl);
|
|
199
|
+
|
|
200
|
+
await expect(server.promise).rejects.toThrow(/OAuth error: access_denied/);
|
|
201
|
+
|
|
202
|
+
const response = await fetchPromise;
|
|
203
|
+
expect(response.status).toBe(400);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("createCallbackServer times out", async () => {
|
|
207
|
+
const server = authServer.createCallbackServer(0, "test-state", 100); // 100ms timeout
|
|
208
|
+
await server.ready;
|
|
209
|
+
|
|
210
|
+
await expect(server.promise).rejects.toThrow(/timeout/i);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("CLI auth commands", () => {
|
|
215
|
+
test("cli: auth login --help shows all options", () => {
|
|
216
|
+
const r = runCli(["auth", "login", "--help"]);
|
|
217
|
+
expect(r.status).toBe(0);
|
|
218
|
+
expect(r.stdout).toMatch(/--set-key/);
|
|
219
|
+
expect(r.stdout).toMatch(/--debug/);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("cli: auth show-key --help works", () => {
|
|
223
|
+
const r = runCli(["auth", "show-key", "--help"]);
|
|
224
|
+
expect(r.status).toBe(0);
|
|
225
|
+
expect(r.stdout).toMatch(/show.*key/i);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("cli: auth remove-key --help works", () => {
|
|
229
|
+
const r = runCli(["auth", "remove-key", "--help"]);
|
|
230
|
+
expect(r.status).toBe(0);
|
|
231
|
+
expect(r.stdout).toMatch(/remove.*key/i);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("maskSecret utility", () => {
|
|
236
|
+
test("masks short secrets completely", () => {
|
|
237
|
+
expect(util.maskSecret("abc")).toBe("****");
|
|
238
|
+
expect(util.maskSecret("12345678")).toBe("****");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("masks medium secrets with visible ends", () => {
|
|
242
|
+
const masked = util.maskSecret("1234567890123456");
|
|
243
|
+
// maskSecret shows first 4 chars, middle masked, last 4 chars for 16-char strings
|
|
244
|
+
expect(masked).toMatch(/^1234\*+3456$/);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("masks long secrets appropriately", () => {
|
|
248
|
+
const secret = "abcdefghij1234567890klmnopqrstuvwxyz";
|
|
249
|
+
const masked = util.maskSecret(secret);
|
|
250
|
+
expect(masked.startsWith("abcdefghij12")).toBe(true);
|
|
251
|
+
expect(masked.endsWith("wxyz")).toBe(true);
|
|
252
|
+
expect(masked).toMatch(/\*+/);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("handles empty string", () => {
|
|
256
|
+
expect(util.maskSecret("")).toBe("");
|
|
257
|
+
});
|
|
258
|
+
});
|