馃 A practical web framework for Gleam
at main 1053 lines 28 kB view raw
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("&lt;script&gt;alert('&amp;');&lt;/script&gt;") 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}