v2.9.0 8ebdfe0

service_test.go RawHistoryFolder

1package proxy
2
3import (
4	"context"
5	"testing"
6
7	"github.com/bnema/zerowrap"
8	"github.com/stretchr/testify/assert"
9	"github.com/stretchr/testify/mock"
10
11	inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
12	outmocks "github.com/bnema/gordon/internal/boundaries/out/mocks"
13	"github.com/bnema/gordon/internal/domain"
14)
15
16func testContext() context.Context {
17	return zerowrap.WithCtx(context.Background(), zerowrap.Default())
18}
19
20func TestService_GetTarget_FromCache(t *testing.T) {
21	runtime := outmocks.NewMockContainerRuntime(t)
22	containerSvc := inmocks.NewMockContainerService(t)
23	configSvc := inmocks.NewMockConfigService(t)
24
25	config := Config{
26		RegistryDomain: "registry.example.com",
27		RegistryPort:   5000,
28	}
29	svc := NewService(runtime, containerSvc, configSvc, config)
30	ctx := testContext()
31
32	// Pre-populate cache
33	cachedTarget := &domain.ProxyTarget{
34		Host:        "192.168.1.100",
35		Port:        8080,
36		ContainerID: "container-123",
37		Scheme:      "http",
38	}
39	svc.targets["app.example.com"] = cachedTarget
40
41	// No mock calls expected - should return from cache
42
43	result, err := svc.GetTarget(ctx, "app.example.com")
44
45	assert.NoError(t, err)
46	assert.Equal(t, cachedTarget, result)
47}
48
49func TestService_GetTarget_ContainerNotFound(t *testing.T) {
50	runtime := outmocks.NewMockContainerRuntime(t)
51	containerSvc := inmocks.NewMockContainerService(t)
52	configSvc := inmocks.NewMockConfigService(t)
53
54	config := Config{}
55	svc := NewService(runtime, containerSvc, configSvc, config)
56	ctx := testContext()
57
58	// Mock external routes (empty - no match)
59	configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{})
60	containerSvc.EXPECT().Get(mock.Anything, "app.example.com").Return(nil, false)
61
62	result, err := svc.GetTarget(ctx, "app.example.com")
63
64	assert.ErrorIs(t, err, domain.ErrNoTargetAvailable)
65	assert.Nil(t, result)
66}
67
68func TestService_GetTarget_ExternalRoute(t *testing.T) {
69	runtime := outmocks.NewMockContainerRuntime(t)
70	containerSvc := inmocks.NewMockContainerService(t)
71	configSvc := inmocks.NewMockConfigService(t)
72
73	svc := NewService(runtime, containerSvc, configSvc, Config{})
74	ctx := testContext()
75
76	// Mock external routes - use public IP to pass SSRF check
77	configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{
78		"reg.example.com": "203.0.113.10:5000", // TEST-NET-3 (RFC 5737) - documentation range
79	})
80
81	result, err := svc.GetTarget(ctx, "reg.example.com")
82
83	assert.NoError(t, err)
84	assert.Equal(t, "203.0.113.10", result.Host)
85	assert.Equal(t, 5000, result.Port)
86	assert.Equal(t, "", result.ContainerID)
87	assert.Equal(t, "http", result.Scheme)
88}
89
90func TestService_GetTarget_ExternalRoute_Cached(t *testing.T) {
91	runtime := outmocks.NewMockContainerRuntime(t)
92	containerSvc := inmocks.NewMockContainerService(t)
93	configSvc := inmocks.NewMockConfigService(t)
94
95	svc := NewService(runtime, containerSvc, configSvc, Config{})
96	ctx := testContext()
97
98	// First call - should resolve external route (use public IP)
99	configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{
100		"reg.example.com": "203.0.113.10:5000",
101	}).Once()
102
103	result1, err := svc.GetTarget(ctx, "reg.example.com")
104	assert.NoError(t, err)
105	assert.Equal(t, "203.0.113.10", result1.Host)
106
107	// Second call - should return from cache (no mock call needed)
108	result2, err := svc.GetTarget(ctx, "reg.example.com")
109	assert.NoError(t, err)
110	assert.Equal(t, result1, result2)
111}
112
113func TestService_GetTarget_ExternalRoute_SSRFBlocked(t *testing.T) {
114	runtime := outmocks.NewMockContainerRuntime(t)
115	containerSvc := inmocks.NewMockContainerService(t)
116	configSvc := inmocks.NewMockConfigService(t)
117
118	svc := NewService(runtime, containerSvc, configSvc, Config{})
119	ctx := testContext()
120
121	tests := []struct {
122		name   string
123		target string
124	}{
125		{"localhost", "localhost:5000"},
126		{"loopback IP", "127.0.0.1:5000"},
127		{"private network 10.x", "10.0.0.1:5000"},
128		{"private network 172.x", "172.16.0.1:5000"},
129		{"private network 192.168.x", "192.168.1.1:5000"},
130		{"AWS metadata", "169.254.169.254:80"},
131	}
132
133	for _, tt := range tests {
134		t.Run(tt.name, func(t *testing.T) {
135			// Clear cache
136			svc.targets = make(map[string]*domain.ProxyTarget)
137
138			configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{
139				"ssrf.example.com": tt.target,
140			}).Once()
141
142			result, err := svc.GetTarget(ctx, "ssrf.example.com")
143
144			assert.Error(t, err)
145			assert.ErrorIs(t, err, domain.ErrSSRFBlocked)
146			assert.Nil(t, result)
147		})
148	}
149}
150
151func TestService_GetTarget_ExternalRoute_InvalidTarget(t *testing.T) {
152	runtime := outmocks.NewMockContainerRuntime(t)
153	containerSvc := inmocks.NewMockContainerService(t)
154	configSvc := inmocks.NewMockConfigService(t)
155
156	svc := NewService(runtime, containerSvc, configSvc, Config{})
157	ctx := testContext()
158
159	// Mock external routes with invalid format (missing port)
160	configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{
161		"invalid.example.com": "not-valid-format",
162	})
163
164	result, err := svc.GetTarget(ctx, "invalid.example.com")
165
166	assert.Error(t, err)
167	assert.Nil(t, result)
168}
169
170func TestService_GetTarget_ExternalRoute_InvalidPort(t *testing.T) {
171	runtime := outmocks.NewMockContainerRuntime(t)
172	containerSvc := inmocks.NewMockContainerService(t)
173	configSvc := inmocks.NewMockConfigService(t)
174
175	svc := NewService(runtime, containerSvc, configSvc, Config{})
176	ctx := testContext()
177
178	// Mock external routes with invalid port
179	configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{
180		"invalid.example.com": "localhost:abc",
181	})
182
183	result, err := svc.GetTarget(ctx, "invalid.example.com")
184
185	assert.Error(t, err)
186	assert.Nil(t, result)
187}
188
189func TestService_RegisterTarget(t *testing.T) {
190	runtime := outmocks.NewMockContainerRuntime(t)
191	containerSvc := inmocks.NewMockContainerService(t)
192	configSvc := inmocks.NewMockConfigService(t)
193
194	svc := NewService(runtime, containerSvc, configSvc, Config{})
195	ctx := testContext()
196
197	target := &domain.ProxyTarget{
198		Host:        "192.168.1.100",
199		Port:        8080,
200		ContainerID: "container-123",
201		Scheme:      "http",
202	}
203
204	err := svc.RegisterTarget(ctx, "app.example.com", target)
205
206	assert.NoError(t, err)
207
208	// Verify target is cached
209	svc.mu.RLock()
210	cached := svc.targets["app.example.com"]
211	svc.mu.RUnlock()
212
213	assert.Equal(t, target, cached)
214}
215
216func TestService_UnregisterTarget(t *testing.T) {
217	runtime := outmocks.NewMockContainerRuntime(t)
218	containerSvc := inmocks.NewMockContainerService(t)
219	configSvc := inmocks.NewMockConfigService(t)
220
221	svc := NewService(runtime, containerSvc, configSvc, Config{})
222	ctx := testContext()
223
224	// Pre-populate
225	svc.targets["app.example.com"] = &domain.ProxyTarget{
226		Host: "192.168.1.100",
227		Port: 8080,
228	}
229
230	err := svc.UnregisterTarget(ctx, "app.example.com")
231
232	assert.NoError(t, err)
233
234	// Verify target is removed
235	svc.mu.RLock()
236	_, exists := svc.targets["app.example.com"]
237	svc.mu.RUnlock()
238
239	assert.False(t, exists)
240}
241
242func TestService_RefreshTargets(t *testing.T) {
243	runtime := outmocks.NewMockContainerRuntime(t)
244	containerSvc := inmocks.NewMockContainerService(t)
245	configSvc := inmocks.NewMockConfigService(t)
246
247	svc := NewService(runtime, containerSvc, configSvc, Config{})
248	ctx := testContext()
249
250	// Pre-populate with some targets
251	svc.targets["app1.example.com"] = &domain.ProxyTarget{Host: "192.168.1.100"}
252	svc.targets["app2.example.com"] = &domain.ProxyTarget{Host: "192.168.1.101"}
253
254	err := svc.RefreshTargets(ctx)
255
256	assert.NoError(t, err)
257
258	// Verify all targets are cleared
259	svc.mu.RLock()
260	count := len(svc.targets)
261	svc.mu.RUnlock()
262
263	assert.Equal(t, 0, count)
264}
265
266func TestService_UpdateConfig(t *testing.T) {
267	runtime := outmocks.NewMockContainerRuntime(t)
268	containerSvc := inmocks.NewMockContainerService(t)
269	configSvc := inmocks.NewMockConfigService(t)
270
271	svc := NewService(runtime, containerSvc, configSvc, Config{
272		RegistryDomain: "old.registry.com",
273		RegistryPort:   5000,
274	})
275
276	newConfig := Config{
277		RegistryDomain: "new.registry.com",
278		RegistryPort:   5001,
279	}
280
281	svc.UpdateConfig(newConfig)
282
283	assert.Equal(t, "new.registry.com", svc.config.RegistryDomain)
284	assert.Equal(t, 5001, svc.config.RegistryPort)
285}
286
287func TestService_isRunningInContainer(t *testing.T) {
288	runtime := outmocks.NewMockContainerRuntime(t)
289	containerSvc := inmocks.NewMockContainerService(t)
290	configSvc := inmocks.NewMockConfigService(t)
291
292	svc := NewService(runtime, containerSvc, configSvc, Config{})
293
294	// This test just verifies the method doesn't panic
295	// The actual result depends on the environment
296	result := svc.isRunningInContainer()
297	assert.IsType(t, true, result) // Just verify it returns a bool
298}
299
300func TestService_InvalidateTarget(t *testing.T) {
301	runtime := outmocks.NewMockContainerRuntime(t)
302	containerSvc := inmocks.NewMockContainerService(t)
303	configSvc := inmocks.NewMockConfigService(t)
304
305	svc := NewService(runtime, containerSvc, configSvc, Config{})
306	ctx := testContext()
307
308	// Pre-populate cache with target
309	svc.targets["app.example.com"] = &domain.ProxyTarget{
310		Host:        "192.168.1.100",
311		Port:        8080,
312		ContainerID: "old-container",
313	}
314
315	// Invalidate the target
316	svc.InvalidateTarget(ctx, "app.example.com")
317
318	// Verify target is removed from cache
319	svc.mu.RLock()
320	_, exists := svc.targets["app.example.com"]
321	svc.mu.RUnlock()
322
323	assert.False(t, exists, "target should be removed from cache after invalidation")
324}
325
326func TestContainerDeployedHandler_CanHandle(t *testing.T) {
327	handler := NewContainerDeployedHandler(testContext(), nil)
328
329	assert.True(t, handler.CanHandle(domain.EventContainerDeployed))
330	assert.False(t, handler.CanHandle(domain.EventImagePushed))
331	assert.False(t, handler.CanHandle(domain.EventConfigReload))
332}
333
334func TestContainerDeployedHandler_Handle_InvalidatesCache(t *testing.T) {
335	runtime := outmocks.NewMockContainerRuntime(t)
336	containerSvc := inmocks.NewMockContainerService(t)
337	configSvc := inmocks.NewMockConfigService(t)
338
339	svc := NewService(runtime, containerSvc, configSvc, Config{})
340	ctx := testContext()
341
342	// Pre-populate cache
343	svc.targets["app.example.com"] = &domain.ProxyTarget{
344		Host:        "192.168.1.100",
345		Port:        8080,
346		ContainerID: "old-container",
347	}
348
349	// Create handler with service as invalidator
350	handler := NewContainerDeployedHandler(ctx, svc)
351
352	// Simulate container deployed event
353	event := domain.Event{
354		ID:    "event-123",
355		Type:  domain.EventContainerDeployed,
356		Route: "app.example.com",
357		Data: &domain.ContainerEventPayload{
358			ContainerID: "new-container",
359			Domain:      "app.example.com",
360		},
361	}
362
363	err := handler.Handle(event)
364
365	assert.NoError(t, err)
366
367	// Verify target was invalidated
368	svc.mu.RLock()
369	_, exists := svc.targets["app.example.com"]
370	svc.mu.RUnlock()
371
372	assert.False(t, exists, "cache should be invalidated after container deployed event")
373}
374
375func TestContainerDeployedHandler_Handle_NoDomain(t *testing.T) {
376	runtime := outmocks.NewMockContainerRuntime(t)
377	containerSvc := inmocks.NewMockContainerService(t)
378	configSvc := inmocks.NewMockConfigService(t)
379
380	svc := NewService(runtime, containerSvc, configSvc, Config{})
381	ctx := testContext()
382
383	handler := NewContainerDeployedHandler(ctx, svc)
384
385	// Event with no domain
386	event := domain.Event{
387		ID:   "event-123",
388		Type: domain.EventContainerDeployed,
389		Data: &domain.ContainerEventPayload{
390			ContainerID: "new-container",
391		},
392	}
393
394	// Should not error, just skip
395	err := handler.Handle(event)
396	assert.NoError(t, err)
397}

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