1package admin
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "net/http"
9 "net/http/httptest"
10 "testing"
11
12 "github.com/bnema/zerowrap"
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/mock"
15
16 inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
17 "github.com/bnema/gordon/internal/domain"
18)
19
20func testLogger() zerowrap.Logger {
21 return zerowrap.Default()
22}
23
24func ctxWithScopes(scopes ...string) context.Context {
25 ctx := context.Background()
26 return context.WithValue(ctx, domain.ContextKeyScopes, scopes)
27}
28
29// Routes endpoint tests
30
31func TestHandler_RoutesGet_RequiresReadScope(t *testing.T) {
32 configSvc := inmocks.NewMockConfigService(t)
33 authSvc := inmocks.NewMockAuthService(t)
34 containerSvc := inmocks.NewMockContainerService(t)
35 secretSvc := inmocks.NewMockSecretService(t)
36
37 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger())
38
39 tests := []struct {
40 name string
41 scopes []string
42 wantStatus int
43 }{
44 {
45 name: "routes read access granted",
46 scopes: []string{"admin:routes:read"},
47 wantStatus: http.StatusOK,
48 },
49 {
50 name: "routes wildcard access granted",
51 scopes: []string{"admin:routes:*"},
52 wantStatus: http.StatusOK,
53 },
54 {
55 name: "all admin access granted",
56 scopes: []string{"admin:*:*"},
57 wantStatus: http.StatusOK,
58 },
59 {
60 name: "only write access denied",
61 scopes: []string{"admin:routes:write"},
62 wantStatus: http.StatusForbidden,
63 },
64 {
65 name: "wrong resource denied",
66 scopes: []string{"admin:secrets:read"},
67 wantStatus: http.StatusForbidden,
68 },
69 {
70 name: "no scopes denied",
71 scopes: []string{},
72 wantStatus: http.StatusForbidden,
73 },
74 }
75
76 for _, tt := range tests {
77 t.Run(tt.name, func(t *testing.T) {
78 if tt.wantStatus == http.StatusOK {
79 configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{}).Maybe()
80 }
81
82 req := httptest.NewRequest("GET", "/admin/routes", nil)
83 req = req.WithContext(ctxWithScopes(tt.scopes...))
84 rec := httptest.NewRecorder()
85
86 handler.ServeHTTP(rec, req)
87
88 assert.Equal(t, tt.wantStatus, rec.Code)
89 })
90 }
91}
92
93func TestHandler_RoutesPost_RequiresWriteScope(t *testing.T) {
94 configSvc := inmocks.NewMockConfigService(t)
95 authSvc := inmocks.NewMockAuthService(t)
96 containerSvc := inmocks.NewMockContainerService(t)
97 secretSvc := inmocks.NewMockSecretService(t)
98 registrySvc := inmocks.NewMockRegistryService(t)
99
100 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger())
101
102 routeJSON := `{"domain": "app.example.com", "image": "myapp:latest"}`
103
104 tests := []struct {
105 name string
106 scopes []string
107 wantStatus int
108 }{
109 {
110 name: "routes write access granted",
111 scopes: []string{"admin:routes:write"},
112 wantStatus: http.StatusCreated,
113 },
114 {
115 name: "routes wildcard access granted",
116 scopes: []string{"admin:routes:*"},
117 wantStatus: http.StatusCreated,
118 },
119 {
120 name: "all admin access granted",
121 scopes: []string{"admin:*:*"},
122 wantStatus: http.StatusCreated,
123 },
124 {
125 name: "only read access denied",
126 scopes: []string{"admin:routes:read"},
127 wantStatus: http.StatusForbidden,
128 },
129 {
130 name: "wrong resource denied",
131 scopes: []string{"admin:secrets:write"},
132 wantStatus: http.StatusForbidden,
133 },
134 }
135
136 for _, tt := range tests {
137 t.Run(tt.name, func(t *testing.T) {
138 if tt.wantStatus == http.StatusCreated {
139 configSvc.EXPECT().AddRoute(mock.Anything, mock.AnythingOfType("domain.Route")).Return(nil).Maybe()
140 registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(nil, nil).Maybe()
141 }
142
143 req := httptest.NewRequest("POST", "/admin/routes", bytes.NewBufferString(routeJSON))
144 req = req.WithContext(ctxWithScopes(tt.scopes...))
145 rec := httptest.NewRecorder()
146
147 handler.ServeHTTP(rec, req)
148
149 assert.Equal(t, tt.wantStatus, rec.Code)
150 })
151 }
152}
153
154func TestHandler_RoutesPut_RequiresWriteScope(t *testing.T) {
155 configSvc := inmocks.NewMockConfigService(t)
156 authSvc := inmocks.NewMockAuthService(t)
157 containerSvc := inmocks.NewMockContainerService(t)
158 secretSvc := inmocks.NewMockSecretService(t)
159 registrySvc := inmocks.NewMockRegistryService(t)
160
161 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger())
162
163 routeJSON := `{"image": "myapp:v2"}`
164
165 tests := []struct {
166 name string
167 scopes []string
168 wantStatus int
169 }{
170 {
171 name: "routes write access granted",
172 scopes: []string{"admin:routes:write"},
173 wantStatus: http.StatusOK,
174 },
175 {
176 name: "only read access denied",
177 scopes: []string{"admin:routes:read"},
178 wantStatus: http.StatusForbidden,
179 },
180 }
181
182 for _, tt := range tests {
183 t.Run(tt.name, func(t *testing.T) {
184 if tt.wantStatus == http.StatusOK {
185 configSvc.EXPECT().UpdateRoute(mock.Anything, mock.AnythingOfType("domain.Route")).Return(nil).Maybe()
186 registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "v2").Return(nil, nil).Maybe()
187 }
188
189 req := httptest.NewRequest("PUT", "/admin/routes/app.example.com", bytes.NewBufferString(routeJSON))
190 req = req.WithContext(ctxWithScopes(tt.scopes...))
191 rec := httptest.NewRecorder()
192
193 handler.ServeHTTP(rec, req)
194
195 assert.Equal(t, tt.wantStatus, rec.Code)
196 })
197 }
198}
199
200func TestHandler_RoutesDelete_RequiresWriteScope(t *testing.T) {
201 configSvc := inmocks.NewMockConfigService(t)
202 authSvc := inmocks.NewMockAuthService(t)
203 containerSvc := inmocks.NewMockContainerService(t)
204 secretSvc := inmocks.NewMockSecretService(t)
205
206 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger())
207
208 tests := []struct {
209 name string
210 scopes []string
211 wantStatus int
212 }{
213 {
214 name: "routes write access granted",
215 scopes: []string{"admin:routes:write"},
216 wantStatus: http.StatusOK,
217 },
218 {
219 name: "only read access denied",
220 scopes: []string{"admin:routes:read"},
221 wantStatus: http.StatusForbidden,
222 },
223 }
224
225 for _, tt := range tests {
226 t.Run(tt.name, func(t *testing.T) {
227 if tt.wantStatus == http.StatusOK {
228 configSvc.EXPECT().RemoveRoute(mock.Anything, "app.example.com").Return(nil).Maybe()
229 }
230
231 req := httptest.NewRequest("DELETE", "/admin/routes/app.example.com", nil)
232 req = req.WithContext(ctxWithScopes(tt.scopes...))
233 rec := httptest.NewRecorder()
234
235 handler.ServeHTTP(rec, req)
236
237 assert.Equal(t, tt.wantStatus, rec.Code)
238 })
239 }
240}
241
242// Secrets endpoint tests
243
244func TestHandler_SecretsGet_RequiresReadScope(t *testing.T) {
245 configSvc := inmocks.NewMockConfigService(t)
246 authSvc := inmocks.NewMockAuthService(t)
247 containerSvc := inmocks.NewMockContainerService(t)
248 secretSvc := inmocks.NewMockSecretService(t)
249
250 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger())
251
252 tests := []struct {
253 name string
254 scopes []string
255 wantStatus int
256 }{
257 {
258 name: "secrets read access granted",
259 scopes: []string{"admin:secrets:read"},
260 wantStatus: http.StatusOK,
261 },
262 {
263 name: "secrets wildcard access granted",
264 scopes: []string{"admin:secrets:*"},
265 wantStatus: http.StatusOK,
266 },
267 {
268 name: "all admin access granted",
269 scopes: []string{"admin:*:*"},
270 wantStatus: http.StatusOK,
271 },
272 {
273 name: "only write access denied",
274 scopes: []string{"admin:secrets:write"},
275 wantStatus: http.StatusForbidden,
276 },
277 {
278 name: "wrong resource denied",
279 scopes: []string{"admin:routes:read"},
280 wantStatus: http.StatusForbidden,
281 },
282 }
283
284 for _, tt := range tests {
285 t.Run(tt.name, func(t *testing.T) {
286 if tt.wantStatus == http.StatusOK {
287 secretSvc.EXPECT().ListKeysWithAttachments(mock.Anything, "app.example.com").Return([]string{}, nil, nil).Maybe()
288 }
289
290 req := httptest.NewRequest("GET", "/admin/secrets/app.example.com", nil)
291 req = req.WithContext(ctxWithScopes(tt.scopes...))
292 rec := httptest.NewRecorder()
293
294 handler.ServeHTTP(rec, req)
295
296 assert.Equal(t, tt.wantStatus, rec.Code)
297 })
298 }
299}
300
301func TestHandler_SecretsPost_RequiresWriteScope(t *testing.T) {
302 configSvc := inmocks.NewMockConfigService(t)
303 authSvc := inmocks.NewMockAuthService(t)
304 containerSvc := inmocks.NewMockContainerService(t)
305 secretSvc := inmocks.NewMockSecretService(t)
306
307 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger())
308
309 secretsJSON := `{"API_KEY": "secret123"}`
310
311 tests := []struct {
312 name string
313 scopes []string
314 wantStatus int
315 }{
316 {
317 name: "secrets write access granted",
318 scopes: []string{"admin:secrets:write"},
319 wantStatus: http.StatusOK,
320 },
321 {
322 name: "only read access denied",
323 scopes: []string{"admin:secrets:read"},
324 wantStatus: http.StatusForbidden,
325 },
326 {
327 name: "wrong resource denied",
328 scopes: []string{"admin:routes:write"},
329 wantStatus: http.StatusForbidden,
330 },
331 }
332
333 for _, tt := range tests {
334 t.Run(tt.name, func(t *testing.T) {
335 if tt.wantStatus == http.StatusOK {
336 secretSvc.EXPECT().Set(mock.Anything, "app.example.com", mock.AnythingOfType("map[string]string")).Return(nil).Maybe()
337 }
338
339 req := httptest.NewRequest("POST", "/admin/secrets/app.example.com", bytes.NewBufferString(secretsJSON))
340 req = req.WithContext(ctxWithScopes(tt.scopes...))
341 rec := httptest.NewRecorder()
342
343 handler.ServeHTTP(rec, req)
344
345 assert.Equal(t, tt.wantStatus, rec.Code)
346 })
347 }
348}
349
350func TestHandler_SecretsDelete_RequiresWriteScope(t *testing.T) {
351 configSvc := inmocks.NewMockConfigService(t)
352 authSvc := inmocks.NewMockAuthService(t)
353 containerSvc := inmocks.NewMockContainerService(t)
354 secretSvc := inmocks.NewMockSecretService(t)
355
356 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger())
357
358 tests := []struct {
359 name string
360 scopes []string
361 wantStatus int
362 }{
363 {
364 name: "secrets write access granted",
365 scopes: []string{"admin:secrets:write"},
366 wantStatus: http.StatusOK,
367 },
368 {
369 name: "only read access denied",
370 scopes: []string{"admin:secrets:read"},
371 wantStatus: http.StatusForbidden,
372 },
373 }
374
375 for _, tt := range tests {
376 t.Run(tt.name, func(t *testing.T) {
377 if tt.wantStatus == http.StatusOK {
378 secretSvc.EXPECT().Delete(mock.Anything, "app.example.com", "API_KEY").Return(nil).Maybe()
379 }
380
381 req := httptest.NewRequest("DELETE", "/admin/secrets/app.example.com/API_KEY", nil)
382 req = req.WithContext(ctxWithScopes(tt.scopes...))
383 rec := httptest.NewRecorder()
384
385 handler.ServeHTTP(rec, req)
386
387 assert.Equal(t, tt.wantStatus, rec.Code)
388 })
389 }
390}
391
392// Status endpoint tests
393
394func TestHandler_Status_RequiresReadScope(t *testing.T) {
395 configSvc := inmocks.NewMockConfigService(t)
396 authSvc := inmocks.NewMockAuthService(t)
397 containerSvc := inmocks.NewMockContainerService(t)
398 secretSvc := inmocks.NewMockSecretService(t)
399
400 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger())
401
402 tests := []struct {
403 name string
404 scopes []string
405 wantStatus int
406 }{
407 {
408 name: "status read access granted",
409 scopes: []string{"admin:status:read"},
410 wantStatus: http.StatusOK,
411 },
412 {
413 name: "all admin access granted",
414 scopes: []string{"admin:*:*"},
415 wantStatus: http.StatusOK,
416 },
417 {
418 name: "wrong resource denied",
419 scopes: []string{"admin:routes:read"},
420 wantStatus: http.StatusForbidden,
421 },
422 }
423
424 for _, tt := range tests {
425 t.Run(tt.name, func(t *testing.T) {
426 if tt.wantStatus == http.StatusOK {
427 configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{}).Maybe()
428 configSvc.EXPECT().GetRegistryDomain().Return("registry.example.com").Maybe()
429 configSvc.EXPECT().GetRegistryPort().Return(5000).Maybe()
430 configSvc.EXPECT().GetServerPort().Return(8080).Maybe()
431 configSvc.EXPECT().IsAutoRouteEnabled().Return(true).Maybe()
432 configSvc.EXPECT().IsNetworkIsolationEnabled().Return(false).Maybe()
433 }
434
435 req := httptest.NewRequest("GET", "/admin/status", nil)
436 req = req.WithContext(ctxWithScopes(tt.scopes...))
437 rec := httptest.NewRecorder()
438
439 handler.ServeHTTP(rec, req)
440
441 assert.Equal(t, tt.wantStatus, rec.Code)
442 })
443 }
444}
445
446// Config endpoint tests
447
448func TestHandler_Config_RequiresReadScope(t *testing.T) {
449 configSvc := inmocks.NewMockConfigService(t)
450 authSvc := inmocks.NewMockAuthService(t)
451 containerSvc := inmocks.NewMockContainerService(t)
452 secretSvc := inmocks.NewMockSecretService(t)
453
454 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger())
455
456 tests := []struct {
457 name string
458 scopes []string
459 wantStatus int
460 }{
461 {
462 name: "config read access granted",
463 scopes: []string{"admin:config:read"},
464 wantStatus: http.StatusOK,
465 },
466 {
467 name: "all admin access granted",
468 scopes: []string{"admin:*:*"},
469 wantStatus: http.StatusOK,
470 },
471 {
472 name: "wrong resource denied",
473 scopes: []string{"admin:routes:read"},
474 wantStatus: http.StatusForbidden,
475 },
476 }
477
478 for _, tt := range tests {
479 t.Run(tt.name, func(t *testing.T) {
480 if tt.wantStatus == http.StatusOK {
481 configSvc.EXPECT().GetServerPort().Return(8080).Maybe()
482 configSvc.EXPECT().GetRegistryPort().Return(5000).Maybe()
483 configSvc.EXPECT().GetRegistryDomain().Return("registry.example.com").Maybe()
484 configSvc.EXPECT().GetDataDir().Return("/var/lib/gordon").Maybe()
485 configSvc.EXPECT().IsAutoRouteEnabled().Return(true).Maybe()
486 configSvc.EXPECT().IsNetworkIsolationEnabled().Return(false).Maybe()
487 configSvc.EXPECT().GetNetworkPrefix().Return("gordon").Maybe()
488 configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{}).Maybe()
489 configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{}).Maybe()
490 }
491
492 req := httptest.NewRequest("GET", "/admin/config", nil)
493 req = req.WithContext(ctxWithScopes(tt.scopes...))
494 rec := httptest.NewRecorder()
495
496 handler.ServeHTTP(rec, req)
497
498 assert.Equal(t, tt.wantStatus, rec.Code)
499 })
500 }
501}
502
503// Reload endpoint tests
504
505func TestHandler_Reload_RequiresWriteScope(t *testing.T) {
506 configSvc := inmocks.NewMockConfigService(t)
507 authSvc := inmocks.NewMockAuthService(t)
508 containerSvc := inmocks.NewMockContainerService(t)
509 secretSvc := inmocks.NewMockSecretService(t)
510
511 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger())
512
513 tests := []struct {
514 name string
515 scopes []string
516 wantStatus int
517 }{
518 {
519 name: "config write access granted",
520 scopes: []string{"admin:config:write"},
521 wantStatus: http.StatusOK,
522 },
523 {
524 name: "all admin access granted",
525 scopes: []string{"admin:*:*"},
526 wantStatus: http.StatusOK,
527 },
528 {
529 name: "only read access denied",
530 scopes: []string{"admin:config:read"},
531 wantStatus: http.StatusForbidden,
532 },
533 {
534 name: "wrong resource denied",
535 scopes: []string{"admin:routes:write"},
536 wantStatus: http.StatusForbidden,
537 },
538 }
539
540 for _, tt := range tests {
541 t.Run(tt.name, func(t *testing.T) {
542 if tt.wantStatus == http.StatusOK {
543 configSvc.EXPECT().Reload(mock.Anything).Return(nil).Maybe()
544 }
545
546 req := httptest.NewRequest("POST", "/admin/reload", nil)
547 req = req.WithContext(ctxWithScopes(tt.scopes...))
548 rec := httptest.NewRecorder()
549
550 handler.ServeHTTP(rec, req)
551
552 assert.Equal(t, tt.wantStatus, rec.Code)
553 })
554 }
555}
556
557// Functional tests
558
559func TestHandler_RoutesGet_ReturnsRoutes(t *testing.T) {
560 configSvc := inmocks.NewMockConfigService(t)
561 authSvc := inmocks.NewMockAuthService(t)
562 containerSvc := inmocks.NewMockContainerService(t)
563 secretSvc := inmocks.NewMockSecretService(t)
564
565 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger())
566
567 expectedRoutes := []domain.Route{
568 {Domain: "app1.example.com", Image: "app1:latest"},
569 {Domain: "app2.example.com", Image: "app2:v1.0"},
570 }
571 configSvc.EXPECT().GetRoutes(mock.Anything).Return(expectedRoutes)
572
573 req := httptest.NewRequest("GET", "/admin/routes", nil)
574 req = req.WithContext(ctxWithScopes("admin:routes:read"))
575 rec := httptest.NewRecorder()
576
577 handler.ServeHTTP(rec, req)
578
579 assert.Equal(t, http.StatusOK, rec.Code)
580
581 var response struct {
582 Routes []struct {
583 Domain string `json:"domain"`
584 Image string `json:"image"`
585 HTTPS bool `json:"https"`
586 } `json:"routes"`
587 }
588 err := json.NewDecoder(rec.Body).Decode(&response)
589 assert.NoError(t, err)
590
591 assert.Len(t, response.Routes, 2)
592}
593
594func TestHandler_RoutesGet_SingleRoute(t *testing.T) {
595 configSvc := inmocks.NewMockConfigService(t)
596 authSvc := inmocks.NewMockAuthService(t)
597 containerSvc := inmocks.NewMockContainerService(t)
598 secretSvc := inmocks.NewMockSecretService(t)
599
600 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger())
601
602 configSvc.EXPECT().GetRoute(mock.Anything, "app.example.com").Return(&domain.Route{
603 Domain: "app.example.com", Image: "app:latest",
604 }, nil)
605
606 req := httptest.NewRequest("GET", "/admin/routes/app.example.com", nil)
607 req = req.WithContext(ctxWithScopes("admin:routes:read"))
608 rec := httptest.NewRecorder()
609
610 handler.ServeHTTP(rec, req)
611
612 assert.Equal(t, http.StatusOK, rec.Code)
613
614 var route struct {
615 Domain string `json:"domain"`
616 Image string `json:"image"`
617 HTTPS bool `json:"https"`
618 }
619 err := json.NewDecoder(rec.Body).Decode(&route)
620 assert.NoError(t, err)
621 assert.Equal(t, "app.example.com", route.Domain)
622 assert.Equal(t, "app:latest", route.Image)
623}
624
625func TestHandler_RoutesGet_NotFound(t *testing.T) {
626 configSvc := inmocks.NewMockConfigService(t)
627 authSvc := inmocks.NewMockAuthService(t)
628 containerSvc := inmocks.NewMockContainerService(t)
629 secretSvc := inmocks.NewMockSecretService(t)
630
631 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger())
632
633 configSvc.EXPECT().GetRoute(mock.Anything, "nonexistent.example.com").Return(nil, domain.ErrRouteNotFound)
634
635 req := httptest.NewRequest("GET", "/admin/routes/nonexistent.example.com", nil)
636 req = req.WithContext(ctxWithScopes("admin:routes:read"))
637 rec := httptest.NewRecorder()
638
639 handler.ServeHTTP(rec, req)
640
641 assert.Equal(t, http.StatusNotFound, rec.Code)
642}
643
644func TestHandler_RoutesPost_MissingFields(t *testing.T) {
645 tests := []struct {
646 name string
647 body string
648 mockErr error
649 }{
650 {
651 name: "missing domain",
652 body: `{"image": "app:latest"}`,
653 mockErr: domain.ErrRouteDomainEmpty,
654 },
655 {
656 name: "missing image",
657 body: `{"domain": "app.example.com"}`,
658 mockErr: domain.ErrRouteImageEmpty,
659 },
660 {
661 name: "empty object",
662 body: `{}`,
663 mockErr: domain.ErrRouteDomainEmpty,
664 },
665 }
666
667 for _, tt := range tests {
668 t.Run(tt.name, func(t *testing.T) {
669 configSvc := inmocks.NewMockConfigService(t)
670 authSvc := inmocks.NewMockAuthService(t)
671 containerSvc := inmocks.NewMockContainerService(t)
672 secretSvc := inmocks.NewMockSecretService(t)
673 registrySvc := inmocks.NewMockRegistryService(t)
674
675 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger())
676
677 configSvc.EXPECT().AddRoute(mock.Anything, mock.Anything).Return(tt.mockErr)
678 registrySvc.EXPECT().GetManifest(mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Maybe()
679
680 req := httptest.NewRequest("POST", "/admin/routes", bytes.NewBufferString(tt.body))
681 req = req.WithContext(ctxWithScopes("admin:routes:write"))
682 rec := httptest.NewRecorder()
683
684 handler.ServeHTTP(rec, req)
685
686 assert.Equal(t, http.StatusBadRequest, rec.Code)
687 })
688 }
689}
690
691func TestHandler_RoutesPost_ImageNotFound(t *testing.T) {
692 configSvc := inmocks.NewMockConfigService(t)
693 authSvc := inmocks.NewMockAuthService(t)
694 containerSvc := inmocks.NewMockContainerService(t)
695 secretSvc := inmocks.NewMockSecretService(t)
696 registrySvc := inmocks.NewMockRegistryService(t)
697
698 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger())
699
700 tests := []struct {
701 name string
702 image string
703 body string
704 manifestName string
705 manifestRef string
706 }{
707 {
708 name: "tagged image not found",
709 image: "myapp:latest",
710 body: `{"domain": "app.example.com", "image": "myapp:latest"}`,
711 manifestName: "myapp",
712 manifestRef: "latest",
713 },
714 {
715 name: "digest image not found",
716 image: "myapp@sha256:abc123",
717 body: `{"domain": "app.example.com", "image": "myapp@sha256:abc123"}`,
718 manifestName: "myapp",
719 manifestRef: "sha256:abc123",
720 },
721 {
722 name: "implicit latest tag not found",
723 image: "myapp",
724 body: `{"domain": "app.example.com", "image": "myapp"}`,
725 manifestName: "myapp",
726 manifestRef: "latest",
727 },
728 }
729
730 for _, tt := range tests {
731 t.Run(tt.name, func(t *testing.T) {
732 registrySvc.EXPECT().GetManifest(mock.Anything, tt.manifestName, tt.manifestRef).Return(nil, domain.ErrManifestNotFound)
733
734 req := httptest.NewRequest("POST", "/admin/routes", bytes.NewBufferString(tt.body))
735 req = req.WithContext(ctxWithScopes("admin:routes:write"))
736 rec := httptest.NewRecorder()
737
738 handler.ServeHTTP(rec, req)
739
740 assert.Equal(t, http.StatusBadRequest, rec.Code)
741 assert.Contains(t, rec.Body.String(), fmt.Sprintf("image '%s' not found", tt.image))
742 })
743 }
744}
745
746func TestHandler_RoutesPut_MissingDomain(t *testing.T) {
747 configSvc := inmocks.NewMockConfigService(t)
748 authSvc := inmocks.NewMockAuthService(t)
749 containerSvc := inmocks.NewMockContainerService(t)
750 secretSvc := inmocks.NewMockSecretService(t)
751
752 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger())
753
754 req := httptest.NewRequest("PUT", "/admin/routes/", bytes.NewBufferString(`{"image": "app:latest"}`))
755 req = req.WithContext(ctxWithScopes("admin:routes:write"))
756 rec := httptest.NewRecorder()
757
758 handler.ServeHTTP(rec, req)
759
760 assert.Equal(t, http.StatusBadRequest, rec.Code)
761}
762
763func TestHandler_RoutesDelete_MissingDomain(t *testing.T) {
764 configSvc := inmocks.NewMockConfigService(t)
765 authSvc := inmocks.NewMockAuthService(t)
766 containerSvc := inmocks.NewMockContainerService(t)
767 secretSvc := inmocks.NewMockSecretService(t)
768
769 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger())
770
771 req := httptest.NewRequest("DELETE", "/admin/routes/", nil)
772 req = req.WithContext(ctxWithScopes("admin:routes:write"))
773 rec := httptest.NewRecorder()
774
775 handler.ServeHTTP(rec, req)
776
777 assert.Equal(t, http.StatusBadRequest, rec.Code)
778}
779
780func TestHandler_Secrets_MissingDomain(t *testing.T) {
781 configSvc := inmocks.NewMockConfigService(t)
782 authSvc := inmocks.NewMockAuthService(t)
783 containerSvc := inmocks.NewMockContainerService(t)
784 secretSvc := inmocks.NewMockSecretService(t)
785
786 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger())
787
788 req := httptest.NewRequest("GET", "/admin/secrets/", nil)
789 req = req.WithContext(ctxWithScopes("admin:secrets:read"))
790 rec := httptest.NewRecorder()
791
792 handler.ServeHTTP(rec, req)
793
794 assert.Equal(t, http.StatusBadRequest, rec.Code)
795}
796
797func TestHandler_SecretsDelete_MissingKey(t *testing.T) {
798 configSvc := inmocks.NewMockConfigService(t)
799 authSvc := inmocks.NewMockAuthService(t)
800 containerSvc := inmocks.NewMockContainerService(t)
801 secretSvc := inmocks.NewMockSecretService(t)
802
803 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger())
804
805 req := httptest.NewRequest("DELETE", "/admin/secrets/app.example.com", nil)
806 req = req.WithContext(ctxWithScopes("admin:secrets:write"))
807 rec := httptest.NewRecorder()
808
809 handler.ServeHTTP(rec, req)
810
811 assert.Equal(t, http.StatusBadRequest, rec.Code)
812}
813
814func TestHandler_MethodNotAllowed(t *testing.T) {
815 configSvc := inmocks.NewMockConfigService(t)
816 authSvc := inmocks.NewMockAuthService(t)
817 containerSvc := inmocks.NewMockContainerService(t)
818 secretSvc := inmocks.NewMockSecretService(t)
819
820 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger())
821
822 tests := []struct {
823 method string
824 path string
825 }{
826 {"DELETE", "/admin/status"},
827 {"PUT", "/admin/status"},
828 {"GET", "/admin/reload"},
829 {"DELETE", "/admin/reload"},
830 {"POST", "/admin/config"},
831 {"DELETE", "/admin/config"},
832 }
833
834 for _, tt := range tests {
835 t.Run(tt.method+" "+tt.path, func(t *testing.T) {
836 req := httptest.NewRequest(tt.method, tt.path, nil)
837 req = req.WithContext(ctxWithScopes("admin:*:*"))
838 rec := httptest.NewRecorder()
839
840 handler.ServeHTTP(rec, req)
841
842 assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
843 })
844 }
845}
846
847func TestHandler_NotFound(t *testing.T) {
848 configSvc := inmocks.NewMockConfigService(t)
849 authSvc := inmocks.NewMockAuthService(t)
850 containerSvc := inmocks.NewMockContainerService(t)
851 secretSvc := inmocks.NewMockSecretService(t)
852
853 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger())
854
855 req := httptest.NewRequest("GET", "/admin/unknown", nil)
856 req = req.WithContext(ctxWithScopes("admin:*:*"))
857 rec := httptest.NewRecorder()
858
859 handler.ServeHTTP(rec, req)
860
861 assert.Equal(t, http.StatusNotFound, rec.Code)
862}