···
5
+
let get_free_port () =
10
+
let string_contains s sub =
12
+
let _ = Str.search_forward (Str.regexp_string sub) s 0 in
14
+
with Not_found -> false
16
+
module Test_server = struct
19
+
let make_server ~port handler env =
21
+
Eio.Net.listen env#net ~sw:env#sw ~backlog:128 ~reuse_addr:true
22
+
(`Tcp (Eio.Net.Ipaddr.V4.loopback, port))
24
+
let callback _conn req body =
25
+
let (resp, body_content) = handler ~request:req ~body in
26
+
Server.respond_string () ~status:(Http.Response.status resp)
27
+
~headers:(Http.Response.headers resp)
30
+
let server = Server.make ~callback () in
31
+
Server.run server_socket server ~on_error:(fun exn ->
32
+
Logs.err (fun m -> m "Server error: %s" (Printexc.to_string exn))
35
+
let echo_handler ~request ~body =
36
+
let uri = Http.Request.resource request in
37
+
let meth = Http.Request.meth request in
38
+
let headers = Http.Request.headers request in
39
+
let body_str = Eio.Flow.read_all body in
43
+
"method", `String (Cohttp.Code.string_of_method meth);
46
+
Cohttp.Header.to_lines headers
47
+
|> List.map (fun line ->
48
+
match String.split_on_char ':' line with
49
+
| [k; v] -> (String.trim k, `String (String.trim v))
50
+
| _ -> ("", `String line)
53
+
"body", `String body_str;
55
+
|> Yojson.Basic.to_string
58
+
let resp = Http.Response.make ~status:`OK () in
59
+
let resp_headers = Cohttp.Header.add_unless_exists
60
+
(Http.Response.headers resp) "content-type" "application/json"
62
+
({ resp with headers = resp_headers }, response_body)
64
+
let status_handler status_code ~request:_ ~body:_ =
65
+
let status = Cohttp.Code.status_of_code status_code in
66
+
let resp = Http.Response.make ~status () in
69
+
let redirect_handler target_path ~request:_ ~body:_ =
70
+
let resp = Http.Response.make ~status:`Moved_permanently () in
71
+
let headers = Cohttp.Header.add
72
+
(Http.Response.headers resp) "location" target_path
74
+
({ resp with headers }, "")
76
+
let cookie_handler ~request ~body:_ =
77
+
let headers = Http.Request.headers request in
79
+
match Cohttp.Header.get headers "cookie" with
80
+
| Some cookie_str -> cookie_str
81
+
| None -> "no cookies"
84
+
let resp = Http.Response.make ~status:`OK () in
86
+
Http.Response.headers resp
87
+
|> (fun h -> Cohttp.Header.add h "set-cookie" "test_cookie=test_value; Path=/")
88
+
|> (fun h -> Cohttp.Header.add h "set-cookie" "session=abc123; Path=/; HttpOnly")
90
+
({ resp with headers = resp_headers },
93
+
let auth_handler ~request ~body:_ =
94
+
let headers = Http.Request.headers request in
96
+
match Cohttp.Header.get headers "authorization" with
98
+
if String.starts_with ~prefix:"Bearer " auth then
99
+
let token = String.sub auth 7 (String.length auth - 7) in
100
+
if token = "valid_token" then "authorized"
101
+
else "invalid token"
102
+
else if String.starts_with ~prefix:"Basic " auth then
103
+
"basic auth received"
104
+
else "unknown auth"
105
+
| None -> "no auth"
109
+
if auth_result = "authorized" || auth_result = "basic auth received"
113
+
let resp = Http.Response.make ~status () in
114
+
(resp, auth_result)
116
+
let json_handler ~request:_ ~body =
117
+
let body_str = Eio.Flow.read_all body in
120
+
let parsed = Yojson.Basic.from_string body_str in
122
+
"received", parsed;
123
+
"echo", `Bool true;
127
+
"error", `String "invalid json";
128
+
"received", `String body_str;
132
+
let resp = Http.Response.make ~status:`OK () in
133
+
let resp_headers = Cohttp.Header.add_unless_exists
134
+
(Http.Response.headers resp) "content-type" "application/json"
136
+
({ resp with headers = resp_headers },
137
+
Yojson.Basic.to_string json)
139
+
let timeout_handler clock delay ~request:_ ~body:_ =
140
+
Eio.Time.sleep clock delay;
141
+
let resp = Http.Response.make ~status:`OK () in
142
+
(resp,"delayed response")
144
+
let chunked_handler _clock chunks ~request:_ ~body:_ =
145
+
let resp = Http.Response.make ~status:`OK () in
146
+
let body_str = String.concat "" chunks in
149
+
let large_response_handler size ~request:_ ~body:_ =
150
+
let data = String.make size 'X' in
151
+
let resp = Http.Response.make ~status:`OK () in
154
+
let multipart_handler ~request ~body =
155
+
let headers = Http.Request.headers request in
156
+
let content_type = Cohttp.Header.get headers "content-type" in
157
+
let body_str = Eio.Flow.read_all body in
160
+
match content_type with
161
+
| Some ct when String.starts_with ~prefix:"multipart/form-data" ct ->
162
+
Printf.sprintf "Multipart received: %d bytes" (String.length body_str)
163
+
| _ -> "Not multipart"
166
+
let resp = Http.Response.make ~status:`OK () in
169
+
let router clock ~request ~body =
170
+
let uri = Http.Request.resource request in
172
+
| "/" | "/echo" -> echo_handler ~request ~body
173
+
| "/status/200" -> status_handler 200 ~request ~body
174
+
| "/status/404" -> status_handler 404 ~request ~body
175
+
| "/status/500" -> status_handler 500 ~request ~body
176
+
| "/redirect" -> redirect_handler "/redirected" ~request ~body
178
+
let resp = Http.Response.make ~status:`OK () in
179
+
(resp,"redirect successful")
180
+
| "/cookies" -> cookie_handler ~request ~body
181
+
| "/auth" -> auth_handler ~request ~body
182
+
| "/json" -> json_handler ~request ~body
183
+
| "/timeout" -> timeout_handler clock 2.0 ~request ~body
185
+
chunked_handler clock ["chunk1"; "chunk2"; "chunk3"] ~request ~body
186
+
| "/large" -> large_response_handler 10000 ~request ~body
187
+
| "/multipart" -> multipart_handler ~request ~body
188
+
| _ -> status_handler 404 ~request ~body
190
+
let start_server ~port env =
191
+
Eio.Fiber.fork ~sw:env#sw (fun () ->
192
+
make_server ~port (router env#clock) env
194
+
Eio.Time.sleep env#clock 0.1
197
+
let test_get_request () =
199
+
Eio.Switch.run @@ fun sw ->
200
+
let port = get_free_port () in
201
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
203
+
let test_env = object
204
+
method clock = env#clock
205
+
method net = env#net
208
+
Test_server.start_server ~port test_env;
210
+
let req = Requests.create ~sw env in
211
+
let response = Requests.get req (base_url ^ "/echo") in
213
+
Alcotest.(check int) "GET status" 200 (Requests.Response.status_code response);
215
+
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
216
+
let json = Yojson.Basic.from_string body_str in
218
+
json |> Yojson.Basic.Util.member "method" |> Yojson.Basic.Util.to_string
221
+
Alcotest.(check string) "GET method" "GET" method_str
223
+
let test_post_request () =
225
+
Eio.Switch.run @@ fun sw ->
226
+
let port = get_free_port () in
227
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
229
+
let test_env = object
230
+
method clock = env#clock
231
+
method net = env#net
234
+
Test_server.start_server ~port test_env;
236
+
let req = Requests.create ~sw env in
237
+
let body = Requests.Body.text "test post data" in
238
+
let response = Requests.post req ~body (base_url ^ "/echo") in
240
+
Alcotest.(check int) "POST status" 200 (Requests.Response.status_code response);
242
+
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
243
+
let json = Yojson.Basic.from_string body_str in
244
+
let received_body =
245
+
json |> Yojson.Basic.Util.member "body" |> Yojson.Basic.Util.to_string
248
+
Alcotest.(check string) "POST body" "test post data" received_body
250
+
let test_put_request () =
252
+
Eio.Switch.run @@ fun sw ->
253
+
let port = get_free_port () in
254
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
256
+
let test_env = object
257
+
method clock = env#clock
258
+
method net = env#net
261
+
Test_server.start_server ~port test_env;
263
+
let req = Requests.create ~sw env in
264
+
let body = Requests.Body.text "put data" in
265
+
let response = Requests.put req ~body (base_url ^ "/echo") in
267
+
Alcotest.(check int) "PUT status" 200 (Requests.Response.status_code response);
269
+
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
270
+
let json = Yojson.Basic.from_string body_str in
272
+
json |> Yojson.Basic.Util.member "method" |> Yojson.Basic.Util.to_string
275
+
Alcotest.(check string) "PUT method" "PUT" method_str
277
+
let test_delete_request () =
279
+
Eio.Switch.run @@ fun sw ->
280
+
let port = get_free_port () in
281
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
283
+
let test_env = object
284
+
method clock = env#clock
285
+
method net = env#net
288
+
Test_server.start_server ~port test_env;
290
+
let req = Requests.create ~sw env in
291
+
let response = Requests.delete req (base_url ^ "/echo") in
293
+
Alcotest.(check int) "DELETE status" 200 (Requests.Response.status_code response);
295
+
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
296
+
let json = Yojson.Basic.from_string body_str in
298
+
json |> Yojson.Basic.Util.member "method" |> Yojson.Basic.Util.to_string
301
+
Alcotest.(check string) "DELETE method" "DELETE" method_str
303
+
let test_patch_request () =
305
+
Eio.Switch.run @@ fun sw ->
306
+
let port = get_free_port () in
307
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
309
+
let test_env = object
310
+
method clock = env#clock
311
+
method net = env#net
314
+
Test_server.start_server ~port test_env;
316
+
let req = Requests.create ~sw env in
317
+
let body = Requests.Body.of_string Requests.Mime.json {|{"patch": "data"}|} in
318
+
let response = Requests.patch req ~body (base_url ^ "/echo") in
320
+
Alcotest.(check int) "PATCH status" 200 (Requests.Response.status_code response);
322
+
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
323
+
let json = Yojson.Basic.from_string body_str in
325
+
json |> Yojson.Basic.Util.member "method" |> Yojson.Basic.Util.to_string
328
+
Alcotest.(check string) "PATCH method" "PATCH" method_str
330
+
let test_head_request () =
332
+
Eio.Switch.run @@ fun sw ->
333
+
let port = get_free_port () in
334
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
336
+
let test_env = object
337
+
method clock = env#clock
338
+
method net = env#net
341
+
Test_server.start_server ~port test_env;
343
+
let req = Requests.create ~sw env in
344
+
let response = Requests.head req (base_url ^ "/echo") in
346
+
Alcotest.(check int) "HEAD status" 200 (Requests.Response.status_code response)
348
+
let test_options_request () =
350
+
Eio.Switch.run @@ fun sw ->
351
+
let port = get_free_port () in
352
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
354
+
let test_env = object
355
+
method clock = env#clock
356
+
method net = env#net
359
+
Test_server.start_server ~port test_env;
361
+
let req = Requests.create ~sw env in
362
+
let response = Requests.options req (base_url ^ "/echo") in
364
+
Alcotest.(check int) "OPTIONS status" 200 (Requests.Response.status_code response)
366
+
let test_custom_headers () =
368
+
Eio.Switch.run @@ fun sw ->
369
+
let port = get_free_port () in
370
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
372
+
let test_env = object
373
+
method clock = env#clock
374
+
method net = env#net
377
+
Test_server.start_server ~port test_env;
379
+
let req = Requests.create ~sw env in
381
+
Requests.Headers.empty
382
+
|> Requests.Headers.set "X-Custom-Header" "custom-value"
383
+
|> Requests.Headers.set "User-Agent" "test-agent"
385
+
let response = Requests.get req ~headers (base_url ^ "/echo") in
387
+
Alcotest.(check int) "Headers status" 200 (Requests.Response.status_code response);
389
+
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
390
+
let json = Yojson.Basic.from_string body_str in
391
+
let headers_obj = json |> Yojson.Basic.Util.member "headers" in
393
+
let custom_header =
395
+
|> Yojson.Basic.Util.member "x-custom-header"
396
+
|> Yojson.Basic.Util.to_string_option
397
+
|> Option.value ~default:""
400
+
Alcotest.(check string) "Custom header" "custom-value" custom_header
402
+
let test_query_params () =
404
+
Eio.Switch.run @@ fun sw ->
405
+
let port = get_free_port () in
406
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
408
+
let test_env = object
409
+
method clock = env#clock
410
+
method net = env#net
413
+
Test_server.start_server ~port test_env;
415
+
let req = Requests.create ~sw env in
416
+
let params = [("key1", "value1"); ("key2", "value2")] in
417
+
let response = Requests.get req ~params (base_url ^ "/echo") in
419
+
Alcotest.(check int) "Query params status" 200 (Requests.Response.status_code response);
421
+
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
422
+
let json = Yojson.Basic.from_string body_str in
423
+
let uri = json |> Yojson.Basic.Util.member "uri" |> Yojson.Basic.Util.to_string in
425
+
Alcotest.(check bool) "Query params present" true
426
+
(string_contains uri "key1=value1" && string_contains uri "key2=value2")
428
+
let test_json_body () =
430
+
Eio.Switch.run @@ fun sw ->
431
+
let port = get_free_port () in
432
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
434
+
let test_env = object
435
+
method clock = env#clock
436
+
method net = env#net
439
+
Test_server.start_server ~port test_env;
441
+
let req = Requests.create ~sw env in
442
+
let json_data = {|{"name": "test", "value": 42}|} in
443
+
let body = Requests.Body.of_string Requests.Mime.json json_data in
444
+
let response = Requests.post req ~body (base_url ^ "/json") in
446
+
Alcotest.(check int) "JSON body status" 200 (Requests.Response.status_code response);
448
+
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
449
+
let json = Yojson.Basic.from_string body_str in
450
+
let received = json |> Yojson.Basic.Util.member "received" in
451
+
let name = received |> Yojson.Basic.Util.member "name" |> Yojson.Basic.Util.to_string in
453
+
Alcotest.(check string) "JSON field" "test" name
455
+
let test_form_data () =
457
+
Eio.Switch.run @@ fun sw ->
458
+
let port = get_free_port () in
459
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
461
+
let test_env = object
462
+
method clock = env#clock
463
+
method net = env#net
466
+
Test_server.start_server ~port test_env;
468
+
let req = Requests.create ~sw env in
469
+
let form_data = [("field1", "value1"); ("field2", "value2")] in
470
+
let body = Requests.Body.form form_data in
471
+
let response = Requests.post req ~body (base_url ^ "/echo") in
473
+
Alcotest.(check int) "Form data status" 200 (Requests.Response.status_code response);
475
+
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
476
+
let json = Yojson.Basic.from_string body_str in
477
+
let received_body =
478
+
json |> Yojson.Basic.Util.member "body" |> Yojson.Basic.Util.to_string
481
+
Alcotest.(check bool) "Form data encoded" true
482
+
(string_contains received_body "field1=value1" &&
483
+
string_contains received_body "field2=value2")
485
+
let test_status_codes () =
487
+
Eio.Switch.run @@ fun sw ->
488
+
let port = get_free_port () in
489
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
491
+
let test_env = object
492
+
method clock = env#clock
493
+
method net = env#net
496
+
Test_server.start_server ~port test_env;
498
+
let req = Requests.create ~sw env in
500
+
let resp_200 = Requests.get req (base_url ^ "/status/200") in
501
+
Alcotest.(check int) "Status 200" 200 (Requests.Response.status_code resp_200);
503
+
let resp_404 = Requests.get req (base_url ^ "/status/404") in
504
+
Alcotest.(check int) "Status 404" 404 (Requests.Response.status_code resp_404);
506
+
let resp_500 = Requests.get req (base_url ^ "/status/500") in
507
+
Alcotest.(check int) "Status 500" 500 (Requests.Response.status_code resp_500)
509
+
let test_redirects () =
511
+
Eio.Switch.run @@ fun sw ->
512
+
let port = get_free_port () in
513
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
515
+
let test_env = object
516
+
method clock = env#clock
517
+
method net = env#net
520
+
Test_server.start_server ~port test_env;
522
+
let req = Requests.create ~sw ~follow_redirects:true env in
523
+
let response = Requests.get req (base_url ^ "/redirect") in
525
+
Alcotest.(check int) "Redirect followed" 200 (Requests.Response.status_code response);
527
+
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
528
+
Alcotest.(check string) "Redirect result" "redirect successful" body_str
530
+
let test_no_redirect () =
532
+
Eio.Switch.run @@ fun sw ->
533
+
let port = get_free_port () in
534
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
536
+
let test_env = object
537
+
method clock = env#clock
538
+
method net = env#net
541
+
Test_server.start_server ~port test_env;
543
+
let req = Requests.create ~sw env in
544
+
let response = Requests.request req ~follow_redirects:false ~method_:`GET (base_url ^ "/redirect") in
546
+
Alcotest.(check int) "Redirect not followed" 301
547
+
(Requests.Response.status_code response)
549
+
let test_cookies () =
551
+
Eio.Switch.run @@ fun sw ->
552
+
let port = get_free_port () in
553
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
555
+
let test_env = object
556
+
method clock = env#clock
557
+
method net = env#net
560
+
Test_server.start_server ~port test_env;
562
+
let req = Requests.create ~sw env in
564
+
let _first_response = Requests.get req (base_url ^ "/cookies") in
566
+
let second_response = Requests.get req (base_url ^ "/cookies") in
567
+
let body_str = Requests.Response.body second_response |> Eio.Flow.read_all in
569
+
Alcotest.(check bool) "Cookies sent back" true
570
+
(string_contains body_str "test_cookie=test_value")
572
+
let test_bearer_auth () =
574
+
Eio.Switch.run @@ fun sw ->
575
+
let port = get_free_port () in
576
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
578
+
let test_env = object
579
+
method clock = env#clock
580
+
method net = env#net
583
+
Test_server.start_server ~port test_env;
585
+
let req = Requests.create ~sw env in
586
+
let auth = Requests.Auth.bearer ~token:"valid_token" in
587
+
let response = Requests.get req ~auth (base_url ^ "/auth") in
589
+
Alcotest.(check int) "Bearer auth status" 200 (Requests.Response.status_code response);
591
+
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
592
+
Alcotest.(check string) "Bearer auth result" "authorized" body_str
594
+
let test_basic_auth () =
596
+
Eio.Switch.run @@ fun sw ->
597
+
let port = get_free_port () in
598
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
600
+
let test_env = object
601
+
method clock = env#clock
602
+
method net = env#net
605
+
Test_server.start_server ~port test_env;
607
+
let req = Requests.create ~sw env in
608
+
let auth = Requests.Auth.basic ~username:"user" ~password:"pass" in
609
+
let response = Requests.get req ~auth (base_url ^ "/auth") in
611
+
Alcotest.(check int) "Basic auth status" 200 (Requests.Response.status_code response);
613
+
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
614
+
Alcotest.(check string) "Basic auth result" "basic auth received" body_str
616
+
let test_timeout () =
618
+
Eio.Switch.run @@ fun sw ->
619
+
let port = get_free_port () in
620
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
622
+
let test_env = object
623
+
method clock = env#clock
624
+
method net = env#net
627
+
Test_server.start_server ~port test_env;
629
+
let req = Requests.create ~sw env in
630
+
let timeout = Requests.Timeout.create ~total:0.5 () in
632
+
let exception_raised =
634
+
let _ = Requests.get req ~timeout (base_url ^ "/timeout") in
639
+
Alcotest.(check bool) "Timeout triggered" true exception_raised
641
+
let test_concurrent_requests () =
643
+
Eio.Switch.run @@ fun sw ->
644
+
let port = get_free_port () in
645
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
647
+
let test_env = object
648
+
method clock = env#clock
649
+
method net = env#net
652
+
Test_server.start_server ~port test_env;
654
+
let req = Requests.create ~sw env in
656
+
let r1 = ref None in
657
+
let r2 = ref None in
658
+
let r3 = ref None in
661
+
(fun () -> r1 := Some (Requests.get req (base_url ^ "/status/200")));
662
+
(fun () -> r2 := Some (Requests.get req (base_url ^ "/status/404")));
663
+
(fun () -> r3 := Some (Requests.get req (base_url ^ "/status/500")));
666
+
let r1 = Option.get !r1 in
667
+
let r2 = Option.get !r2 in
668
+
let r3 = Option.get !r3 in
670
+
Alcotest.(check int) "Concurrent 1" 200 (Requests.Response.status_code r1);
671
+
Alcotest.(check int) "Concurrent 2" 404 (Requests.Response.status_code r2);
672
+
Alcotest.(check int) "Concurrent 3" 500 (Requests.Response.status_code r3)
674
+
let test_large_response () =
676
+
Eio.Switch.run @@ fun sw ->
677
+
let port = get_free_port () in
678
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
680
+
let test_env = object
681
+
method clock = env#clock
682
+
method net = env#net
685
+
Test_server.start_server ~port test_env;
687
+
let req = Requests.create ~sw env in
688
+
let response = Requests.get req (base_url ^ "/large") in
690
+
Alcotest.(check int) "Large response status" 200 (Requests.Response.status_code response);
692
+
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
693
+
Alcotest.(check int) "Large response size" 10000 (String.length body_str)
695
+
let test_one_module () =
697
+
Eio.Switch.run @@ fun sw ->
698
+
let port = get_free_port () in
699
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
701
+
let test_env = object
702
+
method clock = env#clock
703
+
method net = env#net
706
+
Test_server.start_server ~port test_env;
708
+
let client = Requests.One.create ~clock:env#clock ~net:env#net () in
709
+
let response = Requests.One.get ~sw ~client (base_url ^ "/echo") in
711
+
Alcotest.(check int) "One module status" 200 (Requests.Response.status_code response)
713
+
let test_multipart () =
715
+
Eio.Switch.run @@ fun sw ->
716
+
let port = get_free_port () in
717
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
719
+
let test_env = object
720
+
method clock = env#clock
721
+
method net = env#net
724
+
Test_server.start_server ~port test_env;
726
+
let req = Requests.create ~sw env in
728
+
{ Requests.Body.name = "field1";
730
+
content_type = Requests.Mime.text;
731
+
content = `String "value1" };
732
+
{ Requests.Body.name = "field2";
733
+
filename = Some "test.txt";
734
+
content_type = Requests.Mime.text;
735
+
content = `String "file content" };
737
+
let body = Requests.Body.multipart parts in
738
+
let response = Requests.post req ~body (base_url ^ "/multipart") in
740
+
Alcotest.(check int) "Multipart status" 200 (Requests.Response.status_code response);
742
+
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
743
+
Alcotest.(check bool) "Multipart recognized" true
744
+
(String.starts_with ~prefix:"Multipart received:" body_str)
746
+
let test_response_headers () =
748
+
Eio.Switch.run @@ fun sw ->
749
+
let port = get_free_port () in
750
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
752
+
let test_env = object
753
+
method clock = env#clock
754
+
method net = env#net
757
+
Test_server.start_server ~port test_env;
759
+
let req = Requests.create ~sw env in
760
+
let response = Requests.get req (base_url ^ "/json") in
763
+
Requests.Response.headers response
764
+
|> Requests.Headers.get "content-type"
767
+
Alcotest.(check (option string)) "Response content-type"
768
+
(Some "application/json") content_type
770
+
let test_default_headers () =
772
+
Eio.Switch.run @@ fun sw ->
773
+
let port = get_free_port () in
774
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
776
+
let test_env = object
777
+
method clock = env#clock
778
+
method net = env#net
781
+
Test_server.start_server ~port test_env;
783
+
let default_headers =
784
+
Requests.Headers.empty
785
+
|> Requests.Headers.set "X-Default" "default-value"
787
+
let req = Requests.create ~sw ~default_headers env in
788
+
let response = Requests.get req (base_url ^ "/echo") in
790
+
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
791
+
let json = Yojson.Basic.from_string body_str in
792
+
let headers_obj = json |> Yojson.Basic.Util.member "headers" in
794
+
let default_header =
796
+
|> Yojson.Basic.Util.member "x-default"
797
+
|> Yojson.Basic.Util.to_string_option
798
+
|> Option.value ~default:""
801
+
Alcotest.(check string) "Default header present" "default-value" default_header
803
+
let test_session_persistence () =
805
+
Eio.Switch.run @@ fun sw ->
806
+
let port = get_free_port () in
807
+
let base_url = Printf.sprintf "http://127.0.0.1:%d" port in
809
+
let test_env = object
810
+
method clock = env#clock
811
+
method net = env#net
814
+
Test_server.start_server ~port test_env;
816
+
let req = Requests.create ~sw env in
818
+
Requests.set_default_header req "X-Session" "session-123";
820
+
let auth = Requests.Auth.bearer ~token:"test_token" in
821
+
Requests.set_auth req auth;
823
+
let response1 = Requests.get req (base_url ^ "/echo") in
824
+
let body_str1 = Requests.Response.body response1 |> Eio.Flow.read_all in
825
+
let json1 = Yojson.Basic.from_string body_str1 in
826
+
let headers1 = json1 |> Yojson.Basic.Util.member "headers" in
828
+
let session_header =
830
+
|> Yojson.Basic.Util.member "x-session"
831
+
|> Yojson.Basic.Util.to_string_option
832
+
|> Option.value ~default:""
835
+
Alcotest.(check string) "Session header persisted" "session-123" session_header;
837
+
Requests.remove_default_header req "X-Session";
839
+
let response2 = Requests.get req (base_url ^ "/echo") in
840
+
let body_str2 = Requests.Response.body response2 |> Eio.Flow.read_all in
841
+
let json2 = Yojson.Basic.from_string body_str2 in
842
+
let headers2 = json2 |> Yojson.Basic.Util.member "headers" in
844
+
let session_header2 =
846
+
|> Yojson.Basic.Util.member "x-session"
847
+
|> Yojson.Basic.Util.to_string_option
850
+
Alcotest.(check (option string)) "Session header removed" None session_header2
853
+
Logs.set_reporter (Logs.format_reporter ());
854
+
Logs.set_level (Some Logs.Warning);
856
+
let open Alcotest in
857
+
run "Requests Tests" [
859
+
test_case "GET request" `Quick test_get_request;
860
+
test_case "POST request" `Quick test_post_request;
861
+
test_case "PUT request" `Quick test_put_request;
862
+
test_case "DELETE request" `Quick test_delete_request;
863
+
test_case "PATCH request" `Quick test_patch_request;
864
+
test_case "HEAD request" `Quick test_head_request;
865
+
test_case "OPTIONS request" `Quick test_options_request;
867
+
"Request Features", [
868
+
test_case "Custom headers" `Quick test_custom_headers;
869
+
test_case "Query parameters" `Quick test_query_params;
870
+
test_case "JSON body" `Quick test_json_body;
871
+
test_case "Form data" `Quick test_form_data;
872
+
test_case "Multipart upload" `Quick test_multipart;
873
+
test_case "Default headers" `Quick test_default_headers;
875
+
"Response Handling", [
876
+
test_case "Status codes" `Quick test_status_codes;
877
+
test_case "Response headers" `Quick test_response_headers;
878
+
test_case "Large response" `Quick test_large_response;
881
+
test_case "Follow redirects" `Quick test_redirects;
882
+
test_case "No follow redirects" `Quick test_no_redirect;
884
+
"Authentication", [
885
+
test_case "Bearer auth" `Quick test_bearer_auth;
886
+
test_case "Basic auth" `Quick test_basic_auth;
888
+
"Session Features", [
889
+
test_case "Cookies" `Quick test_cookies;
890
+
test_case "Session persistence" `Quick test_session_persistence;
893
+
test_case "Timeout handling" `Quick test_timeout;
894
+
test_case "Concurrent requests" `Quick test_concurrent_requests;
895
+
test_case "One module" `Quick test_one_module;