From 13f14d93f7c7c119ded8781c84468923292ec0c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Melvyn=20La=C3=AFly?= Date: Mon, 20 Jun 2022 21:33:32 +0200 Subject: [PATCH 1/2] Use a local http test server instead of calling internet servers --- .../FSharp.Data.Tests.fsproj | 3 + tests/FSharp.Data.Tests/Http.fs | 86 +++++++++++++++++-- 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/tests/FSharp.Data.Tests/FSharp.Data.Tests.fsproj b/tests/FSharp.Data.Tests/FSharp.Data.Tests.fsproj index d757c46b2..0b65efeeb 100755 --- a/tests/FSharp.Data.Tests/FSharp.Data.Tests.fsproj +++ b/tests/FSharp.Data.Tests/FSharp.Data.Tests.fsproj @@ -10,6 +10,9 @@ true + + + PreserveNewest diff --git a/tests/FSharp.Data.Tests/Http.fs b/tests/FSharp.Data.Tests/Http.fs index cf51ccbcc..62d7a91bf 100644 --- a/tests/FSharp.Data.Tests/Http.fs +++ b/tests/FSharp.Data.Tests/Http.fs @@ -8,17 +8,78 @@ open System.Net open FSharp.Data open FSharp.Data.HttpRequestHeaders open System.Text +open System.Threading.Tasks +open Microsoft.AspNetCore.Builder +open Microsoft.AspNetCore.Http +open System.Net.NetworkInformation + +type ITestHttpServer = + inherit IDisposable + abstract member BaseAddress: string + abstract member WorkerTask: Task + +let startHttpLocalServer() = + let app = WebApplication.CreateBuilder().Build() + app.Map("/{status}", (fun (ctx: HttpContext) -> + async { + match ctx.Request.RouteValues.TryGetValue("status") with + | true, (:? string as status) -> + let status = status |> int + + match ctx.Request.Query.TryGetValue("sleep") with + | true, values when values.Count = 1 -> + let value = values[0] |> int + do! Async.Sleep value + | _ -> () + + if ctx.Request.Body <> null then + let buffer = Array.create 8192 (byte 0) + let mutable read = -1 + while read <> 0 do + let! x = ctx.Request.Body.ReadAsync(buffer, 0, 8192) |> Async.AwaitTask + read <- x + + Results.StatusCode(status).ExecuteAsync(ctx) + |> Async.AwaitTask + |> ignore + | _ -> failwith "Unexpected request." + + } |> Async.StartAsTask :> Task + )) |> ignore + + let freePort = + let mutable port = 55555 // base listener port for the tests + while + IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners() + |> Array.map (fun x -> x.Port) + |> Array.contains port do + port <- port + 1 + port + + let baseAddress = $"http://localhost:{freePort}" + + let workerTask = app.RunAsync(baseAddress) + printfn $"Started local http server with address {baseAddress}" + + { new ITestHttpServer with + member this.Dispose() = + app.StopAsync() |> Async.AwaitTask |> ignore + printfn $"Stopped local http server with address {baseAddress}" + member this.WorkerTask = workerTask + member this.BaseAddress = baseAddress } [] let ``Don't throw exceptions on http error`` () = - let response = Http.Request("http://httpstat.us/401", silentHttpErrors = true) + use localServer = startHttpLocalServer() + let response = Http.Request(localServer.BaseAddress + "/401", silentHttpErrors = true) response.StatusCode |> should equal 401 [] let ``Throw exceptions on http error`` () = + use localServer = startHttpLocalServer() let exceptionThrown = try - Http.RequestString("http://api.themoviedb.org/3/search/movie") |> ignore + Http.RequestString(localServer.BaseAddress + "/401") |> ignore false with e -> true @@ -129,20 +190,25 @@ let ``Cookies is not added in cookieContainer but is still returned when addCook [] let ``Web request's timeout is used`` () = + use localServer = startHttpLocalServer() let exc = Assert.Throws (fun () -> - Http.Request("http://httpstat.us/200?sleep=1000", customizeHttpRequest = (fun req -> req.Timeout <- 1; req)) |> ignore) + Http.Request(localServer.BaseAddress + "/200?sleep=1000", customizeHttpRequest = (fun req -> req.Timeout <- 1; req)) |> ignore) + Assert.AreEqual(typeof, exc.InnerException.GetType()) [] let ``Timeout argument is used`` () = + use localServer = startHttpLocalServer() let exc = Assert.Throws (fun () -> - Http.Request("http://httpstat.us/200?sleep=1000", timeout = 1) |> ignore) + Http.Request(localServer.BaseAddress + "/200?sleep=1000", timeout = 1) |> ignore) + Assert.AreEqual(typeof, exc.InnerException.GetType()) [] let ``Setting timeout in customizeHttpRequest overrides timeout argument`` () = + use localServer = startHttpLocalServer() let response = - Http.Request("http://httpstat.us/401?sleep=1000", silentHttpErrors = true, + Http.Request(localServer.BaseAddress + "/401?sleep=1000", silentHttpErrors = true, customizeHttpRequest = (fun req -> req.Timeout <- Threading.Timeout.Infinite; req), timeout = 1) response.StatusCode |> should equal 401 @@ -156,18 +222,20 @@ let testFormDataSizesInBytes = [ ] [] -let testFormDataBodySize (size: int) = +let testFormDataBodySize (size: int) = + use localServer = startHttpLocalServer() let bodyString = seq {for i in 0..size -> "x\n"} |> String.concat "" let body = FormValues([("input", bodyString)]) - Assert.DoesNotThrowAsync(fun () -> Http.AsyncRequest (url="http://httpstat.us/200", httpMethod="POST", body=body, timeout = 10000) |> Async.Ignore |> Async.StartAsTask :> _) + Assert.DoesNotThrowAsync(fun () -> Http.AsyncRequest (url= localServer.BaseAddress + "/200", httpMethod="POST", body=body, timeout = 10000) |> Async.Ignore |> Async.StartAsTask :> _) [] -let testMultipartFormDataBodySize (size: int) = +let testMultipartFormDataBodySize (size: int) = + use localServer = startHttpLocalServer() let bodyString = seq {for i in 0..size -> "x\n"} |> String.concat "" let multipartItem = [ MultipartItem("input", "input.txt", new MemoryStream(Encoding.UTF8.GetBytes(bodyString)) :> Stream) ] let body = Multipart(Guid.NewGuid().ToString(), multipartItem) - Assert.DoesNotThrowAsync(fun () -> Http.AsyncRequest (url="http://httpstat.us/200", httpMethod="POST", body=body, timeout = 10000) |> Async.Ignore |> Async.StartAsTask :> _) + Assert.DoesNotThrowAsync(fun () -> Http.AsyncRequest (url= localServer.BaseAddress + "/200", httpMethod="POST", body=body, timeout = 10000) |> Async.Ignore |> Async.StartAsTask :> _) [] let ``escaping of url parameters`` () = From c5f1a7743ba5a38e0ab4fe8593205f5298cf6d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Melvyn=20La=C3=AFly?= Date: Sun, 26 Jun 2022 11:31:55 +0200 Subject: [PATCH 2/2] Fix http timeout test assertion I believe there is a race condition between the http stack timeout and the async timeout. If the http stack timeout is triggered first, the inner exception is null. In both cases, the WebException status should indicate a timeout. --- tests/FSharp.Data.Tests/Http.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/FSharp.Data.Tests/Http.fs b/tests/FSharp.Data.Tests/Http.fs index 62d7a91bf..30b240e7a 100644 --- a/tests/FSharp.Data.Tests/Http.fs +++ b/tests/FSharp.Data.Tests/Http.fs @@ -194,7 +194,7 @@ let ``Web request's timeout is used`` () = let exc = Assert.Throws (fun () -> Http.Request(localServer.BaseAddress + "/200?sleep=1000", customizeHttpRequest = (fun req -> req.Timeout <- 1; req)) |> ignore) - Assert.AreEqual(typeof, exc.InnerException.GetType()) + exc.Status |> should equal WebExceptionStatus.Timeout [] let ``Timeout argument is used`` () = @@ -202,7 +202,7 @@ let ``Timeout argument is used`` () = let exc = Assert.Throws (fun () -> Http.Request(localServer.BaseAddress + "/200?sleep=1000", timeout = 1) |> ignore) - Assert.AreEqual(typeof, exc.InnerException.GetType()) + exc.Status |> should equal WebExceptionStatus.Timeout [] let ``Setting timeout in customizeHttpRequest overrides timeout argument`` () =