furl - furl_test.go
1 package furl
2
3 import (
4 "fmt"
5 "io"
6 "net/http"
7 "net/http/httptest"
8 "strings"
9 "testing"
10 )
11
12 func TestOptions(t *testing.T) {
13 f := New(SetStore(NewStore(Data(map[string]string{
14 "AAA": "http://www.google.com",
15 }))), KeyValidator(func(key string) bool {
16 return key != "ABCD"
17 }))
18 for n, test := range [...]struct {
19 Path, Response string
20 Code int
21 }{
22 { // 1
23 Path: "/",
24 Code: http.StatusNoContent,
25 Response: optionsPost,
26 },
27 { // 2
28 Path: "/AAA",
29 Code: http.StatusNoContent,
30 Response: optionsGetHead,
31 },
32 { // 3
33 Path: "/BBB",
34 Code: http.StatusNoContent,
35 Response: optionsPost,
36 },
37 { // 4
38 Path: "/ABCD",
39 Code: http.StatusUnprocessableEntity,
40 Response: invalidKey,
41 },
42 } {
43 w := httptest.NewRecorder()
44 f.ServeHTTP(w, httptest.NewRequest(http.MethodOptions, test.Path, nil))
45 if w.Code != test.Code {
46 t.Errorf("test %d: expecting response code %d, got %d", n+1, test.Code, w.Code)
47 } else if test.Code != http.StatusNoContent {
48 if response := strings.TrimSpace(w.Body.String()); response != test.Response {
49 t.Errorf("test %d: expecting response %q, got %q", n+1, test.Response, response)
50 }
51 } else if allowed := w.Header().Get("Allow"); allowed != test.Response {
52 t.Errorf("test %d: expecting Allow header of %q, got %q", n+1, test.Response, allowed)
53 }
54 }
55 }
56
57 func TestGet(t *testing.T) {
58 f := New(SetStore(NewStore(Data(map[string]string{
59 "AAA": "http://www.google.com",
60 }))), KeyValidator(func(key string) bool {
61 return key != "ABCD"
62 }))
63 for n, test := range [...]struct {
64 Path, Response string
65 Code int
66 }{
67 { // 1
68 Path: "/",
69 Code: http.StatusNotFound,
70 Response: "404 page not found",
71 },
72 { // 2
73 Path: "/AAA",
74 Response: "http://www.google.com",
75 Code: http.StatusMovedPermanently,
76 },
77 { // 3
78 Path: "/BBB",
79 Code: http.StatusNotFound,
80 Response: "404 page not found",
81 },
82 { // 4
83 Path: "/ABCD",
84 Code: http.StatusUnprocessableEntity,
85 Response: invalidKey,
86 },
87 } {
88 w := httptest.NewRecorder()
89 f.ServeHTTP(w, httptest.NewRequest(http.MethodGet, test.Path, nil))
90 if w.Code != test.Code {
91 t.Errorf("test %d: expecting response code %d, got %d", n+1, test.Code, w.Code)
92 } else if test.Code == http.StatusMovedPermanently {
93 if url := w.Header().Get("Location"); url != test.Response {
94 t.Errorf("test %d: expecting Location header to be %q, got %q", n+1, test.Response, url)
95 }
96 } else if response := strings.TrimSpace(w.Body.String()); response != test.Response {
97 t.Errorf("test %d: expecting response %q, got %q", n+1, test.Response, response)
98 }
99 }
100 }
101
102 type nonrand []int64
103
104 func (n *nonrand) Int63() int64 {
105 if len(*n) == 0 {
106 return 0
107 }
108 i := (*n)[0]
109 *n = (*n)[1:]
110 return i
111 }
112
113 func (nonrand) Seed(_ int64) {}
114
115 type postTest struct {
116 Body, Key, Response string
117 Status int
118 }
119
120 func testPost(t *testing.T, contentType string, tests []postTest) {
121 rs := nonrand{0, 0, 1, 2}
122 f := New(SetStore(NewStore(Data(map[string]string{
123 "AA": "http://www.google.com",
124 }))), RandomSource(&rs), KeyLength(1), URLValidator(HTTPURL), KeyValidator(func(key string) bool {
125 return key != "ABC"
126 }))
127 responseType := contentType
128 if contentType == "application/x-www-form-urlencoded" {
129 responseType = "text/html"
130 }
131 for n, test := range tests {
132 w := httptest.NewRecorder()
133 r := httptest.NewRequest(http.MethodPost, "/"+test.Key, strings.NewReader(test.Body))
134 r.Header.Set("Content-Type", contentType)
135 f.ServeHTTP(w, r)
136 if w.Code != test.Status {
137 t.Errorf("test %d: expecting response code %d, got %d", n+1, test.Status, w.Code)
138 } else if response := strings.TrimSpace(w.Body.String()); response != test.Response {
139 t.Errorf("test %d: expecting response %q, got %q", n+1, test.Response, response)
140 } else if contentType := w.Header().Get("Content-Type"); w.Code == 200 && contentType != responseType {
141 t.Errorf("test %d: expecting return content type %q, got %q", n+1, responseType, contentType)
142 }
143 }
144 }
145
146 func TestPostText(t *testing.T) {
147 testPost(t, "text/plain", []postTest{
148 { // 1
149 Body: "ftp://google.com",
150 Response: invalidURL,
151 Status: http.StatusBadRequest,
152 },
153 { // 2
154 Body: "http://google.com/" + strings.Repeat("A", maxURLLength),
155 Response: invalidURL,
156 Status: http.StatusBadRequest,
157 },
158 { // 3
159 Response: invalidURL,
160 Status: http.StatusBadRequest,
161 },
162 { // 4
163 Body: "http://google.com",
164 Key: "ABC",
165 Response: invalidKey,
166 Status: http.StatusUnprocessableEntity,
167 },
168 { // 5
169 Body: "http://google.com",
170 Key: "A" + strings.Repeat("A", maxKeyLength),
171 Response: invalidKey,
172 Status: http.StatusUnprocessableEntity,
173 },
174 { // 6
175 Body: "http://google.com",
176 Key: "AA",
177 Response: keyExists,
178 Status: http.StatusMethodNotAllowed,
179 },
180 { // 7
181 Body: "http://google.com",
182 Response: "AQ",
183 Status: http.StatusOK,
184 },
185 { // 8
186 Body: "http://google.com",
187 Key: "AQ",
188 Response: keyExists,
189 Status: http.StatusMethodNotAllowed,
190 },
191 { // 9
192 Body: "http://google.com",
193 Key: "Ag",
194 Response: "Ag",
195 Status: http.StatusOK,
196 },
197 { // 10
198 Body: "http://google.com",
199 Response: "AAA",
200 Status: http.StatusOK,
201 },
202 })
203 }
204
205 func TestPostJSON(t *testing.T) {
206 testPost(t, "application/json", []postTest{
207 { // 1
208 Body: `{"url":a}`,
209 Response: fmt.Sprintf(`{"error":%q}`, failedReadRequest),
210 Status: http.StatusBadRequest,
211 },
212 { // 2
213 Body: `{"url":"ftp://google.com"}`,
214 Response: fmt.Sprintf(`{"error":%q}`, invalidURL),
215 Status: http.StatusBadRequest,
216 },
217 { // 3
218 Body: `{"url":"http://google.com/` + strings.Repeat("A", maxURLLength) + `"}`,
219 Response: fmt.Sprintf(`{"error":%q}`, invalidURL),
220 Status: http.StatusBadRequest,
221 },
222 { // 4
223 Body: `{}`,
224 Response: fmt.Sprintf(`{"error":%q}`, invalidURL),
225 Status: http.StatusBadRequest,
226 },
227 { // 5
228 Body: `{"url":"http://google.com"}`,
229 Key: "ABC",
230 Response: fmt.Sprintf(`{"error":%q}`, invalidKey),
231 Status: http.StatusUnprocessableEntity,
232 },
233 { // 6
234 Body: `{"key":"ABC","url":"http://google.com"}`,
235 Response: fmt.Sprintf(`{"error":%q}`, invalidKey),
236 Status: http.StatusUnprocessableEntity,
237 },
238 { // 7
239 Body: `{"key":"A` + strings.Repeat("A", maxKeyLength) + `","url":"http://google.com"}`,
240 Response: fmt.Sprintf(`{"error":%q}`, invalidKey),
241 Status: http.StatusUnprocessableEntity,
242 },
243 { // 8
244 Body: `{"url":"http://google.com"}`,
245 Key: "AA",
246 Response: fmt.Sprintf(`{"error":%q}`, keyExists),
247 Status: http.StatusMethodNotAllowed,
248 },
249 { // 9
250 Body: `{"key":"AA","url":"http://google.com"}`,
251 Response: fmt.Sprintf(`{"error":%q}`, keyExists),
252 Status: http.StatusMethodNotAllowed,
253 },
254 { // 10
255 Body: `{"url":"http://google.com"}`,
256 Response: `{"key":"AQ","url":"http://google.com"}`,
257 Status: http.StatusOK,
258 },
259 { // 11
260 Body: `{"url":"http://google.com"}`,
261 Key: "AQ",
262 Response: fmt.Sprintf(`{"error":%q}`, keyExists),
263 Status: http.StatusMethodNotAllowed,
264 },
265 { // 12
266 Body: `{"key":"AQ","url":"http://google.com"}`,
267 Response: fmt.Sprintf(`{"error":%q}`, keyExists),
268 Status: http.StatusMethodNotAllowed,
269 },
270 { // 13
271 Body: `{"url":"http://google.com"}`,
272 Key: "Ag",
273 Response: `{"key":"Ag","url":"http://google.com"}`,
274 Status: http.StatusOK,
275 },
276 { // 14
277 Body: `{"url":"http://google.com"}`,
278 Response: `{"key":"AAA","url":"http://google.com"}`,
279 Status: http.StatusOK,
280 },
281 { // 15
282 Body: `{"key":"ABCD","url":"http://google.com"}`,
283 Response: `{"key":"ABCD","url":"http://google.com"}`,
284 Status: http.StatusOK,
285 },
286 })
287 }
288
289 func TestPostXML(t *testing.T) {
290 testPost(t, "text/xml", []postTest{
291 { // 1
292 Body: "<furl><url>",
293 Response: fmt.Sprintf("<furl><error>%s</error></furl>", failedReadRequest),
294 Status: http.StatusBadRequest,
295 },
296 { // 2
297 Body: "<furl><url>ftp://google.com</url></furl>",
298 Response: fmt.Sprintf("<furl><error>%s</error></furl>", invalidURL),
299 Status: http.StatusBadRequest,
300 },
301 { // 3
302 Body: "<furl><url>http://google.com/" + strings.Repeat("A", maxURLLength) + "</url></furl>",
303 Response: fmt.Sprintf("<furl><error>%s</error></furl>", invalidURL),
304 Status: http.StatusBadRequest,
305 },
306 { // 4
307 Body: "<furl></furl>",
308 Response: fmt.Sprintf("<furl><error>%s</error></furl>", invalidURL),
309 Status: http.StatusBadRequest,
310 },
311 { // 5
312 Body: "<furl><url>http://google.com</url></furl>",
313 Key: "ABC",
314 Response: fmt.Sprintf("<furl><error>%s</error></furl>", invalidKey),
315 Status: http.StatusUnprocessableEntity,
316 },
317 { // 6
318 Body: "<furl><key>ABC</key><url>http://google.com</url></furl>",
319 Response: fmt.Sprintf("<furl><error>%s</error></furl>", invalidKey),
320 Status: http.StatusUnprocessableEntity,
321 },
322 { // 7
323 Body: "<furl><key>A" + strings.Repeat("A", maxKeyLength) + "</key><url>http://google.com</url></furl>",
324 Response: fmt.Sprintf("<furl><error>%s</error></furl>", invalidKey),
325 Status: http.StatusUnprocessableEntity,
326 },
327 { // 8
328 Body: "<furl><url>http://google.com</url></furl>",
329 Key: "AA",
330 Response: fmt.Sprintf("<furl><error>%s</error></furl>", keyExists),
331 Status: http.StatusMethodNotAllowed,
332 },
333 { // 9
334 Body: "<furl><key>AA</key><url>http://google.com</url></furl>",
335 Response: fmt.Sprintf("<furl><error>%s</error></furl>", keyExists),
336 Status: http.StatusMethodNotAllowed,
337 },
338 { // 10
339 Body: "<furl><url>http://google.com</url></furl>",
340 Response: "<furl><key>AQ</key><url>http://google.com</url></furl>",
341 Status: http.StatusOK,
342 },
343 { // 11
344 Body: "<furl><url>http://google.com</url></furl>",
345 Key: "AQ",
346 Response: fmt.Sprintf("<furl><error>%s</error></furl>", keyExists),
347 Status: http.StatusMethodNotAllowed,
348 },
349 { // 12
350 Body: "<furl><key>AQ</key><url>http://google.com</url></furl>",
351 Response: fmt.Sprintf("<furl><error>%s</error></furl>", keyExists),
352 Status: http.StatusMethodNotAllowed,
353 },
354 { // 13
355 Body: "<furl><url>http://google.com</url></furl>",
356 Key: "Ag",
357 Response: "<furl><key>Ag</key><url>http://google.com</url></furl>",
358 Status: http.StatusOK,
359 },
360 { // 14
361 Body: "<furl><url>http://google.com</url></furl>",
362 Response: "<furl><key>AAA</key><url>http://google.com</url></furl>",
363 Status: http.StatusOK,
364 },
365 { // 15
366 Body: "<furl><key>ABCD</key><url>http://google.com</url></furl>",
367 Response: "<furl><key>ABCD</key><url>http://google.com</url></furl>",
368 Status: http.StatusOK,
369 },
370 })
371 }
372
373 func TestPostForm(t *testing.T) {
374 testPost(t, "application/x-www-form-urlencoded", []postTest{
375 { // 1
376 Body: "url=;",
377 Response: failedReadRequest,
378 Status: http.StatusBadRequest,
379 },
380 { // 2
381 Body: "url=",
382 Response: invalidURL,
383 Status: http.StatusBadRequest,
384 },
385 { // 3
386 Body: "url=http://google.com/" + strings.Repeat("A", maxURLLength),
387 Response: invalidURL,
388 Status: http.StatusBadRequest,
389 },
390 { // 4
391 Response: invalidURL,
392 Status: http.StatusBadRequest,
393 },
394 { // 5
395 Body: "url=http://google.com",
396 Key: "ABC",
397 Response: invalidKey,
398 Status: http.StatusUnprocessableEntity,
399 },
400 { // 6
401 Body: "key=ABC&url=http://google.com",
402 Response: invalidKey,
403 Status: http.StatusUnprocessableEntity,
404 },
405 { // 7
406 Body: "key=A" + strings.Repeat("A", maxKeyLength) + "&url=http://google.com",
407 Response: invalidKey,
408 Status: http.StatusUnprocessableEntity,
409 },
410 { // 8
411 Body: "url=http://google.com",
412 Key: "AA",
413 Response: keyExists,
414 Status: http.StatusMethodNotAllowed,
415 },
416 { // 9
417 Body: "key=AA&url=http://google.com",
418 Response: keyExists,
419 Status: http.StatusMethodNotAllowed,
420 },
421 { // 10
422 Body: "url=http://google.com",
423 Response: "AQ",
424 Status: http.StatusOK,
425 },
426 { // 11
427 Body: "url=http://google.com",
428 Key: "AQ",
429 Response: keyExists,
430 Status: http.StatusMethodNotAllowed,
431 },
432 { // 12
433 Body: "key=AQ&url=http://google.com",
434 Response: keyExists,
435 Status: http.StatusMethodNotAllowed,
436 },
437 { // 13
438 Body: "url=http://google.com",
439 Key: "Ag",
440 Response: "Ag",
441 Status: http.StatusOK,
442 },
443 { // 14
444 Body: "url=http://google.com",
445 Response: "AAA",
446 Status: http.StatusOK,
447 },
448 { // 15
449 Body: "key=ABCD&url=http://google.com",
450 Response: "ABCD",
451 Status: http.StatusOK,
452 },
453 })
454 }
455
456 func TestPostOther(t *testing.T) {
457 testPost(t, "unknown", []postTest{
458 { // 1
459 Body: "http://google.com",
460 Response: unrecognisedContentType,
461 Status: http.StatusUnsupportedMediaType,
462 },
463 })
464 f := New(CollisionRetries(1), KeyValidator(func(key string) bool {
465 return false
466 }))
467 for n, test := range [...]struct {
468 Body, ContentType, Response string
469 }{
470 { // 2
471 Body: "http://google.com",
472 ContentType: "text/plain",
473 Response: failedKeyGeneration,
474 },
475 { // 3
476 Body: `{"url":"http://google.com"}`,
477 ContentType: "text/json",
478 Response: fmt.Sprintf(`{"error":%q}`, failedKeyGeneration),
479 },
480 { // 4
481 Body: "<furl><url>http://google.com</url></furl>",
482 ContentType: "application/xml",
483 Response: fmt.Sprintf("<furl><error>%s</error></furl>", failedKeyGeneration),
484 },
485 { // 5
486 Body: "url=http://google.com",
487 ContentType: "application/x-www-form-urlencoded",
488 Response: failedKeyGeneration,
489 },
490 } {
491 w := httptest.NewRecorder()
492 r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(test.Body))
493 r.Header.Set("Content-Type", test.ContentType)
494 f.ServeHTTP(w, r)
495 if test.ContentType == "application/x-www-form-urlencoded" {
496 test.ContentType = "text/html"
497 }
498 if w.Code != http.StatusInternalServerError {
499 t.Errorf("test %d: expecting response code 500, got %d", n+2, w.Code)
500 } else if response := strings.TrimSpace(w.Body.String()); response != test.Response {
501 t.Errorf("test %d: expecting response %q, got %q", n+2, test.Response, response)
502 } else if contentType := w.Header().Get("Content-Type"); w.Code == 200 && contentType != test.ContentType {
503 t.Errorf("test %d: expecting return content type %q, got %q", n+2, test.ContentType, contentType)
504 }
505 }
506 }
507
508 func TestIndex(t *testing.T) {
509 var rs nonrand
510 f := New(RandomSource(&rs), CollisionRetries(1), KeyValidator(func(key string) bool {
511 return key == "AAAAAAAA" || key == "GOODKEY"
512 }), URLValidator(func(url string) bool {
513 return url != "BADURL"
514 }), Index(func(w http.ResponseWriter, r *http.Request, code int, data string) {
515 if r.Method == http.MethodGet {
516 w.WriteHeader(code)
517 io.WriteString(w, strings.ToUpper(data))
518 } else {
519 w.WriteHeader(code)
520 io.WriteString(w, data+"!")
521 }
522 }))
523 for n, test := range [...]struct {
524 Method, Path, Body, Response string
525 }{
526 { // 1
527 Method: "GET",
528 Path: "/AAAAAAAA",
529 Response: "404 PAGE NOT FOUND",
530 },
531 { // 2
532 Method: "GET",
533 Path: "/BADKEY",
534 Response: "INVALID KEY",
535 },
536 { // 3
537 Method: "POST",
538 Path: "/",
539 Body: "url=;",
540 Response: failedReadRequest + "!",
541 },
542 { // 4
543 Method: "POST",
544 Path: "/",
545 Body: "",
546 Response: invalidURL + "!",
547 },
548 { // 5
549 Method: "POST",
550 Path: "/",
551 Body: "url=BADURL",
552 Response: invalidURL + "!",
553 },
554 { // 6
555 Method: "POST",
556 Path: "/BADKEY",
557 Body: "url=GOODURL",
558 Response: invalidKey + "!",
559 },
560 { // 7
561 Method: "POST",
562 Path: "/",
563 Body: "key=BADKEY&url=GOODURL",
564 Response: "invalid key!",
565 },
566 { // 8
567 Method: "POST",
568 Path: "/",
569 Body: "url=GOODURL",
570 Response: "AAAAAAAA!",
571 },
572 { // 9
573 Method: "POST",
574 Path: "/GOODKEY",
575 Body: "url=GOODURL",
576 Response: "GOODKEY!",
577 },
578 { // 10
579 Method: "POST",
580 Path: "/GOODKEY",
581 Body: "url=GOODURL",
582 Response: keyExists + "!",
583 },
584 { // 1
585 Method: "POST",
586 Path: "/",
587 Body: "url=GOODURL",
588 Response: failedKeyGeneration + "!",
589 },
590 } {
591 w := httptest.NewRecorder()
592 r := httptest.NewRequest(test.Method, "/"+test.Path, strings.NewReader(test.Body))
593 if test.Method == "POST" {
594 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
595 }
596 f.ServeHTTP(w, r)
597 if response := strings.TrimSpace(w.Body.String()); response != test.Response {
598 t.Errorf("test %d: expecting response %q, got %q", n+1, test.Response, response)
599 }
600 }
601 }
602