-
Notifications
You must be signed in to change notification settings - Fork 284
Expand file tree
/
Copy pathHttp.fs
More file actions
464 lines (390 loc) · 21.7 KB
/
Http.fs
File metadata and controls
464 lines (390 loc) · 21.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
module FSharp.Data.Tests.Http
open FsUnit
open NUnit.Framework
open System
open System.IO
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.Sockets
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
// Use TcpListener(0) to ask the OS for a free port, then release it.
// The TOCTOU window (between Stop and Kestrel's bind) is microseconds,
// far more reliable than the previous random-port-then-check approach.
let freePort =
let listener = new TcpListener(System.Net.IPAddress.Loopback, 0)
listener.Start()
let port = (listener.LocalEndpoint :?> System.Net.IPEndPoint).Port
listener.Stop()
port
let baseAddress = $"http://127.0.0.1:{freePort}"
let workerTask = app.RunAsync(baseAddress)
printfn $"Started local http server with address {baseAddress}"
{ new ITestHttpServer with
member this.Dispose() =
app.StopAsync() |> Async.AwaitTask |> Async.RunSynchronously
printfn $"Stopped local http server with address {baseAddress}"
member this.WorkerTask = workerTask
member this.BaseAddress = baseAddress }
[<Test>]
let ``AppendQueryToUrl with no query returns url unchanged`` () =
Http.AppendQueryToUrl("https://example.com/api", []) |> should equal "https://example.com/api"
[<Test>]
let ``AppendQueryToUrl appends query parameters`` () =
Http.AppendQueryToUrl("https://example.com/api", [ "key", "value" ])
|> should equal "https://example.com/api?key=value"
[<Test>]
let ``AppendQueryToUrl appends multiple query parameters`` () =
Http.AppendQueryToUrl("https://example.com/api", [ "a", "1"; "b", "2" ])
|> should equal "https://example.com/api?a=1&b=2"
[<Test>]
let ``AppendQueryToUrl uses ampersand when url already contains query`` () =
Http.AppendQueryToUrl("https://example.com/api?existing=x", [ "key", "value" ])
|> should equal "https://example.com/api?existing=x&key=value"
[<Test>]
let ``AppendQueryToUrl percent-encodes special characters in keys and values`` () =
Http.AppendQueryToUrl("https://example.com/search", [ "q", "hello world" ])
|> should equal "https://example.com/search?q=hello%20world"
[<Test>]
let ``ParseLinkHeader returns empty map for empty string`` () =
Http.ParseLinkHeader("") |> should equal Map.empty
[<Test>]
let ``ParseLinkHeader parses next and last relations`` () =
let header =
"<https://api.github.com/repos/octocat/hello-world/releases?page=2>; rel=\"next\", <https://api.github.com/repos/octocat/hello-world/releases?page=5>; rel=\"last\""
let result = Http.ParseLinkHeader(header)
result |> Map.find "next" |> should equal "https://api.github.com/repos/octocat/hello-world/releases?page=2"
result |> Map.find "last" |> should equal "https://api.github.com/repos/octocat/hello-world/releases?page=5"
[<Test>]
let ``ParseLinkHeader parses single relation`` () =
let header = "<https://example.com/items?page=3>; rel=\"next\""
let result = Http.ParseLinkHeader(header)
result |> Map.find "next" |> should equal "https://example.com/items?page=3"
result |> Map.containsKey "prev" |> should equal false
[<Test>]
let ``ParseLinkHeader handles prev, next, first, last`` () =
let header =
"<https://example.com/items?page=1>; rel=\"first\", <https://example.com/items?page=2>; rel=\"prev\", <https://example.com/items?page=4>; rel=\"next\", <https://example.com/items?page=10>; rel=\"last\""
let result = Http.ParseLinkHeader(header)
result |> Map.find "first" |> should equal "https://example.com/items?page=1"
result |> Map.find "prev" |> should equal "https://example.com/items?page=2"
result |> Map.find "next" |> should equal "https://example.com/items?page=4"
result |> Map.find "last" |> should equal "https://example.com/items?page=10"
[<Test>]
let ``Don't throw exceptions on http error`` () =
use localServer = startHttpLocalServer()
let response = Http.Request(localServer.BaseAddress + "/401", silentHttpErrors = true)
response.StatusCode |> should equal 401
[<Test>]
let ``Throw exceptions on http error`` () =
use localServer = startHttpLocalServer()
let exceptionThrown =
try
Http.RequestString(localServer.BaseAddress + "/401") |> ignore
false
with e ->
true
exceptionThrown |> should equal true
[<Test>]
let ``If the same header is added multiple times, throws an exception`` () =
(fun () -> Http.RequestString("http://www.google.com", headers = [ UserAgent "ua1"; UserAgent "ua2" ]) |> ignore)
|> should throw typeof<Exception>
[<Test>]
let ``If a custom header with the same name is added multiple times, an exception is thrown`` () =
(fun () -> Http.RequestString("http://www.google.com", headers = [ "c1", "v1"; "c1", "v2" ]) |> ignore)
|> should throw typeof<Exception>
[<Test>]
let ``Two custom header with different names don't throw an exception`` () =
Http.RequestString("http://www.google.com", headers = [ "c1", "v1"; "c2", "v2" ]) |> ignore
[<Test>]
let ``A request with an invalid url throws an exception`` () =
(fun() -> Http.Request "www.google.com" |> ignore) |> should throw typeof<UriFormatException>
[<Test>]
let ``Cookies with commas are parsed correctly`` () =
let uri = Uri "http://www.nasdaq.com/symbol/ibm/dividend-history"
let cookieHeader = "selectedsymboltype=IBM,COMMON STOCK,NYSE; domain=.nasdaq.com; expires=Sun, 21-May-2017 15:29:03 GMT; path=/,selectedsymbolindustry=IBM,technology; domain=.nasdaq.com; expires=Sun, 21-May-2017 15:29:03 GMT; path=/,NSC_W.TJUFEFGFOEFS.OBTEBR.80=ffffffffc3a08e3045525d5f4f58455e445a4a423660;expires=Sat, 21-May-2016 15:39:03 GMT;path=/;httponly"
let cookies =
CookieHandling.getAllCookiesFromHeader cookieHeader uri
|> Array.map (snd >> (fun c -> c.Name, c.Value))
cookies |> should equal
[| "selectedsymboltype", "IBM,COMMON STOCK,NYSE"
"selectedsymbolindustry", "IBM,technology"
"NSC_W.TJUFEFGFOEFS.OBTEBR.80", "ffffffffc3a08e3045525d5f4f58455e445a4a423660" |]
[<Test>]
let ``Cookies with '=' are parsed correctly`` () =
let uri = Uri "http://nevermind.com"
let cookieHeader = "IdSession=Qze9H7HpvcVoh/ANulOl6Z1P8Omd1cLb9FOLL5o2aOlDMn/dFJi+RWDAPHZ4nDmWeno2puyOTFM/P4yzZMQsPoJ1gvEaJqG54kerM6WW4bv6ql72/Tn3NnCZTaokm8uaboLICgckUM2J7KOx5iL8uGyTN/g04/jZKlP1HgyatQL6kCG4qCQUMrdqZjqkbgW3eCpeyeI9rXF1bNC8hsKaqJ37Du/oBvbIMUbgenogfjzmlCgtAzv4la2Eo8+3cvDHkKPnksCP8kt8JbyXECyXBOjPrpFjtYv9UUGfyhwqRTRNTmH5+5UAsDDFrYe+vonYiDwXel8TfK3AZhQGXcF598AVPVfB1RO5S/mt7faDS7cEfz14nUsYtaNAZcwH7gwm06VJUX5eWiZzlBGx4SVBkNzP0QhLM9AqNP889y9BmZ2JaGb3fJtCWL3MfzM23mbwSemcERkoV3v1rIH8mb6ZgGm0hyEbbtu/RegkLAgNO+YB6c0Os6Pv6OnK0So4xlNakchaWhl1eMfOf4Gx0miJv4o2XbmAmbSNYkybi3n8vz4="
let cookies =
CookieHandling.getAllCookiesFromHeader cookieHeader uri
|> Array.map (snd >> (fun c -> c.Name, c.Value))
cookies |> should equal
[| "IdSession", "Qze9H7HpvcVoh/ANulOl6Z1P8Omd1cLb9FOLL5o2aOlDMn/dFJi+RWDAPHZ4nDmWeno2puyOTFM/P4yzZMQsPoJ1gvEaJqG54kerM6WW4bv6ql72/Tn3NnCZTaokm8uaboLICgckUM2J7KOx5iL8uGyTN/g04/jZKlP1HgyatQL6kCG4qCQUMrdqZjqkbgW3eCpeyeI9rXF1bNC8hsKaqJ37Du/oBvbIMUbgenogfjzmlCgtAzv4la2Eo8+3cvDHkKPnksCP8kt8JbyXECyXBOjPrpFjtYv9UUGfyhwqRTRNTmH5+5UAsDDFrYe+vonYiDwXel8TfK3AZhQGXcF598AVPVfB1RO5S/mt7faDS7cEfz14nUsYtaNAZcwH7gwm06VJUX5eWiZzlBGx4SVBkNzP0QhLM9AqNP889y9BmZ2JaGb3fJtCWL3MfzM23mbwSemcERkoV3v1rIH8mb6ZgGm0hyEbbtu/RegkLAgNO+YB6c0Os6Pv6OnK0So4xlNakchaWhl1eMfOf4Gx0miJv4o2XbmAmbSNYkybi3n8vz4="|]
[<Test>]
let ``An empty cookie header is parsed correctly`` () =
let uri = Uri "https://news.google.com/news?hl=en&ned=us&ie=UTF8&nolr=1&output=rss&q=FSharp&num=1000"
let cookieHeader = ""
let cookies = CookieHandling.getAllCookiesFromHeader cookieHeader uri
cookies |> should equal [| |]
[<Test>]
let ``Cookies in CookieContainer are returned`` () =
let cookieContainer = CookieContainer()
let someUri = Uri "http://nevermind.com"
cookieContainer.Add(someUri, Cookie("key", "value"))
let header = Map.empty
let cookies = CookieHandling.getCookiesAndManageCookieContainer someUri someUri header cookieContainer true false
cookies |> should haveCount cookieContainer.Count
[<Test>]
let ``Cookies in header are added to CookieContainer and returned`` () =
let cookieContainer = CookieContainer()
let someUri = Uri "http://nevermind.com"
cookieContainer.Add(someUri, Cookie("key1", "value1"))
let header = Map.ofList [(HttpResponseHeaders.SetCookie, ("key2=value2"))]
let cookies = CookieHandling.getCookiesAndManageCookieContainer someUri someUri header cookieContainer true false
cookieContainer.Count |> should equal 2
cookies |> should haveCount cookieContainer.Count
[<Test>]
let ``Cookies in header already existing in CookieContainer are added twice, and only new value is returned`` () =
let cookieContainer = CookieContainer()
let someUri = Uri "http://nevermind.com"
cookieContainer.Add(someUri, Cookie("key", "value1"))
let header = Map.ofList [(HttpResponseHeaders.SetCookie, ("key=value2"))]
let cookies = CookieHandling.getCookiesAndManageCookieContainer someUri someUri header cookieContainer true false
cookieContainer.Count |> should equal 2
cookies |> should haveCount 1
cookies.["key"] |> should equal "value2"
[<Test>]
let ``Cookies with unescaped JSON raise a CookieException (need to avoid cookieContainer parameter or ignoreCookieErrors`` () =
let uri = Uri "http://nevermind.com"
let header = Map.ofList [(HttpResponseHeaders.SetCookie, "hab={\"echanges\":1,\"notifications\":1,\"messages\":1}")]
let cookieContainer = CookieContainer()
(fun () -> CookieHandling.getCookiesAndManageCookieContainer uri uri header cookieContainer true false |> ignore) |> should throw typeof<CookieException>
[<Test>]
let ``Cookies with unescaped JSON is not added in cookieContainer but is still returned when ignoreCookieErrors parameter is true`` () =
let uri = Uri "http://nevermind.com"
let header = Map.ofList [(HttpResponseHeaders.SetCookie, "hab={\"echanges\":1,\"notifications\":1,\"messages\":1}")]
let cookieContainer = CookieContainer()
let cookies = CookieHandling.getCookiesAndManageCookieContainer uri uri header cookieContainer true true
cookieContainer.Count |> should equal 0
cookies |> should haveCount 1
[<Test>]
let ``Cookies is not added in cookieContainer but is still returned when addCookieInCookieContainer parameter is false (deducted from option cookieContainer passed in InnerRequest method)`` () =
let uri = Uri "http://nevermind.com"
let header = Map.ofList [(HttpResponseHeaders.SetCookie, "hab={\"echanges\":1,\"notifications\":1,\"messages\":1}")]
let cookieContainer = CookieContainer()
let cookies = CookieHandling.getCookiesAndManageCookieContainer uri uri header cookieContainer false false
cookieContainer.Count |> should equal 0
cookies |> should haveCount 1
[<Test>]
let ``Web request's timeout is used`` () =
use localServer = startHttpLocalServer()
let exc = Assert.Throws<WebException> (fun () ->
Http.Request(localServer.BaseAddress + "/200?sleep=1000", customizeHttpRequest = (fun req -> req.Timeout <- 1; req)) |> ignore)
exc.Status |> should equal WebExceptionStatus.Timeout
[<Test>]
let ``Timeout argument is used`` () =
use localServer = startHttpLocalServer()
let exc = Assert.Throws<WebException> (fun () ->
Http.Request(localServer.BaseAddress + "/200?sleep=1000", timeout = 1) |> ignore)
exc.Status |> should equal WebExceptionStatus.Timeout
[<Test>]
let ``Setting timeout in customizeHttpRequest overrides timeout argument`` () =
// Skip this test on Windows when running in CI because of flaky timeout behavior on some Windows CI agents.
let isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)
let inCi =
let env v = Environment.GetEnvironmentVariable v
[ "CI"; "GITHUB_ACTIONS"; "TF_BUILD"; "APPVEYOR"; "GITLAB_CI"; "JENKINS_URL" ]
|> List.exists (fun e -> not (String.IsNullOrEmpty (env e)))
if isWindows && inCi then
Assert.Ignore("Skipping test on Windows in CI")
else
use localServer = startHttpLocalServer()
let response =
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
let testFormDataSizesInBytes = [
4000 // previous test size
20000 // previous test size
40000 // > 80k, reported by user @danyx23 on full-framework
100000 // > 200k, reported by user danyx23 on .net core
200000 // > 400k, just future-proofing
]
[<Test; TestCaseSource("testFormDataSizesInBytes")>]
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= localServer.BaseAddress + "/200", httpMethod="POST", body=body, timeout = 10000) |> Async.Ignore |> Async.StartAsTask :> _)
[<Test; TestCaseSource("testFormDataSizesInBytes")>]
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= localServer.BaseAddress + "/200", httpMethod="POST", body=body, timeout = 10000) |> Async.Ignore |> Async.StartAsTask :> _)
[<Test>]
let ``escaping of url parameters`` () =
let url = "https://graph.microsoft.com/beta/me/insights/shared"
let queryParams = [
"$select", "Property1,Property2/SubProperty,*"
"$filter", "Subject eq 'A? & #B = C/D'"
"$top", "10"
"$expand", "ExtendedProperties($filter=PropertyId eq 'String {36FF76DC-215F-4246-9544-DAB709259CE8} Name Some/Property2.0')"
"$orderby", "Property2 desc"
]
Http.AppendQueryToUrl(url, queryParams)
|> should equal "https://graph.microsoft.com/beta/me/insights/shared?%24select=Property1%2CProperty2%2FSubProperty%2C%2A&%24filter=Subject%20eq%20%27A%3F%20%26%20%23B%20%3D%20C%2FD%27&%24top=10&%24expand=ExtendedProperties%28%24filter%3DPropertyId%20eq%20%27String%20%7B36FF76DC-215F-4246-9544-DAB709259CE8%7D%20Name%20Some%2FProperty2.0%27%29&%24orderby=Property2%20desc"
[<Test>]
let ``escaping of reserve characters in query`` () =
let url = "http://nevermind.com"
let queryParams = [
"key!", "v@lue1"
"key#", "value2&(value:/?#[]@*+,;=)"
]
Http.AppendQueryToUrl(url, queryParams)
|> should equal "http://nevermind.com?key%21=v%40lue1&key%23=value2%26%28value%3A%2F%3F%23%5B%5D%40%2A%2B%2C%3B%3D%29"
[<Test>]
let ``correct multipart content format`` () =
let numFiles = 2
let boundary = "**"
let content = "Text file content"
let multiPartItem (content: string) name = MultipartItem(name, name, (new MemoryStream(Encoding.UTF8.GetBytes(content))) :> Stream)
let multiparts = seq {for i in [0..numFiles] -> multiPartItem content (i.ToString()) }
let combinedStream = HttpHelpers.writeMultipart boundary multiparts Encoding.UTF8
use ms = new MemoryStream()
combinedStream.CopyTo(ms)
let str = Encoding.UTF8.GetString(ms.ToArray())
Console.WriteLine(str)
let singleMultipartFormat file = sprintf "--%s\r\nContent-Disposition: form-data; name=\"%i\"; filename=\"%i\"\r\nContent-Type: application/octet-stream\r\n\r\n%s\r\n" boundary file file content
// No need extra newline /r/n before closing delimiter
let finalFormat = [sprintf "--%s--" boundary] |> Seq.append (seq {for i in [0..numFiles] -> singleMultipartFormat i }) |> String.concat ""
str |> should equal finalFormat
[<Test>]
let ``CombinedStream has length with Some length`` () =
use combinedStream = new HttpHelpers.CombinedStream(Some 10L, [])
combinedStream.Length |> should equal 10L
[<Test>]
let ``CombinedStream can seek with Some length`` () =
use combinedStream = new HttpHelpers.CombinedStream(Some 10L, [])
combinedStream.CanSeek |> should equal true
[<Test>]
let ``CombinedStream length throws with None length`` () =
use combinedStream = new HttpHelpers.CombinedStream(None, [])
(fun () -> combinedStream.Length |> ignore) |> should throw typeof<NotSupportedException>
[<Test>]
let ``CombinedStream cannot seek with None length`` () =
use combinedStream = new HttpHelpers.CombinedStream(None, [])
combinedStream.CanSeek |> should equal false
type nonSeekableStream (b: byte[]) =
inherit MemoryStream(b)
override _.Length with get():Int64 = failwith "Im not seekable"
override _.CanSeek with get() = false
[<Test>]
let ``Non-seekable streams create non-seekable CombinedStream`` () =
use nonSeekms = new nonSeekableStream(Array.zeroCreate 10)
let multiparts = [MultipartItem("","", nonSeekms)]
let combinedStream = HttpHelpers.writeMultipart "-" multiparts Encoding.UTF8
(fun () -> combinedStream.Length |> ignore) |> should throw typeof<NotSupportedException>
combinedStream.CanSeek |> should equal false
[<Test>]
let ``Seekable streams create Seekable CombinedStream`` () =
let byteLen = 10L
let result = byteLen + 108L // As no extra /r/n, 2 bytes removed, 108 is headers
use ms = new MemoryStream(Array.zeroCreate (int byteLen))
let multiparts = [MultipartItem("","", ms)]
let combinedStream = HttpHelpers.writeMultipart "-" multiparts Encoding.UTF8
combinedStream.Length |> should equal result
combinedStream.CanSeek |> should equal true
#nowarn "44" // Use of deprecated HttpWebRequest
[<Test>]
let ``HttpWebRequest length is set with seekable streams`` () =
use ms = new MemoryStream(Array.zeroCreate 10)
let wr = HttpWebRequest.Create("http://x") :?> HttpWebRequest
wr.Method <- "POST"
HttpHelpers.writeBody wr ms |> Async.RunSynchronously
wr.ContentLength |> should equal 10
[<Test>]
let ``HttpWebRequest length is not set with non-seekable streams`` () =
use nonSeekms = new nonSeekableStream(Array.zeroCreate 10)
let wr = HttpWebRequest.Create("http://x") :?> HttpWebRequest
wr.Method <- "POST"
HttpHelpers.writeBody wr nonSeekms |> Async.RunSynchronously
wr.ContentLength |> should equal 0
// --------------------------------------------------------------------------------------
// Tests for HttpContentTypes module
[<Test>]
let ``HttpContentTypes.Any has correct value``() =
HttpContentTypes.Any |> should equal "*/*"
[<Test>]
let ``HttpContentTypes.Text has correct value``() =
HttpContentTypes.Text |> should equal "text/plain"
[<Test>]
let ``HttpContentTypes.Binary has correct value``() =
HttpContentTypes.Binary |> should equal "application/octet-stream"
[<Test>]
let ``HttpContentTypes.Zip has correct value``() =
HttpContentTypes.Zip |> should equal "application/zip"
[<Test>]
let ``HttpContentTypes.GZip has correct value``() =
HttpContentTypes.GZip |> should equal "application/gzip"
[<Test>]
let ``HttpContentTypes.Json has correct value``() =
HttpContentTypes.Json |> should equal "application/json"
[<Test>]
let ``HttpContentTypes.Xml has correct value``() =
HttpContentTypes.Xml |> should equal "application/xml"
[<Test>]
let ``HttpContentTypes.JavaScript has correct value``() =
HttpContentTypes.JavaScript |> should equal "application/javascript"
[<Test>]
let ``HttpContentTypes.JsonRpc has correct value``() =
HttpContentTypes.JsonRpc |> should equal "application/json-rpc"
[<Test>]
let ``HttpContentTypes.FormValues has correct value``() =
HttpContentTypes.FormValues |> should equal "application/x-www-form-urlencoded"
[<Test>]
let ``HttpContentTypes constants are used in Http text detection logic``() =
// Test that these constants work as expected in the actual HTTP library logic
// by checking if they would be detected as text content types
let textTypes = [
HttpContentTypes.Text
HttpContentTypes.Json
HttpContentTypes.Xml
HttpContentTypes.JavaScript
HttpContentTypes.JsonRpc
]
// These should all be considered text-based content types
textTypes |> List.iter (fun ct ->
(ct.StartsWith("text/") || ct.Contains("json") || ct.Contains("xml") || ct.Contains("javascript"))
|> should equal true)