config_test.go

  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}