1package main
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8)
9
10func TestConfigEnvironmentExpansionWithProjectError(t *testing.T) {
11 for _, kind := range []string{"malformed", "unreadable"} {
12 t.Run(kind, func(t *testing.T) {
13 t.Setenv("ZOT_HOME", t.TempDir())
14 t.Setenv("MCP_TEST_VALUE", "synthetic-command")
15 const absent = "MCP_TEST_PROJECT_ERROR_UNSET"
16 // Setenv restores the original value after the test, including absence.
17 t.Setenv(absent, "")
18 if err := os.Unsetenv(absent); err != nil {
19 t.Fatal(err)
20 }
21 writeJSON(t, filepath.Join(os.Getenv("ZOT_HOME"), "mcp.json"), `{"mcpServers":{
22 "valid":{"command":"${MCP_TEST_VALUE}"},
23 "invalid":{"headers":{"Authorization":"secret-prefix ${MCP_TEST_PROJECT_ERROR_UNSET}"}}
24 }}`)
25 project := t.TempDir()
26 path := filepath.Join(project, ".zot", "mcp.json")
27 if kind == "malformed" {
28 writeJSON(t, path, `{`)
29 } else if err := os.MkdirAll(path, 0o755); err != nil {
30 // A directory produces a read error even when tests run as root.
31 t.Fatal(err)
32 }
33 cfg, err := loadConfig(project)
34 if err == nil {
35 t.Fatal("expected config error")
36 }
37 for _, want := range []string{"project config", path, "invalid", absent} {
38 if !strings.Contains(err.Error(), want) {
39 t.Errorf("diagnostic missing %q: %v", want, err)
40 }
41 }
42 if strings.Contains(err.Error(), "secret-prefix") {
43 t.Error("diagnostic exposed field value")
44 }
45 if got := cfg.MCPServers["valid"].Command; got != "synthetic-command" {
46 t.Errorf("global command = %q, want synthetic-command", got)
47 }
48 if _, ok := cfg.MCPServers["invalid"]; ok {
49 t.Error("server with missing variable must not be retained")
50 }
51 })
52 }
53}
54
55func TestConfigEnvironmentExpansion(t *testing.T) {
56 t.Setenv("ZOT_HOME", t.TempDir())
57 t.Setenv("MCP_TEST_VALUE", "synthetic\"\\token")
58 t.Setenv("MCP_TEST_EMPTY", "")
59 t.Setenv("MCP_TEST_LITERAL", "${MCP_TEST_VALUE}")
60 const absent = "MCP_TEST_UNSET"
61 old, set := os.LookupEnv(absent)
62 os.Unsetenv(absent)
63 t.Cleanup(func() {
64 if set {
65 os.Setenv(absent, old)
66 } else {
67 os.Unsetenv(absent)
68 }
69 })
70 project := t.TempDir()
71 writeJSON(t, filepath.Join(os.Getenv("ZOT_HOME"), "mcp.json"), `{"mcpServers":{"override":{"command":"${MCP_TEST_UNSET}"}}}`)
72 writeJSON(t, filepath.Join(project, ".zot", "mcp.json"), `{"mcpServers":{
73 "override":{"command":"ok"},
74 "valid":{"command":"${MCP_TEST_VALUE}","args":["${MCP_TEST_UNSET:-fallback}","${MCP_TEST_EMPTY:-fallback}","$HOME","${MCP_TEST_LITERAL}","${MCP_TEST_UNSET:-}"],"env":{"TOKEN":"${MCP_TEST_VALUE}"},"url":"${MCP_TEST_UNSET:-https://example.test}/mcp","headers":{"Authorization":"Bearer ${MCP_TEST_VALUE}"}},
75 "invalid":{"headers":{"Authorization":"secret-prefix ${MCP_TEST_UNSET}"}}
76 }}`)
77 cfg, err := loadConfig(project)
78 if err == nil || !strings.Contains(err.Error(), absent) || !strings.Contains(err.Error(), "invalid") || strings.Contains(err.Error(), "secret-prefix") {
79 t.Fatalf("unexpected diagnostic: %v", err)
80 }
81 if _, ok := cfg.MCPServers["invalid"]; ok {
82 t.Fatal("invalid server must not be started")
83 }
84 if cfg.MCPServers["override"].Command != "ok" {
85 t.Fatal("expand after merging")
86 }
87 s := cfg.MCPServers["valid"]
88 if s.Command != os.Getenv("MCP_TEST_VALUE") || s.Env["TOKEN"] != s.Command || s.Headers["Authorization"] != "Bearer "+s.Command {
89 t.Fatal("command/env/header expansion failed")
90 }
91 if s.URL != "https://example.test/mcp" || strings.Join(s.Args, "|") != "fallback||$HOME|${MCP_TEST_VALUE}|" {
92 t.Fatalf("unexpected URL/args: %s %v", s.URL, s.Args)
93 }
94}