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 "gordon/internal/boundaries/in/mocks"
12 outmocks "gordon/internal/boundaries/out/mocks"
13 "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
77 configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{
78 "reg.example.com": "localhost:5000",
79 })
80
81 result, err := svc.GetTarget(ctx, "reg.example.com")
82
83 assert.NoError(t, err)
84 assert.Equal(t, "localhost", 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
99 configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{
100 "reg.example.com": "localhost:5000",
101 }).Once()
102
103 result1, err := svc.GetTarget(ctx, "reg.example.com")
104 assert.NoError(t, err)
105 assert.Equal(t, "localhost", 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_InvalidTarget(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 // Mock external routes with invalid format (missing port)
122 configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{
123 "invalid.example.com": "not-valid-format",
124 })
125
126 result, err := svc.GetTarget(ctx, "invalid.example.com")
127
128 assert.Error(t, err)
129 assert.Nil(t, result)
130}
131
132func TestService_GetTarget_ExternalRoute_InvalidPort(t *testing.T) {
133 runtime := outmocks.NewMockContainerRuntime(t)
134 containerSvc := inmocks.NewMockContainerService(t)
135 configSvc := inmocks.NewMockConfigService(t)
136
137 svc := NewService(runtime, containerSvc, configSvc, Config{})
138 ctx := testContext()
139
140 // Mock external routes with invalid port
141 configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{
142 "invalid.example.com": "localhost:abc",
143 })
144
145 result, err := svc.GetTarget(ctx, "invalid.example.com")
146
147 assert.Error(t, err)
148 assert.Nil(t, result)
149}
150
151func TestService_RegisterTarget(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 target := &domain.ProxyTarget{
160 Host: "192.168.1.100",
161 Port: 8080,
162 ContainerID: "container-123",
163 Scheme: "http",
164 }
165
166 err := svc.RegisterTarget(ctx, "app.example.com", target)
167
168 assert.NoError(t, err)
169
170 // Verify target is cached
171 svc.mu.RLock()
172 cached := svc.targets["app.example.com"]
173 svc.mu.RUnlock()
174
175 assert.Equal(t, target, cached)
176}
177
178func TestService_UnregisterTarget(t *testing.T) {
179 runtime := outmocks.NewMockContainerRuntime(t)
180 containerSvc := inmocks.NewMockContainerService(t)
181 configSvc := inmocks.NewMockConfigService(t)
182
183 svc := NewService(runtime, containerSvc, configSvc, Config{})
184 ctx := testContext()
185
186 // Pre-populate
187 svc.targets["app.example.com"] = &domain.ProxyTarget{
188 Host: "192.168.1.100",
189 Port: 8080,
190 }
191
192 err := svc.UnregisterTarget(ctx, "app.example.com")
193
194 assert.NoError(t, err)
195
196 // Verify target is removed
197 svc.mu.RLock()
198 _, exists := svc.targets["app.example.com"]
199 svc.mu.RUnlock()
200
201 assert.False(t, exists)
202}
203
204func TestService_RefreshTargets(t *testing.T) {
205 runtime := outmocks.NewMockContainerRuntime(t)
206 containerSvc := inmocks.NewMockContainerService(t)
207 configSvc := inmocks.NewMockConfigService(t)
208
209 svc := NewService(runtime, containerSvc, configSvc, Config{})
210 ctx := testContext()
211
212 // Pre-populate with some targets
213 svc.targets["app1.example.com"] = &domain.ProxyTarget{Host: "192.168.1.100"}
214 svc.targets["app2.example.com"] = &domain.ProxyTarget{Host: "192.168.1.101"}
215
216 err := svc.RefreshTargets(ctx)
217
218 assert.NoError(t, err)
219
220 // Verify all targets are cleared
221 svc.mu.RLock()
222 count := len(svc.targets)
223 svc.mu.RUnlock()
224
225 assert.Equal(t, 0, count)
226}
227
228func TestService_UpdateConfig(t *testing.T) {
229 runtime := outmocks.NewMockContainerRuntime(t)
230 containerSvc := inmocks.NewMockContainerService(t)
231 configSvc := inmocks.NewMockConfigService(t)
232
233 svc := NewService(runtime, containerSvc, configSvc, Config{
234 RegistryDomain: "old.registry.com",
235 RegistryPort: 5000,
236 })
237
238 newConfig := Config{
239 RegistryDomain: "new.registry.com",
240 RegistryPort: 5001,
241 }
242
243 svc.UpdateConfig(newConfig)
244
245 assert.Equal(t, "new.registry.com", svc.config.RegistryDomain)
246 assert.Equal(t, 5001, svc.config.RegistryPort)
247}
248
249func TestService_isRunningInContainer(t *testing.T) {
250 runtime := outmocks.NewMockContainerRuntime(t)
251 containerSvc := inmocks.NewMockContainerService(t)
252 configSvc := inmocks.NewMockConfigService(t)
253
254 svc := NewService(runtime, containerSvc, configSvc, Config{})
255
256 // This test just verifies the method doesn't panic
257 // The actual result depends on the environment
258 result := svc.isRunningInContainer()
259 assert.IsType(t, true, result) // Just verify it returns a bool
260}
261
262func TestService_InvalidateTarget(t *testing.T) {
263 runtime := outmocks.NewMockContainerRuntime(t)
264 containerSvc := inmocks.NewMockContainerService(t)
265 configSvc := inmocks.NewMockConfigService(t)
266
267 svc := NewService(runtime, containerSvc, configSvc, Config{})
268 ctx := testContext()
269
270 // Pre-populate cache with target
271 svc.targets["app.example.com"] = &domain.ProxyTarget{
272 Host: "192.168.1.100",
273 Port: 8080,
274 ContainerID: "old-container",
275 }
276
277 // Invalidate the target
278 svc.InvalidateTarget(ctx, "app.example.com")
279
280 // Verify target is removed from cache
281 svc.mu.RLock()
282 _, exists := svc.targets["app.example.com"]
283 svc.mu.RUnlock()
284
285 assert.False(t, exists, "target should be removed from cache after invalidation")
286}
287
288func TestContainerDeployedHandler_CanHandle(t *testing.T) {
289 handler := NewContainerDeployedHandler(testContext(), nil)
290
291 assert.True(t, handler.CanHandle(domain.EventContainerDeployed))
292 assert.False(t, handler.CanHandle(domain.EventImagePushed))
293 assert.False(t, handler.CanHandle(domain.EventConfigReload))
294}
295
296func TestContainerDeployedHandler_Handle_InvalidatesCache(t *testing.T) {
297 runtime := outmocks.NewMockContainerRuntime(t)
298 containerSvc := inmocks.NewMockContainerService(t)
299 configSvc := inmocks.NewMockConfigService(t)
300
301 svc := NewService(runtime, containerSvc, configSvc, Config{})
302 ctx := testContext()
303
304 // Pre-populate cache
305 svc.targets["app.example.com"] = &domain.ProxyTarget{
306 Host: "192.168.1.100",
307 Port: 8080,
308 ContainerID: "old-container",
309 }
310
311 // Create handler with service as invalidator
312 handler := NewContainerDeployedHandler(ctx, svc)
313
314 // Simulate container deployed event
315 event := domain.Event{
316 ID: "event-123",
317 Type: domain.EventContainerDeployed,
318 Route: "app.example.com",
319 Data: &domain.ContainerEventPayload{
320 ContainerID: "new-container",
321 Domain: "app.example.com",
322 },
323 }
324
325 err := handler.Handle(event)
326
327 assert.NoError(t, err)
328
329 // Verify target was invalidated
330 svc.mu.RLock()
331 _, exists := svc.targets["app.example.com"]
332 svc.mu.RUnlock()
333
334 assert.False(t, exists, "cache should be invalidated after container deployed event")
335}
336
337func TestContainerDeployedHandler_Handle_NoDomain(t *testing.T) {
338 runtime := outmocks.NewMockContainerRuntime(t)
339 containerSvc := inmocks.NewMockContainerService(t)
340 configSvc := inmocks.NewMockConfigService(t)
341
342 svc := NewService(runtime, containerSvc, configSvc, Config{})
343 ctx := testContext()
344
345 handler := NewContainerDeployedHandler(ctx, svc)
346
347 // Event with no domain
348 event := domain.Event{
349 ID: "event-123",
350 Type: domain.EventContainerDeployed,
351 Data: &domain.ContainerEventPayload{
352 ContainerID: "new-container",
353 },
354 }
355
356 // Should not error, just skip
357 err := handler.Handle(event)
358 assert.NoError(t, err)
359}