1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: LicenseRef-MutuaL-1.2
4
5package appconfig
6
7import (
8 "context"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13
14 "git.secluded.site/cooked-mcp/internal/configvalue"
15)
16
17func TestLoadACIDAuthenticationCredentials21ResolvesTOMLConfigValues(t *testing.T) {
18 t.Setenv("COOKED_USERNAME_FROM_MY_SHELL", "shell-user")
19
20 configPath := filepath.Join(t.TempDir(), "config.toml")
21 writeConfig(t, configPath, `[user]
22name = "$COOKED_USERNAME_FROM_MY_SHELL"
23password = "!printf shell-password"
24`)
25
26 got, err := Load(context.Background(), configPath, configvalue.NewResolver())
27 if err != nil {
28 t.Fatalf("Load() error = %v", err)
29 }
30
31 if got.Username != "shell-user" {
32 t.Fatalf("Username = %q, want shell-user", got.Username)
33 }
34 if got.Password != "shell-password" {
35 t.Fatalf("Password = %q, want shell-password", got.Password)
36 }
37}
38
39func TestLoadACIDAuthenticationCredentials11EnvironmentOverridesTOML(t *testing.T) {
40 t.Setenv("COOKED_LOGIN_USERNAME", "override-user")
41 t.Setenv("COOKED_LOGIN_PASSWORD", "override-password")
42
43 configPath := filepath.Join(t.TempDir(), "config.toml")
44 writeConfig(t, configPath, `[user]
45name = "toml-user"
46password = "toml-password"
47`)
48
49 got, err := Load(context.Background(), configPath, configvalue.NewResolver())
50 if err != nil {
51 t.Fatalf("Load() error = %v", err)
52 }
53
54 if got.Username != "override-user" {
55 t.Fatalf("Username = %q, want override-user", got.Username)
56 }
57 if got.Password != "override-password" {
58 t.Fatalf("Password = %q, want override-password", got.Password)
59 }
60}
61
62func TestLoadDefaultsHTTPAddr(t *testing.T) {
63 configPath := filepath.Join(t.TempDir(), "config.toml")
64 writeConfig(t, configPath, `[user]
65name = "toml-user"
66password = "toml-password"
67`)
68
69 got, err := Load(context.Background(), configPath, configvalue.NewResolver())
70 if err != nil {
71 t.Fatalf("Load() error = %v", err)
72 }
73
74 if got.HTTPAddr != "127.0.0.1:8123" {
75 t.Fatalf("HTTPAddr = %q, want default loopback address", got.HTTPAddr)
76 }
77}
78
79// server.CONFIG.8-2
80func TestLoadACIDServerConfig8_2LoadsTOMLHTTPAddr(t *testing.T) {
81 configPath := filepath.Join(t.TempDir(), "config.toml")
82 writeConfig(t, configPath, `[user]
83name = "toml-user"
84password = "toml-password"
85
86[mcp]
87http_addr = "127.0.0.1:9000"
88`)
89
90 got, err := Load(context.Background(), configPath, configvalue.NewResolver())
91 if err != nil {
92 t.Fatalf("Load() error = %v", err)
93 }
94
95 if got.HTTPAddr != "127.0.0.1:9000" {
96 t.Fatalf("HTTPAddr = %q, want TOML address", got.HTTPAddr)
97 }
98}
99
100// server.CONFIG.3 server.CONFIG.8-1
101func TestLoadACIDServerConfig3And8_1EnvironmentOverridesTOMLHTTPAddr(t *testing.T) {
102 t.Setenv("COOKED_MCP_HTTP_ADDR", "127.0.0.1:9001")
103
104 configPath := filepath.Join(t.TempDir(), "config.toml")
105 writeConfig(t, configPath, `[user]
106name = "toml-user"
107password = "toml-password"
108
109[mcp]
110http_addr = "127.0.0.1:9000"
111`)
112
113 got, err := Load(context.Background(), configPath, configvalue.NewResolver())
114 if err != nil {
115 t.Fatalf("Load() error = %v", err)
116 }
117
118 if got.HTTPAddr != "127.0.0.1:9001" {
119 t.Fatalf("HTTPAddr = %q, want environment address", got.HTTPAddr)
120 }
121}
122
123// server.SECURITY.4 server.SECURITY.9
124func TestLoadACIDServerSecurity4And9EnvironmentOverridesTOMLHTTPToken(t *testing.T) {
125 t.Setenv("COOKED_MCP_HTTP_TOKEN", "env-token")
126
127 configPath := filepath.Join(t.TempDir(), "config.toml")
128 writeConfig(t, configPath, `[user]
129name = "toml-user"
130password = "toml-password"
131
132[mcp]
133http_token = "toml-token"
134`)
135
136 got, err := Load(context.Background(), configPath, configvalue.NewResolver())
137 if err != nil {
138 t.Fatalf("Load() error = %v", err)
139 }
140
141 if got.HTTPToken != "env-token" {
142 t.Fatalf("HTTPToken = %q, want environment token", got.HTTPToken)
143 }
144}
145
146// server.SECURITY.5 server.SECURITY.10 server.SECURITY.12 server.SECURITY.13
147func TestLoadACIDServerSecurity5And10ResolvesTOMLHTTPTokenConfigValue(t *testing.T) {
148 t.Setenv("TEST_HTTP_TOKEN", "shell-token")
149
150 configPath := filepath.Join(t.TempDir(), "config.toml")
151 writeConfig(t, configPath, `[user]
152name = "toml-user"
153password = "toml-password"
154
155[mcp]
156http_token = "$TEST_HTTP_TOKEN"
157`)
158
159 got, err := Load(context.Background(), configPath, configvalue.NewResolver())
160 if err != nil {
161 t.Fatalf("Load() error = %v", err)
162 }
163
164 if got.HTTPToken != "shell-token" {
165 t.Fatalf("HTTPToken = %q, want resolved TOML token", got.HTTPToken)
166 }
167}
168
169// server.SECURITY.11
170func TestLoadACIDServerSecurity11RejectsEmptyEnvironmentHTTPToken(t *testing.T) {
171 t.Setenv("COOKED_MCP_HTTP_TOKEN", "")
172
173 configPath := filepath.Join(t.TempDir(), "config.toml")
174 writeConfig(t, configPath, `[user]
175name = "toml-user"
176password = "toml-password"
177`)
178
179 _, err := Load(context.Background(), configPath, configvalue.NewResolver())
180 if err == nil {
181 t.Fatal("Load() error = nil, want empty HTTP token error")
182 }
183 if !strings.Contains(err.Error(), "MCP HTTP token must not be empty") {
184 t.Fatalf("Load() error = %v, want empty HTTP token error", err)
185 }
186}
187
188// server.SECURITY.11
189func TestLoadACIDServerSecurity11RejectsEmptyTOMLHTTPToken(t *testing.T) {
190 configPath := filepath.Join(t.TempDir(), "config.toml")
191 writeConfig(t, configPath, `[user]
192name = "toml-user"
193password = "toml-password"
194
195[mcp]
196http_token = ""
197`)
198
199 _, err := Load(context.Background(), configPath, configvalue.NewResolver())
200 if err == nil {
201 t.Fatal("Load() error = nil, want empty HTTP token error")
202 }
203 if !strings.Contains(err.Error(), "MCP HTTP token must not be empty") {
204 t.Fatalf("Load() error = %v, want empty HTTP token error", err)
205 }
206}
207
208func TestLoadRejectsMissingExplicitConfigFile(t *testing.T) {
209 _, err := Load(context.Background(), filepath.Join(t.TempDir(), "missing.toml"), configvalue.NewResolver())
210 if err == nil {
211 t.Fatal("Load() error = nil, want missing config error")
212 }
213}
214
215func TestLoadACIDServerConfig72RejectsMissingDefaultConfigHome(t *testing.T) {
216 t.Setenv("XDG_CONFIG_HOME", "")
217
218 originalUserHomeDir := userHomeDir
219 userHomeDir = func() (string, error) {
220 return "", os.ErrNotExist
221 }
222 t.Cleanup(func() {
223 userHomeDir = originalUserHomeDir
224 })
225
226 _, err := Load(context.Background(), "", configvalue.NewResolver())
227 if err == nil {
228 t.Fatal("Load() error = nil, want default config path error")
229 }
230 if !strings.Contains(err.Error(), "resolve default config path") {
231 t.Fatalf("Load() error = %v, want default config path error", err)
232 }
233}
234
235func TestLoadRejectsNonLocalHTTPBaseURL(t *testing.T) {
236 configPath := filepath.Join(t.TempDir(), "config.toml")
237 writeConfig(t, configPath, `[user]
238name = "toml-user"
239password = "toml-password"
240
241[cooked]
242base_url = "http://example.com"
243`)
244
245 _, err := Load(context.Background(), configPath, configvalue.NewResolver())
246 if err == nil {
247 t.Fatal("Load() error = nil, want non-local HTTP base URL error")
248 }
249 if !strings.Contains(err.Error(), "must use https") {
250 t.Fatalf("Load() error = %v, want https error", err)
251 }
252}
253
254func writeConfig(t *testing.T, path, content string) {
255 t.Helper()
256
257 if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
258 t.Fatalf("write config: %v", err)
259 }
260}