1package onboarding_test
2
3import (
4 "io"
5 "net/http"
6 "net/http/httptest"
7 "testing"
8
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"
11
12 "github.com/bnema/gordon/internal/adapters/in/http/onboarding"
13)
14
15const (
16 testFingerprint = "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
17 testHTTPPort = 8088
18 testTLSPort = 8443
19)
20
21func newTestHandler() (*onboarding.Handler, []byte, []byte) {
22 rootPEM := []byte("-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----\n")
23 mobileconfig := []byte("<plist>com.apple.security.root</plist>")
24
25 h := onboarding.NewHandler(rootPEM, mobileconfig, testFingerprint, testHTTPPort, testTLSPort)
26 return h, rootPEM, mobileconfig
27}
28
29func newTestServer(t *testing.T) (*httptest.Server, []byte, []byte) {
30 t.Helper()
31 h, rootPEM, mobileconfig := newTestHandler()
32
33 mux := http.NewServeMux()
34 mux.HandleFunc("GET /.well-known/gordon/{$}", h.ServeOnboardingPage)
35 mux.HandleFunc("HEAD /.well-known/gordon/{$}", h.ServeOnboardingPage)
36 mux.HandleFunc("GET /.well-known/gordon/ca", h.ServeOnboardingPage)
37 mux.HandleFunc("HEAD /.well-known/gordon/ca", h.ServeOnboardingPage)
38 mux.HandleFunc("GET /.well-known/gordon/ca.crt", h.ServeCACert)
39 mux.HandleFunc("HEAD /.well-known/gordon/ca.crt", h.ServeCACert)
40 mux.HandleFunc("GET /.well-known/gordon/ca.mobileconfig", h.ServeMobileconfig)
41 mux.HandleFunc("HEAD /.well-known/gordon/ca.mobileconfig", h.ServeMobileconfig)
42
43 srv := httptest.NewServer(mux)
44 t.Cleanup(srv.Close)
45 return srv, rootPEM, mobileconfig
46}
47
48// getOnboardingBody issues a GET /.well-known/gordon/ca with a custom Host header and returns the body.
49func getOnboardingBody(t *testing.T, srv *httptest.Server, host string) string {
50 t.Helper()
51 req, err := http.NewRequest(http.MethodGet, srv.URL+"/.well-known/gordon/ca", nil)
52 require.NoError(t, err)
53 req.Host = host
54
55 resp, err := http.DefaultClient.Do(req)
56 require.NoError(t, err)
57 defer resp.Body.Close()
58
59 require.Equal(t, http.StatusOK, resp.StatusCode)
60 body, err := io.ReadAll(resp.Body)
61 require.NoError(t, err)
62 return string(body)
63}
64
65func TestHandler_CACert(t *testing.T) {
66 srv, rootPEM, _ := newTestServer(t)
67
68 resp, err := http.Get(srv.URL + "/.well-known/gordon/ca.crt")
69 require.NoError(t, err)
70 defer resp.Body.Close()
71
72 body, err := io.ReadAll(resp.Body)
73 require.NoError(t, err)
74
75 assert.Equal(t, http.StatusOK, resp.StatusCode)
76 assert.Equal(t, "application/x-x509-ca-cert", resp.Header.Get("Content-Type"))
77 assert.Equal(t, rootPEM, body)
78}
79
80func TestHandler_Mobileconfig(t *testing.T) {
81 srv, _, _ := newTestServer(t)
82
83 resp, err := http.Get(srv.URL + "/.well-known/gordon/ca.mobileconfig")
84 require.NoError(t, err)
85 defer resp.Body.Close()
86
87 body, err := io.ReadAll(resp.Body)
88 require.NoError(t, err)
89
90 assert.Equal(t, http.StatusOK, resp.StatusCode)
91 assert.Equal(t, "application/x-apple-aspen-config", resp.Header.Get("Content-Type"))
92 assert.Contains(t, string(body), "com.apple.security.root")
93}
94
95func TestHandler_OnboardingPage(t *testing.T) {
96 srv, _, _ := newTestServer(t)
97
98 resp, err := http.Get(srv.URL + "/.well-known/gordon/")
99 require.NoError(t, err)
100 defer resp.Body.Close()
101
102 body, err := io.ReadAll(resp.Body)
103 require.NoError(t, err)
104
105 assert.Equal(t, http.StatusOK, resp.StatusCode)
106 assert.Equal(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
107 assert.Contains(t, string(body), "Gordon")
108 assert.Contains(t, string(body), "/.well-known/gordon/ca.crt")
109 assert.Contains(t, string(body), "/.well-known/gordon/ca.mobileconfig")
110}
111
112func TestHandler_OnboardingPage_DefaultHTTPSURLOmitsInternalTLSPort(t *testing.T) {
113 srv, _, _ := newTestServer(t)
114 body := getOnboardingBody(t, srv, "o2.bnema.dev")
115
116 assert.Contains(t, body, "https://o2.bnema.dev/")
117 assert.NotContains(t, body, ":8443")
118}
119
120func TestHandler_OnboardingPage_ExplicitHTTPPortMapsToTLSPort(t *testing.T) {
121 srv, _, _ := newTestServer(t)
122 body := getOnboardingBody(t, srv, "o2.bnema.dev:8088")
123
124 assert.Contains(t, body, "https://o2.bnema.dev:8443/")
125}
126
127func TestHandler_OnboardingPage_ExplicitNonHTTPPortIsPreserved(t *testing.T) {
128 srv, _, _ := newTestServer(t)
129 body := getOnboardingBody(t, srv, "o2.bnema.dev:9999")
130
131 assert.Contains(t, body, "https://o2.bnema.dev:9999/")
132}
133
134func TestHandler_OnboardingPage_IPHostHidesGoToSiteLink(t *testing.T) {
135 srv, _, _ := newTestServer(t)
136 body := getOnboardingBody(t, srv, "100.83.240.53")
137
138 assert.NotContains(t, body, "Go to site")
139}
140
141func TestHandler_OnboardingPage_IncludesFingerprintAndClientImportCopy(t *testing.T) {
142 srv, _, _ := newTestServer(t)
143 body := getOnboardingBody(t, srv, "o2.bnema.dev")
144
145 // Fingerprint must be visible
146 assert.Contains(t, body, testFingerprint)
147
148 // Client-import focused: Firefox/Zen guidance present
149 assert.Contains(t, body, "Firefox")
150 assert.Contains(t, body, "Zen")
151
152 // Should NOT suggest gordon ca install as the primary action
153 assert.NotContains(t, body, "sudo gordon ca install")
154}
155
156func TestHandler_OnboardingPage_GoToSiteLinkIsPlainLink(t *testing.T) {
157 srv, _, _ := newTestServer(t)
158
159 body := getOnboardingBody(t, srv, "o2.bnema.dev")
160
161 assert.Contains(t, body, `<a href="https://o2.bnema.dev/">Go to site →</a>`)
162 assert.NotContains(t, body, "onclick=")
163 assert.NotContains(t, body, "document.cookie")
164}
165
166func TestHandler_OnboardingPage_HEAD(t *testing.T) {
167 srv, _, _ := newTestServer(t)
168
169 req, err := http.NewRequest(http.MethodHead, srv.URL+"/.well-known/gordon/ca", nil)
170 require.NoError(t, err)
171
172 resp, err := http.DefaultClient.Do(req)
173 require.NoError(t, err)
174 defer resp.Body.Close()
175
176 assert.Equal(t, http.StatusOK, resp.StatusCode)
177 assert.Equal(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
178
179 body, err := io.ReadAll(resp.Body)
180 require.NoError(t, err)
181 assert.Empty(t, body)
182}
183
184func TestHandler_CACert_HEAD(t *testing.T) {
185 srv, _, _ := newTestServer(t)
186
187 req, err := http.NewRequest(http.MethodHead, srv.URL+"/.well-known/gordon/ca.crt", nil)
188 require.NoError(t, err)
189
190 resp, err := http.DefaultClient.Do(req)
191 require.NoError(t, err)
192 defer resp.Body.Close()
193
194 assert.Equal(t, http.StatusOK, resp.StatusCode)
195 assert.Equal(t, "application/x-x509-ca-cert", resp.Header.Get("Content-Type"))
196
197 body, err := io.ReadAll(resp.Body)
198 require.NoError(t, err)
199 assert.Empty(t, body)
200}
201
202func TestHandler_Mobileconfig_HEAD(t *testing.T) {
203 srv, _, _ := newTestServer(t)
204
205 req, err := http.NewRequest(http.MethodHead, srv.URL+"/.well-known/gordon/ca.mobileconfig", nil)
206 require.NoError(t, err)
207
208 resp, err := http.DefaultClient.Do(req)
209 require.NoError(t, err)
210 defer resp.Body.Close()
211
212 assert.Equal(t, http.StatusOK, resp.StatusCode)
213 assert.Equal(t, "application/x-apple-aspen-config", resp.Header.Get("Content-Type"))
214
215 body, err := io.ReadAll(resp.Body)
216 require.NoError(t, err)
217 assert.Empty(t, body)
218}