config_test.go
8077 bytes
1package config
2
3import (
4 "os"
5 "strings"
6 "testing"
7
8 "github.com/charmbracelet/soft-serve/pkg/access"
9 "github.com/matryer/is"
10)
11
12func TestParseMultipleKeys(t *testing.T) {
13 is := is.New(t)
14 td := t.TempDir()
15 is.NoErr(os.Setenv("SOFT_SERVE_INITIAL_ADMIN_KEYS", "testdata/k1.pub\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxIobhwtfdwN7m1TFt9wx3PsfvcAkISGPxmbmbauST8 a@b"))
16 is.NoErr(os.Setenv("SOFT_SERVE_DATA_PATH", td))
17 t.Cleanup(func() {
18 is.NoErr(os.Unsetenv("SOFT_SERVE_INITIAL_ADMIN_KEYS"))
19 is.NoErr(os.Unsetenv("SOFT_SERVE_DATA_PATH"))
20 })
21 cfg := DefaultConfig()
22 is.NoErr(cfg.ParseEnv())
23 is.Equal(cfg.InitialAdminKeys, []string{
24 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINMwLvyV3ouVrTysUYGoJdl5Vgn5BACKov+n9PlzfPwH",
25 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxIobhwtfdwN7m1TFt9wx3PsfvcAkISGPxmbmbauST8",
26 })
27}
28
29func TestMergeInitAdminKeys(t *testing.T) {
30 is := is.New(t)
31 is.NoErr(os.Setenv("SOFT_SERVE_INITIAL_ADMIN_KEYS", "testdata/k1.pub"))
32 t.Cleanup(func() { is.NoErr(os.Unsetenv("SOFT_SERVE_INITIAL_ADMIN_KEYS")) })
33 cfg := &Config{
34 DataPath: t.TempDir(),
35 InitialAdminKeys: []string{"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxIobhwtfdwN7m1TFt9wx3PsfvcAkISGPxmbmbauST8 a@b"},
36 }
37 is.NoErr(cfg.WriteConfig())
38 is.NoErr(cfg.Parse())
39 is.Equal(cfg.InitialAdminKeys, []string{
40 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINMwLvyV3ouVrTysUYGoJdl5Vgn5BACKov+n9PlzfPwH",
41 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxIobhwtfdwN7m1TFt9wx3PsfvcAkISGPxmbmbauST8",
42 })
43}
44
45func TestValidateInitAdminKeys(t *testing.T) {
46 is := is.New(t)
47 cfg := &Config{
48 DataPath: t.TempDir(),
49 InitialAdminKeys: []string{
50 "testdata/k1.pub",
51 "abc",
52 "",
53 },
54 }
55 is.NoErr(cfg.WriteConfig())
56 is.NoErr(cfg.Parse())
57 is.Equal(cfg.InitialAdminKeys, []string{
58 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINMwLvyV3ouVrTysUYGoJdl5Vgn5BACKov+n9PlzfPwH",
59 })
60}
61
62func TestCustomConfigLocation(t *testing.T) {
63 is := is.New(t)
64 td := t.TempDir()
65 t.Cleanup(func() {
66 is.NoErr(os.Unsetenv("SOFT_SERVE_CONFIG_LOCATION"))
67 })
68
69 // Test that we get data from the custom file location, and not from the data dir.
70 is.NoErr(os.Setenv("SOFT_SERVE_CONFIG_LOCATION", "testdata/config.yaml"))
71 is.NoErr(os.Setenv("SOFT_SERVE_DATA_PATH", td))
72 cfg := DefaultConfig()
73 is.NoErr(cfg.Parse())
74 is.Equal(cfg.Name, "Test server name")
75 // If we unset the custom location, then use the default location.
76 is.NoErr(os.Unsetenv("SOFT_SERVE_CONFIG_LOCATION"))
77 cfg = DefaultConfig()
78 is.Equal(cfg.Name, "Soft Serve")
79 // Test that if the custom config location doesn't exist, default to datapath config.
80 is.NoErr(os.Setenv("SOFT_SERVE_CONFIG_LOCATION", "testdata/config_nonexistent.yaml"))
81 cfg = DefaultConfig()
82 is.Equal(cfg.Name, "Soft Serve")
83}
84
85func TestDefaultRepoDisabledByDefault(t *testing.T) {
86 is := is.New(t)
87 is.Equal(DefaultConfig().DefaultRepo, "")
88}
89
90func TestWebUIDisabledByDefault(t *testing.T) {
91 is := is.New(t)
92 is.Equal(DefaultConfig().HTTP.WebUI.Enabled, false)
93}
94
95func TestParseWebUIEnabled(t *testing.T) {
96 is := is.New(t)
97
98 cfg := DefaultConfig()
99 path := t.TempDir() + "/config.yaml"
100 is.NoErr(os.WriteFile(path, []byte("http:\n web_ui:\n enabled: true\n build_status_url: http://builder:8080\n"), 0o600))
101 is.NoErr(parseFile(cfg, path))
102 is.Equal(cfg.HTTP.WebUI.Enabled, true)
103 is.Equal(cfg.HTTP.WebUI.BuildStatusURL, "http://builder:8080")
104
105 is.NoErr(os.Setenv("SOFT_SERVE_HTTP_WEB_UI_ENABLED", "true"))
106 t.Cleanup(func() {
107 is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_WEB_UI_ENABLED"))
108 is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_WEB_UI_BUILD_STATUS_URL"))
109 })
110 cfg = DefaultConfig()
111 is.NoErr(cfg.ParseEnv())
112 is.Equal(cfg.HTTP.WebUI.Enabled, true)
113
114 is.NoErr(os.Setenv("SOFT_SERVE_HTTP_WEB_UI_BUILD_STATUS_URL", "http://builder:8080"))
115 cfg = DefaultConfig()
116 is.NoErr(cfg.ParseEnv())
117 is.Equal(cfg.HTTP.WebUI.BuildStatusURL, "http://builder:8080")
118}
119
120func TestWebUIEnviron(t *testing.T) {
121 cfg := DefaultConfig()
122 cfg.HTTP.WebUI.Enabled = true
123 cfg.HTTP.WebUI.BuildStatusURL = "http://builder:8080"
124 environment := strings.Join(cfg.Environ(), "\n")
125 if !strings.Contains(environment, "SOFT_SERVE_HTTP_WEB_UI_ENABLED=true") {
126 t.Error("Config.Environ() did not include SOFT_SERVE_HTTP_WEB_UI_ENABLED=true")
127 }
128 if !strings.Contains(environment, "SOFT_SERVE_HTTP_WEB_UI_BUILD_STATUS_URL=http://builder:8080") {
129 t.Error("Config.Environ() did not include SOFT_SERVE_HTTP_WEB_UI_BUILD_STATUS_URL")
130 }
131}
132
133func TestParseDefaultRepo(t *testing.T) {
134 is := is.New(t)
135 is.NoErr(os.Setenv("SOFT_SERVE_DEFAULT_REPO", "gitops"))
136 t.Cleanup(func() {
137 is.NoErr(os.Unsetenv("SOFT_SERVE_DEFAULT_REPO"))
138 })
139 cfg := DefaultConfig()
140 is.NoErr(cfg.ParseEnv())
141 is.Equal(cfg.DefaultRepo, "gitops")
142}
143
144func TestParseDefaultRepoFromFile(t *testing.T) {
145 is := is.New(t)
146 cfg := &Config{DataPath: t.TempDir()}
147 is.NoErr(parseFile(cfg, "testdata/config_default_repo.yaml"))
148 is.Equal(cfg.DefaultRepo, "gitops")
149}
150
151func TestParseMultipleHeaders(t *testing.T) {
152 is := is.New(t)
153 is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS", "Accept,Accept-Language,User-Agent"))
154 t.Cleanup(func() {
155 is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS"))
156 })
157 cfg := DefaultConfig()
158 is.NoErr(cfg.ParseEnv())
159 is.Equal(cfg.HTTP.CORS.AllowedHeaders, []string{
160 "Accept",
161 "Accept-Language",
162 "User-Agent",
163 })
164}
165
166func TestParseMultipleOrigins(t *testing.T) {
167 is := is.New(t)
168 is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS", "http://example.com,https://example.com"))
169 t.Cleanup(func() {
170 is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS"))
171 })
172 cfg := DefaultConfig()
173 is.NoErr(cfg.ParseEnv())
174 is.Equal(cfg.HTTP.CORS.AllowedOrigins, []string{
175 "http://localhost:23232",
176 "http://example.com",
177 "https://example.com",
178 })
179}
180
181func TestParseMultipleMethods(t *testing.T) {
182 is := is.New(t)
183 is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS", "GET,POST,PUT"))
184 t.Cleanup(func() {
185 is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS"))
186 })
187 cfg := DefaultConfig()
188 is.NoErr(cfg.ParseEnv())
189 is.Equal(cfg.HTTP.CORS.AllowedMethods, []string{
190 "GET",
191 "POST",
192 "PUT",
193 })
194}
195
196func TestAnonAccessEnvUnsetByDefault(t *testing.T) {
197 is := is.New(t)
198 cfg := DefaultConfig()
199 is.NoErr(cfg.ParseEnv())
200 // nil is the "no override" sentinel.
201 is.True(cfg.AnonAccess == nil)
202}
203
204func TestParseAnonAccessEnv(t *testing.T) {
205 is := is.New(t)
206 is.NoErr(os.Setenv("SOFT_SERVE_ANON_ACCESS", access.AdminAccess.String()))
207 t.Cleanup(func() {
208 is.NoErr(os.Unsetenv("SOFT_SERVE_ANON_ACCESS"))
209 })
210 cfg := DefaultConfig()
211 is.NoErr(cfg.ParseEnv())
212 is.True(cfg.AnonAccess != nil)
213 is.Equal(*cfg.AnonAccess, access.AdminAccess)
214}
215
216// An invalid anon-access level is rejected at parse time by AccessLevel's
217// TextUnmarshaler, so it never reaches Validate as a bad value.
218func TestParseRejectsInvalidAnonAccess(t *testing.T) {
219 is := is.New(t)
220 is.NoErr(os.Setenv("SOFT_SERVE_ANON_ACCESS", "not-a-real-access-level"))
221 t.Cleanup(func() {
222 is.NoErr(os.Unsetenv("SOFT_SERVE_ANON_ACCESS"))
223 })
224 cfg := DefaultConfig()
225 err := cfg.ParseEnv()
226 is.True(err != nil)
227}
228
229func TestAllowKeylessEnvUnsetByDefault(t *testing.T) {
230 is := is.New(t)
231 cfg := DefaultConfig()
232 is.NoErr(cfg.ParseEnv())
233 // nil is the "no override" sentinel — distinct from an explicit false.
234 is.True(cfg.AllowKeyless == nil)
235}
236
237func TestParseAllowKeylessEnvTrue(t *testing.T) {
238 is := is.New(t)
239 is.NoErr(os.Setenv("SOFT_SERVE_ALLOW_KEYLESS", "true"))
240 t.Cleanup(func() {
241 is.NoErr(os.Unsetenv("SOFT_SERVE_ALLOW_KEYLESS"))
242 })
243 cfg := DefaultConfig()
244 is.NoErr(cfg.ParseEnv())
245 is.True(cfg.AllowKeyless != nil)
246 is.Equal(*cfg.AllowKeyless, true)
247}
248
249func TestParseAllowKeylessEnvFalse(t *testing.T) {
250 is := is.New(t)
251 is.NoErr(os.Setenv("SOFT_SERVE_ALLOW_KEYLESS", "false"))
252 t.Cleanup(func() {
253 is.NoErr(os.Unsetenv("SOFT_SERVE_ALLOW_KEYLESS"))
254 })
255 cfg := DefaultConfig()
256 is.NoErr(cfg.ParseEnv())
257 // Explicit false must survive as a real override, not collapse back to
258 // "unset" — that's the whole reason this field is a *bool.
259 is.True(cfg.AllowKeyless != nil)
260 is.Equal(*cfg.AllowKeyless, false)
261}