1package publictls
2
3import (
4 "context"
5 "errors"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/mock"
10 "github.com/stretchr/testify/require"
11
12 "github.com/bnema/gordon/internal/boundaries/out"
13 outmocks "github.com/bnema/gordon/internal/boundaries/out/mocks"
14 "github.com/bnema/gordon/internal/domain"
15)
16
17func newSecretResolverMock(t *testing.T, value out.SecretValue, err error) *outmocks.MockSecretResolver {
18 t.Helper()
19 resolver := outmocks.NewMockSecretResolver(t)
20 resolver.EXPECT().ResolveCloudflareToken(mock.Anything).Return(value, err)
21 return resolver
22}
23
24func TestResolveEffectiveChallenge(t *testing.T) {
25 t.Run("disabled returns auto mode with none source", func(t *testing.T) {
26 cfg := Config{Enabled: false}
27 ec, err := ResolveEffectiveChallenge(context.Background(), cfg, nil)
28 require.NoError(t, err)
29 assert.Equal(t, domain.ACMEChallengeAuto, ec.ConfiguredMode)
30 assert.Equal(t, domain.ACMEChallengeAuto, ec.Mode)
31 assert.Equal(t, domain.ACMETokenSourceNone, ec.TokenSource)
32 assert.Equal(t, "ACME disabled", ec.Reason)
33 })
34
35 t.Run("disabled preserves configured challenge in ConfiguredMode", func(t *testing.T) {
36 cfg := Config{Enabled: false, Challenge: "http-01"}
37 ec, err := ResolveEffectiveChallenge(context.Background(), cfg, nil)
38 require.NoError(t, err)
39 assert.Equal(t, domain.ACMEChallengeHTTP01, ec.ConfiguredMode, "should preserve http-01")
40 assert.Equal(t, domain.ACMEChallengeAuto, ec.Mode, "Mode should remain auto")
41 assert.Equal(t, domain.ACMETokenSourceNone, ec.TokenSource)
42 assert.Equal(t, "ACME disabled", ec.Reason)
43 })
44
45 t.Run("disabled preserves cloudflare-dns-01 in ConfiguredMode", func(t *testing.T) {
46 cfg := Config{Enabled: false, Challenge: "cloudflare-dns-01"}
47 ec, err := ResolveEffectiveChallenge(context.Background(), cfg, nil)
48 require.NoError(t, err)
49 assert.Equal(t, domain.ACMEChallengeCloudflareDNS01, ec.ConfiguredMode, "should preserve cloudflare-dns-01")
50 assert.Equal(t, domain.ACMEChallengeAuto, ec.Mode, "Mode should remain auto")
51 assert.Equal(t, domain.ACMETokenSourceNone, ec.TokenSource)
52 assert.Equal(t, "ACME disabled", ec.Reason)
53 })
54
55 t.Run("disabled with unparseable challenge defaults ConfiguredMode to auto", func(t *testing.T) {
56 cfg := Config{Enabled: false, Challenge: "unknown-challenge"}
57 ec, err := ResolveEffectiveChallenge(context.Background(), cfg, nil)
58 require.NoError(t, err)
59 assert.Equal(t, domain.ACMEChallengeAuto, ec.ConfiguredMode, "unparseable defaults to auto")
60 assert.Equal(t, domain.ACMEChallengeAuto, ec.Mode)
61 assert.Equal(t, "ACME disabled", ec.Reason)
62 })
63
64 t.Run("enabled requires non-empty email", func(t *testing.T) {
65 cfg := Config{Enabled: true, Email: "", TLSPort: 8443}
66 _, err := ResolveEffectiveChallenge(context.Background(), cfg, nil)
67 require.Error(t, err)
68 assert.ErrorIs(t, err, domain.ErrACMEEmailRequired)
69 })
70
71 t.Run("enabled requires valid tls port in 1..65535", func(t *testing.T) {
72 cfg := Config{Enabled: true, Email: "admin@example.com", TLSPort: 0}
73 _, err := ResolveEffectiveChallenge(context.Background(), cfg, nil)
74 require.Error(t, err)
75 assert.ErrorIs(t, err, domain.ErrACMEChallengeInvalid)
76 assert.Contains(t, err.Error(), "tls_port")
77
78 cfg2 := Config{Enabled: true, Email: "admin@example.com", TLSPort: 65536}
79 _, err2 := ResolveEffectiveChallenge(context.Background(), cfg2, nil)
80 require.Error(t, err2)
81 assert.ErrorIs(t, err2, domain.ErrACMEChallengeInvalid)
82 assert.Contains(t, err2.Error(), "tls_port")
83 })
84
85 t.Run("auto with cloudflare token returns dns-01", func(t *testing.T) {
86 cfg := Config{Enabled: true, Email: "admin@example.com", Challenge: "auto", TLSPort: 8443, HTTPPort: 8088}
87 resolver := newSecretResolverMock(t, out.SecretValue{Value: "token", Source: domain.ACMETokenSourcePass}, nil)
88 ec, err := ResolveEffectiveChallenge(context.Background(), cfg, resolver)
89 require.NoError(t, err)
90 assert.Equal(t, domain.ACMEChallengeCloudflareDNS01, ec.Mode)
91 assert.Equal(t, domain.ACMETokenSourcePass, ec.TokenSource)
92 assert.Equal(t, "token", ec.Token)
93 assert.Equal(t, domain.ACMEChallengeAuto, ec.ConfiguredMode)
94 })
95
96 t.Run("auto without token returns http-01", func(t *testing.T) {
97 cfg := Config{Enabled: true, Email: "admin@example.com", Challenge: "auto", TLSPort: 8443, HTTPPort: 8088}
98 resolver := newSecretResolverMock(t, out.SecretValue{Value: "", Source: domain.ACMETokenSourceNone}, nil)
99 ec, err := ResolveEffectiveChallenge(context.Background(), cfg, resolver)
100 require.NoError(t, err)
101 assert.Equal(t, domain.ACMEChallengeHTTP01, ec.Mode)
102 assert.Equal(t, domain.ACMETokenSourceNone, ec.TokenSource)
103 assert.Equal(t, domain.ACMEChallengeAuto, ec.ConfiguredMode)
104 })
105
106 t.Run("forced cloudflare-dns-01 requires token", func(t *testing.T) {
107 cfg := Config{Enabled: true, Email: "admin@example.com", Challenge: "cloudflare-dns-01", TLSPort: 8443, HTTPPort: 8088}
108 resolver := newSecretResolverMock(t, out.SecretValue{Value: "", Source: domain.ACMETokenSourceNone}, nil)
109 _, err := ResolveEffectiveChallenge(context.Background(), cfg, resolver)
110 require.Error(t, err)
111 assert.ErrorIs(t, err, domain.ErrCloudflareTokenMissing)
112 })
113
114 t.Run("http-01 requires valid http port in 1..65535", func(t *testing.T) {
115 cfg := Config{Enabled: true, Email: "admin@example.com", Challenge: "http-01", TLSPort: 8443, HTTPPort: 0}
116 _, err := ResolveEffectiveChallenge(context.Background(), cfg, nil)
117 require.Error(t, err)
118 assert.ErrorIs(t, err, domain.ErrACMEChallengeInvalid)
119 assert.Contains(t, err.Error(), "http_port")
120
121 cfg2 := Config{Enabled: true, Email: "admin@example.com", Challenge: "http-01", TLSPort: 8443, HTTPPort: 65536}
122 _, err2 := ResolveEffectiveChallenge(context.Background(), cfg2, nil)
123 require.Error(t, err2)
124 assert.ErrorIs(t, err2, domain.ErrACMEChallengeInvalid)
125 assert.Contains(t, err2.Error(), "http_port")
126 })
127
128 t.Run("cloudflare-dns-01 preserves resolver errors", func(t *testing.T) {
129 cfg := Config{Enabled: true, Email: "admin@example.com", Challenge: "cloudflare-dns-01", TLSPort: 8443}
130 resolver := newSecretResolverMock(t, out.SecretValue{}, errors.New("pass failed"))
131 _, err := ResolveEffectiveChallenge(context.Background(), cfg, resolver)
132 require.Error(t, err)
133 assert.Contains(t, err.Error(), "resolve Cloudflare token")
134 assert.False(t, errors.Is(err, domain.ErrCloudflareTokenMissing), "must not collapse to missing token")
135 })
136
137 t.Run("nil resolver in auto mode returns http-01", func(t *testing.T) {
138 cfg := Config{Enabled: true, Email: "admin@example.com", Challenge: "auto", TLSPort: 8443, HTTPPort: 8088}
139 ec, err := ResolveEffectiveChallenge(context.Background(), cfg, nil)
140 require.NoError(t, err)
141 assert.Equal(t, domain.ACMEChallengeHTTP01, ec.Mode)
142 assert.Equal(t, domain.ACMETokenSourceNone, ec.TokenSource)
143 })
144
145 t.Run("forced cloudflare-dns-01 with nil resolver returns missing token", func(t *testing.T) {
146 cfg := Config{Enabled: true, Email: "admin@example.com", Challenge: "cloudflare-dns-01", TLSPort: 8443}
147 _, err := ResolveEffectiveChallenge(context.Background(), cfg, nil)
148 require.Error(t, err)
149 assert.ErrorIs(t, err, domain.ErrCloudflareTokenMissing)
150 })
151}