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