1package auth
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "strings"
8 "testing"
9 "time"
10)
11
12func TestAPIKeyCommandHelperProcess(t *testing.T) {
13 if os.Getenv("ZUT_API_KEY_COMMAND_HELPER") != "1" {
14 return
15 }
16 args := os.Args
17 for len(args) > 0 && args[0] != "--" {
18 args = args[1:]
19 }
20 if len(args) < 2 {
21 os.Exit(2)
22 }
23 switch args[1] {
24 case "print":
25 fmt.Print(os.Getenv("ZUT_API_KEY_COMMAND_VALUE"))
26 case "fail":
27 fmt.Fprint(os.Stderr, os.Getenv("ZUT_API_KEY_COMMAND_VALUE"))
28 os.Exit(1)
29 case "sleep":
30 time.Sleep(5 * time.Second)
31 case "large":
32 fmt.Print(strings.Repeat("x", maxAPIKeyCommandOutput+1))
33 case "count":
34 if len(args) < 3 {
35 os.Exit(2)
36 }
37 f, err := os.OpenFile(args[2], os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
38 if err != nil {
39 os.Exit(3)
40 }
41 _, _ = f.WriteString("run\n")
42 _ = f.Close()
43 fmt.Print("cached-key\n")
44 default:
45 os.Exit(2)
46 }
47 os.Exit(0)
48}
49
50func apiKeyHelperCommand(mode string, extra ...string) APIKeyCommand {
51 args := []string{"-test.run=^TestAPIKeyCommandHelperProcess$", "--", mode}
52 args = append(args, extra...)
53 return APIKeyCommand{Program: os.Args[0], Args: args}
54}
55
56func resetAPIKeyCommandCache(t *testing.T) {
57 t.Helper()
58 apiKeyCommandCache.Lock()
59 oldValues := apiKeyCommandCache.values
60 oldInflight := apiKeyCommandCache.inflight
61 apiKeyCommandCache.values = make(map[[32]byte]string)
62 apiKeyCommandCache.inflight = make(map[[32]byte]*apiKeyCommandCall)
63 apiKeyCommandCache.Unlock()
64 t.Cleanup(func() {
65 apiKeyCommandCache.Lock()
66 apiKeyCommandCache.values = oldValues
67 apiKeyCommandCache.inflight = oldInflight
68 apiKeyCommandCache.Unlock()
69 })
70}
71
72func TestResolveAPIKeyCommand(t *testing.T) {
73 resetAPIKeyCommandCache(t)
74 t.Setenv("ZUT_API_KEY_COMMAND_HELPER", "1")
75 t.Setenv("ZUT_API_KEY_COMMAND_VALUE", "secret-value\r\n")
76
77 got, err := ResolveAPIKeyCommand(context.Background(), apiKeyHelperCommand("print"))
78 if err != nil {
79 t.Fatal(err)
80 }
81 if got != "secret-value" {
82 t.Fatalf("ResolveAPIKeyCommand() = %q, want %q", got, "secret-value")
83 }
84}
85
86func TestResolveAPIKeyCommandCachesSuccess(t *testing.T) {
87 resetAPIKeyCommandCache(t)
88 t.Setenv("ZUT_API_KEY_COMMAND_HELPER", "1")
89 countPath := t.TempDir() + string(os.PathSeparator) + "count"
90 command := apiKeyHelperCommand("count", countPath)
91
92 for range 2 {
93 got, err := ResolveAPIKeyCommand(context.Background(), command)
94 if err != nil {
95 t.Fatal(err)
96 }
97 if got != "cached-key" {
98 t.Fatalf("got %q", got)
99 }
100 }
101 contents, err := os.ReadFile(countPath)
102 if err != nil {
103 t.Fatal(err)
104 }
105 if got := strings.Count(string(contents), "run\n"); got != 1 {
106 t.Fatalf("command ran %d times, want 1", got)
107 }
108}
109
110func TestResolveAPIKeyCommandFailureDoesNotExposeOutput(t *testing.T) {
111 resetAPIKeyCommandCache(t)
112 t.Setenv("ZUT_API_KEY_COMMAND_HELPER", "1")
113 t.Setenv("ZUT_API_KEY_COMMAND_VALUE", "must-not-appear")
114
115 _, err := ResolveAPIKeyCommand(context.Background(), apiKeyHelperCommand("fail"))
116 if err == nil {
117 t.Fatal("expected an error")
118 }
119 if strings.Contains(err.Error(), "must-not-appear") {
120 t.Fatalf("error exposed command output: %v", err)
121 }
122}
123
124func TestResolveAPIKeyCommandTimeoutAndOutputLimit(t *testing.T) {
125 resetAPIKeyCommandCache(t)
126 t.Setenv("ZUT_API_KEY_COMMAND_HELPER", "1")
127
128 t.Run("timeout", func(t *testing.T) {
129 command := apiKeyHelperCommand("sleep")
130 command.TimeoutMS = 20
131 if _, err := ResolveAPIKeyCommand(context.Background(), command); err == nil || !strings.Contains(err.Error(), "timed out") {
132 t.Fatalf("error = %v, want timeout", err)
133 }
134 })
135 t.Run("output limit", func(t *testing.T) {
136 if _, err := ResolveAPIKeyCommand(context.Background(), apiKeyHelperCommand("large")); err == nil || !strings.Contains(err.Error(), "exceeds") {
137 t.Fatalf("error = %v, want output limit", err)
138 }
139 })
140}