ocaml http/1, http/2 and websocket client and server library
at main 44 kB view raw
1(** HCS Test Suite 2 3 This test suite covers: 4 - Codec module (identity, string codecs) 5 - Stream module (sync and async streams) 6 - Http module (request builder DSL) 7 - Router module (path parsing, route matching) 8 - Property-based tests using QCheck *) 9 10open Alcotest 11 12(* ================================================================== *) 13(* Codec Tests *) 14(* ================================================================== *) 15 16module Test_codec = struct 17 open Hcs.Codec 18 19 let test_identity_codec_encode () = 20 let encoder : Cstruct.t Identity_codec.encoder = Fun.id in 21 let input = Cstruct.of_string "hello world" in 22 match Identity_codec.encode encoder input with 23 | Ok result -> 24 check string "encode" "hello world" (Cstruct.to_string result) 25 | Error e -> fail e 26 27 let test_identity_codec_decode () = 28 let decoder : Cstruct.t Identity_codec.decoder = fun cs -> Ok cs in 29 let input = Cstruct.of_string "hello world" in 30 match Identity_codec.decode decoder input with 31 | Ok result -> 32 check string "decode" "hello world" (Cstruct.to_string result) 33 | Error e -> fail e 34 35 let test_identity_codec_content_type () = 36 check string "content_type" "application/octet-stream" 37 Identity_codec.content_type 38 39 let test_string_codec_encode () = 40 let encoder : string String_codec.encoder = Fun.id in 41 match String_codec.encode encoder "hello world" with 42 | Ok result -> 43 check string "encode" "hello world" (Cstruct.to_string result) 44 | Error e -> fail e 45 46 let test_string_codec_decode () = 47 let decoder : string String_codec.decoder = fun s -> Ok s in 48 let input = Cstruct.of_string "hello world" in 49 match String_codec.decode decoder input with 50 | Ok result -> check string "decode" "hello world" result 51 | Error e -> fail e 52 53 let test_string_codec_content_type () = 54 check string "content_type" "text/plain; charset=utf-8" 55 String_codec.content_type 56 57 let test_codec_error_to_string () = 58 check string "encode_error" "Encode error: test" 59 (codec_error_to_string (Encode_error "test")); 60 check string "decode_error" "Decode error: test" 61 (codec_error_to_string (Decode_error "test")); 62 check string "unsupported" "Unsupported body type for codec operation" 63 (codec_error_to_string Unsupported_body_type) 64 65 let test_with_codec_encode_body () = 66 let module W = With_codec (String_codec) in 67 let encoder : string String_codec.encoder = String.uppercase_ascii in 68 match W.encode_body encoder "hello" with 69 | Ok result -> check string "encode_body" "HELLO" result 70 | Error e -> fail (codec_error_to_string e) 71 72 let test_with_codec_decode_body () = 73 let module W = With_codec (String_codec) in 74 let decoder : string String_codec.decoder = 75 fun s -> Ok (String.uppercase_ascii s) 76 in 77 match W.decode_body decoder "hello" with 78 | Ok result -> check string "decode_body" "HELLO" result 79 | Error e -> fail (codec_error_to_string e) 80 81 let tests = 82 [ 83 test_case "identity codec encode" `Quick test_identity_codec_encode; 84 test_case "identity codec decode" `Quick test_identity_codec_decode; 85 test_case "identity codec content_type" `Quick 86 test_identity_codec_content_type; 87 test_case "string codec encode" `Quick test_string_codec_encode; 88 test_case "string codec decode" `Quick test_string_codec_decode; 89 test_case "string codec content_type" `Quick 90 test_string_codec_content_type; 91 test_case "codec error to string" `Quick test_codec_error_to_string; 92 test_case "with_codec encode_body" `Quick test_with_codec_encode_body; 93 test_case "with_codec decode_body" `Quick test_with_codec_decode_body; 94 ] 95end 96 97(* ================================================================== *) 98(* Stream Tests *) 99(* ================================================================== *) 100 101module Test_stream = struct 102 open Hcs.Stream 103 104 (* Sync stream tests *) 105 let test_sync_empty () = 106 let s = Sync.empty in 107 check (list int) "empty" [] (Sync.to_list s) 108 109 let test_sync_singleton () = 110 let s = Sync.singleton 42 in 111 check (list int) "singleton" [ 42 ] (Sync.to_list s) 112 113 let test_sync_of_list () = 114 let s = Sync.of_list [ 1; 2; 3 ] in 115 check (list int) "of_list" [ 1; 2; 3 ] (Sync.to_list s) 116 117 let test_sync_map () = 118 let s = Sync.of_list [ 1; 2; 3 ] |> Sync.map (fun x -> x * 2) in 119 check (list int) "map" [ 2; 4; 6 ] (Sync.to_list s) 120 121 let test_sync_filter () = 122 let s = 123 Sync.of_list [ 1; 2; 3; 4; 5 ] |> Sync.filter (fun x -> x mod 2 = 0) 124 in 125 check (list int) "filter" [ 2; 4 ] (Sync.to_list s) 126 127 let test_sync_take () = 128 let s = Sync.of_list [ 1; 2; 3; 4; 5 ] |> Sync.take 3 in 129 check (list int) "take" [ 1; 2; 3 ] (Sync.to_list s) 130 131 let test_sync_drop () = 132 let s = Sync.of_list [ 1; 2; 3; 4; 5 ] |> Sync.drop 2 in 133 check (list int) "drop" [ 3; 4; 5 ] (Sync.to_list s) 134 135 let test_sync_fold () = 136 let result = Sync.of_list [ 1; 2; 3; 4; 5 ] |> Sync.fold ( + ) 0 in 137 check int "fold" 15 result 138 139 let test_sync_chunks () = 140 let s = Sync.of_list [ 1; 2; 3; 4; 5 ] |> Sync.chunks 2 in 141 check 142 (list (list int)) 143 "chunks" 144 [ [ 1; 2 ]; [ 3; 4 ]; [ 5 ] ] 145 (Sync.to_list s) 146 147 let test_sync_repeat () = 148 let s = Sync.repeat 3 "x" in 149 check (list string) "repeat" [ "x"; "x"; "x" ] (Sync.to_list s) 150 151 let test_sync_unfold () = 152 let s = Sync.unfold (fun n -> if n > 0 then Some (n, n - 1) else None) 3 in 153 check (list int) "unfold" [ 3; 2; 1 ] (Sync.to_list s) 154 155 let test_sync_append () = 156 let s1 = Sync.of_list [ 1; 2 ] in 157 let s2 = Sync.of_list [ 3; 4 ] in 158 let s = Sync.append s1 s2 in 159 check (list int) "append" [ 1; 2; 3; 4 ] (Sync.to_list s) 160 161 let test_sync_cstructs_to_string () = 162 let s = 163 Sync.of_list 164 [ 165 Cstruct.of_string "hello"; 166 Cstruct.of_string " "; 167 Cstruct.of_string "world"; 168 ] 169 in 170 check string "cstructs_to_string" "hello world" (Sync.cstructs_to_string s) 171 172 let test_sync_string_to_cstructs () = 173 let s = Sync.string_to_cstructs ~chunk_size:5 "hello world" in 174 let result = 175 s |> Sync.map Cstruct.to_string |> Sync.to_list |> String.concat "" 176 in 177 check string "string_to_cstructs" "hello world" result 178 179 (* Async stream tests *) 180 let test_async_empty () = 181 let s = Async.empty in 182 check (list int) "async empty" [] (Async.to_list s) 183 184 let test_async_singleton () = 185 let s = Async.singleton 42 in 186 check (list int) "async singleton" [ 42 ] (Async.to_list s) 187 188 let test_async_of_list () = 189 let s = Async.of_list [ 1; 2; 3 ] in 190 check (list int) "async of_list" [ 1; 2; 3 ] (Async.to_list s) 191 192 let test_async_map () = 193 let s = Async.of_list [ 1; 2; 3 ] in 194 let mapped = Async.map (fun x -> x * 2) s in 195 check (list int) "async map" [ 2; 4; 6 ] (Async.to_list mapped) 196 197 let test_async_filter () = 198 let s = Async.of_list [ 1; 2; 3; 4; 5 ] in 199 let filtered = Async.filter (fun x -> x mod 2 = 0) s in 200 check (list int) "async filter" [ 2; 4 ] (Async.to_list filtered) 201 202 let test_async_take () = 203 let s = Async.of_list [ 1; 2; 3; 4; 5 ] in 204 let taken = Async.take 3 s in 205 check (list int) "async take" [ 1; 2; 3 ] (Async.to_list taken) 206 207 let test_async_fold () = 208 let s = Async.of_list [ 1; 2; 3; 4; 5 ] in 209 let result = Async.fold ( + ) 0 s in 210 check int "async fold" 15 result 211 212 (* Chunked encoding tests *) 213 let test_chunked_encode () = 214 let chunks = Sync.of_list [ Cstruct.of_string "hello" ] in 215 let encoded = Chunked.encode chunks in 216 let result = Sync.cstructs_to_string encoded in 217 (* 5 in hex is "5", chunk format: "5\r\nhello\r\n" followed by terminator "0\r\n\r\n" *) 218 check string "chunked encode" "5\r\nhello\r\n0\r\n\r\n" result 219 220 let test_chunked_content_length () = 221 let chunks = 222 Sync.of_list [ Cstruct.of_string "hello"; Cstruct.of_string " world" ] 223 in 224 let len = Chunked.content_length chunks in 225 check int "chunked content_length" 11 len 226 227 let tests = 228 [ 229 (* Sync tests *) 230 test_case "sync empty" `Quick test_sync_empty; 231 test_case "sync singleton" `Quick test_sync_singleton; 232 test_case "sync of_list" `Quick test_sync_of_list; 233 test_case "sync map" `Quick test_sync_map; 234 test_case "sync filter" `Quick test_sync_filter; 235 test_case "sync take" `Quick test_sync_take; 236 test_case "sync drop" `Quick test_sync_drop; 237 test_case "sync fold" `Quick test_sync_fold; 238 test_case "sync chunks" `Quick test_sync_chunks; 239 test_case "sync repeat" `Quick test_sync_repeat; 240 test_case "sync unfold" `Quick test_sync_unfold; 241 test_case "sync append" `Quick test_sync_append; 242 test_case "sync cstructs_to_string" `Quick test_sync_cstructs_to_string; 243 test_case "sync string_to_cstructs" `Quick test_sync_string_to_cstructs; 244 (* Async tests *) 245 test_case "async empty" `Quick test_async_empty; 246 test_case "async singleton" `Quick test_async_singleton; 247 test_case "async of_list" `Quick test_async_of_list; 248 test_case "async map" `Quick test_async_map; 249 test_case "async filter" `Quick test_async_filter; 250 test_case "async take" `Quick test_async_take; 251 test_case "async fold" `Quick test_async_fold; 252 (* Chunked tests *) 253 test_case "chunked encode" `Quick test_chunked_encode; 254 test_case "chunked content_length" `Quick test_chunked_content_length; 255 ] 256end 257 258(* ================================================================== *) 259(* HTTP Request Builder Tests *) 260(* ================================================================== *) 261 262module Test_http = struct 263 open Hcs.Http 264 265 let test_get () = 266 let req = get "https://example.com/path" |> build in 267 check string "method" "GET" (meth_to_string req.req_meth); 268 check string "host" "example.com" (host req); 269 check string "path" "/path" (path req); 270 check bool "is_https" true (is_https req) 271 272 let test_post () = 273 let req = post "http://example.com/api" |> build in 274 check string "method" "POST" (meth_to_string req.req_meth); 275 check bool "is_https" false (is_https req) 276 277 let test_put () = 278 let req = put "http://example.com/resource" |> build in 279 check string "method" "PUT" (meth_to_string req.req_meth) 280 281 let test_delete () = 282 let req = delete "http://example.com/resource/1" |> build in 283 check string "method" "DELETE" (meth_to_string req.req_meth) 284 285 let test_patch () = 286 let req = patch "http://example.com/resource/1" |> build in 287 check string "method" "PATCH" (meth_to_string req.req_meth) 288 289 let test_head () = 290 let req = head "http://example.com/" |> build in 291 check string "method" "HEAD" (meth_to_string req.req_meth) 292 293 let test_options () = 294 let req = options "http://example.com/" |> build in 295 check string "method" "OPTIONS" (meth_to_string req.req_meth) 296 297 let test_header () = 298 let req = 299 get "http://example.com" |> header "X-Custom" "value" 300 |> header "X-Another" "another" 301 |> build 302 in 303 check bool "has X-Custom" 304 (List.mem ("X-Custom", "value") req.req_headers) 305 true; 306 check bool "has X-Another" 307 (List.mem ("X-Another", "another") req.req_headers) 308 true 309 310 let test_content_type () = 311 let req = 312 post "http://example.com" |> content_type "application/json" |> build 313 in 314 check bool "has Content-Type" 315 (List.mem ("Content-Type", "application/json") req.req_headers) 316 true 317 318 let test_accept () = 319 let req = get "http://example.com" |> accept "text/html" |> build in 320 check bool "has Accept" 321 (List.mem ("Accept", "text/html") req.req_headers) 322 true 323 324 let test_user_agent () = 325 let req = get "http://example.com" |> user_agent "HCS/1.0" |> build in 326 check bool "has User-Agent" 327 (List.mem ("User-Agent", "HCS/1.0") req.req_headers) 328 true 329 330 let test_bearer () = 331 let req = get "http://example.com" |> bearer "token123" |> build in 332 check bool "has Authorization" 333 (List.mem ("Authorization", "Bearer token123") req.req_headers) 334 true 335 336 let test_query () = 337 let req = 338 get "http://example.com/search" 339 |> query "q" "ocaml" |> query "limit" "10" |> build 340 in 341 let pq = path_and_query req in 342 check bool "has q param" (String.length pq > 0) true 343 344 let test_body_string () = 345 let req = 346 post "http://example.com" 347 |> body_string ~content_type:"text/plain" "hello" 348 |> build 349 in 350 check string "body" "hello" (body_to_string req.req_body); 351 check bool "has Content-Type" 352 (List.mem ("Content-Type", "text/plain") req.req_headers) 353 true 354 355 let test_body_json () = 356 let req = 357 post "http://example.com" |> body_json {|{"key": "value"}|} |> build 358 in 359 check string "body" {|{"key": "value"}|} (body_to_string req.req_body); 360 check bool "has Content-Type" 361 (List.mem ("Content-Type", "application/json") req.req_headers) 362 true 363 364 let test_form () = 365 let req = 366 post "http://example.com" 367 |> form [ ("name", "Alice"); ("age", "30") ] 368 |> build 369 in 370 let body_str = body_to_string req.req_body in 371 check bool "has name" (String.length body_str > 0) true; 372 check bool "has Content-Type" 373 (List.mem 374 ("Content-Type", "application/x-www-form-urlencoded") 375 req.req_headers) 376 true 377 378 let test_port_default_http () = 379 let req = get "http://example.com/" |> build in 380 check int "port" 80 (port req) 381 382 let test_port_default_https () = 383 let req = get "https://example.com/" |> build in 384 check int "port" 443 (port req) 385 386 let test_port_explicit () = 387 let req = get "http://example.com:8080/" |> build in 388 check int "port" 8080 (port req) 389 390 let test_meth_of_string () = 391 check string "GET" "GET" (meth_to_string (meth_of_string "GET")); 392 check string "POST" "POST" (meth_to_string (meth_of_string "POST")); 393 check string "CUSTOM" "CUSTOM" (meth_to_string (meth_of_string "CUSTOM")) 394 395 let test_body_length () = 396 check int "empty body" 0 (body_length Empty); 397 check int "string body" 5 (body_length (String "hello")); 398 check int "form body" (String.length "a=1&b=2") 399 (body_length (Form [ ("a", "1"); ("b", "2") ])) 400 401 let test_cookie () = 402 let req = get "http://example.com" |> cookie "session" "abc123" |> build in 403 check bool "has Cookie" 404 (List.exists 405 (fun (n, v) -> n = "Cookie" && String.length v > 0) 406 req.req_headers) 407 true 408 409 let test_cookies_multiple () = 410 let req = 411 get "http://example.com" |> cookie "a" "1" |> cookie "b" "2" |> build 412 in 413 let cookie_header = 414 List.find_opt (fun (n, _) -> n = "Cookie") req.req_headers 415 in 416 match cookie_header with 417 | Some (_, v) -> check bool "has a" (String.length v > 0) true 418 | None -> fail "no Cookie header" 419 420 let tests = 421 [ 422 test_case "get" `Quick test_get; 423 test_case "post" `Quick test_post; 424 test_case "put" `Quick test_put; 425 test_case "delete" `Quick test_delete; 426 test_case "patch" `Quick test_patch; 427 test_case "head" `Quick test_head; 428 test_case "options" `Quick test_options; 429 test_case "header" `Quick test_header; 430 test_case "content_type" `Quick test_content_type; 431 test_case "accept" `Quick test_accept; 432 test_case "user_agent" `Quick test_user_agent; 433 test_case "bearer" `Quick test_bearer; 434 test_case "query" `Quick test_query; 435 test_case "body_string" `Quick test_body_string; 436 test_case "body_json" `Quick test_body_json; 437 test_case "form" `Quick test_form; 438 test_case "port default http" `Quick test_port_default_http; 439 test_case "port default https" `Quick test_port_default_https; 440 test_case "port explicit" `Quick test_port_explicit; 441 test_case "meth_of_string" `Quick test_meth_of_string; 442 test_case "body_length" `Quick test_body_length; 443 test_case "cookie" `Quick test_cookie; 444 test_case "cookies multiple" `Quick test_cookies_multiple; 445 ] 446end 447 448module Test_pipeline = struct 449 open Hcs.Pipeline 450 451 let test_empty () = check int "empty length" 0 (length empty) 452 453 let test_create () = 454 let p = create [ Fun.id; Fun.id ] in 455 check int "create length" 2 (length p) 456 457 let test_plug () = 458 let p = empty |> fun t -> plug t Fun.id in 459 check int "plug length" 1 (length p) 460 461 let test_plug_first () = 462 let p = empty |> fun t -> plug_first t Fun.id in 463 check int "plug_first length" 1 (length p) 464 465 let test_compose () = 466 let p1 = create [ Fun.id ] in 467 let p2 = create [ Fun.id; Fun.id ] in 468 let combined = compose p1 p2 in 469 check int "compose length" 3 (length combined) 470 471 let test_is_empty () = 472 check bool "empty is empty" true (is_empty empty); 473 check bool "non-empty not empty" false (is_empty (create [ Fun.id ])) 474 475 let test_apply () = 476 let called = ref false in 477 let mark_plug : Hcs.Plug.t = 478 fun handler req -> 479 called := true; 480 handler req 481 in 482 let p = create [ mark_plug ] in 483 let dummy_req : Hcs.Server.request = 484 { meth = `GET; target = "/"; headers = []; body = ""; version = HTTP_1_1 } 485 in 486 let handler _req = Hcs.Server.respond "ok" in 487 let wrapped = apply p handler in 488 let _ = wrapped dummy_req in 489 check bool "plug was called" true !called 490 491 let tests = 492 [ 493 test_case "empty" `Quick test_empty; 494 test_case "create" `Quick test_create; 495 test_case "plug" `Quick test_plug; 496 test_case "plug_first" `Quick test_plug_first; 497 test_case "compose" `Quick test_compose; 498 test_case "is_empty" `Quick test_is_empty; 499 test_case "apply" `Quick test_apply; 500 ] 501end 502 503module Test_router = struct 504 open Hcs.Router 505 506 let test_parse_path_empty () = 507 check (list string) "empty" [] 508 (List.map 509 (function Literal s -> s | Param s -> ":" ^ s | Wildcard -> "*") 510 (parse_path "")) 511 512 let test_parse_path_root () = 513 check (list string) "root" [] 514 (List.map 515 (function Literal s -> s | Param s -> ":" ^ s | Wildcard -> "*") 516 (parse_path "/")) 517 518 let test_parse_path_simple () = 519 let segments = parse_path "/users/list" in 520 check int "length" 2 (List.length segments); 521 match segments with 522 | [ Literal "users"; Literal "list" ] -> () 523 | _ -> fail "unexpected segments" 524 525 let test_parse_path_with_params () = 526 let segments = parse_path "/users/:id/posts/:post_id" in 527 check int "length" 4 (List.length segments); 528 match segments with 529 | [ Literal "users"; Param "id"; Literal "posts"; Param "post_id" ] -> () 530 | _ -> fail "unexpected segments" 531 532 let test_parse_path_with_wildcard () = 533 let segments = parse_path "/files/*" in 534 check int "length" 2 (List.length segments); 535 match segments with 536 | [ Literal "files"; Wildcard ] -> () 537 | _ -> fail "unexpected segments" 538 539 let test_router_literal_match () = 540 let router = empty () in 541 add_route router ~method_:(Some `GET) ~path:"/users" 542 ~handler:"users_handler" ~plugs:[]; 543 match lookup router ~method_:`GET ~path:"/users" with 544 | Some { handler; _ } -> check string "handler" "users_handler" handler 545 | None -> fail "no match" 546 547 let test_router_param_match () = 548 let router = empty () in 549 add_route router ~method_:(Some `GET) ~path:"/users/:id" 550 ~handler:"user_handler" ~plugs:[]; 551 match lookup router ~method_:`GET ~path:"/users/123" with 552 | Some { handler; params; _ } -> 553 check string "handler" "user_handler" handler; 554 check (option string) "id param" (Some "123") (param "id" params) 555 | None -> fail "no match" 556 557 let test_router_multiple_params () = 558 let router = empty () in 559 add_route router ~method_:(Some `GET) ~path:"/users/:user_id/posts/:post_id" 560 ~handler:"post_handler" ~plugs:[]; 561 match lookup router ~method_:`GET ~path:"/users/42/posts/100" with 562 | Some { handler; params; _ } -> 563 check string "handler" "post_handler" handler; 564 check (option string) "user_id" (Some "42") (param "user_id" params); 565 check (option string) "post_id" (Some "100") (param "post_id" params) 566 | None -> fail "no match" 567 568 let test_router_wildcard_match () = 569 let router = empty () in 570 add_route router ~method_:(Some `GET) ~path:"/files/*" 571 ~handler:"files_handler" ~plugs:[]; 572 match lookup router ~method_:`GET ~path:"/files/path/to/file.txt" with 573 | Some { handler; params; _ } -> 574 check string "handler" "files_handler" handler; 575 check (option string) "wildcard" (Some "path/to/file.txt") 576 (param "*" params) 577 | None -> fail "no match" 578 579 let test_router_method_match () = 580 let router = empty () in 581 add_route router ~method_:(Some `GET) ~path:"/users" ~handler:"get_handler" 582 ~plugs:[]; 583 add_route router ~method_:(Some `POST) ~path:"/users" 584 ~handler:"post_handler" ~plugs:[]; 585 (match lookup router ~method_:`GET ~path:"/users" with 586 | Some { handler; _ } -> check string "GET handler" "get_handler" handler 587 | None -> fail "no GET match"); 588 match lookup router ~method_:`POST ~path:"/users" with 589 | Some { handler; _ } -> check string "POST handler" "post_handler" handler 590 | None -> fail "no POST match" 591 592 let test_router_any_method () = 593 let router = empty () in 594 add_route router ~method_:None ~path:"/health" ~handler:"health_handler" 595 ~plugs:[]; 596 (match lookup router ~method_:`GET ~path:"/health" with 597 | Some { handler; _ } -> check string "GET" "health_handler" handler 598 | None -> fail "no GET match"); 599 match lookup router ~method_:`POST ~path:"/health" with 600 | Some { handler; _ } -> check string "POST" "health_handler" handler 601 | None -> fail "no POST match" 602 603 let test_router_no_match () = 604 let router = empty () in 605 add_route router ~method_:(Some `GET) ~path:"/users" ~handler:"handler" 606 ~plugs:[]; 607 match lookup router ~method_:`GET ~path:"/posts" with 608 | Some _ -> fail "unexpected match" 609 | None -> () 610 611 let test_router_compile () = 612 let routes = 613 [ 614 Route.get "/users" "list_users"; 615 Route.post "/users" "create_user"; 616 Route.get "/users/:id" "get_user"; 617 Route.delete "/users/:id" "delete_user"; 618 ] 619 in 620 let router = compile routes in 621 (match lookup router ~method_:`GET ~path:"/users" with 622 | Some { handler; _ } -> check string "list" "list_users" handler 623 | None -> fail "no list match"); 624 match lookup router ~method_:`GET ~path:"/users/42" with 625 | Some { handler; params; _ } -> 626 check string "get" "get_user" handler; 627 check (option string) "id" (Some "42") (param "id" params) 628 | None -> fail "no get match" 629 630 let test_param_helpers () = 631 let params = [ ("id", "42"); ("name", "alice") ] in 632 check (option string) "param" (Some "42") (param "id" params); 633 check (option string) "param missing" None (param "foo" params); 634 check string "param_or" "default" (param_or "foo" ~default:"default" params); 635 check (option int) "param_int" (Some 42) (param_int "id" params); 636 check int "param_int_or" 0 (param_int_or "foo" ~default:0 params) 637 638 let test_scope_prefix () = 639 let routes = scope "/api" [ Route.get "/users" "list_users" ] in 640 let router = compile routes in 641 match lookup router ~method_:`GET ~path:"/api/users" with 642 | Some { handler; _ } -> check string "scoped handler" "list_users" handler 643 | None -> fail "no match for scoped route" 644 645 let test_scope_root_prefix () = 646 let routes = scope "/" [ Route.get "/users" "list_users" ] in 647 let router = compile routes in 648 match lookup router ~method_:`GET ~path:"/users" with 649 | Some { handler; _ } -> check string "root scope" "list_users" handler 650 | None -> fail "no match" 651 652 let test_scope_nested_path () = 653 let routes = scope "/api/v1" [ Route.get "/users/:id" "get_user" ] in 654 let router = compile routes in 655 match lookup router ~method_:`GET ~path:"/api/v1/users/42" with 656 | Some { handler; params; _ } -> 657 check string "nested scope" "get_user" handler; 658 check (option string) "id param" (Some "42") (param "id" params) 659 | None -> fail "no match" 660 661 let test_compile_scopes () = 662 let api_routes = scope "/api" [ Route.get "/posts" "list_posts" ] in 663 let web_routes = scope "/" [ Route.get "/" "home" ] in 664 let router = compile_scopes [ api_routes; web_routes ] in 665 (match lookup router ~method_:`GET ~path:"/api/posts" with 666 | Some { handler; _ } -> check string "api route" "list_posts" handler 667 | None -> fail "no api match"); 668 match lookup router ~method_:`GET ~path:"/" with 669 | Some { handler; _ } -> check string "web route" "home" handler 670 | None -> fail "no web match" 671 672 let test_route_with_plug () = 673 let dummy_plug : Hcs.Plug.t = fun handler -> handler in 674 let base_route = Route.get "/admin" "admin_handler" in 675 let route = Route.plug dummy_plug base_route in 676 let router = compile [ route ] in 677 match lookup router ~method_:`GET ~path:"/admin" with 678 | Some { handler; plugs; _ } -> 679 check string "handler" "admin_handler" handler; 680 check int "has plugs" 1 (List.length plugs) 681 | None -> fail "no match" 682 683 let tests = 684 [ 685 test_case "parse_path empty" `Quick test_parse_path_empty; 686 test_case "parse_path root" `Quick test_parse_path_root; 687 test_case "parse_path simple" `Quick test_parse_path_simple; 688 test_case "parse_path with params" `Quick test_parse_path_with_params; 689 test_case "parse_path with wildcard" `Quick test_parse_path_with_wildcard; 690 test_case "router literal match" `Quick test_router_literal_match; 691 test_case "router param match" `Quick test_router_param_match; 692 test_case "router multiple params" `Quick test_router_multiple_params; 693 test_case "router wildcard match" `Quick test_router_wildcard_match; 694 test_case "router method match" `Quick test_router_method_match; 695 test_case "router any method" `Quick test_router_any_method; 696 test_case "router no match" `Quick test_router_no_match; 697 test_case "router compile" `Quick test_router_compile; 698 test_case "param helpers" `Quick test_param_helpers; 699 test_case "scope prefix" `Quick test_scope_prefix; 700 test_case "scope root prefix" `Quick test_scope_root_prefix; 701 test_case "scope nested path" `Quick test_scope_nested_path; 702 test_case "compile scopes" `Quick test_compile_scopes; 703 test_case "route with plug" `Quick test_route_with_plug; 704 ] 705end 706 707(* ================================================================== *) 708(* Log Tests *) 709(* ================================================================== *) 710 711module Test_log = struct 712 open Hcs.Log 713 714 let test_level_to_string () = 715 check string "debug" "DEBUG" (level_to_string Debug); 716 check string "info" "INFO" (level_to_string Info); 717 check string "warn" "WARN" (level_to_string Warn); 718 check string "error" "ERROR" (level_to_string Error) 719 720 let test_level_of_string () = 721 check bool "DEBUG" true (level_of_string "DEBUG" = Some Debug); 722 check bool "debug" true (level_of_string "debug" = Some Debug); 723 check bool "INFO" true (level_of_string "INFO" = Some Info); 724 check bool "WARN" true (level_of_string "WARN" = Some Warn); 725 check bool "unknown" true (level_of_string "unknown" = None) 726 727 let test_level_gte () = 728 check bool "error >= debug" true (level_gte Error Debug); 729 check bool "error >= error" true (level_gte Error Error); 730 check bool "debug >= error" false (level_gte Debug Error); 731 check bool "warn >= info" true (level_gte Warn Info) 732 733 let test_method_to_string () = 734 check string "GET" "GET" (method_to_string GET); 735 check string "POST" "POST" (method_to_string POST); 736 check string "Other" "CUSTOM" (method_to_string (Other "CUSTOM")) 737 738 let test_event_to_string_request_start () = 739 let e = 740 Request_start { id = "req-1"; meth = GET; uri = "/test"; headers = [] } 741 in 742 let s = event_to_string e in 743 check bool "contains id" (String.length s > 0) true; 744 check bool "contains GET" true (String.length s > 0) 745 746 let test_event_to_string_request_end () = 747 let e = 748 Request_end 749 { 750 id = "req-1"; 751 status = 200; 752 duration_ms = 42.5; 753 body_size = Some 1024; 754 } 755 in 756 let s = event_to_string e in 757 check bool "contains status" (String.length s > 0) true 758 759 let test_event_to_string_error () = 760 let e = 761 Error 762 { id = Some "req-1"; error = "connection failed"; context = Some "TLS" } 763 in 764 let s = event_to_string e in 765 check bool "contains error" (String.length s > 0) true 766 767 let test_event_to_json_request_start () = 768 let e = 769 Request_start { id = "req-1"; meth = GET; uri = "/test"; headers = [] } 770 in 771 let j = event_to_json e in 772 check bool "is json" true (String.sub j 0 1 = "{"); 773 check bool "contains event" true (String.length j > 10) 774 775 let test_event_to_json_custom () = 776 let e = Custom { name = "test_event"; data = [ ("key", "value") ] } in 777 let j = event_to_json e in 778 check bool "is json" true (String.sub j 0 1 = "{"); 779 check bool "contains name" true (String.length j > 10) 780 781 let test_null_logger () = 782 (* Should not raise *) 783 null Info 784 (Request_start { id = "req-1"; meth = GET; uri = "/"; headers = [] }); 785 null Error (Error { id = None; error = "test"; context = None }) 786 787 let test_custom_logger () = 788 let logged = ref [] in 789 let logger = custom (fun level msg -> logged := (level, msg) :: !logged) in 790 logger Info 791 (Request_start { id = "req-1"; meth = GET; uri = "/"; headers = [] }); 792 check int "logged count" 1 (List.length !logged) 793 794 let test_combine_loggers () = 795 let count1 = ref 0 in 796 let count2 = ref 0 in 797 let logger1 = custom (fun _ _ -> incr count1) in 798 let logger2 = custom (fun _ _ -> incr count2) in 799 let combined = combine [ logger1; logger2 ] in 800 combined Info (Custom { name = "test"; data = [] }); 801 check int "logger1 count" 1 !count1; 802 check int "logger2 count" 1 !count2 803 804 let test_with_min_level () = 805 let logged = ref [] in 806 let base = custom (fun level msg -> logged := (level, msg) :: !logged) in 807 let filtered = with_min_level Warn base in 808 filtered Debug (Custom { name = "debug"; data = [] }); 809 filtered Info (Custom { name = "info"; data = [] }); 810 filtered Warn (Custom { name = "warn"; data = [] }); 811 filtered Error (Custom { name = "error"; data = [] }); 812 check int "logged count" 2 (List.length !logged) 813 814 let test_generate_request_id () = 815 let id1 = generate_request_id () in 816 let id2 = generate_request_id () in 817 check bool "starts with req-" true (String.sub id1 0 4 = "req-"); 818 check bool "unique" true (id1 <> id2) 819 820 let tests = 821 [ 822 test_case "level_to_string" `Quick test_level_to_string; 823 test_case "level_of_string" `Quick test_level_of_string; 824 test_case "level_gte" `Quick test_level_gte; 825 test_case "method_to_string" `Quick test_method_to_string; 826 test_case "event_to_string request_start" `Quick 827 test_event_to_string_request_start; 828 test_case "event_to_string request_end" `Quick 829 test_event_to_string_request_end; 830 test_case "event_to_string error" `Quick test_event_to_string_error; 831 test_case "event_to_json request_start" `Quick 832 test_event_to_json_request_start; 833 test_case "event_to_json custom" `Quick test_event_to_json_custom; 834 test_case "null logger" `Quick test_null_logger; 835 test_case "custom logger" `Quick test_custom_logger; 836 test_case "combine loggers" `Quick test_combine_loggers; 837 test_case "with_min_level" `Quick test_with_min_level; 838 test_case "generate_request_id" `Quick test_generate_request_id; 839 ] 840end 841 842module Test_tls_config = struct 843 open Hcs.Tls_config 844 845 let test_alpn_constants () = 846 check string "h2" "h2" alpn_h2; 847 check string "http/1.1" "http/1.1" alpn_http11; 848 check string "h2c" "h2c" alpn_h2c 849 850 let test_protocol_of_alpn () = 851 check (option string) "h2 -> HTTP_2" (Some "HTTP_2") 852 (Option.map 853 (function HTTP_2 -> "HTTP_2" | HTTP_1_1 -> "HTTP_1_1") 854 (protocol_of_alpn "h2")); 855 check (option string) "http/1.1 -> HTTP_1_1" (Some "HTTP_1_1") 856 (Option.map 857 (function HTTP_2 -> "HTTP_2" | HTTP_1_1 -> "HTTP_1_1") 858 (protocol_of_alpn "http/1.1")); 859 check (option string) "unknown -> None" None 860 (Option.map 861 (function HTTP_2 -> "HTTP_2" | HTTP_1_1 -> "HTTP_1_1") 862 (protocol_of_alpn "unknown")) 863 864 let test_alpn_of_protocol () = 865 check string "HTTP_2 -> h2" "h2" (alpn_of_protocol HTTP_2); 866 check string "HTTP_1_1 -> http/1.1" "http/1.1" (alpn_of_protocol HTTP_1_1) 867 868 let test_client_configs () = 869 check 870 (option (list string)) 871 "default has http/1.1" (Some [ "http/1.1" ]) Client.default.alpn_protocols; 872 check 873 (option (list string)) 874 "h2 has h2" (Some [ "h2" ]) Client.h2.alpn_protocols; 875 check 876 (option (list string)) 877 "h2_or_http11 has both" 878 (Some [ "h2"; "http/1.1" ]) 879 Client.h2_or_http11.alpn_protocols 880 881 let tests = 882 [ 883 test_case "alpn constants" `Quick test_alpn_constants; 884 test_case "protocol_of_alpn" `Quick test_protocol_of_alpn; 885 test_case "alpn_of_protocol" `Quick test_alpn_of_protocol; 886 test_case "client configs" `Quick test_client_configs; 887 ] 888end 889 890(* ================================================================== *) 891(* Request Tests *) 892(* ================================================================== *) 893 894module Test_request = struct 895 open Hcs 896 897 let make_request ?(body = "") target : Server.request = 898 { meth = `GET; target; headers = []; body; version = Server.HTTP_1_1 } 899 900 let test_query_basic () = 901 let req = make_request "/search?q=ocaml&limit=10" in 902 check (option string) "q" (Some "ocaml") (Request.query req "q"); 903 check (option string) "limit" (Some "10") (Request.query req "limit"); 904 check (option string) "missing" None (Request.query req "missing") 905 906 let test_query_or () = 907 let req = make_request "/search?q=ocaml" in 908 check string "existing" "ocaml" (Request.query_or ~default:"" req "q"); 909 check string "missing" "default" 910 (Request.query_or ~default:"default" req "x") 911 912 let test_query_int () = 913 let req = make_request "/page?num=42&bad=abc" in 914 check (option int) "valid int" (Some 42) (Request.query_int req "num"); 915 check (option int) "invalid int" None (Request.query_int req "bad"); 916 check (option int) "missing" None (Request.query_int req "missing") 917 918 let test_query_int_or () = 919 let req = make_request "/page?num=42" in 920 check int "existing" 42 (Request.query_int_or ~default:0 req "num"); 921 check int "missing" 1 (Request.query_int_or ~default:1 req "page") 922 923 let test_query_bool () = 924 let req = 925 make_request "/flags?a=true&b=1&c=yes&d=on&e=false&f=0&g=no&h=off&i=bad" 926 in 927 check (option bool) "true" (Some true) (Request.query_bool req "a"); 928 check (option bool) "1" (Some true) (Request.query_bool req "b"); 929 check (option bool) "yes" (Some true) (Request.query_bool req "c"); 930 check (option bool) "on" (Some true) (Request.query_bool req "d"); 931 check (option bool) "false" (Some false) (Request.query_bool req "e"); 932 check (option bool) "0" (Some false) (Request.query_bool req "f"); 933 check (option bool) "no" (Some false) (Request.query_bool req "g"); 934 check (option bool) "off" (Some false) (Request.query_bool req "h"); 935 check (option bool) "bad" None (Request.query_bool req "i") 936 937 let test_query_float () = 938 let req = make_request "/calc?x=3.14&y=-2.5&bad=abc" in 939 check 940 (option (float 0.001)) 941 "valid float" (Some 3.14) 942 (Request.query_float req "x"); 943 check 944 (option (float 0.001)) 945 "negative" (Some (-2.5)) 946 (Request.query_float req "y"); 947 check (option (float 0.001)) "invalid" None (Request.query_float req "bad") 948 949 let test_query_all () = 950 let req = make_request "/tags?t=a&t=b&t=c" in 951 check (list string) "all values" [ "a"; "b"; "c" ] 952 (Request.query_all req "t") 953 954 let test_query_url_decode () = 955 let req = make_request "/search?q=hello%20world&name=foo%2Bbar" in 956 check (option string) "space" (Some "hello world") (Request.query req "q"); 957 check (option string) "plus encoded" (Some "foo+bar") 958 (Request.query req "name") 959 960 let test_query_empty () = 961 let req = make_request "/path" in 962 check (option string) "no query string" None (Request.query req "q"); 963 check 964 (list (pair string string)) 965 "empty params" [] 966 (Request.query_params_from_target "/path") 967 968 let test_form_field_basic () = 969 let req = make_request ~body:"name=alice&age=30" "/" in 970 check (option string) "name" (Some "alice") (Request.form_field req "name"); 971 check (option string) "age" (Some "30") (Request.form_field req "age"); 972 check (option string) "missing" None (Request.form_field req "missing") 973 974 let test_form_field_or () = 975 let req = make_request ~body:"name=bob" "/" in 976 check string "existing" "bob" (Request.form_field_or ~default:"" req "name"); 977 check string "missing" "unknown" 978 (Request.form_field_or ~default:"unknown" req "email") 979 980 let test_form_int () = 981 let req = make_request ~body:"count=42&bad=xyz" "/" in 982 check (option int) "valid" (Some 42) (Request.form_int req "count"); 983 check (option int) "invalid" None (Request.form_int req "bad"); 984 check int "with default" 0 (Request.form_int_or ~default:0 req "missing") 985 986 let test_form_plus_as_space () = 987 let req = make_request ~body:"msg=hello+world&name=foo%2Bbar" "/" in 988 check (option string) "plus becomes space" (Some "hello world") 989 (Request.form_field req "msg"); 990 check (option string) "encoded plus" (Some "foo+bar") 991 (Request.form_field req "name") 992 993 let test_form_url_decode () = 994 let req = make_request ~body:"text=hello%20world&special=%26%3D%3F" "/" in 995 check (option string) "percent space" (Some "hello world") 996 (Request.form_field req "text"); 997 check (option string) "special chars" (Some "&=?") 998 (Request.form_field req "special") 999 1000 let test_form_empty_body () = 1001 let req = make_request ~body:"" "/" in 1002 check (option string) "empty body" None (Request.form_field req "anything"); 1003 check 1004 (list (pair string string)) 1005 "empty form_data" [] 1006 (Request.form_data_from_body "") 1007 1008 let tests = 1009 [ 1010 test_case "query basic" `Quick test_query_basic; 1011 test_case "query_or" `Quick test_query_or; 1012 test_case "query_int" `Quick test_query_int; 1013 test_case "query_int_or" `Quick test_query_int_or; 1014 test_case "query_bool" `Quick test_query_bool; 1015 test_case "query_float" `Quick test_query_float; 1016 test_case "query_all" `Quick test_query_all; 1017 test_case "query url decode" `Quick test_query_url_decode; 1018 test_case "query empty" `Quick test_query_empty; 1019 test_case "form_field basic" `Quick test_form_field_basic; 1020 test_case "form_field_or" `Quick test_form_field_or; 1021 test_case "form_int" `Quick test_form_int; 1022 test_case "form plus as space" `Quick test_form_plus_as_space; 1023 test_case "form url decode" `Quick test_form_url_decode; 1024 test_case "form empty body" `Quick test_form_empty_body; 1025 ] 1026end 1027 1028(* ================================================================== *) 1029(* Property-Based Tests (QCheck) *) 1030(* ================================================================== *) 1031 1032module Test_properties = struct 1033 (* Stream properties *) 1034 let prop_sync_map_identity = 1035 QCheck.Test.make ~name:"sync map identity" ~count:100 1036 QCheck.(list int) 1037 (fun l -> 1038 let open Hcs.Stream.Sync in 1039 let s = of_list l in 1040 to_list (map Fun.id s) = l) 1041 1042 let prop_sync_map_composition = 1043 QCheck.Test.make ~name:"sync map composition" ~count:100 1044 QCheck.(list nat_small) 1045 (fun l -> 1046 let open Hcs.Stream.Sync in 1047 let f x = x + 1 in 1048 let g x = x * 2 in 1049 let s = of_list l in 1050 to_list (map (fun x -> g (f x)) s) = to_list (map g (map f (of_list l)))) 1051 1052 let prop_sync_filter_all = 1053 QCheck.Test.make ~name:"sync filter true keeps all" ~count:100 1054 QCheck.(list int) 1055 (fun l -> 1056 let open Hcs.Stream.Sync in 1057 to_list (filter (fun _ -> true) (of_list l)) = l) 1058 1059 let prop_sync_filter_none = 1060 QCheck.Test.make ~name:"sync filter false removes all" ~count:100 1061 QCheck.(list int) 1062 (fun l -> 1063 let open Hcs.Stream.Sync in 1064 to_list (filter (fun _ -> false) (of_list l)) = []) 1065 1066 let prop_sync_take_drop = 1067 QCheck.Test.make ~name:"sync take n ++ drop n = original" ~count:100 1068 QCheck.(pair (list int) nat_small) 1069 (fun (l, n) -> 1070 let open Hcs.Stream.Sync in 1071 let s1 = of_list l in 1072 let s2 = of_list l in 1073 let taken = to_list (take n s1) in 1074 let dropped = to_list (drop n s2) in 1075 taken @ dropped = l) 1076 1077 let prop_sync_fold_sum = 1078 QCheck.Test.make ~name:"sync fold sum = List.fold_left sum" ~count:100 1079 QCheck.(list nat_small) 1080 (fun l -> 1081 let open Hcs.Stream.Sync in 1082 fold ( + ) 0 (of_list l) = List.fold_left ( + ) 0 l) 1083 1084 (* Router properties *) 1085 let prop_router_param_extraction = 1086 QCheck.Test.make ~name:"router extracts all params" ~count:100 1087 QCheck.(pair string_small string_small) 1088 (fun (id, name) -> 1089 (* Skip edge cases: empty strings or strings containing / *) 1090 if 1091 id = "" || name = "" || String.contains id '/' 1092 || String.contains name '/' 1093 then true (* vacuously true for invalid inputs *) 1094 else 1095 let open Hcs.Router in 1096 let router = empty () in 1097 add_route router ~method_:(Some `GET) ~path:"/users/:id/items/:name" 1098 ~handler:"handler" ~plugs:[]; 1099 match 1100 lookup router ~method_:`GET 1101 ~path:(Printf.sprintf "/users/%s/items/%s" id name) 1102 with 1103 | Some { params; _ } -> 1104 param "id" params = Some id && param "name" params = Some name 1105 | None -> false) 1106 1107 (* HTTP builder properties *) 1108 let prop_http_method_roundtrip = 1109 QCheck.Test.make ~name:"http method roundtrip" ~count:20 1110 QCheck.( 1111 make 1112 Gen.( 1113 oneof 1114 [ 1115 return "GET"; 1116 return "POST"; 1117 return "PUT"; 1118 return "DELETE"; 1119 return "PATCH"; 1120 return "HEAD"; 1121 return "OPTIONS"; 1122 ])) 1123 (fun m -> 1124 let open Hcs.Http in 1125 meth_to_string (meth_of_string m) = m) 1126 1127 let tests = 1128 List.map QCheck_alcotest.to_alcotest 1129 [ 1130 prop_sync_map_identity; 1131 prop_sync_map_composition; 1132 prop_sync_filter_all; 1133 prop_sync_filter_none; 1134 prop_sync_take_drop; 1135 prop_sync_fold_sum; 1136 prop_router_param_extraction; 1137 prop_http_method_roundtrip; 1138 ] 1139end 1140 1141(* ================================================================== *) 1142(* Main *) 1143(* ================================================================== *) 1144 1145let () = 1146 run "HCS" 1147 [ 1148 ("Codec", Test_codec.tests); 1149 ("Stream", Test_stream.tests); 1150 ("Http", Test_http.tests); 1151 ("Pipeline", Test_pipeline.tests); 1152 ("Router", Test_router.tests); 1153 ("Log", Test_log.tests); 1154 ("Tls_config", Test_tls_config.tests); 1155 ("Request", Test_request.tests); 1156 ("Properties", Test_properties.tests); 1157 ]