Skip to content

Commit f01cadc

Browse files
test(sec): cover all GOOS branches + rename-failure cleanup for 100% patch gate
Address the coverage-gate failure on PR #20: * Extract browserLauncherForGOOS + openBrowserOn so all per-OS branches (darwin / linux / windows / unknown) and the exec-failure path are reachable from a single Linux CI runner via injected GOOS strings. * Add tests for tokens.Save's rename-failure cleanup branch — make the target path a non-empty directory so os.Rename fails and the best-effort os.Remove(tmp) runs. Local verify: go build ./... -> 0 go test ./cmd/ ./internal/tokens -count=1 -> ok coverage on touched lines -> 100 % Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f3dc28c commit f01cadc

3 files changed

Lines changed: 167 additions & 15 deletions

File tree

cmd/login.go

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -334,27 +334,54 @@ func safeBrowserURL(raw string) (string, error) {
334334
return raw, nil
335335
}
336336

337-
// openBrowser opens url in the user's default browser, best-effort.
337+
// browserLauncherForGOOS returns the helper binary + arg list that opens a
338+
// URL in the user's default browser on the given GOOS. Extracted so the
339+
// per-platform fan-out is testable from a single-OS CI runner (the variant
340+
// not matching runtime.GOOS would otherwise be uncovered, which is what
341+
// our 100%-patch-coverage gate cares about).
338342
//
339-
// The url is validated by safeBrowserURL before being passed to any helper
340-
// binary; a server-controlled URL with a hostile scheme or leading-dash
341-
// payload is refused with a clear stderr message rather than executed.
342-
func openBrowser(rawURL string) {
343+
// nil result means "no known helper for this GOOS"; caller should skip the
344+
// exec attempt and tell the user to open the URL manually.
345+
func browserLauncherForGOOS(goos, safeURL string) (name string, args []string) {
346+
switch goos {
347+
case "darwin":
348+
return "open", []string{safeURL}
349+
case "linux":
350+
return "xdg-open", []string{safeURL}
351+
case "windows":
352+
return "rundll32", []string{"url.dll,FileProtocolHandler", safeURL}
353+
}
354+
return "", nil
355+
}
356+
357+
// openBrowserOn is the GOOS-injectable core of openBrowser; the public
358+
// wrapper passes runtime.GOOS but tests can drive every per-OS branch
359+
// (including the unknown-GOOS fallback and the exec-failure path) from a
360+
// single CI runner. Returns "ok" / "refused" / "no-helper" / "exec-failed"
361+
// so a test can assert outcome without parsing stderr.
362+
func openBrowserOn(goos, rawURL string) string {
343363
safe, verr := safeBrowserURL(rawURL)
344364
if verr != nil {
345365
fmt.Fprintf(os.Stderr, "Refusing to open URL: %v\n", verr)
346-
return
366+
return "refused"
347367
}
348-
var err error
349-
switch runtime.GOOS {
350-
case "darwin":
351-
err = exec.Command("open", safe).Start()
352-
case "linux":
353-
err = exec.Command("xdg-open", safe).Start()
354-
case "windows":
355-
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", safe).Start()
368+
name, args := browserLauncherForGOOS(goos, safe)
369+
if name == "" {
370+
fmt.Fprintf(os.Stderr, "Could not open browser automatically. Visit the URL above manually.\n")
371+
return "no-helper"
356372
}
357-
if err != nil {
373+
if err := exec.Command(name, args...).Start(); err != nil {
358374
fmt.Fprintf(os.Stderr, "Could not open browser automatically. Visit the URL above manually.\n")
375+
return "exec-failed"
359376
}
377+
return "ok"
378+
}
379+
380+
// openBrowser opens url in the user's default browser, best-effort.
381+
//
382+
// The url is validated by safeBrowserURL before being passed to any helper
383+
// binary; a server-controlled URL with a hostile scheme or leading-dash
384+
// payload is refused with a clear stderr message rather than executed.
385+
func openBrowser(rawURL string) {
386+
_ = openBrowserOn(runtime.GOOS, rawURL)
360387
}

cmd/login_safe_url_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,97 @@ func TestOpenBrowser_RefuseDoesNotPanic(t *testing.T) {
6767
openBrowser("javascript:alert(1)")
6868
openBrowser("")
6969
}
70+
71+
// TestOpenBrowser_HappyPathDoesNotPanic hits the real-runtime path on a
72+
// known-good URL on whatever runtime.GOOS the test runs on — we don't
73+
// assert on stderr (the helper may or may not be installed in CI), only
74+
// that the wrapper returns cleanly.
75+
func TestOpenBrowser_HappyPathDoesNotPanic(t *testing.T) {
76+
defer func() {
77+
if r := recover(); r != nil {
78+
t.Fatalf("openBrowser panicked: %v", r)
79+
}
80+
}()
81+
// A reachable-looking URL — the exec may fail in CI (no $DISPLAY,
82+
// no `open`, no `rundll32`), and that's fine: openBrowser is
83+
// best-effort and just writes to stderr.
84+
openBrowser("https://instanode.dev/")
85+
}
86+
87+
// TestOpenBrowserOn_AllBranches drives openBrowserOn across every outcome
88+
// the function can return, satisfying the 100%-patch-coverage gate on a
89+
// single-OS CI runner.
90+
func TestOpenBrowserOn_AllBranches(t *testing.T) {
91+
// Refused (bad URL).
92+
if got := openBrowserOn("linux", "-FattackerPath"); got != "refused" {
93+
t.Errorf("bad URL: got %q, want refused", got)
94+
}
95+
// No helper for the GOOS.
96+
if got := openBrowserOn("plan9", "https://instanode.dev/"); got != "no-helper" {
97+
t.Errorf("unknown GOOS: got %q, want no-helper", got)
98+
}
99+
// Exec-failed: pretend the OS is linux but use a URL that's valid AND
100+
// pass a GOOS string we map to a binary that doesn't exist on PATH.
101+
// browserLauncherForGOOS returns "xdg-open" for linux; in CI it may
102+
// or may not exist. We force the exec-failed branch by mocking via
103+
// the unknown-GOOS path… actually the cleanest forcing function is
104+
// to verify the function returns SOMETHING from the known set on a
105+
// real OS. The "exec-failed" branch is covered indirectly when the
106+
// helper is missing from $PATH on the CI runner. We assert only that
107+
// the return is in the valid set.
108+
got := openBrowserOn("linux", "https://instanode.dev/")
109+
switch got {
110+
case "ok", "exec-failed":
111+
// either is fine — both exercise the launcher path
112+
default:
113+
t.Errorf("real-helper path: got %q, want ok or exec-failed", got)
114+
}
115+
}
116+
117+
// TestOpenBrowserOn_ExecFailedForcedViaWindows forces the exec-failed
118+
// branch on a Linux CI runner by asking for the windows launcher
119+
// ("rundll32") which is guaranteed not to exist on PATH there.
120+
func TestOpenBrowserOn_ExecFailedForced(t *testing.T) {
121+
// On any non-windows host, rundll32 is missing → exec.Start() returns
122+
// ErrNotExist → openBrowserOn returns "exec-failed". On a windows
123+
// host this test will accept "ok" too (no harm).
124+
got := openBrowserOn("windows", "https://instanode.dev/")
125+
if got != "exec-failed" && got != "ok" {
126+
t.Errorf("windows launcher: got %q, want exec-failed (or ok on a real windows runner)", got)
127+
}
128+
}
129+
130+
// TestBrowserLauncherForGOOS asserts the per-OS helper choice across every
131+
// branch (darwin / linux / windows / unknown). Decoupling from runtime.GOOS
132+
// lets a Linux CI runner cover the macOS + Windows + fallback branches too
133+
// — required for the 100%-patch-coverage gate.
134+
func TestBrowserLauncherForGOOS(t *testing.T) {
135+
cases := []struct {
136+
goos string
137+
wantName string
138+
wantArg0 string
139+
}{
140+
{goos: "darwin", wantName: "open", wantArg0: "https://x.example/"},
141+
{goos: "linux", wantName: "xdg-open", wantArg0: "https://x.example/"},
142+
{goos: "windows", wantName: "rundll32", wantArg0: "url.dll,FileProtocolHandler"},
143+
{goos: "plan9", wantName: "", wantArg0: ""},
144+
{goos: "", wantName: "", wantArg0: ""},
145+
}
146+
for _, tc := range cases {
147+
t.Run(tc.goos, func(t *testing.T) {
148+
name, args := browserLauncherForGOOS(tc.goos, "https://x.example/")
149+
if name != tc.wantName {
150+
t.Fatalf("name = %q, want %q", name, tc.wantName)
151+
}
152+
if name == "" {
153+
if args != nil {
154+
t.Fatalf("args should be nil on unknown GOOS, got %v", args)
155+
}
156+
return
157+
}
158+
if len(args) == 0 || args[0] != tc.wantArg0 {
159+
t.Fatalf("args[0] = %v, want %q", args, tc.wantArg0)
160+
}
161+
})
162+
}
163+
}

internal/tokens/store_atomic_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,37 @@ func TestSave_AtomicNoTempLeak(t *testing.T) {
2727
}
2828
}
2929

30+
// TestSave_RenameFailureReturnsError exercises the rename-failure branch.
31+
// Pointing the store at a path whose parent directory is a regular file
32+
// makes os.WriteFile of the .tmp succeed (we're writing inside the parent
33+
// of `path`, which is the temp dir) BUT — wait, no: WriteFile would fail
34+
// upstream. To hit the rename-failure branch specifically we craft a path
35+
// where the .tmp can be written but the rename target is a directory: on
36+
// POSIX, os.Rename(file, existingDir) returns ENOTEMPTY / EISDIR, which is
37+
// exactly the failure mode our cleanup branch handles.
38+
func TestSave_RenameFailureReturnsError(t *testing.T) {
39+
dir := setupTempHome(t)
40+
storePath := filepath.Join(dir, ".instant-tokens")
41+
// Make storePath a non-empty directory so os.Rename(tmp, storePath)
42+
// returns an error and we exercise the cleanup branch.
43+
if err := os.Mkdir(storePath, 0700); err != nil {
44+
t.Fatalf("mkdir: %v", err)
45+
}
46+
if err := os.WriteFile(filepath.Join(storePath, "block"), []byte("x"), 0600); err != nil {
47+
t.Fatalf("seed inside dir: %v", err)
48+
}
49+
50+
s := &Store{path: storePath}
51+
err := s.Add(Entry{Token: "tok-rename-fail", Name: "x", Type: "postgres", URL: "postgres://x"})
52+
if err == nil {
53+
t.Fatalf("expected Save to return error when target path is a non-empty dir")
54+
}
55+
// The .tmp sibling MUST be cleaned up by the failure-cleanup branch.
56+
if _, statErr := os.Stat(storePath + ".tmp"); !os.IsNotExist(statErr) {
57+
t.Errorf(".tmp file should have been removed after rename failure, got err=%v", statErr)
58+
}
59+
}
60+
3061
// TestSave_RenameOverwritesExistingFile verifies the rename idiom replaces
3162
// an existing file (not appended). Cross-platform — works on Linux + macOS.
3263
func TestSave_RenameOverwritesExistingFile(t *testing.T) {

0 commit comments

Comments
 (0)