🧚 A practical web framework for Gleam

Further tests!

+159 -126
+43 -122
src/wisp.gleam
··· 95 95 } 96 96 97 97 fn mist_send_file(path: String) -> mist.ResponseData { 98 - mist.send_file(path, offset: 0, limit: option.None) 99 - |> result.lazy_unwrap(fn() { 100 - // TODO: log error 101 - mist.Bytes(bit_builder.new()) 102 - }) 98 + case mist.send_file(path, offset: 0, limit: option.None) { 99 + Ok(body) -> body 100 + Error(error) -> { 101 + // TODO: log error 102 + io.println("ERROR: " <> string.inspect(error)) 103 + // TODO: return 500 104 + mist.Bytes(bit_builder.new()) 105 + } 106 + } 103 107 } 104 108 105 109 // ··· 291 295 pub type Request = 292 296 HttpRequest(Connection) 293 297 294 - // TODO: test 295 298 // TODO: document 296 299 pub fn require_method( 297 300 request: HttpRequest(t), ··· 347 350 |> result.unwrap(request) 348 351 } 349 352 350 - // TODO: test 353 + // TODO: don't always return entity to large. Other errors are possible, such as 354 + // network errors. 351 355 // TODO: document 352 356 pub fn require_string_body( 353 357 request: Request, ··· 505 509 Ok(value) -> Ok(value) 506 510 Error(error) -> { 507 511 // TODO: log error 508 - io.debug(error) 512 + io.println("ERROR: " <> string.inspect(error)) 509 513 Error(internal_server_error()) 510 514 } 511 515 } ··· 613 617 list.sort(pairs, fn(a, b) { string.compare(a.0, b.0) }) 614 618 } 615 619 616 - // TODO: test 617 620 // TODO: document 618 621 pub fn require( 619 622 result: Result(value, error), ··· 640 643 // MIME types 641 644 // 642 645 643 - // TODO: test 644 - // TODO: move to another package 645 - pub fn mime_type_to_extensions(mime_type: String) -> List(String) { 646 - case mime_type { 647 - "application/atom+xml" -> ["atom"] 648 - "application/epub+zip" -> ["epub"] 649 - "application/gzip" -> ["gz"] 650 - "application/java-archive" -> ["jar"] 651 - "application/javascript" -> ["js"] 652 - "application/json" -> ["json"] 653 - "application/json-patch+json" -> ["json-patch"] 654 - "application/ld+json" -> ["jsonld"] 655 - "application/manifest+json" -> ["webmanifest"] 656 - "application/msword" -> ["doc"] 657 - "application/octet-stream" -> ["bin"] 658 - "application/ogg" -> ["ogx"] 659 - "application/pdf" -> ["pdf"] 660 - "application/postscript" -> ["ps", "eps", "ai"] 661 - "application/rss+xml" -> ["rss"] 662 - "application/rtf" -> ["rtf"] 663 - "application/vnd.amazon.ebook" -> ["azw"] 664 - "application/vnd.api+json" -> ["json-api"] 665 - "application/vnd.apple.installer+xml" -> ["mpkg"] 666 - "application/vnd.etsi.asic-e+zip" -> ["asice", "sce"] 667 - "application/vnd.etsi.asic-s+zip" -> ["asics", "scs"] 668 - "application/vnd.mozilla.xul+xml" -> ["xul"] 669 - "application/vnd.ms-excel" -> ["xls"] 670 - "application/vnd.ms-fontobject" -> ["eot"] 671 - "application/vnd.ms-powerpoint" -> ["ppt"] 672 - "application/vnd.oasis.opendocument.presentation" -> ["odp"] 673 - "application/vnd.oasis.opendocument.spreadsheet" -> ["ods"] 674 - "application/vnd.oasis.opendocument.text" -> ["odt"] 675 - "application/vnd.openxmlformats-officedocument.presentationml.presentation" -> [ 676 - "pptx", 677 - ] 678 - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" -> [ 679 - "xlsx", 680 - ] 681 - "application/vnd.openxmlformats-officedocument.wordprocessingml.document" -> [ 682 - "docx", 683 - ] 684 - "application/vnd.rar" -> ["rar"] 685 - "application/vnd.visio" -> ["vsd"] 686 - "application/wasm" -> ["wasm"] 687 - "application/x-7z-compressed" -> ["7z"] 688 - "application/x-abiword" -> ["abw"] 689 - "application/x-bzip" -> ["bz"] 690 - "application/x-bzip2" -> ["bz2"] 691 - "application/x-cdf" -> ["cda"] 692 - "application/x-csh" -> ["csh"] 693 - "application/x-freearc" -> ["arc"] 694 - "application/x-httpd-php" -> ["php"] 695 - "application/x-msaccess" -> ["mdb"] 696 - "application/x-sh" -> ["sh"] 697 - "application/x-shockwave-flash" -> ["swf"] 698 - "application/x-tar" -> ["tar"] 699 - "application/xhtml+xml" -> ["xhtml"] 700 - "application/xml" -> ["xml"] 701 - "application/zip" -> ["zip"] 702 - "audio/3gpp" -> ["3gp"] 703 - "audio/3gpp2" -> ["3g2"] 704 - "audio/aac" -> ["aac"] 705 - "audio/midi" -> ["mid", "midi"] 706 - "audio/mpeg" -> ["mp3"] 707 - "audio/ogg" -> ["oga"] 708 - "audio/opus" -> ["opus"] 709 - "audio/wav" -> ["wav"] 710 - "audio/webm" -> ["weba"] 711 - "font/otf" -> ["otf"] 712 - "font/ttf" -> ["ttf"] 713 - "font/woff" -> ["woff"] 714 - "font/woff2" -> ["woff2"] 715 - "image/avif" -> ["avif"] 716 - "image/bmp" -> ["bmp"] 717 - "image/gif" -> ["gif"] 718 - "image/heic" -> ["heic"] 719 - "image/heif" -> ["heif"] 720 - "image/jpeg" -> ["jpg", "jpeg"] 721 - "image/jxl" -> ["jxl"] 722 - "image/png" -> ["png"] 723 - "image/svg+xml" -> ["svg", "svgz"] 724 - "image/tiff" -> ["tiff", "tif"] 725 - "image/vnd.adobe.photoshop" -> ["psd"] 726 - "image/vnd.microsoft.icon" -> ["ico"] 727 - "image/webp" -> ["webp"] 728 - "text/calendar" -> ["ics"] 729 - "text/css" -> ["css"] 730 - "text/csv" -> ["csv"] 731 - "text/html" -> ["html", "htm"] 732 - "text/javascript" -> ["js", "mjs"] 733 - "text/markdown" -> ["md", "markdown"] 734 - "text/plain" -> ["txt", "text"] 735 - "text/xml" -> ["xml"] 736 - "video/3gpp" -> ["3gp"] 737 - "video/3gpp2" -> ["3g2"] 738 - "video/mp2t" -> ["ts"] 739 - "video/mp4" -> ["mp4"] 740 - "video/mpeg" -> ["mpeg", "mpg"] 741 - "video/ogg" -> ["ogv"] 742 - "video/quicktime" -> ["mov"] 743 - "video/webm" -> ["webm"] 744 - "video/x-ms-wmv" -> ["wmv"] 745 - "video/x-msvideo" -> ["avi"] 746 - _ -> [] 747 - } 748 - } 749 - 750 - // TODO: test 751 646 // TODO: move to another package 752 647 fn extension_to_mime_type(extension: String) -> String { 753 648 case extension { ··· 862 757 // Middleware 863 758 // 864 759 865 - // TODO: test 866 760 // TODO: document 867 761 pub fn rescue_crashes(service: fn() -> Response) -> Response { 868 762 case erlang.rescue(service) { 869 763 Ok(response) -> response 870 764 Error(error) -> { 871 765 // TODO: log the error 872 - io.debug(error) 766 + io.println("ERROR: " <> string.inspect(error)) 873 767 internal_server_error() 874 768 } 875 769 } ··· 892 786 response 893 787 } 894 788 789 + fn remove_preceeding_slashes(string: String) -> String { 790 + case string { 791 + "/" <> rest -> remove_preceeding_slashes(rest) 792 + _ -> string 793 + } 794 + } 795 + 796 + // TODO: replace with simplifile function when it exists 797 + fn join_path(a: String, b: String) -> String { 798 + let b = remove_preceeding_slashes(b) 799 + case string.ends_with(a, "/") { 800 + True -> a <> b 801 + False -> a <> "/" <> b 802 + } 803 + } 804 + 895 805 // TODO: test 896 806 // TODO: document 897 807 // TODO: remove requirement for preceeding slash on prefix ··· 901 811 from directory: String, 902 812 next service: fn() -> Response, 903 813 ) -> Response { 904 - case req.method, string.starts_with(req.path, prefix) { 814 + let path = remove_preceeding_slashes(req.path) 815 + let prefix = remove_preceeding_slashes(prefix) 816 + case req.method, string.starts_with(path, prefix) { 905 817 http.Get, True -> { 906 818 let path = 907 - req.path 819 + path 908 820 |> string.drop_left(string.length(prefix)) 909 821 |> string.replace(each: "..", with: "") 910 822 |> string.replace(each: "//", with: "/") 911 - |> string.append(directory, _) 823 + |> remove_preceeding_slashes 824 + |> join_path(directory, _) 912 825 913 826 let mime_type = 914 827 req.path ··· 972 885 make_connection(fn(_size) { 973 886 Ok(Chunk(body, fn(_size) { Ok(ReadingFinished) })) 974 887 }) 888 + } 889 + 890 + // TODO: better API 891 + // TODO: test 892 + // TODO: document 893 + pub fn test_request(body: BitString) -> Request { 894 + request.new() 895 + |> request.set_body(test_connection(body)) 975 896 } 976 897 977 898 // TODO: test
+116 -4
test/wisp_test.gleam
··· 83 83 } 84 84 85 85 pub fn set_get_max_body_size_test() { 86 - let request = 87 - request.new() 88 - |> request.set_body(wisp.test_connection(<<>>)) 86 + let request = wisp.test_request(<<>>) 89 87 90 88 request 91 89 |> wisp.get_max_body_size ··· 202 200 { 203 201 let request = request.set_method(request.new(), http.Post) 204 202 use <- wisp.require_method(request, http.Get) 205 - wisp.ok() 203 + panic as "should be unreachable" 206 204 } 207 205 |> should.equal(wisp.method_not_allowed([http.Get])) 208 206 } 207 + 208 + pub fn require_ok_test() { 209 + { 210 + use x <- wisp.require(Ok(1)) 211 + x 212 + |> should.equal(1) 213 + wisp.accepted() 214 + } 215 + |> should.equal(wisp.accepted()) 216 + } 217 + 218 + pub fn require_error_test() { 219 + { 220 + use _ <- wisp.require(Error(1)) 221 + panic as "should be unreachable" 222 + } 223 + |> should.equal(wisp.bad_request()) 224 + } 225 + 226 + pub fn require_string_body_test() { 227 + { 228 + let request = wisp.test_request(<<"Hello, Joe!":utf8>>) 229 + use body <- wisp.require_string_body(request) 230 + body 231 + |> should.equal("Hello, Joe!") 232 + wisp.accepted() 233 + } 234 + |> should.equal(wisp.accepted()) 235 + } 236 + 237 + pub fn require_string_body_invalid_test() { 238 + { 239 + let request = wisp.test_request(<<254>>) 240 + use _ <- wisp.require_string_body(request) 241 + panic as "should be unreachable" 242 + } 243 + |> should.equal(wisp.bad_request()) 244 + } 245 + 246 + pub fn rescue_crashes_error_test() { 247 + { 248 + use <- wisp.rescue_crashes 249 + panic as "we need to crash to test the middleware" 250 + } 251 + |> should.equal(wisp.internal_server_error()) 252 + } 253 + 254 + pub fn rescue_crashes_ok_test() { 255 + { 256 + use <- wisp.rescue_crashes 257 + wisp.ok() 258 + } 259 + |> should.equal(wisp.ok()) 260 + } 261 + 262 + pub fn serve_static_test() { 263 + let request = 264 + wisp.test_request(<<>>) 265 + |> request.set_path("/stuff/README.md") 266 + let response = { 267 + use <- wisp.serve_static(request, under: "/stuff", from: "./") 268 + wisp.ok() 269 + } 270 + response.status 271 + |> should.equal(200) 272 + response.headers 273 + |> should.equal([#("content-type", "text/markdown")]) 274 + response.body 275 + |> should.equal(wisp.File("./README.md")) 276 + } 277 + 278 + pub fn serve_static_under_has_no_preceeding_slash_test() { 279 + let request = 280 + wisp.test_request(<<>>) 281 + |> request.set_path("/stuff/README.md") 282 + let response = { 283 + use <- wisp.serve_static(request, under: "stuff", from: "./") 284 + wisp.ok() 285 + } 286 + response.status 287 + |> should.equal(200) 288 + response.headers 289 + |> should.equal([#("content-type", "text/markdown")]) 290 + response.body 291 + |> should.equal(wisp.File("./README.md")) 292 + } 293 + 294 + pub fn serve_static_from_has_no_trailing_slash_test() { 295 + let request = 296 + wisp.test_request(<<>>) 297 + |> request.set_path("/stuff/README.md") 298 + let response = { 299 + use <- wisp.serve_static(request, under: "stuff", from: ".") 300 + wisp.ok() 301 + } 302 + response.status 303 + |> should.equal(200) 304 + response.headers 305 + |> should.equal([#("content-type", "text/markdown")]) 306 + response.body 307 + |> should.equal(wisp.File("./README.md")) 308 + } 309 + 310 + pub fn serve_static_not_found_test() { 311 + let request = 312 + wisp.test_request(<<>>) 313 + |> request.set_path("/stuff/credit_card_details.txt") 314 + { 315 + use <- wisp.serve_static(request, under: "/stuff", from: "./") 316 + wisp.ok() 317 + } 318 + |> should.equal(wisp.ok()) 319 + } 320 + // TODO: More tests for serve_static!