1package cli
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 "github.com/bnema/gordon/internal/adapters/dto"
9 inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
10 "github.com/bnema/gordon/internal/domain"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/mock"
13 "github.com/stretchr/testify/require"
14)
15
16func TestLocalControlPlane_GetStatus(t *testing.T) {
17 t.Parallel()
18
19 configSvc := inmocks.NewMockConfigService(t)
20 containerSvc := inmocks.NewMockContainerService(t)
21
22 ctx := context.Background()
23 configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{{Domain: "app.local", Image: "repo/app:latest"}})
24 configSvc.EXPECT().GetRegistryDomain().Return("registry.local")
25 configSvc.EXPECT().GetRegistryPort().Return(5000)
26 configSvc.EXPECT().GetServerPort().Return(80)
27 configSvc.EXPECT().IsAutoRouteEnabled().Return(true)
28 configSvc.EXPECT().IsNetworkIsolationEnabled().Return(true)
29 containerSvc.EXPECT().List(mock.Anything).Return(map[string]*domain.Container{
30 "app.local": {ID: "abc123", Status: "running"},
31 })
32
33 cp := &localControlPlane{configSvc: configSvc, containerSvc: containerSvc}
34 status, err := cp.GetStatus(ctx)
35 require.NoError(t, err)
36 require.Equal(t, 1, status.Routes)
37 require.Equal(t, "registry.local", status.RegistryDomain)
38 require.Equal(t, "running", status.ContainerStatus["app.local"])
39}
40
41func TestLocalControlPlane_DeployUsesInternalDeployContext(t *testing.T) {
42 t.Parallel()
43
44 configSvc := inmocks.NewMockConfigService(t)
45 containerSvc := inmocks.NewMockContainerService(t)
46
47 ctx := context.Background()
48 route := &domain.Route{Domain: "app.local", Image: "repo/app:latest"}
49
50 require.False(t, domain.IsInternalDeploy(ctx))
51
52 configSvc.EXPECT().GetRoute(mock.Anything, "app.local").Return(route, nil)
53 containerSvc.EXPECT().Deploy(mock.Anything, *route).RunAndReturn(func(deployCtx context.Context, deployedRoute domain.Route) (*domain.Container, error) {
54 require.True(t, domain.IsInternalDeploy(deployCtx))
55 require.Equal(t, *route, deployedRoute)
56 return &domain.Container{ID: "container-1"}, nil
57 })
58
59 cp := &localControlPlane{configSvc: configSvc, containerSvc: containerSvc}
60 result, err := cp.Deploy(ctx, "app.local")
61 require.NoError(t, err)
62 require.NotNil(t, result)
63 require.Equal(t, "deployed", result.Status)
64 require.Equal(t, "app.local", result.Domain)
65 require.Equal(t, "container-1", result.ContainerID)
66}
67
68func TestLocalControlPlane_Backups(t *testing.T) {
69 t.Parallel()
70
71 backupSvc := inmocks.NewMockBackupService(t)
72 ctx := context.Background()
73
74 now := time.Now().UTC()
75 jobs := []domain.BackupJob{{ID: "b1", Domain: "app.local", DBName: "postgres", Status: domain.BackupStatusCompleted, StartedAt: now}}
76 backupSvc.EXPECT().ListBackups(mock.Anything, "app.local").Return(jobs, nil)
77 backupSvc.EXPECT().Status(mock.Anything).Return(jobs, nil)
78 backupSvc.EXPECT().RunBackup(mock.Anything, "app.local", "postgres").Return(&domain.BackupResult{Job: jobs[0]}, nil)
79 backupSvc.EXPECT().DetectDatabases(mock.Anything, "app.local").Return([]domain.DBInfo{{Type: domain.DBTypePostgreSQL, Name: "postgres", Host: "postgres", Port: 5432}}, nil)
80
81 cp := &localControlPlane{backupSvc: backupSvc}
82
83 list, err := cp.ListBackups(ctx, "app.local")
84 require.NoError(t, err)
85 require.Len(t, list, 1)
86
87 status, err := cp.BackupStatus(ctx)
88 require.NoError(t, err)
89 require.Len(t, status, 1)
90
91 run, err := cp.RunBackup(ctx, "app.local", "postgres")
92 require.NoError(t, err)
93 require.NotNil(t, run.Backup)
94 require.Equal(t, "b1", run.Backup.ID)
95
96 dbs, err := cp.DetectDatabases(ctx, "app.local")
97 require.NoError(t, err)
98 require.Len(t, dbs, 1)
99 require.Equal(t, "postgres", dbs[0].Name)
100}
101
102func TestLocalControlPlane_ListTags(t *testing.T) {
103 t.Parallel()
104
105 registrySvc := inmocks.NewMockRegistryService(t)
106 registrySvc.EXPECT().ListTags(mock.Anything, "repo/app").Return([]string{"v1.0.0", "latest"}, nil)
107
108 cp := &localControlPlane{registrySvc: registrySvc}
109 tags, err := cp.ListTags(context.Background(), "repo/app")
110 require.NoError(t, err)
111 require.Equal(t, []string{"v1.0.0", "latest"}, tags)
112}
113
114func TestLocalControlPlane_RemoveRoutePersistsConfigThenReconcilesRuntime(t *testing.T) {
115 t.Parallel()
116
117 configSvc := inmocks.NewMockConfigService(t)
118 containerSvc := inmocks.NewMockContainerService(t)
119 report := &domain.CleanupReport{
120 Domain: "app.local",
121 RemovedContainers: []domain.CleanupContainer{{
122 ID: "container-1",
123 Name: "gordon-app.local",
124 }},
125 }
126
127 removeCall := configSvc.EXPECT().RemoveRoute(mock.Anything, "app.local").Return(nil).Once()
128 cleanupCall := containerSvc.EXPECT().ReconcileRemovedRoute(mock.Anything, "app.local").Return(report, nil).Once()
129 mock.InOrder(removeCall, cleanupCall)
130
131 cp := &localControlPlane{configSvc: configSvc, containerSvc: containerSvc}
132 err := cp.RemoveRoute(context.Background(), "app.local")
133 require.NoError(t, err)
134}
135
136func TestLocalControlPlane_RestartWithAttachments(t *testing.T) {
137 t.Parallel()
138
139 containerSvc := inmocks.NewMockContainerService(t)
140 containerSvc.EXPECT().SyncContainers(mock.Anything).Return(nil)
141 containerSvc.EXPECT().Restart(mock.Anything, "app.local", true).Return(nil)
142
143 cp := &localControlPlane{containerSvc: containerSvc}
144 result, err := cp.Restart(context.Background(), "app.local", true)
145 require.NoError(t, err)
146 require.Equal(t, "app.local", result.Domain)
147}
148
149func TestLocalControlPlane_ListRoutesWithDetailsSyncsBeforeListing(t *testing.T) {
150 t.Parallel()
151
152 containerSvc := inmocks.NewMockContainerService(t)
153 syncCall := containerSvc.EXPECT().SyncContainers(mock.Anything).Return(nil).Once()
154 listCall := containerSvc.EXPECT().ListRoutesWithDetails(mock.Anything).Return([]domain.RouteInfo{{
155 Domain: "app.local",
156 Image: "repo/app:latest",
157 ContainerID: "container-1",
158 ContainerStatus: "running",
159 }}).Once()
160 mock.InOrder(syncCall, listCall)
161
162 cp := &localControlPlane{containerSvc: containerSvc}
163 routes, err := cp.ListRoutesWithDetails(context.Background())
164 require.NoError(t, err)
165 require.Len(t, routes, 1)
166 require.Equal(t, "app.local", routes[0].Domain)
167 require.Equal(t, "repo/app:latest", routes[0].Image)
168 require.Equal(t, "container-1", routes[0].ContainerID)
169 require.Equal(t, "running", routes[0].ContainerStatus)
170}
171
172func TestLocalControlPlane_ListRoutesWithDetails_IncludesConfiguredRouteWithoutRuntimeDetail(t *testing.T) {
173 t.Parallel()
174
175 configSvc := inmocks.NewMockConfigService(t)
176 containerSvc := inmocks.NewMockContainerService(t)
177
178 configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{{
179 Domain: "app.local",
180 Image: "repo/app:latest",
181 }}).Once()
182 containerSvc.EXPECT().SyncContainers(mock.Anything).Return(nil).Once()
183 containerSvc.EXPECT().ListRoutesWithDetails(mock.Anything).Return(nil).Once()
184
185 cp := &localControlPlane{configSvc: configSvc, containerSvc: containerSvc}
186 routes, err := cp.ListRoutesWithDetails(context.Background())
187 require.NoError(t, err)
188 require.Len(t, routes, 1)
189 require.Equal(t, "app.local", routes[0].Domain)
190 require.Equal(t, "repo/app:latest", routes[0].Image)
191 require.Empty(t, routes[0].ContainerID)
192 require.Empty(t, routes[0].ContainerStatus)
193}
194
195func TestLocalControlPlane_GetTLSStatusWithoutService(t *testing.T) {
196 t.Parallel()
197
198 cp := &localControlPlane{}
199 status, err := cp.GetTLSStatus(context.Background())
200 require.NoError(t, err)
201 require.Equal(t, &dto.TLSStatusResponse{
202 ACMEEnabled: false,
203 SelectionReason: "public TLS service not configured",
204 }, status)
205}
206
207func TestLocalControlPlane_GetContainerLogs(t *testing.T) {
208 t.Parallel()
209
210 logSvc := inmocks.NewMockLogService(t)
211 logSvc.EXPECT().GetContainerLogs(mock.Anything, "app.local", 50).Return([]string{"line1", "line2"}, nil)
212
213 cp := &localControlPlane{logSvc: logSvc}
214 lines, err := cp.GetContainerLogs(context.Background(), "app.local", 50)
215 require.NoError(t, err)
216 require.Equal(t, []string{"line1", "line2"}, lines)
217}
218
219func TestLocalControlPlane_RemoveRouteReconcilesRuntimeWhenRouteAlreadyMissing(t *testing.T) {
220 ctx := context.Background()
221 configSvc := inmocks.NewMockConfigService(t)
222 containerSvc := inmocks.NewMockContainerService(t)
223 report := &domain.CleanupReport{Domain: "app.local"}
224
225 configSvc.EXPECT().RemoveRoute(mock.Anything, "app.local").Return(domain.ErrRouteNotFound).Once()
226 containerSvc.EXPECT().ReconcileRemovedRoute(mock.Anything, "app.local").Return(report, nil).Once()
227
228 cp := &localControlPlane{configSvc: configSvc, containerSvc: containerSvc}
229 resp, err := cp.RemoveRouteWithCleanup(ctx, "app.local")
230 require.NoError(t, err)
231 require.NotNil(t, resp)
232 assert.Equal(t, dto.CleanupReportFromDomain(report), resp.Cleanup)
233}