1package cli
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "testing"
8
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"
11
12 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
13 "github.com/bnema/gordon/internal/domain"
14)
15
16type deployTestDeployer struct {
17 result *remote.DeployResult
18 err error
19}
20
21func (d *deployTestDeployer) Deploy(context.Context, string) (*remote.DeployResult, error) {
22 return d.result, d.err
23}
24
25func TestRunDeploy_LocalTypedErrorUsesDeployFormatter(t *testing.T) {
26 root := &domain.DeployFailureError{
27 Summary: "failed to deploy",
28 Cause: "health check failed",
29 Hint: "check DATABASE_URL",
30 Logs: []string{"booting app", "connection refused"},
31 }
32 err := runDeploy(context.Background(), &deployTestDeployer{err: root}, false, "app.example.com", &bytes.Buffer{}, false)
33
34 require.Error(t, err)
35 assert.Contains(t, err.Error(), "failed to deploy\nCause: health check failed\nHint: check DATABASE_URL\n\nRecent container logs:\n booting app\n connection refused")
36 assert.ErrorIs(t, err, root)
37}
38
39func TestRunDeploy_StructuredRemoteNoFallbackReturnsFormattedError(t *testing.T) {
40 originalSendDeploySignal := sendDeploySignal
41 sendDeploySignal = func(string) (string, error) {
42 t.Fatalf("sendDeploySignal should not be called for structured deploy failures")
43 return "", nil
44 }
45 defer func() { sendDeploySignal = originalSendDeploySignal }()
46
47 root := &remote.HTTPError{
48 StatusCode: 503,
49 Status: "503 Service Unavailable",
50 Body: "failed to deploy",
51 Cause: "health check failed",
52 Hint: "check DATABASE_URL",
53 Logs: []string{"booting app", "connection refused"},
54 Structured: true,
55 }
56 err := runDeploy(context.Background(), &deployTestDeployer{err: root}, true, "app.example.com", &bytes.Buffer{}, false)
57
58 require.Error(t, err)
59 assert.Equal(t, "failed to deploy\nCause: health check failed\nHint: check DATABASE_URL\n\nRecent container logs:\n booting app\n connection refused", err.Error())
60 assert.ErrorIs(t, err, root)
61}
62
63func TestRunDeploy_UsesResultDomainWhenPresent(t *testing.T) {
64 var out bytes.Buffer
65 err := runDeploy(context.Background(), &deployTestDeployer{result: &remote.DeployResult{
66 Status: "deployed",
67 Domain: "actual.example.com",
68 ContainerID: "1234567890abcdef",
69 }}, false, "requested.example.com", &out, false)
70
71 require.NoError(t, err)
72 assert.Contains(t, out.String(), "Deployed actual.example.com (container: 1234567890ab)")
73 assert.NotContains(t, out.String(), "requested.example.com")
74}
75
76func TestRunDeploy_RemoteFallbackUsesJSONWhenRequested(t *testing.T) {
77 originalSendDeploySignal := sendDeploySignal
78 sendDeploySignal = func(string) (string, error) {
79 return "app.example.com", nil
80 }
81 defer func() { sendDeploySignal = originalSendDeploySignal }()
82
83 var out bytes.Buffer
84 err := runDeploy(context.Background(), &deployTestDeployer{err: errors.New("503 Service Unavailable: Registry Unavailable")}, true, "app.example.com", &out, true)
85
86 require.NoError(t, err)
87 assert.JSONEq(t, `{"domain":"app.example.com","status":"success","warning":"Remote deploy failed (503 Service Unavailable: Registry Unavailable), used local signal fallback"}`, out.String())
88}
89
90func TestHandleLocalDeployFallback_WrapsRemoteAndLocalErrors(t *testing.T) {
91 originalSendDeploySignal := sendDeploySignal
92 localErr := errors.New("local signal failed")
93 sendDeploySignal = func(string) (string, error) {
94 return "", localErr
95 }
96 defer func() { sendDeploySignal = originalSendDeploySignal }()
97
98 remoteErr := errors.New("remote deploy failed")
99 err := handleLocalDeployFallback(remoteErr, "app.example.com", &bytes.Buffer{}, false)
100
101 require.Error(t, err)
102 assert.Equal(t, "failed to deploy: remote error: remote deploy failed; local fallback also failed: local signal failed", err.Error())
103 assert.ErrorIs(t, err, remoteErr)
104 assert.ErrorIs(t, err, localErr)
105}
106
107func TestRunDeploy_RemoteFallbackSanitizesWarning(t *testing.T) {
108 originalSendDeploySignal := sendDeploySignal
109 sendDeploySignal = func(string) (string, error) {
110 return "app.example.com", nil
111 }
112 defer func() { sendDeploySignal = originalSendDeploySignal }()
113
114 var out bytes.Buffer
115 err := runDeploy(context.Background(), &deployTestDeployer{err: errors.New("503 Service Unavailable:\n\x1b[31mRegistry Unavailable\x1b[0m")}, true, "app.example.com", &out, true)
116
117 require.NoError(t, err)
118 assert.JSONEq(t, `{"domain":"app.example.com","status":"success","warning":"Remote deploy failed (503 Service Unavailable:Registry Unavailable), used local signal fallback"}`, out.String())
119}