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