| Initial import of the rickub CLI as a standalone public project 1a1d430 Olivier Girardot 10h ago | 1 | package config |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | func 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 | |
| 42 | func 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 | |
| 52 | func 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 | |
| 76 | func 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. |
| 96 | func 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 | |
| 120 | func 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 | |
| 151 | func 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 | |
| 166 | func 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 | |
| 187 | func 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 | } |