claude-aws-mfa 1.1.0 → 1.2.1

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.
@@ -1,5 +1,32 @@
1
1
  import { describe, test, expect } from "bun:test";
2
- import { buildHtml, FIELDS, FIELD_PATTERNS } from "../src/dialog-html";
2
+ import { buildHtml, buildErrorHtml, FIELDS, FIELD_PATTERNS } from "../src/dialog-html";
3
+
4
+ describe("profile banner", () => {
5
+ const profile = { name: "work", path: "~/.claude-profiles/work" };
6
+
7
+ test("is shown for the default profile too, never blank", () => {
8
+ for (const html of [buildHtml({}), buildErrorHtml("boom")]) {
9
+ expect(html).toContain('class="profile"');
10
+ expect(html).toContain("<b>default</b>");
11
+ expect(html).toContain("~/.claude");
12
+ }
13
+ });
14
+
15
+ test("names the profile in both the credential and error dialogs", () => {
16
+ for (const html of [buildHtml({}, profile), buildErrorHtml("boom", profile)]) {
17
+ expect(html).toContain('class="profile"');
18
+ expect(html).toContain("<b>work</b>");
19
+ expect(html).toContain("~/.claude-profiles/work");
20
+ }
21
+ });
22
+
23
+ test("escapes the profile name and path", () => {
24
+ const html = buildHtml({}, { name: '<img src=x onerror="alert(1)">', path: "/a&b" });
25
+ expect(html).not.toContain("<img src=x");
26
+ expect(html).toContain("&lt;img");
27
+ expect(html).toContain("/a&amp;b");
28
+ });
29
+ });
3
30
 
4
31
  describe("buildHtml", () => {
5
32
  const config = {
@@ -161,3 +188,28 @@ describe("buildHtml", () => {
161
188
  expect(html).toContain("currentMode()");
162
189
  });
163
190
  });
191
+
192
+ describe("buildErrorHtml", () => {
193
+ test("produces valid HTML with doctype", () => {
194
+ const html = buildErrorHtml("Something went wrong");
195
+ expect(html).toStartWith("<!DOCTYPE html>");
196
+ expect(html).toContain("</html>");
197
+ });
198
+
199
+ test("includes the message text", () => {
200
+ const html = buildErrorHtml("STS AssumeRole failed: AccessDenied");
201
+ expect(html).toContain("STS AssumeRole failed: AccessDenied");
202
+ });
203
+
204
+ test("has an OK button", () => {
205
+ const html = buildErrorHtml("error");
206
+ expect(html).toContain("_ok()");
207
+ });
208
+
209
+ test("escapes HTML special characters in the message", () => {
210
+ const html = buildErrorHtml('<script>alert(1)</script> & "quoted"');
211
+ expect(html).not.toContain("<script>alert(1)</script>");
212
+ expect(html).toContain("&lt;script&gt;");
213
+ expect(html).toContain("&amp;");
214
+ });
215
+ });
@@ -41,7 +41,10 @@ describe.skipIf(!process.env.GUI_TEST)("GUI screenshot tests", () => {
41
41
  typeof import("node-screenshots").Window
42
42
  > | null = null;
43
43
 
44
+ let titles: string[] = [];
45
+
44
46
  beforeAll(async () => {
47
+ const start = Date.now();
45
48
  windowModule = await import("node-screenshots");
46
49
 
47
50
  mkdirSync(SCREENSHOT_DIR, { recursive: true });
@@ -52,28 +55,42 @@ describe.skipIf(!process.env.GUI_TEST)("GUI screenshot tests", () => {
52
55
  stderr: "inherit",
53
56
  });
54
57
 
55
- // Poll for the window to appear (up to 20s).
56
- // Window.all() may throw if the window manager hasn't registered
57
- // EWMH atoms yet (e.g. _NET_CLIENT_LIST_STACKING on Linux), so
58
- // we catch and retry.
59
- const deadline = Date.now() + 20_000;
58
+ // Poll for the window to appear. The budget is 90s rather than a snug
59
+ // multiple of the ~8s a warm runner takes: on Windows the first WebView2
60
+ // launch on a fresh image bootstraps its runtime and user-data folder
61
+ // before any window exists, which is what made this test flaky in CI.
62
+ // Waiting longer costs nothing when the window shows up on schedule.
63
+ //
64
+ // Window.all() may throw if the window manager hasn't registered EWMH
65
+ // atoms yet (e.g. _NET_CLIENT_LIST_STACKING on Linux), so we catch and
66
+ // retry.
67
+ const deadline = Date.now() + 90_000;
60
68
  while (Date.now() < deadline) {
61
69
  try {
62
70
  const windows = windowModule.Window.all();
63
- const match = windows.find((w) => {
64
- try { return w.title()?.includes("Claude AWS MFA"); }
65
- catch { return false; }
71
+ titles = windows.map((w) => {
72
+ try { return w.title() ?? ""; } catch { return ""; }
66
73
  });
67
- if (match) {
68
- foundWindow = match;
74
+ const i = titles.findIndex((t) => t.includes("Claude AWS MFA"));
75
+ if (i >= 0) {
76
+ foundWindow = windows[i]!;
69
77
  break;
70
78
  }
71
79
  } catch {
72
80
  // Window.all() not yet available — WM still initializing
73
81
  }
82
+ // A dead launcher will never produce a window; stop and let the
83
+ // "app launches without error" assertion report the exit code.
84
+ if (proc.exitCode !== null) break;
74
85
  await Bun.sleep(500);
75
86
  }
76
- }, 30_000);
87
+ if (!foundWindow) {
88
+ // Without this a failure is a bare `null` — no way to tell "window never
89
+ // opened" from "opened under a title we didn't match".
90
+ console.log(`No matching window after ${Math.round((Date.now() - start) / 1000)}s.`);
91
+ console.log(`Launcher exitCode: ${proc.exitCode}. Visible titles: ${JSON.stringify(titles)}`);
92
+ }
93
+ }, 120_000);
77
94
 
78
95
  afterAll(() => {
79
96
  proc?.kill();
@@ -11,6 +11,11 @@ describe("parseFlags", () => {
11
11
  expect(flags.cacheSession).toBeUndefined();
12
12
  expect(flags.autoMfa).toBeUndefined();
13
13
  expect(flags.singleInstanceLock).toBeUndefined();
14
+ expect(flags.noUi).toBe(false);
15
+ });
16
+
17
+ test("parses --no-ui", () => {
18
+ expect(parseFlags([...base, "--no-ui"]).noUi).toBe(true);
14
19
  });
15
20
 
16
21
  test("parses --setup", () => {
@@ -0,0 +1,59 @@
1
+ import { describe, test, expect, afterEach } from "bun:test";
2
+ import { mkdtempSync, rmSync, symlinkSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { getCredentialExportCommand } from "../src/credential-export";
6
+ import { bin } from "../package.json";
7
+
8
+ const BIN_NAME = Object.keys(bin)[0];
9
+
10
+ describe("getCredentialExportCommand", () => {
11
+ const origArgv = Bun.argv;
12
+ const origWhich = Bun.which;
13
+ let tmpDir: string;
14
+
15
+ afterEach(() => {
16
+ Bun.argv = origArgv;
17
+ Bun.which = origWhich;
18
+ if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
19
+ });
20
+
21
+ test("uses the bare bin name when it resolves via PATH to this same script (bin invoked directly)", () => {
22
+ tmpDir = mkdtempSync(join(tmpdir(), "claude-aws-mfa-setup-test-"));
23
+ const scriptTarget = join(tmpDir, "index.ts");
24
+ writeFileSync(scriptTarget, "");
25
+ // Mirrors real installed-bin behavior: the shim on PATH is a symlink whose
26
+ // target Bun resolves into Bun.argv[1].
27
+ const shim = join(tmpDir, BIN_NAME);
28
+ symlinkSync(scriptTarget, shim);
29
+
30
+ Bun.argv = [process.execPath, scriptTarget];
31
+ Bun.which = ((cmd: string) => (cmd === BIN_NAME ? shim : null)) as typeof Bun.which;
32
+
33
+ expect(getCredentialExportCommand()).toBe(BIN_NAME);
34
+ });
35
+
36
+ test("falls back to an explicit bun+script invocation when not resolvable via PATH", () => {
37
+ tmpDir = mkdtempSync(join(tmpdir(), "claude-aws-mfa-setup-test-"));
38
+ const scriptTarget = join(tmpDir, "index.ts");
39
+ writeFileSync(scriptTarget, "");
40
+
41
+ Bun.argv = [process.execPath, scriptTarget];
42
+ Bun.which = (() => null) as typeof Bun.which;
43
+
44
+ expect(getCredentialExportCommand()).toBe(`${process.execPath} ${scriptTarget}`);
45
+ });
46
+
47
+ test("falls back when PATH resolves the bin name to a different install", () => {
48
+ tmpDir = mkdtempSync(join(tmpdir(), "claude-aws-mfa-setup-test-"));
49
+ const scriptTarget = join(tmpDir, "index.ts");
50
+ const otherInstall = join(tmpDir, "other-index.ts");
51
+ writeFileSync(scriptTarget, "");
52
+ writeFileSync(otherInstall, "");
53
+
54
+ Bun.argv = [process.execPath, scriptTarget];
55
+ Bun.which = ((cmd: string) => (cmd === BIN_NAME ? otherInstall : null)) as typeof Bun.which;
56
+
57
+ expect(getCredentialExportCommand()).toBe(`${process.execPath} ${scriptTarget}`);
58
+ });
59
+ });