v2.4.2 7da87b2

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	"gordon/internal/boundaries/out/mocks"
15	"gordon/internal/domain"
16)
17
18func testContext() context.Context {
19	return zerowrap.WithCtx(context.Background(), zerowrap.Default())
20}
21
22func TestService_Load(t *testing.T) {
23	v := viper.New()
24	v.Set("server.port", 8080)
25	v.Set("server.registry_port", 5000)
26	v.Set("server.registry_domain", "registry.example.com")
27	v.Set("server.data_dir", "/var/gordon")
28	v.Set("auto_route.enabled", true)
29	v.Set("network_isolation.enabled", true)
30	v.Set("network_isolation.network_prefix", "gordon")
31	v.Set("registry_auth.enabled", true)
32	v.Set("registry_auth.username", "admin")
33	v.Set("registry_auth.password", "secret")
34	v.Set("volumes.auto_create", true)
35	v.Set("volumes.prefix", "gordon")
36	v.Set("volumes.preserve", false)
37	v.Set("routes", map[string]interface{}{
38		"app.example.com": "myapp:latest",
39	})
40
41	eventBus := mocks.NewMockEventPublisher(t)
42	svc := NewService(v, eventBus)
43	ctx := testContext()
44
45	err := svc.Load(ctx)
46
47	assert.NoError(t, err)
48	assert.Equal(t, 8080, svc.GetServerPort())
49	assert.Equal(t, 5000, svc.GetRegistryPort())
50	assert.Equal(t, "registry.example.com", svc.GetRegistryDomain())
51	assert.Equal(t, "/var/gordon", svc.GetDataDir())
52	assert.True(t, svc.IsAutoRouteEnabled())
53	assert.True(t, svc.IsNetworkIsolationEnabled())
54	assert.Equal(t, "gordon", svc.GetNetworkPrefix())
55
56	enabled, username, password := svc.GetRegistryAuthConfig()
57	assert.True(t, enabled)
58	assert.Equal(t, "admin", username)
59	assert.Equal(t, "secret", password)
60
61	autoCreate, prefix, preserve := svc.GetVolumeConfig()
62	assert.True(t, autoCreate)
63	assert.Equal(t, "gordon", prefix)
64	assert.False(t, preserve)
65}
66
67func TestService_GetRoutes(t *testing.T) {
68	v := viper.New()
69	v.Set("routes", map[string]interface{}{
70		"app1.example.com":        "myapp:latest",
71		"app2.example.com":        "otherapp:v2",
72		"http://insecure.example": "insecureapp:latest",
73	})
74
75	eventBus := mocks.NewMockEventPublisher(t)
76	svc := NewService(v, eventBus)
77	ctx := testContext()
78
79	_ = svc.Load(ctx)
80
81	routes := svc.GetRoutes(ctx)
82
83	assert.Len(t, routes, 3)
84
85	// Check routes (order is not guaranteed)
86	routeMap := make(map[string]domain.Route)
87	for _, r := range routes {
88		routeMap[r.Domain] = r
89	}
90
91	assert.Equal(t, "myapp:latest", routeMap["app1.example.com"].Image)
92	assert.True(t, routeMap["app1.example.com"].HTTPS)
93
94	assert.Equal(t, "otherapp:v2", routeMap["app2.example.com"].Image)
95	assert.True(t, routeMap["app2.example.com"].HTTPS)
96
97	assert.Equal(t, "insecureapp:latest", routeMap["insecure.example"].Image)
98	assert.False(t, routeMap["insecure.example"].HTTPS)
99}
100
101func TestService_GetRoute(t *testing.T) {
102	v := viper.New()
103	v.Set("routes", map[string]interface{}{
104		"app.example.com":         "myapp:latest",
105		"http://insecure.example": "insecureapp:latest",
106	})
107
108	eventBus := mocks.NewMockEventPublisher(t)
109	svc := NewService(v, eventBus)
110	ctx := testContext()
111
112	_ = svc.Load(ctx)
113
114	t.Run("existing route", func(t *testing.T) {
115		route, err := svc.GetRoute(ctx, "app.example.com")
116		require.NoError(t, err)
117		assert.Equal(t, "app.example.com", route.Domain)
118		assert.Equal(t, "myapp:latest", route.Image)
119		assert.True(t, route.HTTPS)
120	})
121
122	t.Run("non-existent route", func(t *testing.T) {
123		route, err := svc.GetRoute(ctx, "notfound.example.com")
124		assert.ErrorIs(t, err, domain.ErrRouteNotFound)
125		assert.Nil(t, route)
126	})
127}
128
129func TestService_AddRoute(t *testing.T) {
130	t.Run("success", func(t *testing.T) {
131		// Create temp config file for Save() to work
132		tmpDir := t.TempDir()
133		configFile := filepath.Join(tmpDir, "gordon.toml")
134		err := os.WriteFile(configFile, []byte("[routes]\n"), 0600)
135		require.NoError(t, err)
136
137		v := viper.New()
138		v.SetConfigFile(configFile)
139		eventBus := mocks.NewMockEventPublisher(t)
140		svc := NewService(v, eventBus)
141		ctx := testContext()
142
143		_ = svc.Load(ctx)
144
145		route := domain.Route{
146			Domain: "new.example.com",
147			Image:  "newapp:latest",
148		}
149
150		err = svc.AddRoute(ctx, route)
151
152		assert.NoError(t, err)
153
154		// Verify route was added
155		config := svc.GetConfig()
156		assert.Equal(t, "newapp:latest", config.Routes["new.example.com"])
157	})
158
159	t.Run("empty domain", func(t *testing.T) {
160		v := viper.New()
161		eventBus := mocks.NewMockEventPublisher(t)
162		svc := NewService(v, eventBus)
163		ctx := testContext()
164
165		route := domain.Route{
166			Domain: "",
167			Image:  "myapp:latest",
168		}
169
170		err := svc.AddRoute(ctx, route)
171		assert.ErrorIs(t, err, domain.ErrRouteDomainEmpty)
172	})
173
174	t.Run("empty image", func(t *testing.T) {
175		v := viper.New()
176		eventBus := mocks.NewMockEventPublisher(t)
177		svc := NewService(v, eventBus)
178		ctx := testContext()
179
180		route := domain.Route{
181			Domain: "example.com",
182			Image:  "",
183		}
184
185		err := svc.AddRoute(ctx, route)
186		assert.ErrorIs(t, err, domain.ErrRouteImageEmpty)
187	})
188}
189
190func TestService_UpdateRoute(t *testing.T) {
191	t.Run("success", func(t *testing.T) {
192		// Create temp config file for Save() to work
193		tmpDir := t.TempDir()
194		configFile := filepath.Join(tmpDir, "gordon.toml")
195		err := os.WriteFile(configFile, []byte("[routes]\n\"app.example.com\" = \"myapp:v1\"\n"), 0600)
196		require.NoError(t, err)
197
198		v := viper.New()
199		v.SetConfigFile(configFile)
200		v.Set("routes", map[string]any{
201			"app.example.com": "myapp:v1",
202		})
203
204		eventBus := mocks.NewMockEventPublisher(t)
205		svc := NewService(v, eventBus)
206		ctx := testContext()
207
208		_ = svc.Load(ctx)
209
210		route := domain.Route{
211			Domain: "app.example.com",
212			Image:  "myapp:v2",
213		}
214
215		err = svc.UpdateRoute(ctx, route)
216
217		assert.NoError(t, err)
218
219		config := svc.GetConfig()
220		assert.Equal(t, "myapp:v2", config.Routes["app.example.com"])
221	})
222
223	t.Run("not found", func(t *testing.T) {
224		v := viper.New()
225		eventBus := mocks.NewMockEventPublisher(t)
226		svc := NewService(v, eventBus)
227		ctx := testContext()
228
229		_ = svc.Load(ctx)
230
231		route := domain.Route{
232			Domain: "nonexistent.example.com",
233			Image:  "myapp:latest",
234		}
235
236		err := svc.UpdateRoute(ctx, route)
237		assert.ErrorIs(t, err, domain.ErrRouteNotFound)
238	})
239
240	t.Run("empty domain", func(t *testing.T) {
241		v := viper.New()
242		eventBus := mocks.NewMockEventPublisher(t)
243		svc := NewService(v, eventBus)
244		ctx := testContext()
245
246		route := domain.Route{
247			Domain: "",
248			Image:  "myapp:latest",
249		}
250
251		err := svc.UpdateRoute(ctx, route)
252		assert.ErrorIs(t, err, domain.ErrRouteDomainEmpty)
253	})
254
255	t.Run("empty image", func(t *testing.T) {
256		v := viper.New()
257		eventBus := mocks.NewMockEventPublisher(t)
258		svc := NewService(v, eventBus)
259		ctx := testContext()
260
261		route := domain.Route{
262			Domain: "example.com",
263			Image:  "",
264		}
265
266		err := svc.UpdateRoute(ctx, route)
267		assert.ErrorIs(t, err, domain.ErrRouteImageEmpty)
268	})
269}
270
271func TestService_RemoveRoute(t *testing.T) {
272	// Create temp config file for Save() to work
273	tmpDir := t.TempDir()
274	configFile := filepath.Join(tmpDir, "gordon.toml")
275	err := os.WriteFile(configFile, []byte("[routes]\n\"app.example.com\" = \"myapp:latest\"\n"), 0600)
276	require.NoError(t, err)
277
278	v := viper.New()
279	v.SetConfigFile(configFile)
280	v.Set("routes", map[string]interface{}{
281		"app.example.com": "myapp:latest",
282	})
283
284	eventBus := mocks.NewMockEventPublisher(t)
285	svc := NewService(v, eventBus)
286	ctx := testContext()
287
288	_ = svc.Load(ctx)
289
290	err = svc.RemoveRoute(ctx, "app.example.com")
291
292	assert.NoError(t, err)
293
294	config := svc.GetConfig()
295	_, exists := config.Routes["app.example.com"]
296	assert.False(t, exists)
297}
298
299func TestService_RemoveRoute_NotFound(t *testing.T) {
300	v := viper.New()
301	eventBus := mocks.NewMockEventPublisher(t)
302	svc := NewService(v, eventBus)
303	ctx := testContext()
304
305	_ = svc.Load(ctx)
306
307	err := svc.RemoveRoute(ctx, "nonexistent.example.com")
308
309	assert.ErrorIs(t, err, domain.ErrRouteNotFound)
310}
311
312func TestService_GetNetworkGroups(t *testing.T) {
313	v := viper.New()
314	v.Set("network_groups", map[string]interface{}{
315		"frontend": []interface{}{"app1.example.com", "app2.example.com"},
316		"backend":  []interface{}{"api.example.com", "db.example.com"},
317	})
318
319	eventBus := mocks.NewMockEventPublisher(t)
320	svc := NewService(v, eventBus)
321	ctx := testContext()
322
323	_ = svc.Load(ctx)
324
325	groups := svc.GetNetworkGroups()
326
327	assert.Len(t, groups, 2)
328	assert.ElementsMatch(t, []string{"app1.example.com", "app2.example.com"}, groups["frontend"])
329	assert.ElementsMatch(t, []string{"api.example.com", "db.example.com"}, groups["backend"])
330}
331
332func TestService_GetAttachments(t *testing.T) {
333	v := viper.New()
334	v.Set("attachments", map[string]interface{}{
335		"app.example.com": []interface{}{"redis:latest", "postgres:15"},
336	})
337
338	eventBus := mocks.NewMockEventPublisher(t)
339	svc := NewService(v, eventBus)
340	ctx := testContext()
341
342	_ = svc.Load(ctx)
343
344	attachments := svc.GetAttachments()
345
346	assert.Len(t, attachments, 1)
347	assert.ElementsMatch(t, []string{"redis:latest", "postgres:15"}, attachments["app.example.com"])
348}
349
350func TestService_GetExternalRoutes(t *testing.T) {
351	v := viper.New()
352	v.Set("external_routes", map[string]interface{}{
353		"reg.example.com":   "localhost:5000",
354		"cache.example.com": "127.0.0.1:6379",
355	})
356
357	eventBus := mocks.NewMockEventPublisher(t)
358	svc := NewService(v, eventBus)
359	ctx := testContext()
360
361	_ = svc.Load(ctx)
362
363	routes := svc.GetExternalRoutes()
364
365	assert.Len(t, routes, 2)
366	assert.Equal(t, "localhost:5000", routes["reg.example.com"])
367	assert.Equal(t, "127.0.0.1:6379", routes["cache.example.com"])
368}
369
370func TestService_GetExternalRoutes_Empty(t *testing.T) {
371	v := viper.New()
372
373	eventBus := mocks.NewMockEventPublisher(t)
374	svc := NewService(v, eventBus)
375	ctx := testContext()
376
377	_ = svc.Load(ctx)
378
379	routes := svc.GetExternalRoutes()
380
381	assert.Empty(t, routes)
382}
383
384func TestExtractDomainFromImageName(t *testing.T) {
385	tests := []struct {
386		name          string
387		imageName     string
388		expectedDom   string
389		expectedFound bool
390	}{
391		{
392			name:          "domain like image",
393			imageName:     "myapp.example.com:latest",
394			expectedDom:   "myapp.example.com",
395			expectedFound: true,
396		},
397		{
398			name:          "domain like image without tag",
399			imageName:     "api.backend.io",
400			expectedDom:   "api.backend.io",
401			expectedFound: true,
402		},
403		{
404			name:          "simple image name",
405			imageName:     "nginx:latest",
406			expectedDom:   "",
407			expectedFound: false,
408		},
409		{
410			name:          "registry path",
411			imageName:     "gcr.io/project/image:tag",
412			expectedDom:   "gcr.io/project/image",
413			expectedFound: true,
414		},
415		{
416			name:          "leading dot",
417			imageName:     ".invalid:latest",
418			expectedDom:   "",
419			expectedFound: false,
420		},
421		{
422			name:          "trailing dot",
423			imageName:     "invalid.:latest",
424			expectedDom:   "",
425			expectedFound: false,
426		},
427	}
428
429	for _, tt := range tests {
430		t.Run(tt.name, func(t *testing.T) {
431			domain, found := ExtractDomainFromImageName(tt.imageName)
432			assert.Equal(t, tt.expectedDom, domain)
433			assert.Equal(t, tt.expectedFound, found)
434		})
435	}
436}
437
438func TestLoadStringMap(t *testing.T) {
439	tests := []struct {
440		name     string
441		input    any
442		expected map[string]string
443	}{
444		{
445			name:     "nil input",
446			input:    nil,
447			expected: map[string]string{},
448		},
449		{
450			name: "valid map",
451			input: map[string]any{
452				"key1": "value1",
453				"key2": "value2",
454			},
455			expected: map[string]string{
456				"key1": "value1",
457				"key2": "value2",
458			},
459		},
460		{
461			name:     "invalid type",
462			input:    "not a map",
463			expected: map[string]string{},
464		},
465		{
466			name: "mixed values",
467			input: map[string]any{
468				"string": "value",
469				"int":    123,
470			},
471			expected: map[string]string{
472				"string": "value",
473			},
474		},
475	}
476
477	for _, tt := range tests {
478		t.Run(tt.name, func(t *testing.T) {
479			result := loadStringMap(tt.input)
480			assert.Equal(t, tt.expected, result)
481		})
482	}
483}
484
485func TestLoadStringArrayMap(t *testing.T) {
486	tests := []struct {
487		name     string
488		input    any
489		expected map[string][]string
490	}{
491		{
492			name:     "nil input",
493			input:    nil,
494			expected: map[string][]string{},
495		},
496		{
497			name: "valid map",
498			input: map[string]any{
499				"group1": []any{"a", "b"},
500				"group2": []any{"c", "d", "e"},
501			},
502			expected: map[string][]string{
503				"group1": {"a", "b"},
504				"group2": {"c", "d", "e"},
505			},
506		},
507		{
508			name:     "invalid type",
509			input:    "not a map",
510			expected: map[string][]string{},
511		},
512		{
513			name: "non-array value",
514			input: map[string]any{
515				"array":  []any{"a", "b"},
516				"string": "not an array",
517			},
518			expected: map[string][]string{
519				"array": {"a", "b"},
520			},
521		},
522	}
523
524	for _, tt := range tests {
525		t.Run(tt.name, func(t *testing.T) {
526			result := loadStringArrayMap(tt.input)
527			assert.Equal(t, tt.expected, result)
528		})
529	}
530}

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 7

Tags 106

↑↓ moveenter openesc close