v3.0.0-alpha.1 6fdddb8

service_test.go RawHistoryFolder

1package config
2
3import (
4	"context"
5	"os"
6	"path/filepath"
7	"testing"
8
9	"github.com/bnema/zerowrap"
10	"github.com/spf13/viper"
11	"github.com/stretchr/testify/assert"
12	"github.com/stretchr/testify/require"
13
14	"github.com/bnema/gordon/internal/boundaries/out/mocks"
15)
16
17func testContext() context.Context {
18	return zerowrap.WithCtx(context.Background(), zerowrap.Default())
19}
20
21func TestService_Load(t *testing.T) {
22	v := viper.New()
23	v.Set("server.port", 8080)
24	v.Set("server.registry_port", 5000)
25	v.Set("server.registry_domain", "registry.example.com")
26	v.Set("server.data_dir", "/var/gordon")
27	v.Set("auto_route.enabled", true)
28	v.Set("network_isolation.enabled", true)
29	v.Set("network_isolation.network_prefix", "gordon")
30	v.Set("auth.enabled", true)
31	v.Set("auth.username", "admin")
32	v.Set("auth.password", "secret")
33	v.Set("volumes.auto_create", true)
34	v.Set("volumes.prefix", "gordon")
35	v.Set("volumes.preserve", false)
36
37	eventBus := mocks.NewMockEventPublisher(t)
38	svc := NewService(v, eventBus)
39	ctx := testContext()
40
41	err := svc.Load(ctx)
42
43	assert.NoError(t, err)
44	assert.Equal(t, 8080, svc.GetServerPort())
45	assert.Equal(t, 5000, svc.GetRegistryPort())
46	assert.Equal(t, "registry.example.com", svc.GetRegistryDomain())
47	assert.Equal(t, "/var/gordon", svc.GetDataDir())
48	assert.True(t, svc.IsNetworkIsolationEnabled())
49	assert.Equal(t, "gordon", svc.GetNetworkPrefix())
50
51	autoCreate, prefix, preserve := svc.GetVolumeConfig()
52	assert.True(t, autoCreate)
53	assert.Equal(t, "gordon", prefix)
54	assert.False(t, preserve)
55}
56
57func TestService_LoadPrefersExplicitRegistryAddress(t *testing.T) {
58	v := viper.New()
59	v.Set("server.gordon_domain", "gordon.example.com")
60	v.Set("server.registry_domain", "100.64.0.10:15000")
61
62	svc := NewService(v, mocks.NewMockEventPublisher(t))
63	require.NoError(t, svc.Load(testContext()))
64	assert.Equal(t, "100.64.0.10:15000", svc.GetRegistryDomain())
65}
66
67func TestService_Reload(t *testing.T) {
68	t.Run("success - picks up config file changes", func(t *testing.T) {
69		// Create temp config file
70		tmpDir := t.TempDir()
71		configFile := filepath.Join(tmpDir, "gordon.toml")
72		initialConfig := `[server]
73port = 8080
74`
75		err := os.WriteFile(configFile, []byte(initialConfig), 0600)
76		require.NoError(t, err)
77
78		v := viper.New()
79		v.SetConfigFile(configFile)
80		err = v.ReadInConfig()
81		require.NoError(t, err)
82
83		eventBus := mocks.NewMockEventPublisher(t)
84		svc := NewService(v, eventBus)
85		ctx := testContext()
86
87		// Initial load
88		err = svc.Load(ctx)
89		require.NoError(t, err)
90		assert.Equal(t, 8080, svc.GetServerPort())
91
92		// Modify config file on disk
93		updatedConfig := `[server]
94port = 9090
95`
96		err = os.WriteFile(configFile, []byte(updatedConfig), 0600)
97		require.NoError(t, err)
98
99		// Reload should pick up new values
100		err = svc.Reload(ctx)
101		require.NoError(t, err)
102
103		assert.Equal(t, 9090, svc.GetServerPort())
104	})
105
106	t.Run("error - config file not found", func(t *testing.T) {
107		tmpDir := t.TempDir()
108		configFile := filepath.Join(tmpDir, "gordon.toml")
109
110		// Create config, load it, then delete it
111		err := os.WriteFile(configFile, []byte("[server]\nport = 8080\n"), 0600)
112		require.NoError(t, err)
113
114		v := viper.New()
115		v.SetConfigFile(configFile)
116		err = v.ReadInConfig()
117		require.NoError(t, err)
118
119		eventBus := mocks.NewMockEventPublisher(t)
120		svc := NewService(v, eventBus)
121		ctx := testContext()
122
123		err = svc.Load(ctx)
124		require.NoError(t, err)
125
126		// Delete the config file
127		err = os.Remove(configFile)
128		require.NoError(t, err)
129
130		// Reload should fail
131		err = svc.Reload(ctx)
132		assert.Error(t, err)
133		assert.Contains(t, err.Error(), "failed to read config file")
134	})
135
136	t.Run("error - invalid config syntax", func(t *testing.T) {
137		tmpDir := t.TempDir()
138		configFile := filepath.Join(tmpDir, "gordon.toml")
139
140		// Create valid config initially
141		err := os.WriteFile(configFile, []byte("[server]\nport = 8080\n"), 0600)
142		require.NoError(t, err)
143
144		v := viper.New()
145		v.SetConfigFile(configFile)
146		err = v.ReadInConfig()
147		require.NoError(t, err)
148
149		eventBus := mocks.NewMockEventPublisher(t)
150		svc := NewService(v, eventBus)
151		ctx := testContext()
152
153		err = svc.Load(ctx)
154		require.NoError(t, err)
155
156		// Write invalid TOML
157		err = os.WriteFile(configFile, []byte("[server\nport = invalid syntax"), 0600)
158		require.NoError(t, err)
159
160		// Reload should fail
161		err = svc.Reload(ctx)
162		assert.Error(t, err)
163	})
164}
165
166func TestLoadExternalRoutes_RejectsInvalidAndDuplicateKeys(t *testing.T) {
167	t.Run("invalid", func(t *testing.T) {
168		_, err := loadExternalRoutes(map[string]any{
169			"bad.example.com:8080": "203.0.113.10:5000",
170		})
171
172		require.Error(t, err)
173		assert.Contains(t, err.Error(), "invalid external route key")
174	})
175
176	t.Run("duplicate canonical", func(t *testing.T) {
177		_, err := loadExternalRoutes(map[string]any{
178			"Reg.Example.com": "203.0.113.10:5000",
179			"reg.example.com": "203.0.113.11:5000",
180		})
181
182		require.Error(t, err)
183		assert.Contains(t, err.Error(), "duplicate external route key")
184	})
185}
186
187func TestService_GetExternalRoutes(t *testing.T) {
188	v := viper.New()
189	v.Set("external_routes", map[string]interface{}{
190		"Reg.Example.com":   "localhost:5000",
191		"cache.example.com": "127.0.0.1:6379",
192	})
193
194	eventBus := mocks.NewMockEventPublisher(t)
195	svc := NewService(v, eventBus)
196	ctx := testContext()
197
198	_ = svc.Load(ctx)
199
200	routes := svc.GetExternalRoutes()
201
202	assert.Len(t, routes, 2)
203	assert.Equal(t, "localhost:5000", routes["reg.example.com"])
204	assert.Equal(t, "127.0.0.1:6379", routes["cache.example.com"])
205}
206
207func TestService_GetExternalRoutes_Empty(t *testing.T) {
208	v := viper.New()
209
210	eventBus := mocks.NewMockEventPublisher(t)
211	svc := NewService(v, eventBus)
212	ctx := testContext()
213
214	_ = svc.Load(ctx)
215
216	routes := svc.GetExternalRoutes()
217
218	assert.Empty(t, routes)
219}
220
221func TestExtractDomainFromImageName(t *testing.T) {
222	tests := []struct {
223		name          string
224		imageName     string
225		expectedDom   string
226		expectedFound bool
227	}{
228		{
229			name:          "domain like image",
230			imageName:     "myapp.example.com:latest",
231			expectedDom:   "myapp.example.com",
232			expectedFound: true,
233		},
234		{
235			name:          "domain like image without tag",
236			imageName:     "api.backend.io",
237			expectedDom:   "api.backend.io",
238			expectedFound: true,
239		},
240		{
241			name:          "simple image name",
242			imageName:     "nginx:latest",
243			expectedDom:   "",
244			expectedFound: false,
245		},
246		{
247			name:          "registry path",
248			imageName:     "gcr.io/project/image:tag",
249			expectedDom:   "gcr.io/project/image",
250			expectedFound: true,
251		},
252		{
253			name:          "leading dot",
254			imageName:     ".invalid:latest",
255			expectedDom:   "",
256			expectedFound: false,
257		},
258		{
259			name:          "trailing dot",
260			imageName:     "invalid.:latest",
261			expectedDom:   "",
262			expectedFound: false,
263		},
264	}
265
266	for _, tt := range tests {
267		t.Run(tt.name, func(t *testing.T) {
268			domain, found := ExtractDomainFromImageName(tt.imageName)
269			assert.Equal(t, tt.expectedDom, domain)
270			assert.Equal(t, tt.expectedFound, found)
271		})
272	}
273}
274
275func TestLoadStringMap(t *testing.T) {
276	tests := []struct {
277		name     string
278		input    any
279		expected map[string]string
280	}{
281		{
282			name:     "nil input",
283			input:    nil,
284			expected: map[string]string{},
285		},
286		{
287			name: "valid map",
288			input: map[string]any{
289				"key1": "value1",
290				"key2": "value2",
291			},
292			expected: map[string]string{
293				"key1": "value1",
294				"key2": "value2",
295			},
296		},
297		{
298			name:     "invalid type",
299			input:    "not a map",
300			expected: map[string]string{},
301		},
302		{
303			name: "mixed values",
304			input: map[string]any{
305				"string": "value",
306				"int":    123,
307			},
308			expected: map[string]string{
309				"string": "value",
310			},
311		},
312	}
313
314	for _, tt := range tests {
315		t.Run(tt.name, func(t *testing.T) {
316			result := loadStringMap(tt.input)
317			assert.Equal(t, tt.expected, result)
318		})
319	}
320}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 106

↑↓ moveenter openesc close