2.0.0 4803e67

handlers_test.go RawHistoryFolder

1package events
2
3import (
4	"context"
5	"fmt"
6	"os"
7	"testing"
8
9	"github.com/spf13/viper"
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/mock"
12	"github.com/stretchr/testify/require"
13
14	"gordon/internal/config"
15	"gordon/pkg/runtime"
16)
17
18// MockContainerManager is a mock implementation of container.ManagerInterface
19type MockContainerManager struct {
20	mock.Mock
21}
22
23func (m *MockContainerManager) DeployContainer(ctx context.Context, route config.Route) (*runtime.Container, error) {
24	args := m.Called(ctx, route)
25	if args.Get(0) == nil {
26		return nil, args.Error(1)
27	}
28	return args.Get(0).(*runtime.Container), args.Error(1)
29}
30
31func (m *MockContainerManager) StopContainer(ctx context.Context, containerID string) error {
32	args := m.Called(ctx, containerID)
33	return args.Error(0)
34}
35
36func (m *MockContainerManager) RemoveContainer(ctx context.Context, containerID string, force bool) error {
37	args := m.Called(ctx, containerID, force)
38	return args.Error(0)
39}
40
41func (m *MockContainerManager) ListContainers() map[string]*runtime.Container {
42	args := m.Called()
43	if args.Get(0) == nil {
44		return nil
45	}
46	return args.Get(0).(map[string]*runtime.Container)
47}
48
49func (m *MockContainerManager) GetContainer(domain string) (*runtime.Container, bool) {
50	args := m.Called(domain)
51	if args.Get(0) == nil {
52		return nil, args.Bool(1)
53	}
54	return args.Get(0).(*runtime.Container), args.Bool(1)
55}
56
57func (m *MockContainerManager) StopContainerByDomain(ctx context.Context, domain string) error {
58	args := m.Called(ctx, domain)
59	return args.Error(0)
60}
61
62func (m *MockContainerManager) Runtime() runtime.Runtime {
63	args := m.Called()
64	return args.Get(0).(runtime.Runtime)
65}
66
67func (m *MockContainerManager) HealthCheck(ctx context.Context) map[string]bool {
68	args := m.Called(ctx)
69	return args.Get(0).(map[string]bool)
70}
71
72func (m *MockContainerManager) SyncContainers(ctx context.Context) error {
73	args := m.Called(ctx)
74	return args.Error(0)
75}
76
77func (m *MockContainerManager) GetContainerPort(ctx context.Context, domain string, internalPort int) (int, error) {
78	args := m.Called(ctx, domain, internalPort)
79	return args.Int(0), args.Error(1)
80}
81
82func (m *MockContainerManager) GetNetworkForApp(domain string) string {
83	args := m.Called(domain)
84	return args.String(0)
85}
86
87func (m *MockContainerManager) AutoStartContainers(ctx context.Context) error {
88	args := m.Called(ctx)
89	return args.Error(0)
90}
91
92func (m *MockContainerManager) StopAllManagedContainers(ctx context.Context) error {
93	args := m.Called(ctx)
94	return args.Error(0)
95}
96
97func TestContainerEventHandler_CanHandle(t *testing.T) {
98	handler := &ContainerEventHandler{}
99	
100	testCases := []struct {
101		eventType EventType
102		expected  bool
103	}{
104		{ImagePushed, true},
105		{ConfigReload, true},
106		{ManualReload, true},
107		{ContainerStop, true},
108		{ContainerStart, true},
109		{ImageDeleted, false},
110		{ContainerHealthCheck, false},
111	}
112	
113	for _, tc := range testCases {
114		t.Run(string(tc.eventType), func(t *testing.T) {
115			result := handler.CanHandle(tc.eventType)
116			assert.Equal(t, tc.expected, result)
117		})
118	}
119}
120
121func TestContainerEventHandler_Handle_ImagePushed(t *testing.T) {
122	mockManager := &MockContainerManager{}
123	mockConfig := &config.Config{
124		Routes: map[string]string{
125			"app.example.com": "nginx:latest",
126		},
127	}
128	
129	handler := &ContainerEventHandler{
130		manager: mockManager,
131		config:  mockConfig,
132	}
133	
134	// Mock container listing (empty list initially)
135	mockManager.On("ListContainers").Return(map[string]*runtime.Container{})
136	
137	// Mock successful deployment
138	expectedRoute := config.Route{
139		Domain: "app.example.com",
140		Image:  "nginx:latest",
141		HTTPS:  true,
142	}
143	mockContainer := &runtime.Container{
144		ID:   "container123",
145		Name: "gordon-app-example-com",
146	}
147	mockManager.On("DeployContainer", mock.Anything, expectedRoute).Return(mockContainer, nil)
148	
149	event := Event{
150		Type:      ImagePushed,
151		ImageName: "nginx",
152		Tag:       "latest",
153	}
154	
155	err := handler.Handle(event)
156	
157	assert.NoError(t, err)
158	mockManager.AssertExpectations(t)
159}
160
161func TestContainerEventHandler_Handle_ImagePushed_NoMatchingRoutes(t *testing.T) {
162	mockManager := &MockContainerManager{}
163	mockConfig := &config.Config{
164		Routes: map[string]string{
165			"app.example.com": "postgres:14", // Different image
166		},
167	}
168	
169	handler := &ContainerEventHandler{
170		manager: mockManager,
171		config:  mockConfig,
172	}
173	
174	event := Event{
175		Type:      ImagePushed,
176		ImageName: "nginx",
177		Tag:       "latest",
178	}
179	
180	err := handler.Handle(event)
181	
182	// Should succeed but not deploy anything
183	assert.NoError(t, err)
184	mockManager.AssertExpectations(t) // No calls expected
185}
186
187func TestContainerEventHandler_Handle_ImagePushed_MultipleRoutes(t *testing.T) {
188	mockManager := &MockContainerManager{}
189	mockConfig := &config.Config{
190		Routes: map[string]string{
191			"app1.example.com": "nginx:latest",
192			"app2.example.com": "nginx:latest",
193			"app3.example.com": "postgres:14", // Different image
194		},
195	}
196	
197	handler := &ContainerEventHandler{
198		manager: mockManager,
199		config:  mockConfig,
200	}
201	
202	// Mock successful deployments for matching routes
203	mockManager.On("ListContainers").Return(map[string]*runtime.Container{}).Times(2)
204	
205	expectedRoute1 := config.Route{Domain: "app1.example.com", Image: "nginx:latest", HTTPS: true}
206	expectedRoute2 := config.Route{Domain: "app2.example.com", Image: "nginx:latest", HTTPS: true}
207	
208	mockContainer1 := &runtime.Container{ID: "container1", Name: "gordon-app1-example-com"}
209	mockContainer2 := &runtime.Container{ID: "container2", Name: "gordon-app2-example-com"}
210	
211	mockManager.On("DeployContainer", mock.Anything, expectedRoute1).Return(mockContainer1, nil)
212	mockManager.On("DeployContainer", mock.Anything, expectedRoute2).Return(mockContainer2, nil)
213	
214	event := Event{
215		Type:      ImagePushed,
216		ImageName: "nginx",
217		Tag:       "latest",
218	}
219	
220	err := handler.Handle(event)
221	
222	assert.NoError(t, err)
223	mockManager.AssertExpectations(t)
224}
225
226func TestContainerEventHandler_Handle_ConfigReload(t *testing.T) {
227	mockManager := &MockContainerManager{}
228	
229	// Mock existing containers
230	existingContainers := map[string]*runtime.Container{
231		"old.example.com": {
232			ID:   "container1",
233			Name: "old-container",
234			Labels: map[string]string{
235				"gordon.route": "old.example.com",
236				"gordon.image": "old:image",
237			},
238		},
239		"app.example.com": {
240			ID:   "container2", 
241			Name: "app-container",
242			Labels: map[string]string{
243				"gordon.route": "app.example.com",
244				"gordon.image": "nginx:latest",
245			},
246		},
247	}
248	
249	// New config with different routes
250	newConfig := &config.Config{
251		Routes: map[string]string{
252			"app.example.com": "nginx:latest", // Keep this one
253			"new.example.com": "postgres:14",  // Add new one
254			// Remove old.example.com
255		},
256	}
257	
258	handler := &ContainerEventHandler{
259		manager: mockManager,
260		config:  newConfig,
261	}
262	
263	// Mock manager calls
264	mockManager.On("ListContainers").Return(existingContainers).Once() // First call in handleConfigReload
265	mockManager.On("StopContainer", mock.Anything, "container1").Return(nil) // Stop removed container
266	mockManager.On("RemoveContainer", mock.Anything, "container1", true).Return(nil) // Remove removed container
267	
268	// Mock deployment for new route
269	expectedRoute := config.Route{Domain: "new.example.com", Image: "postgres:14", HTTPS: true}
270	mockContainer := &runtime.Container{ID: "container3", Name: "gordon-new-example-com"}
271	mockManager.On("ListContainers").Return(map[string]*runtime.Container{}).Once() // Second call in deployContainerForRoute
272	mockManager.On("DeployContainer", mock.Anything, expectedRoute).Return(mockContainer, nil)
273	
274	event := Event{
275		Type: ConfigReload,
276	}
277	
278	err := handler.Handle(event)
279	
280	assert.NoError(t, err)
281	mockManager.AssertExpectations(t)
282}
283
284func TestContainerEventHandler_Handle_ManualReload(t *testing.T) {
285	mockManager := &MockContainerManager{}
286	mockConfig := &config.Config{
287		Routes: map[string]string{
288			"app.example.com": "nginx:latest",
289		},
290	}
291	
292	handler := &ContainerEventHandler{
293		manager: mockManager,
294		config:  mockConfig,
295	}
296	
297	// Mock GetContainer to return true (container exists)
298	mockContainer := &runtime.Container{ID: "container1", Name: "app-container"}
299	mockManager.On("GetContainer", "app.example.com").Return(mockContainer, true)
300	
301	// Mock deployment
302	expectedRoute := config.Route{Domain: "app.example.com", Image: "nginx:latest", HTTPS: true}
303	mockManager.On("DeployContainer", mock.Anything, expectedRoute).Return(mockContainer, nil)
304	
305	event := Event{
306		Type: ManualReload,
307	}
308	
309	err := handler.Handle(event)
310	
311	assert.NoError(t, err)
312	mockManager.AssertExpectations(t)
313}
314
315func TestContainerEventHandler_Handle_ContainerStop(t *testing.T) {
316	mockManager := &MockContainerManager{}
317	
318	handler := &ContainerEventHandler{
319		manager: mockManager,
320	}
321	
322	// Mock container stop
323	mockManager.On("StopContainer", mock.Anything, "container123").Return(nil)
324	
325	event := Event{
326		Type:        ContainerStop,
327		ContainerID: "container123",
328	}
329	
330	err := handler.Handle(event)
331	
332	assert.NoError(t, err)
333	mockManager.AssertExpectations(t)
334}
335
336func TestContainerEventHandler_Handle_ContainerStart(t *testing.T) {
337	handler := &ContainerEventHandler{}
338	
339	event := Event{
340		Type:        ContainerStart,
341		ContainerID: "container123",
342	}
343	
344	err := handler.Handle(event)
345	
346	// Should return error as start is not implemented
347	assert.Error(t, err)
348	assert.Contains(t, err.Error(), "container start not implemented")
349}
350
351func TestContainerEventHandler_Handle_UnsupportedEvent(t *testing.T) {
352	handler := &ContainerEventHandler{}
353	
354	event := Event{
355		Type: ImageDeleted, // Unsupported event type
356	}
357	
358	err := handler.Handle(event)
359	
360	assert.Error(t, err)
361	assert.Contains(t, err.Error(), "unsupported event type")
362}
363
364func TestAutoRouteHandler_CanHandle(t *testing.T) {
365	handler := &AutoRouteHandler{}
366	
367	testCases := []struct {
368		eventType EventType
369		expected  bool
370	}{
371		{ImagePushed, true},
372		{ImageDeleted, false},
373		{ConfigReload, false},
374		{ManualReload, false},
375		{ContainerStop, false},
376		{ContainerStart, false},
377		{ContainerHealthCheck, false},
378	}
379	
380	for _, tc := range testCases {
381		t.Run(string(tc.eventType), func(t *testing.T) {
382			result := handler.CanHandle(tc.eventType)
383			assert.Equal(t, tc.expected, result)
384		})
385	}
386}
387
388func TestAutoRouteHandler_Handle_AutoRouteEnabled(t *testing.T) {
389	mockManager := &MockContainerManager{}
390	
391	// Create a temporary config file for testing
392	tempConfigFile := "/tmp/test-gordon-config.toml"
393	configContent := `[server]
394registry_domain = "registry.example.com"
395
396[auto_route]
397enabled = true
398
399[routes]
400`
401	err := os.WriteFile(tempConfigFile, []byte(configContent), 0644)
402	require.NoError(t, err)
403	defer os.Remove(tempConfigFile)
404	
405	// Load config from temp file to set viper config file path
406	viper.SetConfigFile(tempConfigFile)
407	err = viper.ReadInConfig()
408	require.NoError(t, err)
409	
410	mockConfig := &config.Config{
411		AutoRoute: config.AutoRouteConfig{
412			Enabled: true,
413		},
414		Routes: make(map[string]string),
415		Server: config.ServerConfig{
416			RegistryDomain: "registry.example.com",
417		},
418	}
419	
420	handler := &AutoRouteHandler{
421		config:  mockConfig,
422		manager: mockManager,
423	}
424	
425	payload := ImagePushedPayload{
426		Name:      "myapp.example.com",
427		Reference: "latest",
428	}
429	
430	event := Event{
431		Type: ImagePushed,
432		Data: payload,
433	}
434	
435	err = handler.Handle(event)
436	
437	assert.NoError(t, err)
438	// Check that route was added to config
439	assert.Equal(t, "registry.example.com/myapp.example.com:latest", mockConfig.Routes["myapp.example.com"])
440	mockManager.AssertExpectations(t)
441}
442
443func TestAutoRouteHandler_Handle_AutoRouteDisabled(t *testing.T) {
444	mockManager := &MockContainerManager{}
445	mockConfig := &config.Config{
446		AutoRoute: config.AutoRouteConfig{
447			Enabled: false,
448		},
449		Routes: make(map[string]string),
450	}
451	
452	handler := &AutoRouteHandler{
453		config:  mockConfig,
454		manager: mockManager,
455	}
456	
457	payload := ImagePushedPayload{
458		Name:      "myapp.example.com",
459		Reference: "latest",
460	}
461	
462	event := Event{
463		Type: ImagePushed,
464		Data: payload,
465	}
466	
467	err := handler.Handle(event)
468	
469	// Should succeed but not create route
470	assert.NoError(t, err)
471	assert.Empty(t, mockConfig.Routes)
472	mockManager.AssertExpectations(t) // No calls expected
473}
474
475func TestAutoRouteHandler_Handle_RouteAlreadyExists(t *testing.T) {
476	mockManager := &MockContainerManager{}
477	mockConfig := &config.Config{
478		AutoRoute: config.AutoRouteConfig{
479			Enabled: true,
480		},
481		Routes: map[string]string{
482			"myapp.example.com": "existing:image",
483		},
484	}
485	
486	handler := &AutoRouteHandler{
487		config:  mockConfig,
488		manager: mockManager,
489	}
490	
491	payload := ImagePushedPayload{
492		Name:      "myapp.example.com",
493		Reference: "latest",
494	}
495	
496	event := Event{
497		Type: ImagePushed,
498		Data: payload,
499	}
500	
501	err := handler.Handle(event)
502	
503	// Should succeed but not modify existing route
504	assert.NoError(t, err)
505	assert.Equal(t, "existing:image", mockConfig.Routes["myapp.example.com"])
506	mockManager.AssertExpectations(t) // No calls expected
507}
508
509func TestAutoRouteHandler_Handle_InvalidImageName(t *testing.T) {
510	mockManager := &MockContainerManager{}
511	mockConfig := &config.Config{
512		AutoRoute: config.AutoRouteConfig{
513			Enabled: true,
514		},
515		Routes: make(map[string]string),
516	}
517	
518	handler := &AutoRouteHandler{
519		config:  mockConfig,
520		manager: mockManager,
521	}
522	
523	payload := ImagePushedPayload{
524		Name:      "nginx", // No domain extractable
525		Reference: "latest",
526	}
527	
528	event := Event{
529		Type: ImagePushed,
530		Data: payload,
531	}
532	
533	err := handler.Handle(event)
534	
535	// Should succeed but not create route
536	assert.NoError(t, err)
537	assert.Empty(t, mockConfig.Routes)
538	mockManager.AssertExpectations(t) // No calls expected
539}
540
541func TestNewContainerEventHandler(t *testing.T) {
542	mockManager := &MockContainerManager{}
543	mockConfig := &config.Config{}
544	
545	handler := NewContainerEventHandler(mockManager, mockConfig)
546	
547	require.NotNil(t, handler)
548	assert.Equal(t, mockManager, handler.manager)
549	assert.Equal(t, mockConfig, handler.config)
550}
551
552func TestNewAutoRouteHandler(t *testing.T) {
553	mockManager := &MockContainerManager{}
554	mockConfig := &config.Config{}
555	
556	handler := NewAutoRouteHandler(mockConfig, mockManager)
557	
558	require.NotNil(t, handler)
559	assert.Equal(t, mockManager, handler.manager)
560	assert.Equal(t, mockConfig, handler.config)
561}
562
563func TestVersionHandler_CanHandle(t *testing.T) {
564	handler := &VersionHandler{}
565	
566	testCases := []struct {
567		eventType EventType
568		expected  bool
569	}{
570		{ImagePushed, true},
571		{ImageDeleted, false},
572		{ConfigReload, false},
573		{ManualReload, false},
574		{ContainerStop, false},
575		{ContainerStart, false},
576		{ContainerHealthCheck, false},
577	}
578	
579	for _, tc := range testCases {
580		t.Run(string(tc.eventType), func(t *testing.T) {
581			result := handler.CanHandle(tc.eventType)
582			assert.Equal(t, tc.expected, result)
583		})
584	}
585}
586
587func TestVersionHandler_Handle_VersionedDeployment(t *testing.T) {
588	// Create a temporary config file for testing
589	tempConfigFile := "/tmp/test-gordon-version-config.toml"
590	configContent := `[server]
591registry_domain = "registry.example.com"
592
593[routes]
594"app.example.com" = "myapp:v1.0"
595`
596	err := os.WriteFile(tempConfigFile, []byte(configContent), 0644)
597	require.NoError(t, err)
598	defer os.Remove(tempConfigFile)
599	
600	// Load config from temp file to set viper config file path
601	viper.SetConfigFile(tempConfigFile)
602	err = viper.ReadInConfig()
603	require.NoError(t, err)
604	
605	mockManager := &MockContainerManager{}
606	mockConfig := &config.Config{
607		Routes: map[string]string{
608			"app.example.com": "myapp:v2.0", // Match the payload reference so it finds routes
609		},
610	}
611	
612	handler := &VersionHandler{
613		manager: mockManager,
614		config:  mockConfig,
615	}
616	
617	// Mock deployment
618	expectedRoute := config.Route{Domain: "app.example.com", Image: "myapp:v2.0", HTTPS: true}
619	mockContainer := &runtime.Container{ID: "container1", Name: "app-container"}
620	mockManager.On("DeployContainer", mock.Anything, expectedRoute).Return(mockContainer, nil)
621	
622	payload := ImagePushedPayload{
623		Name:      "myapp",
624		Reference: "v2.0",
625		Annotations: map[string]string{
626			"version": "v2.0",
627		},
628	}
629	
630	event := Event{
631		Type: ImagePushed,
632		Data: payload,
633	}
634	
635	err = handler.Handle(event)
636	
637	assert.NoError(t, err)
638	// Check that route was updated to new version
639	assert.Equal(t, "myapp:v2.0", mockConfig.Routes["app.example.com"])
640	mockManager.AssertExpectations(t)
641}
642
643func TestVersionHandler_Handle_NonVersionedDeployment(t *testing.T) {
644	mockManager := &MockContainerManager{}
645	mockConfig := &config.Config{
646		Routes: make(map[string]string),
647	}
648	
649	handler := &VersionHandler{
650		manager: mockManager,
651		config:  mockConfig,
652	}
653	
654	payload := ImagePushedPayload{
655		Name:        "myapp",
656		Reference:   "latest",
657		Annotations: map[string]string{}, // No version annotations
658	}
659	
660	event := Event{
661		Type: ImagePushed,
662		Data: payload,
663	}
664	
665	err := handler.Handle(event)
666	
667	// Should succeed but not process anything
668	assert.NoError(t, err)
669	assert.Empty(t, mockConfig.Routes)
670	mockManager.AssertExpectations(t) // No calls expected
671}
672
673func TestVersionHandler_Handle_NoMatchingRoutes(t *testing.T) {
674	mockManager := &MockContainerManager{}
675	mockConfig := &config.Config{
676		Routes: map[string]string{
677			"app.example.com": "nginx:latest", // Different image
678		},
679	}
680	
681	handler := &VersionHandler{
682		manager: mockManager,
683		config:  mockConfig,
684	}
685	
686	payload := ImagePushedPayload{
687		Name:      "myapp",
688		Reference: "v2.0",
689		Annotations: map[string]string{
690			"version": "v2.0",
691		},
692	}
693	
694	event := Event{
695		Type: ImagePushed,
696		Data: payload,
697	}
698	
699	err := handler.Handle(event)
700	
701	// Should succeed but not deploy anything
702	assert.NoError(t, err)
703	assert.Equal(t, "nginx:latest", mockConfig.Routes["app.example.com"])
704	mockManager.AssertExpectations(t) // No calls expected
705}
706
707func TestVersionHandler_Handle_InvalidPayload(t *testing.T) {
708	handler := &VersionHandler{}
709	
710	event := Event{
711		Type: ImagePushed,
712		Data: "invalid payload",
713	}
714	
715	err := handler.Handle(event)
716	
717	assert.Error(t, err)
718	assert.Contains(t, err.Error(), "invalid payload type")
719}
720
721func TestVersionHandler_Handle_WrongEventType(t *testing.T) {
722	handler := &VersionHandler{}
723	
724	event := Event{
725		Type: ConfigReload,
726	}
727	
728	err := handler.Handle(event)
729	
730	assert.NoError(t, err) // Should succeed but do nothing
731}
732
733func TestVersionHandler_Handle_DeploymentFailure(t *testing.T) {
734	// Create a temporary config file for testing
735	tempConfigFile := "/tmp/test-gordon-version-fail-config.toml"
736	configContent := `[server]
737registry_domain = "registry.example.com"
738
739[routes]
740"app.example.com" = "myapp:v1.0"
741`
742	err := os.WriteFile(tempConfigFile, []byte(configContent), 0644)
743	require.NoError(t, err)
744	defer os.Remove(tempConfigFile)
745	
746	// Load config from temp file to set viper config file path
747	viper.SetConfigFile(tempConfigFile)
748	err = viper.ReadInConfig()
749	require.NoError(t, err)
750	
751	mockManager := &MockContainerManager{}
752	mockConfig := &config.Config{
753		Routes: map[string]string{
754			"app.example.com": "myapp:v2.0", // Match the payload reference so it finds routes
755		},
756	}
757	
758	handler := &VersionHandler{
759		manager: mockManager,
760		config:  mockConfig,
761	}
762	
763	// Mock deployment failure
764	expectedRoute := config.Route{Domain: "app.example.com", Image: "myapp:v2.0", HTTPS: true}
765	mockManager.On("DeployContainer", mock.Anything, expectedRoute).Return(nil, fmt.Errorf("deployment failed"))
766	
767	payload := ImagePushedPayload{
768		Name:      "myapp",
769		Reference: "v2.0",
770		Annotations: map[string]string{
771			"version": "v2.0",
772		},
773	}
774	
775	event := Event{
776		Type: ImagePushed,
777		Data: payload,
778	}
779	
780	err = handler.Handle(event)
781	
782	assert.Error(t, err)
783	assert.Contains(t, err.Error(), "version deployment completed with")
784	mockManager.AssertExpectations(t)
785}
786
787func TestVersionHandler_Handle_FullImageReference(t *testing.T) {
788	// Create a temporary config file for testing
789	tempConfigFile := "/tmp/test-gordon-version-full-config.toml"
790	configContent := `[server]
791registry_domain = "registry.example.com"
792
793[routes]
794"app.example.com" = "myapp:v1.0"
795`
796	err := os.WriteFile(tempConfigFile, []byte(configContent), 0644)
797	require.NoError(t, err)
798	defer os.Remove(tempConfigFile)
799	
800	// Load config from temp file to set viper config file path
801	viper.SetConfigFile(tempConfigFile)
802	err = viper.ReadInConfig()
803	require.NoError(t, err)
804	
805	mockManager := &MockContainerManager{}
806	mockConfig := &config.Config{
807		Routes: map[string]string{
808			"app.example.com": "myapp:v2.0", // Match the payload reference so it finds routes
809		},
810	}
811	
812	handler := &VersionHandler{
813		manager: mockManager,
814		config:  mockConfig,
815	}
816	
817	// Mock deployment with full image reference
818	expectedRoute := config.Route{Domain: "app.example.com", Image: "registry.example.com/myapp:v2.0", HTTPS: true}
819	mockContainer := &runtime.Container{ID: "container1", Name: "app-container"}
820	mockManager.On("DeployContainer", mock.Anything, expectedRoute).Return(mockContainer, nil)
821	
822	payload := ImagePushedPayload{
823		Name:      "myapp",
824		Reference: "v2.0",
825		Annotations: map[string]string{
826			"version": "registry.example.com/myapp:v2.0", // Full image reference
827		},
828	}
829	
830	event := Event{
831		Type: ImagePushed,
832		Data: payload,
833	}
834	
835	err = handler.Handle(event)
836	
837	assert.NoError(t, err)
838	// Check that route was updated to full image reference
839	assert.Equal(t, "registry.example.com/myapp:v2.0", mockConfig.Routes["app.example.com"])
840	mockManager.AssertExpectations(t)
841}
842
843func TestNewVersionHandler(t *testing.T) {
844	mockManager := &MockContainerManager{}
845	mockConfig := &config.Config{}
846	
847	handler := NewVersionHandler(mockManager, mockConfig)
848	
849	require.NotNil(t, handler)
850	assert.Equal(t, mockManager, handler.manager)
851	assert.Equal(t, mockConfig, handler.config)
852}

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 8

Tags 106

↑↓ moveenter openesc close