v2.4.1 7c3b292

handler_test.go RawHistoryFolder

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}

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