1package cli
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "testing"
8 "time"
9
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12
13 "github.com/bnema/gordon/internal/adapters/dto"
14 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
15 "github.com/bnema/gordon/internal/domain"
16)
17
18// tlsTestControlPlane implements ControlPlane for TLS tests.
19type tlsTestControlPlane struct {
20 getTLSStatus func(context.Context) (*dto.TLSStatusResponse, error)
21}
22
23var _ ControlPlane = (*tlsTestControlPlane)(nil)
24
25func (c *tlsTestControlPlane) ListRoutesWithDetails(context.Context) ([]remote.RouteInfo, error) {
26 panic("unexpected call")
27}
28
29func (c *tlsTestControlPlane) GetHealth(context.Context) (map[string]*remote.RouteHealth, error) {
30 panic("unexpected call")
31}
32
33func (c *tlsTestControlPlane) GetRoute(context.Context, string) (*domain.Route, error) {
34 panic("unexpected call")
35}
36
37func (c *tlsTestControlPlane) FindRoutesByImage(context.Context, string) ([]domain.Route, error) {
38 panic("unexpected call")
39}
40
41func (c *tlsTestControlPlane) AddRoute(context.Context, domain.Route) error {
42 panic("unexpected call")
43}
44
45func (c *tlsTestControlPlane) UpdateRoute(context.Context, domain.Route) error {
46 panic("unexpected call")
47}
48
49func (c *tlsTestControlPlane) RemoveRoute(context.Context, string) error {
50 panic("unexpected call")
51}
52
53func (c *tlsTestControlPlane) Bootstrap(context.Context, dto.BootstrapRequest) (*dto.BootstrapResponse, error) {
54 panic("unexpected call")
55}
56
57func (c *tlsTestControlPlane) ListSecretsWithAttachments(context.Context, string) (*remote.SecretsListResult, error) {
58 panic("unexpected call")
59}
60
61func (c *tlsTestControlPlane) SetSecrets(context.Context, string, map[string]string) error {
62 panic("unexpected call")
63}
64
65func (c *tlsTestControlPlane) DeleteSecret(context.Context, string, string) error {
66 panic("unexpected call")
67}
68
69func (c *tlsTestControlPlane) SetAttachmentSecrets(context.Context, string, string, map[string]string) error {
70 panic("unexpected call")
71}
72
73func (c *tlsTestControlPlane) DeleteAttachmentSecret(context.Context, string, string, string) error {
74 panic("unexpected call")
75}
76
77func (c *tlsTestControlPlane) GetAllAttachmentsConfig(context.Context) (map[string][]string, error) {
78 panic("unexpected call")
79}
80
81func (c *tlsTestControlPlane) GetAttachmentsConfig(context.Context, string) ([]string, error) {
82 panic("unexpected call")
83}
84
85func (c *tlsTestControlPlane) FindAttachmentTargetsByImage(context.Context, string) ([]string, error) {
86 panic("unexpected call")
87}
88
89func (c *tlsTestControlPlane) AddAttachment(context.Context, string, string) error {
90 panic("unexpected call")
91}
92
93func (c *tlsTestControlPlane) RemoveAttachment(context.Context, string, string) error {
94 panic("unexpected call")
95}
96
97func (c *tlsTestControlPlane) GetAutoRouteAllowedDomains(context.Context) ([]string, error) {
98 panic("unexpected call")
99}
100
101func (c *tlsTestControlPlane) AddAutoRouteAllowedDomain(context.Context, string) error {
102 panic("unexpected call")
103}
104
105func (c *tlsTestControlPlane) RemoveAutoRouteAllowedDomain(context.Context, string) error {
106 panic("unexpected call")
107}
108
109func (c *tlsTestControlPlane) GetTLSStatus(ctx context.Context) (*dto.TLSStatusResponse, error) {
110 if c.getTLSStatus != nil {
111 return c.getTLSStatus(ctx)
112 }
113 return nil, nil
114}
115
116func (c *tlsTestControlPlane) GetStatus(context.Context) (*remote.Status, error) {
117 panic("unexpected call")
118}
119
120func (c *tlsTestControlPlane) Reload(context.Context) error {
121 panic("unexpected call")
122}
123
124func (c *tlsTestControlPlane) ListNetworks(context.Context) ([]*domain.NetworkInfo, error) {
125 panic("unexpected call")
126}
127
128func (c *tlsTestControlPlane) GetConfig(context.Context) (*remote.Config, error) {
129 panic("unexpected call")
130}
131
132func (c *tlsTestControlPlane) DeployIntent(context.Context, string) error {
133 panic("unexpected call")
134}
135
136func (c *tlsTestControlPlane) Deploy(context.Context, string) (*remote.DeployResult, error) {
137 panic("unexpected call")
138}
139
140func (c *tlsTestControlPlane) Restart(context.Context, string, bool) (*remote.RestartResult, error) {
141 panic("unexpected call")
142}
143
144func (c *tlsTestControlPlane) ListTags(context.Context, string) ([]string, error) {
145 panic("unexpected call")
146}
147
148func (c *tlsTestControlPlane) ListBackups(context.Context, string) ([]dto.BackupJob, error) {
149 panic("unexpected call")
150}
151
152func (c *tlsTestControlPlane) BackupStatus(context.Context) ([]dto.BackupJob, error) {
153 panic("unexpected call")
154}
155
156func (c *tlsTestControlPlane) RunBackup(context.Context, string, string) (*dto.BackupRunResponse, error) {
157 panic("unexpected call")
158}
159
160func (c *tlsTestControlPlane) DetectDatabases(context.Context, string) ([]dto.DatabaseInfo, error) {
161 panic("unexpected call")
162}
163
164func (c *tlsTestControlPlane) GetProcessLogs(context.Context, int) ([]string, error) {
165 panic("unexpected call")
166}
167
168func (c *tlsTestControlPlane) GetContainerLogs(context.Context, string, int) ([]string, error) {
169 panic("unexpected call")
170}
171
172func (c *tlsTestControlPlane) StreamProcessLogs(context.Context, int) (<-chan string, error) {
173 panic("unexpected call")
174}
175
176func (c *tlsTestControlPlane) StreamContainerLogs(context.Context, string, int) (<-chan string, error) {
177 panic("unexpected call")
178}
179
180func (c *tlsTestControlPlane) ListVolumes(context.Context) ([]dto.Volume, error) {
181 panic("unexpected call")
182}
183
184func (c *tlsTestControlPlane) PruneVolumes(context.Context, dto.VolumePruneRequest) (*dto.VolumePruneResponse, error) {
185 panic("unexpected call")
186}
187
188func TestTLSStatus_HumanOutput(t *testing.T) {
189 notAfter := time.Date(2026, 5, 29, 12, 0, 0, 0, time.UTC)
190
191 cp := &tlsTestControlPlane{
192 getTLSStatus: func(_ context.Context) (*dto.TLSStatusResponse, error) {
193 return &dto.TLSStatusResponse{
194 ACMEEnabled: true,
195 ConfiguredMode: "acme",
196 EffectiveMode: "acme-staging",
197 SelectionReason: "configured",
198 TokenSource: "env",
199 Certificates: []dto.TLSCertificateEntry{
200 {
201 ID: "cert-abc123",
202 Names: []string{"example.com", "www.example.com"},
203 Status: "valid",
204 NotAfter: notAfter,
205 LastError: "",
206 },
207 },
208 Routes: []dto.TLSRouteCoverage{
209 {
210 Domain: "example.com",
211 Covered: true,
212 CoveredBy: "cert-abc123",
213 RequiredACME: true,
214 },
215 {
216 Domain: "internal.local",
217 Covered: false,
218 RequiredACME: false,
219 Error: "self-signed cert",
220 },
221 },
222 Errors: []string{"route internal.local has no ACME cert"},
223 }, nil
224 },
225 }
226
227 var buf bytes.Buffer
228 err := runTLSStatusCmd(context.Background(), cp, &buf, false)
229 require.NoError(t, err)
230
231 output := buf.String()
232 t.Log("Human output:\n", output)
233
234 // Check mode info is present
235 assert.Contains(t, output, "ACME:")
236 assert.Contains(t, output, "enabled")
237 assert.Contains(t, output, "Configured Mode:")
238 assert.Contains(t, output, "acme")
239 assert.Contains(t, output, "Effective Mode:")
240 assert.Contains(t, output, "acme-staging")
241
242 // Check token source (not value)
243 assert.Contains(t, output, "Token Source:")
244 assert.Contains(t, output, "env")
245 assert.NotContains(t, output, "token_secret_value")
246
247 // Check certificate ID
248 assert.Contains(t, output, "cert-abc123")
249 assert.Contains(t, output, "example.com, www.example.com")
250 assert.Contains(t, output, "valid")
251 assert.Contains(t, output, notAfter.Format(time.DateTime))
252
253 // Check route coverage
254 assert.Contains(t, output, "example.com")
255 assert.Contains(t, output, "covered=yes")
256 assert.Contains(t, output, "covered_by=cert-abc123")
257 assert.Contains(t, output, "internal.local")
258 assert.Contains(t, output, "covered=no")
259
260 // Check errors section
261 assert.Contains(t, output, "Errors")
262 assert.Contains(t, output, "route internal.local has no ACME cert")
263}
264
265func TestTLSStatus_JSONOutput(t *testing.T) {
266 notAfter := time.Date(2026, 5, 29, 12, 0, 0, 0, time.UTC)
267
268 cp := &tlsTestControlPlane{
269 getTLSStatus: func(_ context.Context) (*dto.TLSStatusResponse, error) {
270 return &dto.TLSStatusResponse{
271 ACMEEnabled: true,
272 ConfiguredMode: "acme",
273 EffectiveMode: "acme-staging",
274 TokenSource: "env",
275 Certificates: []dto.TLSCertificateEntry{
276 {
277 ID: "cert-abc123",
278 Names: []string{"example.com", "www.example.com"},
279 Status: "valid",
280 NotAfter: notAfter,
281 LastError: "",
282 },
283 },
284 }, nil
285 },
286 }
287
288 var buf bytes.Buffer
289 err := runTLSStatusCmd(context.Background(), cp, &buf, true)
290 require.NoError(t, err)
291
292 var result dto.TLSStatusResponse
293 err = json.Unmarshal(buf.Bytes(), &result)
294 require.NoError(t, err)
295
296 assert.True(t, result.ACMEEnabled)
297 assert.Equal(t, "acme", result.ConfiguredMode)
298 assert.Equal(t, "acme-staging", result.EffectiveMode)
299 assert.Equal(t, "env", result.TokenSource)
300 require.Len(t, result.Certificates, 1)
301 assert.Equal(t, "cert-abc123", result.Certificates[0].ID)
302 assert.Equal(t, []string{"example.com", "www.example.com"}, result.Certificates[0].Names)
303 assert.Equal(t, "valid", result.Certificates[0].Status)
304 assert.Equal(t, notAfter.Format(time.RFC3339), result.Certificates[0].NotAfter.Format(time.RFC3339))
305}