v2.31.1 f37fe67

service_test.go RawHistoryFolder

1package pki_test
2
3import (
4	"context"
5	"crypto/tls"
6	"errors"
7	"testing"
8	"time"
9
10	"github.com/bnema/zerowrap"
11	"github.com/stretchr/testify/assert"
12	"github.com/stretchr/testify/mock"
13	"github.com/stretchr/testify/require"
14
15	pkiadapter "github.com/bnema/gordon/internal/adapters/out/pki"
16	"github.com/bnema/gordon/internal/boundaries/out"
17	"github.com/bnema/gordon/internal/boundaries/out/mocks"
18	"github.com/bnema/gordon/internal/domain"
19	pkiusecase "github.com/bnema/gordon/internal/usecase/pki"
20)
21
22func testLogger() zerowrap.Logger { return zerowrap.Default() }
23
24type blockingCertificateAuthority struct {
25	out.CertificateAuthority
26	started chan struct{}
27	release chan struct{}
28}
29
30func (b *blockingCertificateAuthority) IssueCertificate(domain string) (*tls.Certificate, error) {
31	close(b.started)
32	<-b.release
33	return b.CertificateAuthority.IssueCertificate(domain)
34}
35
36func newRouteCheckerMock(t *testing.T, domains ...string) *mocks.MockRouteChecker {
37	m := mocks.NewMockRouteChecker(t)
38	routes := make([]domain.Route, len(domains))
39	for i, d := range domains {
40		routes[i] = domain.Route{Domain: d}
41	}
42	m.EXPECT().GetRoutes(mock.Anything).Return(routes).Maybe()
43	m.EXPECT().GetExternalRoutes().Return(nil).Maybe()
44	return m
45}
46
47func TestService_GetCertificate_KnownDomain(t *testing.T) {
48	dir := t.TempDir()
49	ca, err := pkiadapter.NewCA(dir, testLogger())
50	require.NoError(t, err)
51
52	cfg := newRouteCheckerMock(t, "app.example.com")
53
54	ctx, cancel := context.WithCancel(t.Context())
55	defer cancel()
56
57	svc := pkiusecase.NewService(ctx, ca, cfg, nil, testLogger())
58	defer svc.Stop()
59
60	hello := &tls.ClientHelloInfo{ServerName: "app.example.com"}
61	cert, err := svc.GetCertificate(hello)
62	require.NoError(t, err)
63	require.NotNil(t, cert)
64}
65
66func TestService_GetCertificate_AdditionalDomain(t *testing.T) {
67	dir := t.TempDir()
68	ca, err := pkiadapter.NewCA(dir, testLogger())
69	require.NoError(t, err)
70
71	cfg := newRouteCheckerMock(t, "app.example.com")
72
73	ctx, cancel := context.WithCancel(t.Context())
74	defer cancel()
75
76	svc := pkiusecase.NewService(ctx, ca, cfg, []string{"gordon.example.com"}, testLogger())
77	defer svc.Stop()
78
79	hello := &tls.ClientHelloInfo{ServerName: "GORDON.EXAMPLE.COM."}
80	cert, err := svc.GetCertificate(hello)
81	require.NoError(t, err)
82	require.NotNil(t, cert)
83}
84
85func TestService_SetAdditionalDomains_ReplacesAuthorization(t *testing.T) {
86	dir := t.TempDir()
87	ca, err := pkiadapter.NewCA(dir, testLogger())
88	require.NoError(t, err)
89
90	cfg := newRouteCheckerMock(t)
91	ctx, cancel := context.WithCancel(t.Context())
92	defer cancel()
93
94	svc := pkiusecase.NewService(ctx, ca, cfg, []string{"old.example.com"}, testLogger())
95	defer svc.Stop()
96
97	oldHello := &tls.ClientHelloInfo{ServerName: "old.example.com"}
98	cert, err := svc.GetCertificate(oldHello)
99	require.NoError(t, err)
100	require.NotNil(t, cert)
101
102	svc.SetAdditionalDomains([]string{"NEW.EXAMPLE.COM."})
103
104	cert, err = svc.GetCertificate(oldHello)
105	require.NoError(t, err)
106	assert.Nil(t, cert)
107
108	cert, err = svc.GetCertificate(&tls.ClientHelloInfo{ServerName: "new.example.com"})
109	require.NoError(t, err)
110	require.NotNil(t, cert)
111}
112
113func TestService_SetAdditionalDomains_RevokesConcurrentIssuance(t *testing.T) {
114	dir := t.TempDir()
115	ca, err := pkiadapter.NewCA(dir, testLogger())
116	require.NoError(t, err)
117	blockingCA := &blockingCertificateAuthority{
118		CertificateAuthority: ca,
119		started:              make(chan struct{}),
120		release:              make(chan struct{}),
121	}
122
123	cfg := newRouteCheckerMock(t)
124	ctx, cancel := context.WithCancel(t.Context())
125	defer cancel()
126
127	svc := pkiusecase.NewService(ctx, blockingCA, cfg, []string{"old.example.com"}, testLogger())
128	defer svc.Stop()
129
130	result := make(chan *tls.Certificate, 1)
131	errs := make(chan error, 1)
132	go func() {
133		cert, getErr := svc.GetCertificate(&tls.ClientHelloInfo{ServerName: "old.example.com"})
134		result <- cert
135		errs <- getErr
136	}()
137
138	require.Eventually(t, func() bool {
139		select {
140		case <-blockingCA.started:
141			return true
142		default:
143			return false
144		}
145	}, time.Second, 10*time.Millisecond, "certificate issuance did not start")
146	svc.SetAdditionalDomains([]string{"new.example.com"})
147	close(blockingCA.release)
148
149	var getErr error
150	require.Eventually(t, func() bool {
151		select {
152		case getErr = <-errs:
153			return true
154		default:
155			return false
156		}
157	}, time.Second, 10*time.Millisecond, "certificate issuance did not return an error result")
158	require.NoError(t, getErr)
159
160	var cert *tls.Certificate
161	require.Eventually(t, func() bool {
162		select {
163		case cert = <-result:
164			return true
165		default:
166			return false
167		}
168	}, time.Second, 10*time.Millisecond, "certificate issuance did not return a certificate result")
169	assert.Nil(t, cert)
170	assert.Equal(t, 0, svc.CachedCertCount())
171}
172
173func TestService_GetCertificate_WrapsIssuanceError(t *testing.T) {
174	issuerErr := errors.New("issuer unavailable")
175	ca := mocks.NewMockCertificateAuthority(t)
176	ca.EXPECT().IntermediateExpiresAt().Return(time.Now().Add(24 * time.Hour)).Maybe()
177	ca.EXPECT().IntermediateLifetime().Return(24 * time.Hour).Maybe()
178	ca.EXPECT().IssueCertificate("broken.example.com").Return(nil, issuerErr).Once()
179	cfg := newRouteCheckerMock(t, "broken.example.com")
180
181	ctx, cancel := context.WithCancel(t.Context())
182	defer cancel()
183	svc := pkiusecase.NewService(ctx, ca, cfg, nil, testLogger())
184	defer svc.Stop()
185
186	cert, err := svc.GetCertificate(&tls.ClientHelloInfo{ServerName: "broken.example.com"})
187
188	assert.Nil(t, cert)
189	require.ErrorIs(t, err, issuerErr)
190	assert.Equal(t, `issue leaf certificate for "broken.example.com": issuer unavailable`, err.Error())
191}
192
193func TestService_GetCertificate_UnknownDomain(t *testing.T) {
194	dir := t.TempDir()
195	ca, err := pkiadapter.NewCA(dir, testLogger())
196	require.NoError(t, err)
197
198	cfg := newRouteCheckerMock(t, "app.example.com")
199
200	ctx, cancel := context.WithCancel(t.Context())
201	defer cancel()
202
203	svc := pkiusecase.NewService(ctx, ca, cfg, nil, testLogger())
204	defer svc.Stop()
205
206	hello := &tls.ClientHelloInfo{ServerName: "unknown.example.com"}
207	cert, err := svc.GetCertificate(hello)
208	assert.NoError(t, err)
209	assert.Nil(t, cert)
210}
211
212func TestService_GetCertificate_Caching(t *testing.T) {
213	dir := t.TempDir()
214	ca, err := pkiadapter.NewCA(dir, testLogger())
215	require.NoError(t, err)
216
217	cfg := newRouteCheckerMock(t, "cached.example.com")
218
219	ctx, cancel := context.WithCancel(t.Context())
220	defer cancel()
221
222	svc := pkiusecase.NewService(ctx, ca, cfg, nil, testLogger())
223	defer svc.Stop()
224
225	hello := &tls.ClientHelloInfo{ServerName: "cached.example.com"}
226	cert1, err := svc.GetCertificate(hello)
227	require.NoError(t, err)
228
229	cert2, err := svc.GetCertificate(hello)
230	require.NoError(t, err)
231
232	// Same pointer = served from cache
233	assert.Same(t, cert1, cert2, "second call should return cached cert")
234}

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