diff --git a/tests/FSharp.Data.Core.Tests/FSharp.Data.Core.Tests.fsproj b/tests/FSharp.Data.Core.Tests/FSharp.Data.Core.Tests.fsproj index f583f64ed..7d051e322 100644 --- a/tests/FSharp.Data.Core.Tests/FSharp.Data.Core.Tests.fsproj +++ b/tests/FSharp.Data.Core.Tests/FSharp.Data.Core.Tests.fsproj @@ -37,6 +37,7 @@ + diff --git a/tests/FSharp.Data.Core.Tests/XmlExtensions.fs b/tests/FSharp.Data.Core.Tests/XmlExtensions.fs new file mode 100644 index 000000000..a30a1d7ae --- /dev/null +++ b/tests/FSharp.Data.Core.Tests/XmlExtensions.fs @@ -0,0 +1,251 @@ +module FSharp.Data.Tests.XmlExtensions + +open FsUnit +open NUnit.Framework +open System +open System.Xml.Linq +open FSharp.Data +open FSharp.Data.HttpRequestHeaders +open Microsoft.AspNetCore.Builder +open Microsoft.AspNetCore.Http +open System.Threading.Tasks +open System.Net.NetworkInformation +open System.IO +open System.Text + +type ITestHttpServer = + inherit IDisposable + abstract member BaseAddress: string + abstract member WorkerTask: Task + +let startXmlHttpLocalServer() = + let app = WebApplication.CreateBuilder().Build() + + // Handle XML POST requests and echo back the received XML + app.Map("/echo", (fun (ctx: HttpContext) -> + async { + use reader = new StreamReader(ctx.Request.Body) + let! body = reader.ReadToEndAsync() |> Async.AwaitTask + + ctx.Response.ContentType <- "application/xml" + ctx.Response.StatusCode <- 200 + + // Echo back the received XML with a wrapper to validate it was received + let responseXml = $"{body}" + return! ctx.Response.WriteAsync(responseXml) |> Async.AwaitTask + } |> Async.StartAsTask :> Task + )) |> ignore + + // Handle different HTTP methods + app.Map("/test/{method}", (fun (ctx: HttpContext) -> + async { + let method = ctx.Request.RouteValues.["method"] :?> string + let actualMethod = ctx.Request.Method + + ctx.Response.ContentType <- "application/xml" + ctx.Response.StatusCode <- 200 + + let responseXml = $"{actualMethod}{method}" + return! ctx.Response.WriteAsync(responseXml) |> Async.AwaitTask + } |> Async.StartAsTask :> Task + )) |> ignore + + let freePort = + let random = new System.Random() + let mutable port = random.Next(10000, 65000) + while + IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners() + |> Array.map (fun x -> x.Port) + |> Array.contains port do + port <- random.Next(10000, 65000) + port + + let baseAddress = $"http://localhost:{freePort}" + let workerTask = app.RunAsync(baseAddress) + + { new ITestHttpServer with + member this.Dispose() = + app.StopAsync() |> Async.AwaitTask |> ignore + member this.WorkerTask = workerTask + member this.BaseAddress = baseAddress } + +[] +let ``XElement.Request sends XML via POST by default`` () = + use localServer = startXmlHttpLocalServer() + System.Threading.Thread.Sleep(100) // Let server start + + let xml = XElement(XName.Get("test"), "sample content") + let response = xml.Request(localServer.BaseAddress + "/echo") + + response.StatusCode |> should equal 200 + match response.Body with + | Text bodyText -> bodyText |> should contain "sample content" + | Binary _ -> failwith "Expected text response, but got binary" + +[] +let ``XElement.Request with custom HTTP method`` () = + use localServer = startXmlHttpLocalServer() + System.Threading.Thread.Sleep(100) + + let xml = XElement(XName.Get("test"), "content") + let response = xml.Request(localServer.BaseAddress + "/test/PUT", httpMethod = HttpMethod.Put) + + response.StatusCode |> should equal 200 + match response.Body with + | Text bodyText -> bodyText |> should contain "PUT" + | Binary _ -> failwith "Expected text response, but got binary" + +[] +let ``XElement.Request includes default User-Agent header`` () = + use localServer = startXmlHttpLocalServer() + System.Threading.Thread.Sleep(100) + + let xml = XElement(XName.Get("test")) + let response = xml.Request(localServer.BaseAddress + "/echo") + + // The User-Agent should be set to the default value + response.StatusCode |> should equal 200 + +[] +let ``XElement.Request with custom headers`` () = + use localServer = startXmlHttpLocalServer() + System.Threading.Thread.Sleep(100) + + let xml = XElement(XName.Get("test")) + let customHeaders = [("X-Custom-Header", "test-value")] + let response = xml.Request(localServer.BaseAddress + "/echo", headers = customHeaders) + + response.StatusCode |> should equal 200 + +[] +let ``XElement.Request preserves existing User-Agent when provided`` () = + use localServer = startXmlHttpLocalServer() + System.Threading.Thread.Sleep(100) + + let xml = XElement(XName.Get("test")) + let customHeaders = [UserAgent "CustomAgent/1.0"] + let response = xml.Request(localServer.BaseAddress + "/echo", headers = customHeaders) + + response.StatusCode |> should equal 200 + +[] +let ``XElement.Request includes XML content type header`` () = + use localServer = startXmlHttpLocalServer() + System.Threading.Thread.Sleep(100) + + let xml = XElement(XName.Get("test")) + let response = xml.Request(localServer.BaseAddress + "/echo") + + // Should include Content-Type: application/xml + response.StatusCode |> should equal 200 + +[] +let ``XElement.Request with complex XML structure`` () = + use localServer = startXmlHttpLocalServer() + System.Threading.Thread.Sleep(100) + + let xml = + XElement(XName.Get("root"), + XElement(XName.Get("child1"), "value1"), + XElement(XName.Get("child2"), + XAttribute(XName.Get("attr"), "attrvalue"), + "value2")) + + let response = xml.Request(localServer.BaseAddress + "/echo") + + response.StatusCode |> should equal 200 + match response.Body with + | Text bodyText -> + bodyText |> should contain "value1" + bodyText |> should contain "value2" + | Binary _ -> failwith "Expected text response, but got binary" + +[] +let ``XElement.RequestAsync sends XML via POST by default`` () = + use localServer = startXmlHttpLocalServer() + System.Threading.Thread.Sleep(100) + + let xml = XElement(XName.Get("test"), "async content") + let response = xml.RequestAsync(localServer.BaseAddress + "/echo") |> Async.RunSynchronously + + response.StatusCode |> should equal 200 + match response.Body with + | Text bodyText -> bodyText |> should contain "async content" + | Binary _ -> failwith "Expected text response, but got binary" + +[] +let ``XElement.RequestAsync with custom HTTP method`` () = + use localServer = startXmlHttpLocalServer() + System.Threading.Thread.Sleep(100) + + let xml = XElement(XName.Get("test")) + let response = xml.RequestAsync(localServer.BaseAddress + "/test/PUT", httpMethod = HttpMethod.Put) |> Async.RunSynchronously + + response.StatusCode |> should equal 200 + match response.Body with + | Text bodyText -> bodyText |> should contain "PUT" + | Binary _ -> failwith "Expected text response, but got binary" + +[] +let ``XElement.RequestAsync with custom headers`` () = + use localServer = startXmlHttpLocalServer() + System.Threading.Thread.Sleep(100) + + let xml = XElement(XName.Get("test")) + let customHeaders = [("X-Async-Header", "async-value")] + let response = xml.RequestAsync(localServer.BaseAddress + "/echo", headers = customHeaders) |> Async.RunSynchronously + + response.StatusCode |> should equal 200 + +[] +let ``XElement.RequestAsync includes default User-Agent header`` () = + use localServer = startXmlHttpLocalServer() + System.Threading.Thread.Sleep(100) + + let xml = XElement(XName.Get("test")) + let response = xml.RequestAsync(localServer.BaseAddress + "/echo") |> Async.RunSynchronously + + response.StatusCode |> should equal 200 + +[] +let ``XElement.RequestAsync preserves existing User-Agent when provided`` () = + use localServer = startXmlHttpLocalServer() + System.Threading.Thread.Sleep(100) + + let xml = XElement(XName.Get("test")) + let customHeaders = [UserAgent "AsyncAgent/1.0"] + let response = xml.RequestAsync(localServer.BaseAddress + "/echo", headers = customHeaders) |> Async.RunSynchronously + + response.StatusCode |> should equal 200 + +[] +let ``XElement with namespaces serializes correctly`` () = + use localServer = startXmlHttpLocalServer() + System.Threading.Thread.Sleep(100) + + let ns = XNamespace.Get("http://example.com/test") + let xml = XElement(ns + "root", XAttribute(XNamespace.Xmlns + "test", ns.NamespaceName), "content") + let response = xml.Request(localServer.BaseAddress + "/echo") + + response.StatusCode |> should equal 200 + match response.Body with + | Text bodyText -> bodyText |> should contain "xmlns:test=\"http://example.com/test\"" + | Binary _ -> failwith "Expected text response, but got binary" + +[] +let ``XElement serialization disables formatting`` () = + use localServer = startXmlHttpLocalServer() + System.Threading.Thread.Sleep(100) + + let xml = + XElement(XName.Get("root"), + XElement(XName.Get("child1"), "value1"), + XElement(XName.Get("child2"), "value2")) + + let response = xml.Request(localServer.BaseAddress + "/echo") + + response.StatusCode |> should equal 200 + // Should be compact without extra whitespace due to SaveOptions.DisableFormatting + match response.Body with + | Text bodyText -> bodyText |> should not' (contain "\n ") + | Binary _ -> failwith "Expected text response, but got binary" \ No newline at end of file