馃 A practical web framework for Gleam
1import gleam/bit_array
2import gleam/crypto
3import gleam/dict
4import gleam/dynamic.{type Dynamic}
5import gleam/erlang
6import gleam/http
7import gleam/http/request
8import gleam/http/response.{Response}
9import gleam/int
10import gleam/list
11import gleam/set
12import gleam/string
13import gleam/string_builder
14import gleeunit
15import gleeunit/should
16import simplifile
17import wisp
18import wisp/testing
19
20pub fn main() {
21 wisp.configure_logger()
22 gleeunit.main()
23}
24
25fn form_handler(
26 request: wisp.Request,
27 callback: fn(wisp.FormData) -> anything,
28) -> wisp.Response {
29 use form <- wisp.require_form(request)
30 callback(form)
31 wisp.ok()
32}
33
34fn json_handler(
35 request: wisp.Request,
36 callback: fn(Dynamic) -> anything,
37) -> wisp.Response {
38 use json <- wisp.require_json(request)
39 callback(json)
40 wisp.ok()
41}
42
43pub fn ok_test() {
44 wisp.ok()
45 |> should.equal(Response(200, [], wisp.Empty))
46}
47
48pub fn created_test() {
49 wisp.created()
50 |> should.equal(Response(201, [], wisp.Empty))
51}
52
53pub fn accepted_test() {
54 wisp.accepted()
55 |> should.equal(Response(202, [], wisp.Empty))
56}
57
58pub fn no_content_test() {
59 wisp.no_content()
60 |> should.equal(Response(204, [], wisp.Empty))
61}
62
63pub fn redirect_test() {
64 wisp.redirect(to: "https://example.com/wibble")
65 |> should.equal(Response(
66 303,
67 [#("location", "https://example.com/wibble")],
68 wisp.Empty,
69 ))
70}
71
72pub fn moved_permanently_test() {
73 wisp.moved_permanently(to: "https://example.com/wobble")
74 |> should.equal(Response(
75 308,
76 [#("location", "https://example.com/wobble")],
77 wisp.Empty,
78 ))
79}
80
81pub fn internal_server_error_test() {
82 wisp.internal_server_error()
83 |> should.equal(Response(500, [], wisp.Empty))
84}
85
86pub fn entity_too_large_test() {
87 wisp.entity_too_large()
88 |> should.equal(Response(413, [], wisp.Empty))
89}
90
91pub fn bad_request_test() {
92 wisp.bad_request()
93 |> should.equal(Response(400, [], wisp.Empty))
94}
95
96pub fn not_found_test() {
97 wisp.not_found()
98 |> should.equal(Response(404, [], wisp.Empty))
99}
100
101pub fn method_not_allowed_test() {
102 wisp.method_not_allowed([http.Get, http.Patch, http.Delete])
103 |> should.equal(Response(405, [#("allow", "DELETE, GET, PATCH")], wisp.Empty))
104}
105
106pub fn unsupported_media_type_test() {
107 wisp.unsupported_media_type(accept: ["application/json", "text/plain"])
108 |> should.equal(Response(
109 415,
110 [#("accept", "application/json, text/plain")],
111 wisp.Empty,
112 ))
113}
114
115pub fn unprocessable_entity_test() {
116 wisp.unprocessable_entity()
117 |> should.equal(Response(422, [], wisp.Empty))
118}
119
120pub fn json_response_test() {
121 let body = string_builder.from_string("{\"one\":1,\"two\":2}")
122 let response = wisp.json_response(body, 201)
123 response.status
124 |> should.equal(201)
125 response.headers
126 |> should.equal([#("content-type", "application/json")])
127 response
128 |> testing.string_body
129 |> should.equal("{\"one\":1,\"two\":2}")
130}
131
132pub fn html_response_test() {
133 let body = string_builder.from_string("Hello, world!")
134 let response = wisp.html_response(body, 200)
135 response.status
136 |> should.equal(200)
137 response.headers
138 |> should.equal([#("content-type", "text/html")])
139 response
140 |> testing.string_body
141 |> should.equal("Hello, world!")
142}
143
144pub fn html_body_test() {
145 let body = string_builder.from_string("Hello, world!")
146 let response =
147 wisp.method_not_allowed([http.Get])
148 |> wisp.html_body(body)
149 response.status
150 |> should.equal(405)
151 response.headers
152 |> should.equal([#("allow", "GET"), #("content-type", "text/html")])
153 response
154 |> testing.string_body
155 |> should.equal("Hello, world!")
156}
157
158pub fn random_string_test() {
159 let count = 10_000
160 let new = fn(_) {
161 let random = wisp.random_string(64)
162 string.length(random)
163 |> should.equal(64)
164 random
165 }
166
167 list.repeat(Nil, count)
168 |> list.map(new)
169 |> set.from_list
170 |> set.size
171 |> should.equal(count)
172}
173
174pub fn set_get_secret_key_base_test() {
175 let request = testing.get("/", [])
176 let valid = wisp.random_string(64)
177 let too_short = wisp.random_string(63)
178
179 request
180 |> wisp.get_secret_key_base
181 |> should.equal(testing.default_secret_key_base)
182
183 request
184 |> wisp.set_secret_key_base(valid)
185 |> wisp.get_secret_key_base
186 |> should.equal(valid)
187
188 // Panics if the key is too short
189 erlang.rescue(fn() { wisp.set_secret_key_base(request, too_short) })
190 |> should.be_error
191}
192
193pub fn set_get_max_body_size_test() {
194 let request = testing.get("/", [])
195
196 request
197 |> wisp.get_max_body_size
198 |> should.equal(8_000_000)
199
200 request
201 |> wisp.set_max_body_size(10)
202 |> wisp.get_max_body_size
203 |> should.equal(10)
204}
205
206pub fn set_get_max_files_size_test() {
207 let request = testing.get("/", [])
208
209 request
210 |> wisp.get_max_files_size
211 |> should.equal(32_000_000)
212
213 request
214 |> wisp.set_max_files_size(10)
215 |> wisp.get_max_files_size
216 |> should.equal(10)
217}
218
219pub fn set_get_read_chunk_size_test() {
220 let request = testing.get("/", [])
221
222 request
223 |> wisp.get_read_chunk_size
224 |> should.equal(1_000_000)
225
226 request
227 |> wisp.set_read_chunk_size(10)
228 |> wisp.get_read_chunk_size
229 |> should.equal(10)
230}
231
232pub fn path_segments_test() {
233 request.new()
234 |> request.set_path("/one/two/three")
235 |> wisp.path_segments
236 |> should.equal(["one", "two", "three"])
237}
238
239pub fn method_override_test() {
240 // These methods can be overridden to
241 use method <- list.each([http.Put, http.Delete, http.Patch])
242
243 let request =
244 request.new()
245 |> request.set_method(method)
246 |> request.set_query([#("_method", http.method_to_string(method))])
247 request
248 |> wisp.method_override
249 |> should.equal(request.set_method(request, method))
250}
251
252pub fn method_override_unacceptable_unoriginal_method_test() {
253 // These methods are not allowed to be overridden
254 use method <- list.each([
255 http.Head,
256 http.Put,
257 http.Delete,
258 http.Trace,
259 http.Connect,
260 http.Options,
261 http.Patch,
262 http.Other("MYSTERY"),
263 ])
264
265 let request =
266 request.new()
267 |> request.set_method(method)
268 |> request.set_query([#("_method", "DELETE")])
269 request
270 |> wisp.method_override
271 |> should.equal(request)
272}
273
274pub fn method_override_unacceptable_target_method_test() {
275 // These methods are not allowed to be overridden to
276 use method <- list.each([
277 http.Get,
278 http.Head,
279 http.Trace,
280 http.Connect,
281 http.Options,
282 http.Other("MYSTERY"),
283 ])
284
285 let request =
286 request.new()
287 |> request.set_method(http.Post)
288 |> request.set_query([#("_method", http.method_to_string(method))])
289 request
290 |> wisp.method_override
291 |> should.equal(request)
292}
293
294pub fn require_method_test() {
295 {
296 let request = request.new()
297 use <- wisp.require_method(request, http.Get)
298 wisp.ok()
299 }
300 |> should.equal(wisp.ok())
301}
302
303pub fn require_method_invalid_test() {
304 {
305 let request = request.set_method(request.new(), http.Post)
306 use <- wisp.require_method(request, http.Get)
307 panic as "should be unreachable"
308 }
309 |> should.equal(wisp.method_not_allowed([http.Get]))
310}
311
312pub fn require_string_body_test() {
313 {
314 let request = testing.post("/", [], "Hello, Joe!")
315 use body <- wisp.require_string_body(request)
316 body
317 |> should.equal("Hello, Joe!")
318 wisp.accepted()
319 }
320 |> should.equal(wisp.accepted())
321}
322
323pub fn require_string_body_invalid_test() {
324 {
325 let request = testing.request(http.Post, "/", [], <<254>>)
326 use _ <- wisp.require_string_body(request)
327 panic as "should be unreachable"
328 }
329 |> should.equal(wisp.bad_request())
330}
331
332pub fn rescue_crashes_error_test() {
333 // TODO: Determine how to silence the logger for this test.
334 {
335 use <- wisp.rescue_crashes
336 panic as "we need to crash to test the middleware"
337 }
338 |> should.equal(wisp.internal_server_error())
339}
340
341pub fn rescue_crashes_ok_test() {
342 {
343 use <- wisp.rescue_crashes
344 wisp.ok()
345 }
346 |> should.equal(wisp.ok())
347}
348
349pub fn serve_static_test() {
350 let handler = fn(request) {
351 use <- wisp.serve_static(request, under: "/stuff", from: "./")
352 wisp.ok()
353 }
354
355 // Get a text file
356 let response =
357 testing.get("/stuff/test/fixture.txt", [])
358 |> handler
359 response.status
360 |> should.equal(200)
361 response.headers
362 |> should.equal([#("content-type", "text/plain")])
363 response.body
364 |> should.equal(wisp.File("./test/fixture.txt"))
365
366 // Get a json file
367 let response =
368 testing.get("/stuff/test/fixture.json", [])
369 |> handler
370 response.status
371 |> should.equal(200)
372 response.headers
373 |> should.equal([#("content-type", "application/json")])
374 response.body
375 |> should.equal(wisp.File("./test/fixture.json"))
376
377 // Get something not handled by the static file server
378 let response =
379 testing.get("/stuff/this-does-not-exist", [])
380 |> handler
381 response.status
382 |> should.equal(200)
383 response.headers
384 |> should.equal([])
385 response.body
386 |> should.equal(wisp.Empty)
387}
388
389pub fn serve_static_under_has_no_trailing_slash_test() {
390 let request =
391 testing.get("/", [])
392 |> request.set_path("/stuff/test/fixture.txt")
393 let response = {
394 use <- wisp.serve_static(request, under: "stuff", from: "./")
395 wisp.ok()
396 }
397 response.status
398 |> should.equal(200)
399 response.headers
400 |> should.equal([#("content-type", "text/plain")])
401 response.body
402 |> should.equal(wisp.File("./test/fixture.txt"))
403}
404
405pub fn serve_static_from_has_no_trailing_slash_test() {
406 let request =
407 testing.get("/", [])
408 |> request.set_path("/stuff/test/fixture.txt")
409 let response = {
410 use <- wisp.serve_static(request, under: "stuff", from: ".")
411 wisp.ok()
412 }
413 response.status
414 |> should.equal(200)
415 response.headers
416 |> should.equal([#("content-type", "text/plain")])
417 response.body
418 |> should.equal(wisp.File("./test/fixture.txt"))
419}
420
421pub fn serve_static_not_found_test() {
422 let request =
423 testing.get("/", [])
424 |> request.set_path("/stuff/credit_card_details.txt")
425 {
426 use <- wisp.serve_static(request, under: "/stuff", from: "./")
427 wisp.ok()
428 }
429 |> should.equal(wisp.ok())
430}
431
432pub fn serve_static_go_up_test() {
433 let request =
434 testing.get("/", [])
435 |> request.set_path("/../test/fixture.txt")
436 {
437 use <- wisp.serve_static(request, under: "/stuff", from: "./src/")
438 wisp.ok()
439 }
440 |> should.equal(wisp.ok())
441}
442
443pub fn temporary_file_test() {
444 // Create tmp files for a first request
445 let request1 = testing.get("/", [])
446 let assert Ok(request1_file1) = wisp.new_temporary_file(request1)
447 let assert Ok(request1_file2) = wisp.new_temporary_file(request1)
448
449 // The files exist
450 request1_file1
451 |> should.not_equal(request1_file2)
452 let assert Ok(_) = simplifile.read(request1_file1)
453 let assert Ok(_) = simplifile.read(request1_file2)
454
455 // Create tmp files for a second request
456 let request2 = testing.get("/", [])
457 let assert Ok(request2_file1) = wisp.new_temporary_file(request2)
458 let assert Ok(request2_file2) = wisp.new_temporary_file(request2)
459
460 // The files exist
461 request2_file1
462 |> should.not_equal(request1_file2)
463 let assert Ok(_) = simplifile.read(request2_file1)
464 let assert Ok(_) = simplifile.read(request2_file2)
465
466 // Delete the files for the first request
467 let assert Ok(_) = wisp.delete_temporary_files(request1)
468
469 // They no longer exist
470 let assert Error(simplifile.Enoent) = simplifile.read(request1_file1)
471 let assert Error(simplifile.Enoent) = simplifile.read(request1_file2)
472
473 // The files for the second request still exist
474 let assert Ok(_) = simplifile.read(request2_file1)
475 let assert Ok(_) = simplifile.read(request2_file2)
476
477 // Delete the files for the first request
478 let assert Ok(_) = wisp.delete_temporary_files(request2)
479
480 // They no longer exist
481 let assert Error(simplifile.Enoent) = simplifile.read(request2_file1)
482 let assert Error(simplifile.Enoent) = simplifile.read(request2_file2)
483}
484
485pub fn require_content_type_test() {
486 {
487 let request = testing.get("/", [#("content-type", "text/plain")])
488 use <- wisp.require_content_type(request, "text/plain")
489 wisp.ok()
490 }
491 |> should.equal(wisp.ok())
492}
493
494pub fn require_content_type_missing_test() {
495 {
496 let request = testing.get("/", [])
497 use <- wisp.require_content_type(request, "text/plain")
498 wisp.ok()
499 }
500 |> should.equal(wisp.unsupported_media_type(["text/plain"]))
501}
502
503pub fn require_content_type_invalid_test() {
504 {
505 let request = testing.get("/", [#("content-type", "text/plain")])
506 use <- wisp.require_content_type(request, "text/html")
507 panic as "should be unreachable"
508 }
509 |> should.equal(wisp.unsupported_media_type(["text/html"]))
510}
511
512pub fn json_test() {
513 testing.post("/", [], "{\"one\":1,\"two\":2}")
514 |> request.set_header("content-type", "application/json")
515 |> json_handler(fn(json) {
516 json
517 |> should.equal(dynamic.from(dict.from_list([#("one", 1), #("two", 2)])))
518 })
519 |> should.equal(wisp.ok())
520}
521
522pub fn json_wrong_content_type_test() {
523 testing.post("/", [], "{\"one\":1,\"two\":2}")
524 |> request.set_header("content-type", "text/plain")
525 |> json_handler(fn(_) { panic as "should be unreachable" })
526 |> should.equal(wisp.unsupported_media_type(["application/json"]))
527}
528
529pub fn json_no_content_type_test() {
530 testing.post("/", [], "{\"one\":1,\"two\":2}")
531 |> json_handler(fn(_) { panic as "should be unreachable" })
532 |> should.equal(wisp.unsupported_media_type(["application/json"]))
533}
534
535pub fn json_too_big_test() {
536 testing.post("/", [], "{\"one\":1,\"two\":2}")
537 |> wisp.set_max_body_size(1)
538 |> request.set_header("content-type", "application/json")
539 |> json_handler(fn(_) { panic as "should be unreachable" })
540 |> should.equal(Response(413, [], wisp.Empty))
541}
542
543pub fn json_syntax_error_test() {
544 testing.post("/", [], "{\"one\":1,\"two\":2")
545 |> request.set_header("content-type", "application/json")
546 |> json_handler(fn(_) { panic as "should be unreachable" })
547 |> should.equal(Response(400, [], wisp.Empty))
548}
549
550pub fn urlencoded_form_test() {
551 testing.post("/", [], "one=1&two=2")
552 |> request.set_header("content-type", "application/x-www-form-urlencoded")
553 |> form_handler(fn(form) {
554 form
555 |> should.equal(wisp.FormData([#("one", "1"), #("two", "2")], []))
556 })
557 |> should.equal(wisp.ok())
558}
559
560pub fn urlencoded_form_with_charset_test() {
561 testing.post("/", [], "one=1&two=2")
562 |> request.set_header(
563 "content-type",
564 "application/x-www-form-urlencoded; charset=UTF-8",
565 )
566 |> form_handler(fn(form) {
567 form
568 |> should.equal(wisp.FormData([#("one", "1"), #("two", "2")], []))
569 })
570 |> should.equal(wisp.ok())
571}
572
573pub fn urlencoded_too_big_form_test() {
574 testing.post("/", [], "12")
575 |> request.set_header("content-type", "application/x-www-form-urlencoded")
576 |> wisp.set_max_body_size(1)
577 |> form_handler(fn(_) { panic as "should be unreachable" })
578 |> should.equal(Response(413, [], wisp.Empty))
579}
580
581pub fn multipart_form_test() {
582 "--theboundary\r
583Content-Disposition: form-data; name=\"one\"\r
584\r
5851\r
586--theboundary\r
587Content-Disposition: form-data; name=\"two\"\r
588\r
5892\r
590--theboundary--\r
591"
592 |> testing.post("/", [], _)
593 |> request.set_header(
594 "content-type",
595 "multipart/form-data; boundary=theboundary",
596 )
597 |> form_handler(fn(form) {
598 form
599 |> should.equal(wisp.FormData([#("one", "1"), #("two", "2")], []))
600 })
601 |> should.equal(wisp.ok())
602}
603
604pub fn multipart_form_too_big_test() {
605 "--theboundary\r
606Content-Disposition: form-data; name=\"one\"\r
607\r
6081\r
609--theboundary--\r
610"
611 |> testing.post("/", [], _)
612 |> wisp.set_max_body_size(1)
613 |> request.set_header(
614 "content-type",
615 "multipart/form-data; boundary=theboundary",
616 )
617 |> form_handler(fn(_) { panic as "should be unreachable" })
618 |> should.equal(Response(413, [], wisp.Empty))
619}
620
621pub fn multipart_form_no_boundary_test() {
622 "--theboundary\r
623Content-Disposition: form-data; name=\"one\"\r
624\r
6251\r
626--theboundary--\r
627"
628 |> testing.post("/", [], _)
629 |> request.set_header("content-type", "multipart/form-data")
630 |> form_handler(fn(_) { panic as "should be unreachable" })
631 |> should.equal(Response(400, [], wisp.Empty))
632}
633
634pub fn multipart_form_invalid_format_test() {
635 "--theboundary\r\n--theboundary--\r\n"
636 |> testing.post("/", [], _)
637 |> request.set_header(
638 "content-type",
639 "multipart/form-data; boundary=theboundary",
640 )
641 |> form_handler(fn(_) { panic as "should be unreachable" })
642 |> should.equal(Response(400, [], wisp.Empty))
643}
644
645pub fn form_unknown_content_type_test() {
646 "one=1&two=2"
647 |> testing.post("/", [], _)
648 |> request.set_header("content-type", "text/form")
649 |> form_handler(fn(_) { panic as "should be unreachable" })
650 |> should.equal(Response(
651 415,
652 [#("accept", "application/x-www-form-urlencoded, multipart/form-data")],
653 wisp.Empty,
654 ))
655}
656
657pub fn multipart_form_with_files_test() {
658 "--theboundary\r
659Content-Disposition: form-data; name=\"one\"\r
660\r
6611\r
662--theboundary\r
663Content-Disposition: form-data; name=\"two\"; filename=\"file.txt\"\r
664\r
665file contents\r
666--theboundary--\r
667"
668 |> testing.post("/", [], _)
669 |> request.set_header(
670 "content-type",
671 "multipart/form-data; boundary=theboundary",
672 )
673 |> form_handler(fn(form) {
674 let assert [#("one", "1")] = form.values
675 let assert [#("two", wisp.UploadedFile("file.txt", path))] = form.files
676 let assert Ok("file contents") = simplifile.read(path)
677 })
678 |> should.equal(wisp.ok())
679}
680
681pub fn multipart_form_files_too_big_test() {
682 let testcase = fn(limit, callback) {
683 "--theboundary\r
684Content-Disposition: form-data; name=\"two\"; filename=\"file.txt\"\r
685\r
68612\r
687--theboundary\r
688Content-Disposition: form-data; name=\"two\"\r
689\r
690this one isn't a file. If it was it would use the entire quota.\r
691--theboundary\r
692Content-Disposition: form-data; name=\"two\"; filename=\"another.txt\"\r
693\r
69434\r
695--theboundary--\r
696"
697 |> testing.post("/", [], _)
698 |> wisp.set_max_files_size(limit)
699 |> request.set_header(
700 "content-type",
701 "multipart/form-data; boundary=theboundary",
702 )
703 |> form_handler(callback)
704 }
705
706 testcase(1, fn(_) { panic as "should be unreachable for limit of 1" })
707 |> should.equal(Response(413, [], wisp.Empty))
708
709 testcase(2, fn(_) { panic as "should be unreachable for limit of 2" })
710 |> should.equal(Response(413, [], wisp.Empty))
711
712 testcase(3, fn(_) { panic as "should be unreachable for limit of 3" })
713 |> should.equal(Response(413, [], wisp.Empty))
714
715 testcase(4, fn(_) { Nil })
716 |> should.equal(Response(200, [], wisp.Empty))
717}
718
719pub fn handle_head_test() {
720 let handler = fn(request, header) {
721 use request <- wisp.handle_head(request)
722 use <- wisp.require_method(request, http.Get)
723
724 list.key_find(request.headers, "x-original-method")
725 |> should.equal(header)
726
727 string_builder.from_string("Hello!")
728 |> wisp.html_response(201)
729 }
730
731 testing.get("/", [])
732 |> request.set_method(http.Get)
733 |> handler(Error(Nil))
734 |> should.equal(Response(
735 201,
736 [#("content-type", "text/html")],
737 wisp.Text(string_builder.from_string("Hello!")),
738 ))
739
740 testing.get("/", [])
741 |> request.set_method(http.Head)
742 |> handler(Ok("HEAD"))
743 |> should.equal(Response(201, [#("content-type", "text/html")], wisp.Empty))
744
745 testing.get("/", [])
746 |> request.set_method(http.Post)
747 |> handler(Error(Nil))
748 |> should.equal(Response(405, [#("allow", "GET")], wisp.Empty))
749}
750
751pub fn multipart_form_fields_are_sorted_test() {
752 "--theboundary\r
753Content-Disposition: form-data; name=\"xx\"\r
754\r
755XX\r
756--theboundary\r
757Content-Disposition: form-data; name=\"zz\"\r
758\r
759ZZ\r
760--theboundary\r
761Content-Disposition: form-data; name=\"yy\"\r
762\r
763YY\r
764--theboundary\r
765Content-Disposition: form-data; name=\"cc\"; filename=\"file.txt\"\r
766\r
767CC\r
768--theboundary\r
769Content-Disposition: form-data; name=\"aa\"; filename=\"file.txt\"\r
770\r
771AA\r
772--theboundary\r
773Content-Disposition: form-data; name=\"bb\"; filename=\"file.txt\"\r
774\r
775BB\r
776--theboundary--\r
777"
778 |> testing.post("/", [], _)
779 |> request.set_header(
780 "content-type",
781 "multipart/form-data; boundary=theboundary",
782 )
783 |> form_handler(fn(form) {
784 // Fields are sorted by name.
785 let assert [#("xx", "XX"), #("yy", "YY"), #("zz", "ZZ")] = form.values
786 let assert [
787 #("aa", wisp.UploadedFile("file.txt", path_a)),
788 #("bb", wisp.UploadedFile("file.txt", path_b)),
789 #("cc", wisp.UploadedFile("file.txt", path_c)),
790 ] = form.files
791 let assert Ok("AA") = simplifile.read(path_a)
792 let assert Ok("BB") = simplifile.read(path_b)
793 let assert Ok("CC") = simplifile.read(path_c)
794 })
795 |> should.equal(wisp.ok())
796}
797
798pub fn urlencoded_form_fields_are_sorted_test() {
799 "xx=XX&zz=ZZ&yy=YY&cc=CC&aa=AA&bb=BB"
800 |> testing.post("/", [], _)
801 |> request.set_header("content-type", "application/x-www-form-urlencoded")
802 |> form_handler(fn(form) {
803 // Fields are sorted by name.
804 let assert [
805 #("aa", "AA"),
806 #("bb", "BB"),
807 #("cc", "CC"),
808 #("xx", "XX"),
809 #("yy", "YY"),
810 #("zz", "ZZ"),
811 ] = form.values
812 })
813 |> should.equal(wisp.ok())
814}
815
816pub fn message_signing_test() {
817 let request = testing.get("/", [])
818 let request1 = wisp.set_secret_key_base(request, wisp.random_string(64))
819 let request2 = wisp.set_secret_key_base(request, wisp.random_string(64))
820
821 let signed1 = wisp.sign_message(request1, <<"a":utf8>>, crypto.Sha512)
822 let signed2 = wisp.sign_message(request2, <<"b":utf8>>, crypto.Sha512)
823
824 let assert Ok(<<"a":utf8>>) = wisp.verify_signed_message(request1, signed1)
825 let assert Ok(<<"b":utf8>>) = wisp.verify_signed_message(request2, signed2)
826
827 let assert Error(Nil) = wisp.verify_signed_message(request1, signed2)
828 let assert Error(Nil) = wisp.verify_signed_message(request2, signed1)
829}
830
831pub fn create_canned_connection_test() {
832 let secret = wisp.random_string(64)
833 let connection = wisp.create_canned_connection(<<"Hello!":utf8>>, secret)
834 let request = request.set_body(request.new(), connection)
835
836 request
837 |> wisp.get_secret_key_base
838 |> should.equal(secret)
839
840 request
841 |> wisp.read_body_to_bitstring
842 |> should.equal(Ok(<<"Hello!":utf8>>))
843}
844
845pub fn escape_html_test() {
846 "<script>alert('&');</script>"
847 |> wisp.escape_html
848 |> should.equal("<script>alert('&');</script>")
849}
850
851pub fn set_header_test() {
852 wisp.ok()
853 |> wisp.set_header("accept", "application/json")
854 |> wisp.set_header("accept", "text/plain")
855 |> wisp.set_header("content-type", "text/html")
856 |> should.equal(Response(
857 200,
858 [#("accept", "text/plain"), #("content-type", "text/html")],
859 wisp.Empty,
860 ))
861}
862
863pub fn string_body_test() {
864 wisp.ok()
865 |> wisp.string_body("Hello, world!")
866 |> should.equal(Response(
867 200,
868 [],
869 wisp.Text(string_builder.from_string("Hello, world!")),
870 ))
871}
872
873pub fn string_builder_body_test() {
874 wisp.ok()
875 |> wisp.string_builder_body(string_builder.from_string("Hello, world!"))
876 |> should.equal(Response(
877 200,
878 [],
879 wisp.Text(string_builder.from_string("Hello, world!")),
880 ))
881}
882
883pub fn json_body_test() {
884 wisp.ok()
885 |> wisp.json_body(string_builder.from_string("{\"one\":1,\"two\":2}"))
886 |> should.equal(Response(
887 200,
888 [#("content-type", "application/json")],
889 wisp.Text(string_builder.from_string("{\"one\":1,\"two\":2}")),
890 ))
891}
892
893pub fn priv_directory_test() {
894 let assert Error(Nil) = wisp.priv_directory("unknown_application")
895
896 let assert Ok(dir) = wisp.priv_directory("wisp")
897 let assert True = string.ends_with(dir, "/wisp/priv")
898
899 let assert Ok(dir) = wisp.priv_directory("gleam_erlang")
900 let assert True = string.ends_with(dir, "/gleam_erlang/priv")
901
902 let assert Ok(dir) = wisp.priv_directory("gleam_stdlib")
903 let assert True = string.ends_with(dir, "/gleam_stdlib/priv")
904}
905
906pub fn set_cookie_plain_test() {
907 let req = testing.get("/", [])
908 let response =
909 wisp.ok()
910 |> wisp.set_cookie(req, "id", "123", wisp.PlainText, 60 * 60 * 24 * 365)
911 |> wisp.set_cookie(req, "flash", "hi-there", wisp.PlainText, 60)
912
913 response.headers
914 |> should.equal([
915 #(
916 "set-cookie",
917 "flash=aGktdGhlcmU; Max-Age=60; Path=/; Secure; HttpOnly; SameSite=Lax",
918 ),
919 #(
920 "set-cookie",
921 "id=MTIz; Max-Age=31536000; Path=/; Secure; HttpOnly; SameSite=Lax",
922 ),
923 ])
924}
925
926pub fn set_cookie_signed_test() {
927 let req = testing.get("/", [])
928 let response =
929 wisp.ok()
930 |> wisp.set_cookie(req, "id", "123", wisp.Signed, 60 * 60 * 24 * 365)
931 |> wisp.set_cookie(req, "flash", "hi-there", wisp.Signed, 60)
932
933 response.headers
934 |> should.equal([
935 #(
936 "set-cookie",
937 "flash=SFM1MTI.aGktdGhlcmU.uWUWvrAleKQ2jsWcU97HzGgPqtLjjUgl4oe40-RPJ5qRRcE_soXPacgmaHTLxK3xZbOJ5DOTIRMI0szD4Re7wA; Max-Age=60; Path=/; Secure; HttpOnly; SameSite=Lax",
938 ),
939 #(
940 "set-cookie",
941 "id=SFM1MTI.MTIz.LT5VxVwopQ7VhZ3OzF6Pgy3sfIIQaiUH5anHXNRt6o3taBMfCNBQskZ-EIkodchsPGSu_AJrAHjMfYPV7D5ogg; Max-Age=31536000; Path=/; Secure; HttpOnly; SameSite=Lax",
942 ),
943 ])
944}
945
946pub fn get_cookie_test() {
947 let request =
948 testing.get("/", [
949 // Plain text
950 #("cookie", "plain=MTIz"),
951 // Signed
952 #(
953 "cookie",
954 "signed=SFM1MTI.aGktdGhlcmU.uWUWvrAleKQ2jsWcU97HzGgPqtLjjUgl4oe40-RPJ5qRRcE_soXPacgmaHTLxK3xZbOJ5DOTIRMI0szD4Re7wA",
955 ),
956 // Signed but tampered with
957 #(
958 "cookie",
959 "signed-and-tampered-with=SFM1MTI.aGktdGhlcmU.uWUWvrAleKQ2jsWcU97HzGgPqtLjjUgl4oe40-RPJ5qRRcE_soXPacgmaHTLxK3xZbOJ5DOTIRMI0szD4Re7wAA",
960 ),
961 ])
962
963 request
964 |> wisp.get_cookie("plain", wisp.PlainText)
965 |> should.equal(Ok("123"))
966 request
967 |> wisp.get_cookie("plain", wisp.Signed)
968 |> should.equal(Error(Nil))
969
970 request
971 |> wisp.get_cookie("signed", wisp.PlainText)
972 |> should.equal(Error(Nil))
973 request
974 |> wisp.get_cookie("signed", wisp.Signed)
975 |> should.equal(Ok("hi-there"))
976
977 request
978 |> wisp.get_cookie("signed-and-tampered-with", wisp.PlainText)
979 |> should.equal(Error(Nil))
980 request
981 |> wisp.get_cookie("signed-and-tampered-with", wisp.Signed)
982 |> should.equal(Error(Nil))
983
984 request
985 |> wisp.get_cookie("unknown", wisp.PlainText)
986 |> should.equal(Error(Nil))
987 request
988 |> wisp.get_cookie("unknown", wisp.Signed)
989 |> should.equal(Error(Nil))
990}
991
992// Let's roundtrip signing and verification a bunch of times to have confidence
993// it works, and that we detect any regressions.
994pub fn cookie_sign_roundtrip_test() {
995 use _ <- list.each(list.repeat(1, 10_000))
996 let message =
997 <<int.to_string(int.random(1_000_000_000_000_000)):utf8>>
998 |> bit_array.base64_encode(True)
999 let req = testing.get("/", [])
1000 let signed = wisp.sign_message(req, <<message:utf8>>, crypto.Sha512)
1001 let req = testing.get("/", [#("cookie", "message=" <> signed)])
1002 let assert Ok(out) = wisp.get_cookie(req, "message", wisp.Signed)
1003 out
1004 |> should.equal(message)
1005}
1006
1007pub fn get_query_test() {
1008 testing.get("/wibble?wobble=1&wubble=2&wobble=3&wabble", [])
1009 |> wisp.get_query
1010 |> should.equal([
1011 #("wobble", "1"),
1012 #("wubble", "2"),
1013 #("wobble", "3"),
1014 #("wabble", ""),
1015 ])
1016}
1017
1018pub fn get_query_no_query_test() {
1019 testing.get("/wibble", [])
1020 |> wisp.get_query
1021 |> should.equal([])
1022}