1package acme
2
3import (
4 "context"
5 "io"
6 "net/http"
7 "net/http/httptest"
8 "testing"
9
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12)
13
14// challengeSvc is a simple map-backed implementation of ChallengeService for testing.
15type challengeSvc map[string]string
16
17func (s challengeSvc) GetHTTP01Challenge(_ context.Context, token string) (string, bool) {
18 keyAuth, ok := s[token]
19 return keyAuth, ok
20}
21
22func newTestServer(t *testing.T, svc ChallengeService) *httptest.Server {
23 t.Helper()
24 server := httptest.NewServer(NewHandler(svc))
25 t.Cleanup(server.Close)
26 return server
27}
28
29func readBody(t *testing.T, resp *http.Response) string {
30 t.Helper()
31 defer resp.Body.Close()
32 body, err := io.ReadAll(resp.Body)
33 require.NoError(t, err)
34 return string(body)
35}
36
37func TestHandlerServesGETChallenge(t *testing.T) {
38 server := newTestServer(t, challengeSvc{"abc": "key-auth"})
39
40 resp, err := http.Get(server.URL + "/.well-known/acme-challenge/abc")
41 require.NoError(t, err)
42
43 assert.Equal(t, http.StatusOK, resp.StatusCode)
44 assert.Equal(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type"))
45 assert.Equal(t, "no-store", resp.Header.Get("Cache-Control"))
46 assert.Equal(t, "key-auth", readBody(t, resp))
47}
48
49func TestHandlerServesHEADChallenge(t *testing.T) {
50 server := newTestServer(t, challengeSvc{"abc": "key-auth"})
51
52 resp, err := http.Head(server.URL + "/.well-known/acme-challenge/abc")
53 require.NoError(t, err)
54 defer resp.Body.Close()
55
56 assert.Equal(t, http.StatusOK, resp.StatusCode)
57 assert.Equal(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type"))
58 assert.Equal(t, "no-store", resp.Header.Get("Cache-Control"))
59}
60
61func TestHandlerRejectsUnsafeToken(t *testing.T) {
62 tests := []struct {
63 name string
64 path string
65 }{
66 {name: "path traversal", path: "/.well-known/acme-challenge/../secret"},
67 {name: "slash in token", path: "/.well-known/acme-challenge/foo/bar"},
68 {name: "empty token", path: "/.well-known/acme-challenge/"},
69 {name: "backslash in token", path: "/.well-known/acme-challenge/foo%5Cbar"},
70 }
71
72 for _, tt := range tests {
73 t.Run(tt.name, func(t *testing.T) {
74 server := newTestServer(t, challengeSvc{"abc": "key-auth"})
75 resp, err := http.Get(server.URL + tt.path)
76 require.NoError(t, err)
77 defer resp.Body.Close()
78
79 assert.Equal(t, http.StatusNotFound, resp.StatusCode, "path: %s", tt.path)
80 })
81 }
82}
83
84func TestHandlerRejectsPOST(t *testing.T) {
85 server := newTestServer(t, challengeSvc{"abc": "key-auth"})
86
87 resp, err := http.Post(server.URL+"/.well-known/acme-challenge/abc", "text/plain", nil)
88 require.NoError(t, err)
89 defer resp.Body.Close()
90
91 assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
92 assert.Equal(t, "GET, HEAD", resp.Header.Get("Allow"))
93}
94
95func TestHandlerRejectsMissingPrefix(t *testing.T) {
96 server := newTestServer(t, challengeSvc{"abc": "key-auth"})
97
98 resp, err := http.Get(server.URL + "/some-other-path")
99 require.NoError(t, err)
100 defer resp.Body.Close()
101
102 assert.Equal(t, http.StatusNotFound, resp.StatusCode)
103}
104
105func TestHandlerRejectsUnknownToken(t *testing.T) {
106 server := newTestServer(t, challengeSvc{"abc": "key-auth"})
107
108 resp, err := http.Get(server.URL + "/.well-known/acme-challenge/unknown")
109 require.NoError(t, err)
110 defer resp.Body.Close()
111
112 assert.Equal(t, http.StatusNotFound, resp.StatusCode)
113}
114
115func TestHandlerRejectsNilService(t *testing.T) {
116 server := newTestServer(t, nil)
117
118 resp, err := http.Get(server.URL + "/.well-known/acme-challenge/abc")
119 require.NoError(t, err)
120 defer resp.Body.Close()
121
122 assert.Equal(t, http.StatusNotFound, resp.StatusCode)
123}