1
2
3
4
5
6
7
8
9
10
11
12
13 package chttp
14
15 import (
16 "context"
17 "net/http"
18 "net/http/cookiejar"
19 "net/http/httptest"
20 "net/url"
21 "strings"
22 "testing"
23 "time"
24
25 "gitlab.com/flimzy/testy"
26 "golang.org/x/net/publicsuffix"
27
28 kivik "github.com/go-kivik/kivik/v4"
29 internal "github.com/go-kivik/kivik/v4/int/errors"
30 "github.com/go-kivik/kivik/v4/int/mock"
31 "github.com/go-kivik/kivik/v4/internal/nettest"
32 )
33
34 func TestCookieAuthAuthenticate(t *testing.T) {
35 type cookieTest struct {
36 dsn string
37 auth *cookieAuth
38 err string
39 status int
40 expectedCookie *http.Cookie
41 }
42
43 tests := testy.NewTable()
44 tests.Add("success", func(t *testing.T) interface{} {
45 var sessCounter int
46 s := nettest.NewHTTPTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
47 h := w.Header()
48 h.Set("Content-Type", "application/json")
49 h.Set("Date", "Sat, 08 Sep 2018 15:49:29 GMT")
50 h.Set("Server", "CouchDB/2.2.0 (Erlang OTP/19)")
51 if r.URL.Path == "/_session" {
52 sessCounter++
53 if sessCounter > 1 {
54 t.Fatal("Too many calls to /_session")
55 }
56 h.Set("Set-Cookie", "AuthSession=YWRtaW46NUI5M0VGODk6eLUGqXf0HRSEV9PPLaZX86sBYes; Version=1; Path=/; HttpOnly")
57 w.WriteHeader(200)
58 _, _ = w.Write([]byte(`{"ok":true,"name":"admin","roles":["_admin"]}`))
59 } else {
60 if cookie := r.Header.Get("Cookie"); cookie != "AuthSession=YWRtaW46NUI5M0VGODk6eLUGqXf0HRSEV9PPLaZX86sBYes" {
61 t.Errorf("Expected cookie not found: %s", cookie)
62 }
63 w.WriteHeader(200)
64 _, _ = w.Write([]byte(`{"ok":true}`))
65 }
66 }))
67 return cookieTest{
68 dsn: s.URL,
69 auth: &cookieAuth{Username: "foo", Password: "bar"},
70 expectedCookie: &http.Cookie{
71 Name: kivik.SessionCookieName,
72 Value: "YWRtaW46NUI5M0VGODk6eLUGqXf0HRSEV9PPLaZX86sBYes",
73 },
74 }
75 })
76 tests.Add("cookie not set", func(t *testing.T) interface{} {
77 s := nettest.NewHTTPTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
78 h := w.Header()
79 h.Set("Content-Type", "application/json")
80 h.Set("Date", "Sat, 08 Sep 2018 15:49:29 GMT")
81 h.Set("Server", "CouchDB/2.2.0 (Erlang OTP/19)")
82 w.WriteHeader(200)
83 }))
84 return cookieTest{
85 dsn: s.URL,
86 auth: &cookieAuth{Username: "foo", Password: "bar"},
87 }
88 })
89
90 tests.Run(t, func(t *testing.T, test cookieTest) {
91 c, err := New(&http.Client{}, test.dsn, mock.NilOption)
92 if err != nil {
93 t.Fatal(err)
94 }
95 if e := test.auth.Authenticate(c); e != nil {
96 t.Fatal(e)
97 }
98 _, err = c.DoError(context.Background(), "GET", "/foo", nil)
99 if d := internal.StatusErrorDiff(test.err, test.status, err); d != "" {
100 t.Error(d)
101 }
102 if d := testy.DiffInterface(test.expectedCookie, test.auth.Cookie()); d != nil {
103 t.Error(d)
104 }
105
106
107 _, err = c.DoError(context.Background(), "GET", "/foo", nil)
108 if d := internal.StatusErrorDiff(test.err, test.status, err); d != "" {
109 t.Error(d)
110 }
111 if d := testy.DiffInterface(test.expectedCookie, test.auth.Cookie()); d != nil {
112 t.Error(d)
113 }
114 })
115 }
116
117 func TestCookie(t *testing.T) {
118 tests := []struct {
119 name string
120 auth *cookieAuth
121 expected *http.Cookie
122 }{
123 {
124 name: "No cookie jar",
125 auth: &cookieAuth{},
126 expected: nil,
127 },
128 {
129 name: "No dsn",
130 auth: &cookieAuth{},
131 expected: nil,
132 },
133 {
134 name: "no cookies",
135 auth: &cookieAuth{},
136 expected: nil,
137 },
138 {
139 name: "cookie found",
140 auth: func() *cookieAuth {
141 dsn, err := url.Parse("http://example.com/")
142 if err != nil {
143 t.Fatal(err)
144 }
145 jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
146 if err != nil {
147 t.Fatal(err)
148 }
149 jar.SetCookies(dsn, []*http.Cookie{
150 {Name: kivik.SessionCookieName, Value: "foo"},
151 {Name: "other", Value: "bar"},
152 })
153 return &cookieAuth{
154 client: &Client{
155 dsn: dsn,
156 Client: &http.Client{
157 Jar: jar,
158 },
159 },
160 }
161 }(),
162 expected: &http.Cookie{Name: kivik.SessionCookieName, Value: "foo"},
163 },
164 }
165 for _, test := range tests {
166 t.Run(test.name, func(t *testing.T) {
167 result := test.auth.Cookie()
168 if d := testy.DiffInterface(test.expected, result); d != nil {
169 t.Error(d)
170 }
171 })
172 }
173 }
174
175 type dummyJar []*http.Cookie
176
177 var _ http.CookieJar = &dummyJar{}
178
179 func (j dummyJar) Cookies(_ *url.URL) []*http.Cookie {
180 return []*http.Cookie(j)
181 }
182
183 func (j *dummyJar) SetCookies(_ *url.URL, cookies []*http.Cookie) {
184 *j = cookies
185 }
186
187 func Test_shouldAuth(t *testing.T) {
188 type tt struct {
189 a *cookieAuth
190 req *http.Request
191 want bool
192 }
193
194 tests := testy.NewTable()
195 tests.Add("no session", tt{
196 a: &cookieAuth{},
197 req: httptest.NewRequest("GET", "/", nil),
198 want: true,
199 })
200 tests.Add("authed request", func() interface{} {
201 req := httptest.NewRequest("GET", "/", nil)
202 req.AddCookie(&http.Cookie{Name: kivik.SessionCookieName})
203 return tt{
204 a: &cookieAuth{},
205 req: req,
206 want: false,
207 }
208 })
209 tests.Add("valid session", func() interface{} {
210 c, _ := New(&http.Client{}, "http://example.com/", mock.NilOption)
211 c.Jar = &dummyJar{&http.Cookie{
212 Name: kivik.SessionCookieName,
213 Expires: time.Now().Add(20 * time.Minute),
214 }}
215 a := &cookieAuth{client: c}
216
217 return tt{
218 a: a,
219 req: httptest.NewRequest("GET", "/", nil),
220 want: false,
221 }
222 })
223 tests.Add("expired session", func() interface{} {
224 c, _ := New(&http.Client{}, "http://example.com/", mock.NilOption)
225 c.Jar = &dummyJar{&http.Cookie{
226 Name: kivik.SessionCookieName,
227 Expires: time.Now().Add(-20 * time.Second),
228 }}
229 a := &cookieAuth{client: c}
230
231 return tt{
232 a: a,
233 req: httptest.NewRequest("GET", "/", nil),
234 want: true,
235 }
236 })
237 tests.Add("no expiry time", func() interface{} {
238 c, _ := New(&http.Client{}, "http://example.com/", mock.NilOption)
239 c.Jar = &dummyJar{&http.Cookie{
240 Name: kivik.SessionCookieName,
241 }}
242 a := &cookieAuth{client: c}
243
244 return tt{
245 a: a,
246 req: httptest.NewRequest("GET", "/", nil),
247 want: false,
248 }
249 })
250 tests.Add("about to expire", func() interface{} {
251 c, _ := New(&http.Client{}, "http://example.com/", mock.NilOption)
252 c.Jar = &dummyJar{&http.Cookie{
253 Name: kivik.SessionCookieName,
254 Expires: time.Now().Add(20 * time.Second),
255 }}
256 a := &cookieAuth{client: c}
257
258 return tt{
259 a: a,
260 req: httptest.NewRequest("GET", "/", nil),
261 want: true,
262 }
263 })
264
265 tests.Run(t, func(t *testing.T, tt tt) {
266 got := tt.a.shouldAuth(tt.req)
267 if got != tt.want {
268 t.Errorf("Want %t, got %t", tt.want, got)
269 }
270 })
271 }
272
273 func Test401Response(t *testing.T) {
274 var sessCounter, getCounter int
275 s := nettest.NewHTTPTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
276 h := w.Header()
277 h.Set("Content-Type", "application/json")
278 h.Set("Date", "Sat, 08 Sep 2018 15:49:29 GMT")
279 h.Set("Server", "CouchDB/2.2.0 (Erlang OTP/19)")
280 if r.URL.Path == "/_session" {
281 sessCounter++
282 if sessCounter > 2 {
283 t.Fatal("Too many calls to /_session")
284 }
285 var cookie string
286 if sessCounter == 1 {
287
288 h.Add("Set-Cookie", "Other=foo; Version=1; Path=/; HttpOnly")
289 cookie = "First"
290 } else {
291 cookie = "Second"
292 }
293 h.Add("Set-Cookie", "AuthSession="+cookie+"; Version=1; Path=/; HttpOnly")
294 w.WriteHeader(200)
295 _, _ = w.Write([]byte(`{"ok":true,"name":"admin","roles":["_admin"]}`))
296 } else {
297 getCounter++
298 cookie := r.Header.Get("Cookie")
299 if !(strings.Contains(cookie, "AuthSession=")) {
300 t.Errorf("Expected cookie not found: %s", cookie)
301 }
302
303
304
305 if getCounter > 1 && !strings.Contains(cookie, "Other=foo") {
306 t.Errorf("Expected cookie not found: %s", cookie)
307 }
308 if getCounter == 2 {
309 w.WriteHeader(401)
310 _, _ = w.Write([]byte(`{"error":"unauthorized","reason":"You are not authorized to access this db."}`))
311 return
312 }
313 w.WriteHeader(200)
314 _, _ = w.Write([]byte(`{"ok":true}`))
315 }
316 }))
317
318 c, err := New(&http.Client{}, s.URL, mock.NilOption)
319 if err != nil {
320 t.Fatal(err)
321 }
322 auth := &cookieAuth{Username: "foo", Password: "bar"}
323 if e := auth.Authenticate(c); e != nil {
324 t.Fatal(e)
325 }
326
327 expectedCookie := &http.Cookie{
328 Name: kivik.SessionCookieName,
329 Value: "First",
330 }
331 newCookie := &http.Cookie{
332 Name: kivik.SessionCookieName,
333 Value: "Second",
334 }
335
336 _, err = c.DoError(context.Background(), "GET", "/foo", nil)
337 if d := internal.StatusErrorDiff("", 0, err); d != "" {
338 t.Error(d)
339 }
340 if d := testy.DiffInterface(expectedCookie, auth.Cookie()); d != nil {
341 t.Error(d)
342 }
343
344 _, err = c.DoError(context.Background(), "GET", "/foo", nil)
345
346
347
348 if !testy.ErrorMatches("Unauthorized: You are not authorized to access this db.", err) {
349 t.Fatalf("Unexpected error: %s", err)
350 }
351 if status := testy.StatusCode(err); status != http.StatusUnauthorized {
352 t.Errorf("Unexpected status code: %d", status)
353 }
354
355 var noCookie *http.Cookie
356 if d := testy.DiffInterface(noCookie, auth.Cookie()); d != nil {
357 t.Error(d)
358 }
359
360 _, err = c.DoError(context.Background(), "GET", "/foo", nil)
361 if d := internal.StatusErrorDiff("", 0, err); d != "" {
362 t.Error(d)
363 }
364 if d := testy.DiffInterface(newCookie, auth.Cookie()); d != nil {
365 t.Error(d)
366 }
367 }
368
View as plain text