v2.1.0 0fb867d

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 TestExtractDomainFromImageName(t *testing.T) {
236	tests := []struct {
237		name          string
238		imageName     string
239		expectedDom   string
240		expectedFound bool
241	}{
242		{
243			name:          "domain like image",
244			imageName:     "myapp.example.com:latest",
245			expectedDom:   "myapp.example.com",
246			expectedFound: true,
247		},
248		{
249			name:          "domain like image without tag",
250			imageName:     "api.backend.io",
251			expectedDom:   "api.backend.io",
252			expectedFound: true,
253		},
254		{
255			name:          "simple image name",
256			imageName:     "nginx:latest",
257			expectedDom:   "",
258			expectedFound: false,
259		},
260		{
261			name:          "registry path",
262			imageName:     "gcr.io/project/image:tag",
263			expectedDom:   "gcr.io/project/image",
264			expectedFound: true,
265		},
266		{
267			name:          "leading dot",
268			imageName:     ".invalid:latest",
269			expectedDom:   "",
270			expectedFound: false,
271		},
272		{
273			name:          "trailing dot",
274			imageName:     "invalid.:latest",
275			expectedDom:   "",
276			expectedFound: false,
277		},
278	}
279
280	for _, tt := range tests {
281		t.Run(tt.name, func(t *testing.T) {
282			domain, found := ExtractDomainFromImageName(tt.imageName)
283			assert.Equal(t, tt.expectedDom, domain)
284			assert.Equal(t, tt.expectedFound, found)
285		})
286	}
287}
288
289func TestLoadStringMap(t *testing.T) {
290	tests := []struct {
291		name     string
292		input    any
293		expected map[string]string
294	}{
295		{
296			name:     "nil input",
297			input:    nil,
298			expected: map[string]string{},
299		},
300		{
301			name: "valid map",
302			input: map[string]any{
303				"key1": "value1",
304				"key2": "value2",
305			},
306			expected: map[string]string{
307				"key1": "value1",
308				"key2": "value2",
309			},
310		},
311		{
312			name:     "invalid type",
313			input:    "not a map",
314			expected: map[string]string{},
315		},
316		{
317			name: "mixed values",
318			input: map[string]any{
319				"string": "value",
320				"int":    123,
321			},
322			expected: map[string]string{
323				"string": "value",
324			},
325		},
326	}
327
328	for _, tt := range tests {
329		t.Run(tt.name, func(t *testing.T) {
330			result := loadStringMap(tt.input)
331			assert.Equal(t, tt.expected, result)
332		})
333	}
334}
335
336func TestLoadStringArrayMap(t *testing.T) {
337	tests := []struct {
338		name     string
339		input    any
340		expected map[string][]string
341	}{
342		{
343			name:     "nil input",
344			input:    nil,
345			expected: map[string][]string{},
346		},
347		{
348			name: "valid map",
349			input: map[string]any{
350				"group1": []any{"a", "b"},
351				"group2": []any{"c", "d", "e"},
352			},
353			expected: map[string][]string{
354				"group1": {"a", "b"},
355				"group2": {"c", "d", "e"},
356			},
357		},
358		{
359			name:     "invalid type",
360			input:    "not a map",
361			expected: map[string][]string{},
362		},
363		{
364			name: "non-array value",
365			input: map[string]any{
366				"array":  []any{"a", "b"},
367				"string": "not an array",
368			},
369			expected: map[string][]string{
370				"array": {"a", "b"},
371			},
372		},
373	}
374
375	for _, tt := range tests {
376		t.Run(tt.name, func(t *testing.T) {
377			result := loadStringArrayMap(tt.input)
378			assert.Equal(t, tt.expected, result)
379		})
380	}
381}

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