@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 275 lines 7.84 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package reverseproxy
16
17 import (
18 "encoding/json"
19 "net/http"
20 "net/http/httptest"
21 "testing"
22 "time"
23 )
24
25 // adminHandlerFixture sets up the global host state for an admin endpoint test
26 // and returns a cleanup function that must be deferred by the caller.
27 //
28 // staticAddrs are inserted into the UsagePool (as a static upstream would be).
29 // dynamicAddrs are inserted into the dynamicHosts map (as a dynamic upstream would be).
30 func adminHandlerFixture(t *testing.T, staticAddrs, dynamicAddrs []string) func() {
31 t.Helper()
32
33 for _, addr := range staticAddrs {
34 u := &Upstream{Dial: addr}
35 u.fillHost()
36 }
37
38 dynamicHostsMu.Lock()
39 for _, addr := range dynamicAddrs {
40 dynamicHosts[addr] = dynamicHostEntry{host: new(Host), lastSeen: time.Now()}
41 }
42 dynamicHostsMu.Unlock()
43
44 return func() {
45 // Remove static entries from the UsagePool.
46 for _, addr := range staticAddrs {
47 _, _ = hosts.Delete(addr)
48 }
49 // Remove dynamic entries.
50 dynamicHostsMu.Lock()
51 for _, addr := range dynamicAddrs {
52 delete(dynamicHosts, addr)
53 }
54 dynamicHostsMu.Unlock()
55 }
56 }
57
58 // callAdminUpstreams fires a GET against handleUpstreams and returns the
59 // decoded response body.
60 func callAdminUpstreams(t *testing.T) []upstreamStatus {
61 t.Helper()
62 req := httptest.NewRequest(http.MethodGet, "/reverse_proxy/upstreams", nil)
63 w := httptest.NewRecorder()
64
65 handler := adminUpstreams{}
66 if err := handler.handleUpstreams(w, req); err != nil {
67 t.Fatalf("handleUpstreams returned unexpected error: %v", err)
68 }
69 if w.Code != http.StatusOK {
70 t.Fatalf("expected 200, got %d", w.Code)
71 }
72 if ct := w.Header().Get("Content-Type"); ct != "application/json" {
73 t.Fatalf("expected Content-Type application/json, got %q", ct)
74 }
75
76 var results []upstreamStatus
77 if err := json.NewDecoder(w.Body).Decode(&results); err != nil {
78 t.Fatalf("failed to decode response: %v", err)
79 }
80 return results
81 }
82
83 // resultsByAddress indexes a slice of upstreamStatus by address for easier
84 // lookup in assertions.
85 func resultsByAddress(statuses []upstreamStatus) map[string]upstreamStatus {
86 m := make(map[string]upstreamStatus, len(statuses))
87 for _, s := range statuses {
88 m[s.Address] = s
89 }
90 return m
91 }
92
93 // TestAdminUpstreamsMethodNotAllowed verifies that non-GET methods are rejected.
94 func TestAdminUpstreamsMethodNotAllowed(t *testing.T) {
95 for _, method := range []string{http.MethodPost, http.MethodPut, http.MethodDelete} {
96 req := httptest.NewRequest(method, "/reverse_proxy/upstreams", nil)
97 w := httptest.NewRecorder()
98 err := (adminUpstreams{}).handleUpstreams(w, req)
99 if err == nil {
100 t.Errorf("method %s: expected an error, got nil", method)
101 continue
102 }
103 apiErr, ok := err.(interface{ HTTPStatus() int })
104 if !ok {
105 // caddy.APIError stores the code in HTTPStatus field, access via the
106 // exported interface it satisfies indirectly; just check non-nil.
107 continue
108 }
109 if code := apiErr.HTTPStatus(); code != http.StatusMethodNotAllowed {
110 t.Errorf("method %s: expected 405, got %d", method, code)
111 }
112 }
113 }
114
115 // TestAdminUpstreamsEmpty verifies that an empty response is valid JSON when
116 // no upstreams are registered.
117 func TestAdminUpstreamsEmpty(t *testing.T) {
118 resetDynamicHosts()
119
120 results := callAdminUpstreams(t)
121 if results == nil {
122 t.Error("expected non-nil (empty) slice, got nil")
123 }
124 if len(results) != 0 {
125 t.Errorf("expected 0 results with empty pools, got %d", len(results))
126 }
127 }
128
129 // TestAdminUpstreamsStaticOnly verifies that static upstreams (from the
130 // UsagePool) appear in the response with correct addresses.
131 func TestAdminUpstreamsStaticOnly(t *testing.T) {
132 resetDynamicHosts()
133 cleanup := adminHandlerFixture(t,
134 []string{"10.0.0.1:80", "10.0.0.2:80"},
135 nil,
136 )
137 defer cleanup()
138
139 results := callAdminUpstreams(t)
140 byAddr := resultsByAddress(results)
141
142 for _, addr := range []string{"10.0.0.1:80", "10.0.0.2:80"} {
143 if _, ok := byAddr[addr]; !ok {
144 t.Errorf("expected static upstream %q in response", addr)
145 }
146 }
147 if len(results) != 2 {
148 t.Errorf("expected exactly 2 results, got %d", len(results))
149 }
150 }
151
152 // TestAdminUpstreamsDynamicOnly verifies that dynamic upstreams (from
153 // dynamicHosts) appear in the response with correct addresses.
154 func TestAdminUpstreamsDynamicOnly(t *testing.T) {
155 resetDynamicHosts()
156 cleanup := adminHandlerFixture(t,
157 nil,
158 []string{"10.0.1.1:80", "10.0.1.2:80"},
159 )
160 defer cleanup()
161
162 results := callAdminUpstreams(t)
163 byAddr := resultsByAddress(results)
164
165 for _, addr := range []string{"10.0.1.1:80", "10.0.1.2:80"} {
166 if _, ok := byAddr[addr]; !ok {
167 t.Errorf("expected dynamic upstream %q in response", addr)
168 }
169 }
170 if len(results) != 2 {
171 t.Errorf("expected exactly 2 results, got %d", len(results))
172 }
173 }
174
175 // TestAdminUpstreamsBothPools verifies that static and dynamic upstreams are
176 // both present in the same response and that there is no overlap or omission.
177 func TestAdminUpstreamsBothPools(t *testing.T) {
178 resetDynamicHosts()
179 cleanup := adminHandlerFixture(t,
180 []string{"10.0.2.1:80"},
181 []string{"10.0.2.2:80"},
182 )
183 defer cleanup()
184
185 results := callAdminUpstreams(t)
186 if len(results) != 2 {
187 t.Fatalf("expected 2 results (1 static + 1 dynamic), got %d", len(results))
188 }
189
190 byAddr := resultsByAddress(results)
191 if _, ok := byAddr["10.0.2.1:80"]; !ok {
192 t.Error("static upstream missing from response")
193 }
194 if _, ok := byAddr["10.0.2.2:80"]; !ok {
195 t.Error("dynamic upstream missing from response")
196 }
197 }
198
199 // TestAdminUpstreamsNoOverlapBetweenPools verifies that an address registered
200 // only as a static upstream does not also appear as a dynamic entry, and
201 // vice-versa.
202 func TestAdminUpstreamsNoOverlapBetweenPools(t *testing.T) {
203 resetDynamicHosts()
204 cleanup := adminHandlerFixture(t,
205 []string{"10.0.3.1:80"},
206 []string{"10.0.3.2:80"},
207 )
208 defer cleanup()
209
210 results := callAdminUpstreams(t)
211 seen := make(map[string]int)
212 for _, r := range results {
213 seen[r.Address]++
214 }
215 for addr, count := range seen {
216 if count > 1 {
217 t.Errorf("address %q appeared %d times; expected exactly once", addr, count)
218 }
219 }
220 }
221
222 // TestAdminUpstreamsReportsFailCounts verifies that fail counts accumulated on
223 // a dynamic upstream's Host are reflected in the response.
224 func TestAdminUpstreamsReportsFailCounts(t *testing.T) {
225 resetDynamicHosts()
226
227 const addr = "10.0.4.1:80"
228 h := new(Host)
229 _ = h.countFail(3)
230
231 dynamicHostsMu.Lock()
232 dynamicHosts[addr] = dynamicHostEntry{host: h, lastSeen: time.Now()}
233 dynamicHostsMu.Unlock()
234 defer func() {
235 dynamicHostsMu.Lock()
236 delete(dynamicHosts, addr)
237 dynamicHostsMu.Unlock()
238 }()
239
240 results := callAdminUpstreams(t)
241 byAddr := resultsByAddress(results)
242
243 status, ok := byAddr[addr]
244 if !ok {
245 t.Fatalf("expected %q in response", addr)
246 }
247 if status.Fails != 3 {
248 t.Errorf("expected Fails=3, got %d", status.Fails)
249 }
250 }
251
252 // TestAdminUpstreamsReportsNumRequests verifies that the active request count
253 // for a static upstream is reflected in the response.
254 func TestAdminUpstreamsReportsNumRequests(t *testing.T) {
255 resetDynamicHosts()
256
257 const addr = "10.0.4.2:80"
258 u := &Upstream{Dial: addr}
259 u.fillHost()
260 defer func() { _, _ = hosts.Delete(addr) }()
261
262 _ = u.Host.countRequest(2)
263 defer func() { _ = u.Host.countRequest(-2) }()
264
265 results := callAdminUpstreams(t)
266 byAddr := resultsByAddress(results)
267
268 status, ok := byAddr[addr]
269 if !ok {
270 t.Fatalf("expected %q in response", addr)
271 }
272 if status.NumRequests != 2 {
273 t.Errorf("expected NumRequests=2, got %d", status.NumRequests)
274 }
275 }