Parent directory

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}