rickub/clipublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/rickub/cli.git
git clone ssh://git@rickub.com/rickub/cli.git
config_test.go · 203 lines · 5.7 KBGo Blame HistoryRaw
Initial import of the rickub CLI as a standalone public project 1a1d430 Olivier Girardot 10h ago1package config
2
3import (
4 "bytes"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9)
10
11func TestSaveLoadRoundTrip(t *testing.T) {
12 dir := t.TempDir()
13 path := filepath.Join(dir, "config.yaml")
14
15 in := &Config{}
16 in.SetToken("http://localhost:3998", "rickub_pat_abc")
17 if err := in.SaveTo(path); err != nil {
18 t.Fatalf("SaveTo: %v", err)
19 }
20
21 // File must be 0600.
22 info, err := os.Stat(path)
23 if err != nil {
24 t.Fatalf("stat: %v", err)
25 }
26 if perm := info.Mode().Perm(); perm != 0o600 {
27 t.Errorf("perm = %o, want 600", perm)
28 }
29
30 out, err := LoadFrom(path)
31 if err != nil {
32 t.Fatalf("LoadFrom: %v", err)
33 }
34 if out.Host != "http://localhost:3998" {
35 t.Errorf("host round-trip: got %q", out.Host)
36 }
37 if got := out.TokenFor("http://localhost:3998"); got != "rickub_pat_abc" {
38 t.Errorf("token round-trip: got %q", got)
39 }
40}
41
42func TestLoadMissingIsEmpty(t *testing.T) {
43 out, err := LoadFrom(filepath.Join(t.TempDir(), "nope.yaml"))
44 if err != nil {
45 t.Fatalf("LoadFrom missing: %v", err)
46 }
47 if out.Host != "" || len(out.Hosts) != 0 {
48 t.Errorf("expected empty config, got %+v", out)
49 }
50}
51
52func TestResolveHostPrecedence(t *testing.T) {
53 cfg := &Config{Host: "http://config-host"}
54
55 // config only
56 t.Setenv(EnvHost, "")
57 if got := ResolveHost("", cfg); got != "http://config-host" {
58 t.Errorf("config host: got %q", got)
59 }
60 // env overrides config
61 t.Setenv(EnvHost, "http://env-host/")
62 if got := ResolveHost("", cfg); got != "http://env-host" {
63 t.Errorf("env host (trailing slash trimmed): got %q", got)
64 }
65 // flag overrides env
66 if got := ResolveHost("http://flag-host", cfg); got != "http://flag-host" {
67 t.Errorf("flag host: got %q", got)
68 }
69 // default when nothing set
70 t.Setenv(EnvHost, "")
71 if got := ResolveHost("", &Config{}); got != DefaultHost {
72 t.Errorf("default host: got %q", got)
73 }
74}
75
76func TestResolveTokenPrecedence(t *testing.T) {
77 const host = "https://rickub.com"
78 cfg := &Config{}
79 cfg.SetToken(host, "cfg-token")
80
81 t.Setenv(EnvToken, "")
82 if got := ResolveToken("", cfg, host); got != "cfg-token" {
83 t.Errorf("config token: got %q", got)
84 }
85 t.Setenv(EnvToken, "env-token")
86 if got := ResolveToken("", cfg, host); got != "env-token" {
87 t.Errorf("env token: got %q", got)
88 }
89 if got := ResolveToken("flag-token", cfg, host); got != "flag-token" {
90 t.Errorf("flag token: got %q", got)
91 }
92}
93
94// The stored token must never follow the host around: this is the leak the
95// per-host binding exists to prevent.
96func TestResolveTokenIsBoundToItsHost(t *testing.T) {
97 t.Setenv(EnvToken, "")
98 cfg := &Config{}
99 cfg.SetToken("https://rickub.com", "rickub_pat_prod")
100
101 if got := ResolveToken("", cfg, "https://evil.example"); got != "" {
102 t.Errorf("stored token leaked to another host: got %q", got)
103 }
104 if got := ResolveToken("", cfg, "http://localhost:3000"); got != "" {
105 t.Errorf("stored token leaked to a local host: got %q", got)
106 }
107 if got := ResolveToken("", cfg, "https://rickub.com"); got != "rickub_pat_prod" {
108 t.Errorf("token not returned for its own host: got %q", got)
109 }
110 // An explicit token is the caller's own choice and works anywhere.
111 if got := ResolveToken("explicit", cfg, "https://evil.example"); got != "explicit" {
112 t.Errorf("explicit token: got %q", got)
113 }
114 t.Setenv(EnvToken, "env-token")
115 if got := ResolveToken("", cfg, "https://evil.example"); got != "env-token" {
116 t.Errorf("env token: got %q", got)
117 }
118}
119
120func TestMultipleHostsKeepSeparateTokens(t *testing.T) {
121 cfg := &Config{}
122 cfg.SetToken("https://rickub.com", "prod")
123 cfg.SetToken("http://localhost:3000/", "dev")
124
125 if got := cfg.TokenFor("https://rickub.com"); got != "prod" {
126 t.Errorf("prod token: got %q", got)
127 }
128 if got := cfg.TokenFor("http://localhost:3000"); got != "dev" {
129 t.Errorf("dev token: got %q", got)
130 }
131 // SetToken makes the host it saved active.
132 if cfg.Host != "http://localhost:3000" {
133 t.Errorf("active host: got %q", cfg.Host)
134 }
135
136 // Logout only affects the named host.
137 if !cfg.ClearToken("http://localhost:3000") {
138 t.Error("ClearToken reported nothing removed")
139 }
140 if got := cfg.TokenFor("http://localhost:3000"); got != "" {
141 t.Errorf("dev token survived logout: got %q", got)
142 }
143 if got := cfg.TokenFor("https://rickub.com"); got != "prod" {
144 t.Errorf("prod token removed by dev logout: got %q", got)
145 }
146 if cfg.ClearToken("http://localhost:3000") {
147 t.Error("ClearToken reported a removal on an empty host")
148 }
149}
150
151func TestNormalizeHost(t *testing.T) {
152 cases := map[string]string{
153 "https://RickUB.com/": "https://rickub.com",
154 "HTTPS://rickub.com": "https://rickub.com",
155 " https://rickub.com ": "https://rickub.com",
156 "http://localhost:3000": "http://localhost:3000",
157 "": "",
158 }
159 for in, want := range cases {
160 if got := NormalizeHost(in); got != want {
161 t.Errorf("NormalizeHost(%q) = %q, want %q", in, got, want)
162 }
163 }
164}
165
166func TestInsecureHostDetection(t *testing.T) {
167 insecure := []string{"http://evil.example", "http://192.0.2.10:3000", "HTTP://Evil.Example/"}
168 for _, h := range insecure {
169 if !IsInsecureHost(h) {
170 t.Errorf("IsInsecureHost(%q) = false, want true", h)
171 }
172 }
173 secure := []string{
174 "https://rickub.com",
175 "http://localhost:3000",
176 "http://127.0.0.1:3000",
177 "http://[::1]:3000",
178 "",
179 }
180 for _, h := range secure {
181 if IsInsecureHost(h) {
182 t.Errorf("IsInsecureHost(%q) = true, want false", h)
183 }
184 }
185}
186
187func TestWarnIfInsecure(t *testing.T) {
188 var buf bytes.Buffer
189 if !WarnIfInsecure(&buf, "http://evil.example") {
190 t.Fatal("expected a warning for a plain-HTTP remote host")
191 }
192 if !strings.Contains(buf.String(), "evil.example") {
193 t.Errorf("warning does not name the host: %q", buf.String())
194 }
195
196 buf.Reset()
197 if WarnIfInsecure(&buf, "http://localhost:3000") {
198 t.Error("warned about loopback")
199 }
200 if buf.Len() != 0 {
201 t.Errorf("unexpected output: %q", buf.String())
202 }
203}