v2.2.7 aefe19e

service_test.go RawHistoryFolder

1package config
2
3import (
4	"context"
5	"testing"
6
7	"github.com/bnema/zerowrap"
8	"github.com/spf13/viper"
9	"github.com/stretchr/testify/assert"
10
11	"gordon/internal/boundaries/out/mocks"
12	"gordon/internal/domain"
13)
14
15func testContext() context.Context {
16	return zerowrap.WithCtx(context.Background(), zerowrap.Default())
17}
18
19func TestService_Load(t *testing.T) {
20	v := viper.New()
21	v.Set("server.port", 8080)
22	v.Set("server.registry_port", 5000)
23	v.Set("server.registry_domain", "registry.example.com")
24	v.Set("server.data_dir", "/var/gordon")
25	v.Set("auto_route.enabled", true)
26	v.Set("network_isolation.enabled", true)
27	v.Set("network_isolation.network_prefix", "gordon")
28	v.Set("registry_auth.enabled", true)
29	v.Set("registry_auth.username", "admin")
30	v.Set("registry_auth.password", "secret")
31	v.Set("volumes.auto_create", true)
32	v.Set("volumes.prefix", "gordon")
33	v.Set("volumes.preserve", false)
34	v.Set("routes", map[string]interface{}{
35		"app.example.com": "myapp:latest",
36	})
37
38	eventBus := mocks.NewMockEventPublisher(t)
39	svc := NewService(v, eventBus)
40	ctx := testContext()
41
42	err := svc.Load(ctx)
43
44	assert.NoError(t, err)
45	assert.Equal(t, 8080, svc.GetServerPort())
46	assert.Equal(t, 5000, svc.GetRegistryPort())
47	assert.Equal(t, "registry.example.com", svc.GetRegistryDomain())
48	assert.Equal(t, "/var/gordon", svc.GetDataDir())
49	assert.True(t, svc.IsAutoRouteEnabled())
50	assert.True(t, svc.IsNetworkIsolationEnabled())
51	assert.Equal(t, "gordon", svc.GetNetworkPrefix())
52
53	enabled, username, password := svc.GetRegistryAuthConfig()
54	assert.True(t, enabled)
55	assert.Equal(t, "admin", username)
56	assert.Equal(t, "secret", password)
57
58	autoCreate, prefix, preserve := svc.GetVolumeConfig()
59	assert.True(t, autoCreate)
60	assert.Equal(t, "gordon", prefix)
61	assert.False(t, preserve)
62}
63
64func TestService_GetRoutes(t *testing.T) {
65	v := viper.New()
66	v.Set("routes", map[string]interface{}{
67		"app1.example.com":        "myapp:latest",
68		"app2.example.com":        "otherapp:v2",
69		"http://insecure.example": "insecureapp:latest",
70	})
71
72	eventBus := mocks.NewMockEventPublisher(t)
73	svc := NewService(v, eventBus)
74	ctx := testContext()
75
76	_ = svc.Load(ctx)
77
78	routes := svc.GetRoutes(ctx)
79
80	assert.Len(t, routes, 3)
81
82	// Check routes (order is not guaranteed)
83	routeMap := make(map[string]domain.Route)
84	for _, r := range routes {
85		routeMap[r.Domain] = r
86	}
87
88	assert.Equal(t, "myapp:latest", routeMap["app1.example.com"].Image)
89	assert.True(t, routeMap["app1.example.com"].HTTPS)
90
91	assert.Equal(t, "otherapp:v2", routeMap["app2.example.com"].Image)
92	assert.True(t, routeMap["app2.example.com"].HTTPS)
93
94	assert.Equal(t, "insecureapp:latest", routeMap["insecure.example"].Image)
95	assert.False(t, routeMap["insecure.example"].HTTPS)
96}
97
98func TestService_AddRoute(t *testing.T) {
99	v := viper.New()
100	eventBus := mocks.NewMockEventPublisher(t)
101	svc := NewService(v, eventBus)
102	ctx := testContext()
103
104	_ = svc.Load(ctx)
105
106	route := domain.Route{
107		Domain: "new.example.com",
108		Image:  "newapp:latest",
109	}
110
111	err := svc.AddRoute(ctx, route)
112
113	assert.NoError(t, err)
114
115	// Verify route was added
116	config := svc.GetConfig()
117	assert.Equal(t, "newapp:latest", config.Routes["new.example.com"])
118}
119
120func TestService_UpdateRoute(t *testing.T) {
121	v := viper.New()
122	v.Set("routes", map[string]interface{}{
123		"app.example.com": "myapp:v1",
124	})
125
126	eventBus := mocks.NewMockEventPublisher(t)
127	svc := NewService(v, eventBus)
128	ctx := testContext()
129
130	_ = svc.Load(ctx)
131
132	route := domain.Route{
133		Domain: "app.example.com",
134		Image:  "myapp:v2",
135	}
136
137	err := svc.UpdateRoute(ctx, route)
138
139	assert.NoError(t, err)
140
141	config := svc.GetConfig()
142	assert.Equal(t, "myapp:v2", config.Routes["app.example.com"])
143}
144
145func TestService_UpdateRoute_NotFound(t *testing.T) {
146	v := viper.New()
147	eventBus := mocks.NewMockEventPublisher(t)
148	svc := NewService(v, eventBus)
149	ctx := testContext()
150
151	_ = svc.Load(ctx)
152
153	route := domain.Route{
154		Domain: "nonexistent.example.com",
155		Image:  "myapp:latest",
156	}
157
158	err := svc.UpdateRoute(ctx, route)
159
160	assert.ErrorIs(t, err, domain.ErrRouteNotFound)
161}
162
163func TestService_RemoveRoute(t *testing.T) {
164	v := viper.New()
165	v.Set("routes", map[string]interface{}{
166		"app.example.com": "myapp:latest",
167	})
168
169	eventBus := mocks.NewMockEventPublisher(t)
170	svc := NewService(v, eventBus)
171	ctx := testContext()
172
173	_ = svc.Load(ctx)
174
175	err := svc.RemoveRoute(ctx, "app.example.com")
176
177	assert.NoError(t, err)
178
179	config := svc.GetConfig()
180	_, exists := config.Routes["app.example.com"]
181	assert.False(t, exists)
182}
183
184func TestService_RemoveRoute_NotFound(t *testing.T) {
185	v := viper.New()
186	eventBus := mocks.NewMockEventPublisher(t)
187	svc := NewService(v, eventBus)
188	ctx := testContext()
189
190	_ = svc.Load(ctx)
191
192	err := svc.RemoveRoute(ctx, "nonexistent.example.com")
193
194	assert.ErrorIs(t, err, domain.ErrRouteNotFound)
195}
196
197func TestService_GetNetworkGroups(t *testing.T) {
198	v := viper.New()
199	v.Set("network_groups", map[string]interface{}{
200		"frontend": []interface{}{"app1.example.com", "app2.example.com"},
201		"backend":  []interface{}{"api.example.com", "db.example.com"},
202	})
203
204	eventBus := mocks.NewMockEventPublisher(t)
205	svc := NewService(v, eventBus)
206	ctx := testContext()
207
208	_ = svc.Load(ctx)
209
210	groups := svc.GetNetworkGroups()
211
212	assert.Len(t, groups, 2)
213	assert.ElementsMatch(t, []string{"app1.example.com", "app2.example.com"}, groups["frontend"])
214	assert.ElementsMatch(t, []string{"api.example.com", "db.example.com"}, groups["backend"])
215}
216
217func TestService_GetAttachments(t *testing.T) {
218	v := viper.New()
219	v.Set("attachments", map[string]interface{}{
220		"app.example.com": []interface{}{"redis:latest", "postgres:15"},
221	})
222
223	eventBus := mocks.NewMockEventPublisher(t)
224	svc := NewService(v, eventBus)
225	ctx := testContext()
226
227	_ = svc.Load(ctx)
228
229	attachments := svc.GetAttachments()
230
231	assert.Len(t, attachments, 1)
232	assert.ElementsMatch(t, []string{"redis:latest", "postgres:15"}, attachments["app.example.com"])
233}
234
235func TestService_GetExternalRoutes(t *testing.T) {
236	v := viper.New()
237	v.Set("external_routes", map[string]interface{}{
238		"reg.example.com":   "localhost:5000",
239		"cache.example.com": "127.0.0.1:6379",
240	})
241
242	eventBus := mocks.NewMockEventPublisher(t)
243	svc := NewService(v, eventBus)
244	ctx := testContext()
245
246	_ = svc.Load(ctx)
247
248	routes := svc.GetExternalRoutes()
249
250	assert.Len(t, routes, 2)
251	assert.Equal(t, "localhost:5000", routes["reg.example.com"])
252	assert.Equal(t, "127.0.0.1:6379", routes["cache.example.com"])
253}
254
255func TestService_GetExternalRoutes_Empty(t *testing.T) {
256	v := viper.New()
257
258	eventBus := mocks.NewMockEventPublisher(t)
259	svc := NewService(v, eventBus)
260	ctx := testContext()
261
262	_ = svc.Load(ctx)
263
264	routes := svc.GetExternalRoutes()
265
266	assert.Empty(t, routes)
267}
268
269func TestExtractDomainFromImageName(t *testing.T) {
270	tests := []struct {
271		name          string
272		imageName     string
273		expectedDom   string
274		expectedFound bool
275	}{
276		{
277			name:          "domain like image",
278			imageName:     "myapp.example.com:latest",
279			expectedDom:   "myapp.example.com",
280			expectedFound: true,
281		},
282		{
283			name:          "domain like image without tag",
284			imageName:     "api.backend.io",
285			expectedDom:   "api.backend.io",
286			expectedFound: true,
287		},
288		{
289			name:          "simple image name",
290			imageName:     "nginx:latest",
291			expectedDom:   "",
292			expectedFound: false,
293		},
294		{
295			name:          "registry path",
296			imageName:     "gcr.io/project/image:tag",
297			expectedDom:   "gcr.io/project/image",
298			expectedFound: true,
299		},
300		{
301			name:          "leading dot",
302			imageName:     ".invalid:latest",
303			expectedDom:   "",
304			expectedFound: false,
305		},
306		{
307			name:          "trailing dot",
308			imageName:     "invalid.:latest",
309			expectedDom:   "",
310			expectedFound: false,
311		},
312	}
313
314	for _, tt := range tests {
315		t.Run(tt.name, func(t *testing.T) {
316			domain, found := ExtractDomainFromImageName(tt.imageName)
317			assert.Equal(t, tt.expectedDom, domain)
318			assert.Equal(t, tt.expectedFound, found)
319		})
320	}
321}
322
323func TestLoadStringMap(t *testing.T) {
324	tests := []struct {
325		name     string
326		input    any
327		expected map[string]string
328	}{
329		{
330			name:     "nil input",
331			input:    nil,
332			expected: map[string]string{},
333		},
334		{
335			name: "valid map",
336			input: map[string]any{
337				"key1": "value1",
338				"key2": "value2",
339			},
340			expected: map[string]string{
341				"key1": "value1",
342				"key2": "value2",
343			},
344		},
345		{
346			name:     "invalid type",
347			input:    "not a map",
348			expected: map[string]string{},
349		},
350		{
351			name: "mixed values",
352			input: map[string]any{
353				"string": "value",
354				"int":    123,
355			},
356			expected: map[string]string{
357				"string": "value",
358			},
359		},
360	}
361
362	for _, tt := range tests {
363		t.Run(tt.name, func(t *testing.T) {
364			result := loadStringMap(tt.input)
365			assert.Equal(t, tt.expected, result)
366		})
367	}
368}
369
370func TestLoadStringArrayMap(t *testing.T) {
371	tests := []struct {
372		name     string
373		input    any
374		expected map[string][]string
375	}{
376		{
377			name:     "nil input",
378			input:    nil,
379			expected: map[string][]string{},
380		},
381		{
382			name: "valid map",
383			input: map[string]any{
384				"group1": []any{"a", "b"},
385				"group2": []any{"c", "d", "e"},
386			},
387			expected: map[string][]string{
388				"group1": {"a", "b"},
389				"group2": {"c", "d", "e"},
390			},
391		},
392		{
393			name:     "invalid type",
394			input:    "not a map",
395			expected: map[string][]string{},
396		},
397		{
398			name: "non-array value",
399			input: map[string]any{
400				"array":  []any{"a", "b"},
401				"string": "not an array",
402			},
403			expected: map[string][]string{
404				"array": {"a", "b"},
405			},
406		},
407	}
408
409	for _, tt := range tests {
410		t.Run(tt.name, func(t *testing.T) {
411			result := loadStringArrayMap(tt.input)
412			assert.Equal(t, tt.expected, result)
413		})
414	}
415}

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