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