diff --git a/GenHTTP.sln b/GenHTTP.sln index d685efac..c8cdae7c 100644 --- a/GenHTTP.sln +++ b/GenHTTP.sln @@ -24,8 +24,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Modules.Scriban", " EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{A7930BE4-0549-4197-B139-B1A73E74B464}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Testing.Acceptance", "Testing\GenHTTP.Testing.Acceptance.csproj", "{3B00D7C4-B8E9-4163-9E4C-67044BB26B34}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Modules.Webservices", "Modules\Webservices\GenHTTP.Modules.Webservices.csproj", "{3096F48C-60D7-4471-A7E4-315B6227C351}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Modules.Razor", "Modules\Razor\GenHTTP.Modules.Razor.csproj", "{9022F8C2-B465-40FD-BC59-428C034C4470}" @@ -102,6 +100,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Modules.Functional" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Modules.Protobuf", "Modules\Protobuf\GenHTTP.Modules.Protobuf.csproj", "{0D9957DE-8FE2-44A1-B9D7-EADD1DBDBD6E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Testing.Acceptance", "Testing\Acceptance\GenHTTP.Testing.Acceptance.csproj", "{C5067243-AFBA-4A17-BD61-DDB503367EA3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Testing", "Testing\Testing\GenHTTP.Testing.csproj", "{FC7F7D69-5ED0-4D3B-B201-EDBCEC71B8DB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -120,10 +122,6 @@ Global {7FA5BD2F-C093-4F30-ACD9-A870C7A0DAF2}.Debug|Any CPU.Build.0 = Debug|Any CPU {7FA5BD2F-C093-4F30-ACD9-A870C7A0DAF2}.Release|Any CPU.ActiveCfg = Release|Any CPU {7FA5BD2F-C093-4F30-ACD9-A870C7A0DAF2}.Release|Any CPU.Build.0 = Release|Any CPU - {3B00D7C4-B8E9-4163-9E4C-67044BB26B34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3B00D7C4-B8E9-4163-9E4C-67044BB26B34}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3B00D7C4-B8E9-4163-9E4C-67044BB26B34}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3B00D7C4-B8E9-4163-9E4C-67044BB26B34}.Release|Any CPU.Build.0 = Release|Any CPU {3096F48C-60D7-4471-A7E4-315B6227C351}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3096F48C-60D7-4471-A7E4-315B6227C351}.Debug|Any CPU.Build.0 = Debug|Any CPU {3096F48C-60D7-4471-A7E4-315B6227C351}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -252,6 +250,14 @@ Global {0D9957DE-8FE2-44A1-B9D7-EADD1DBDBD6E}.Debug|Any CPU.Build.0 = Debug|Any CPU {0D9957DE-8FE2-44A1-B9D7-EADD1DBDBD6E}.Release|Any CPU.ActiveCfg = Release|Any CPU {0D9957DE-8FE2-44A1-B9D7-EADD1DBDBD6E}.Release|Any CPU.Build.0 = Release|Any CPU + {C5067243-AFBA-4A17-BD61-DDB503367EA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5067243-AFBA-4A17-BD61-DDB503367EA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5067243-AFBA-4A17-BD61-DDB503367EA3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5067243-AFBA-4A17-BD61-DDB503367EA3}.Release|Any CPU.Build.0 = Release|Any CPU + {FC7F7D69-5ED0-4D3B-B201-EDBCEC71B8DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC7F7D69-5ED0-4D3B-B201-EDBCEC71B8DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC7F7D69-5ED0-4D3B-B201-EDBCEC71B8DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC7F7D69-5ED0-4D3B-B201-EDBCEC71B8DB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -260,7 +266,6 @@ Global {59E1C489-E9E9-4374-8CB9-A2005722152D} = {AFBFE61E-0C33-42F6-9370-9F5088EB8633} {C2D2B8D0-CA23-4790-8C71-B245A0AB81FB} = {06A861A6-D030-4129-9F8F-4EED698D00A6} {7FA5BD2F-C093-4F30-ACD9-A870C7A0DAF2} = {23B23225-275E-4F52-8B29-6F44C85B6ACE} - {3B00D7C4-B8E9-4163-9E4C-67044BB26B34} = {A7930BE4-0549-4197-B139-B1A73E74B464} {3096F48C-60D7-4471-A7E4-315B6227C351} = {23B23225-275E-4F52-8B29-6F44C85B6ACE} {9022F8C2-B465-40FD-BC59-428C034C4470} = {23B23225-275E-4F52-8B29-6F44C85B6ACE} {9BB6732E-BFE1-4ABE-B8A6-4522DF72EDDD} = {23B23225-275E-4F52-8B29-6F44C85B6ACE} @@ -295,9 +300,11 @@ Global {69D6CA16-1332-41B6-8994-6AD943915F25} = {23B23225-275E-4F52-8B29-6F44C85B6ACE} {2A2BCF94-BBDF-49A7-8B87-931E665507F0} = {23B23225-275E-4F52-8B29-6F44C85B6ACE} {0D9957DE-8FE2-44A1-B9D7-EADD1DBDBD6E} = {23B23225-275E-4F52-8B29-6F44C85B6ACE} + {C5067243-AFBA-4A17-BD61-DDB503367EA3} = {A7930BE4-0549-4197-B139-B1A73E74B464} + {FC7F7D69-5ED0-4D3B-B201-EDBCEC71B8DB} = {A7930BE4-0549-4197-B139-B1A73E74B464} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - LessCompiler = 2603124e-1287-4d61-9540-6ac3efad4eb9 SolutionGuid = {9C67B3AF-0BF6-4E21-8C39-3F74CFCF9632} + LessCompiler = 2603124e-1287-4d61-9540-6ac3efad4eb9 EndGlobalSection EndGlobal diff --git a/Testing/AssertX.cs b/Testing/Acceptance/AssertX.cs similarity index 97% rename from Testing/AssertX.cs rename to Testing/Acceptance/AssertX.cs index cd444ec7..c001eaee 100644 --- a/Testing/AssertX.cs +++ b/Testing/Acceptance/AssertX.cs @@ -1,80 +1,80 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance -{ - - /// - /// Compatibility assertions for XUnit. - /// - public static class AssertX - { - - public static void Contains(string searchFor, string? content) => Assert.IsTrue(content?.Contains(searchFor) ?? false); - - public static void DoesNotContain(string searchFor, string? content) => Assert.IsFalse(content?.Contains(searchFor) ?? false); - - public static void StartsWith(string searchFor, string? content) => Assert.IsTrue(content?.StartsWith(searchFor) ?? false); - - public static void EndsWith(string searchFor, string? content) => Assert.IsTrue(content?.EndsWith(searchFor) ?? false); - - public static void Single(IEnumerable collection) => Assert.IsTrue(collection.Count() == 1); - - public static void Empty(IEnumerable collection) => Assert.IsFalse(collection.Any()); - - public static void Contains(T value, IEnumerable collection) => Assert.IsTrue(collection.Contains(value)); - - public static void DoesNotContain(T value, IEnumerable collection) => Assert.IsFalse(collection.Contains(value)); - - public static void IsNullOrEmpty(string? value) => Assert.IsTrue(string.IsNullOrEmpty(value)); - - /// - /// Raises an assertion expection if the response does not have the expected status code - /// and additionally prints information about the response to be able to further debug - /// issues in workflow runs. - /// - /// The response to be evaluated - /// The expected status code to check for - public static async Task AssertStatusAsync(this HttpResponseMessage response, HttpStatusCode expectedStatus) - { - if (response.StatusCode != expectedStatus) - { - var builder = new StringBuilder(); - - builder.AppendLine($"Response returned with status '{response.StatusCode}', expected '{expectedStatus}'."); - builder.AppendLine(); - - builder.AppendLine("Headers"); - builder.AppendLine(); - - foreach (var header in response.Headers) - { - builder.AppendLine($" {header.Key} = {string.Join(',', header.Value.ToList())}"); - } - - builder.AppendLine(); - - var content = await response.Content.ReadAsStringAsync(); - - if (!string.IsNullOrEmpty(content)) - { - builder.AppendLine("Body"); - builder.AppendLine(); - - builder.AppendLine(content); - } - - throw new AssertFailedException(builder.ToString()); - } - } - - } - -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance +{ + + /// + /// Compatibility assertions for XUnit. + /// + public static class AssertX + { + + public static void Contains(string searchFor, string? content) => Assert.IsTrue(content?.Contains(searchFor) ?? false); + + public static void DoesNotContain(string searchFor, string? content) => Assert.IsFalse(content?.Contains(searchFor) ?? false); + + public static void StartsWith(string searchFor, string? content) => Assert.IsTrue(content?.StartsWith(searchFor) ?? false); + + public static void EndsWith(string searchFor, string? content) => Assert.IsTrue(content?.EndsWith(searchFor) ?? false); + + public static void Single(IEnumerable collection) => Assert.IsTrue(collection.Count() == 1); + + public static void Empty(IEnumerable collection) => Assert.IsFalse(collection.Any()); + + public static void Contains(T value, IEnumerable collection) => Assert.IsTrue(collection.Contains(value)); + + public static void DoesNotContain(T value, IEnumerable collection) => Assert.IsFalse(collection.Contains(value)); + + public static void IsNullOrEmpty(string? value) => Assert.IsTrue(string.IsNullOrEmpty(value)); + + /// + /// Raises an assertion expection if the response does not have the expected status code + /// and additionally prints information about the response to be able to further debug + /// issues in workflow runs. + /// + /// The response to be evaluated + /// The expected status code to check for + public static async Task AssertStatusAsync(this HttpResponseMessage response, HttpStatusCode expectedStatus) + { + if (response.StatusCode != expectedStatus) + { + var builder = new StringBuilder(); + + builder.AppendLine($"Response returned with status '{response.StatusCode}', expected '{expectedStatus}'."); + builder.AppendLine(); + + builder.AppendLine("Headers"); + builder.AppendLine(); + + foreach (var header in response.Headers) + { + builder.AppendLine($" {header.Key} = {string.Join(',', header.Value.ToList())}"); + } + + builder.AppendLine(); + + var content = await response.Content.ReadAsStringAsync(); + + if (!string.IsNullOrEmpty(content)) + { + builder.AppendLine("Body"); + builder.AppendLine(); + + builder.AppendLine(content); + } + + throw new AssertFailedException(builder.ToString()); + } + } + + } + +} diff --git a/Testing/Engine/BasicTests.cs b/Testing/Acceptance/Engine/BasicTests.cs similarity index 65% rename from Testing/Engine/BasicTests.cs rename to Testing/Acceptance/Engine/BasicTests.cs index 6b40fb3a..58d5bfe1 100644 --- a/Testing/Engine/BasicTests.cs +++ b/Testing/Acceptance/Engine/BasicTests.cs @@ -1,80 +1,82 @@ -using System; -using System.Net; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public sealed class BasicTests - { - - [TestMethod] - public async Task TestBuilder() - { - using var runner = new TestRunner(); - - runner.Host.RequestMemoryLimit(128) - .TransferBufferSize(128) - .RequestReadTimeout(TimeSpan.FromSeconds(2)) - .Backlog(1); - - runner.Start(); - - using var response = await runner.GetResponse(); - - await response.AssertStatusAsync(HttpStatusCode.NotFound); - } - - [TestMethod] - public async Task TestLegacyHttp() - { - using var runner = TestRunner.Run(); - - using var client = TestRunner.GetClient(version: new Version(1, 0)); - - using var response = await runner.GetResponse(); - - await response.AssertStatusAsync(HttpStatusCode.NotFound); - } - - [TestMethod] - public async Task TestConnectionClose() - { - using var runner = TestRunner.Run(); - - var request = runner.GetRequest(); - request.Headers.Add("Connection", "close"); - - using var response = await runner.GetResponse(request); - - await response.AssertStatusAsync(HttpStatusCode.NotFound); - Assert.IsTrue(response.Headers.Connection.Contains("Close")); - } - - [TestMethod] - public async Task TestEmptyQuery() - { - using var runner = TestRunner.Run(); - - using var response = await runner.GetResponse("/?"); - - await response.AssertStatusAsync(HttpStatusCode.NotFound); - } - - - [TestMethod] - public async Task TestKeepalive() - { - using var runner = TestRunner.Run(); - - using var response = await runner.GetResponse(); - - Assert.IsTrue(response.Headers.Connection.Contains("Keep-Alive")); - } - - } - -} +using System; +using System.Net; +using System.Threading.Tasks; + +using GenHTTP.Modules.Layouting; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public sealed class BasicTests + { + + [TestMethod] + public async Task TestBuilder() + { + using var runner = new TestHost(Layout.Create()); + + runner.Host.RequestMemoryLimit(128) + .TransferBufferSize(128) + .RequestReadTimeout(TimeSpan.FromSeconds(2)) + .Backlog(1); + + runner.Start(); + + using var response = await runner.GetResponseAsync(); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + [TestMethod] + public async Task TestLegacyHttp() + { + using var runner = TestHost.Run(Layout.Create()); + + using var client = TestHost.GetClient(protocolVersion: new Version(1, 0)); + + using var response = await runner.GetResponseAsync(); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + [TestMethod] + public async Task TestConnectionClose() + { + using var runner = TestHost.Run(Layout.Create()); + + var request = runner.GetRequest(); + request.Headers.Add("Connection", "close"); + + using var response = await runner.GetResponseAsync(request); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + Assert.IsTrue(response.Headers.Connection.Contains("Close")); + } + + [TestMethod] + public async Task TestEmptyQuery() + { + using var runner = TestHost.Run(Layout.Create()); + + using var response = await runner.GetResponseAsync("/?"); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + + [TestMethod] + public async Task TestKeepalive() + { + using var runner = TestHost.Run(Layout.Create()); + + using var response = await runner.GetResponseAsync(); + + Assert.IsTrue(response.Headers.Connection.Contains("Keep-Alive")); + } + + } + +} diff --git a/Testing/Engine/ChecksumTests.cs b/Testing/Acceptance/Engine/ChecksumTests.cs similarity index 59% rename from Testing/Engine/ChecksumTests.cs rename to Testing/Acceptance/Engine/ChecksumTests.cs index bb650751..c81c919b 100644 --- a/Testing/Engine/ChecksumTests.cs +++ b/Testing/Acceptance/Engine/ChecksumTests.cs @@ -1,40 +1,41 @@ -using GenHTTP.Modules.IO; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.Threading.Tasks; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public class ChecksumTests - { - - [TestMethod] - public async Task TestSameErrorSameChecksum() - { - using var runner = TestRunner.Run(); - - using var resp1 = await runner.GetResponse(); - using var resp2 = await runner.GetResponse(); - - Assert.IsNotNull(resp1.GetETag()); - - Assert.AreEqual(resp1.GetETag(), resp2.GetETag()); - } - - [TestMethod] - public async Task TestSameContentSameChecksum() - { - using var runner = TestRunner.Run(Content.From(Resource.FromString("Hello World!"))); - - using var resp1 = await runner.GetResponse(); - using var resp2 = await runner.GetResponse(); - - Assert.IsNotNull(resp1.GetETag()); - - Assert.AreEqual(resp1.GetETag(), resp2.GetETag()); - } - - } - -} +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Layouting; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Threading.Tasks; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public class ChecksumTests + { + + [TestMethod] + public async Task TestSameErrorSameChecksum() + { + using var runner = TestHost.Run(Layout.Create()); + + using var resp1 = await runner.GetResponseAsync(); + using var resp2 = await runner.GetResponseAsync(); + + Assert.IsNotNull(resp1.GetETag()); + + Assert.AreEqual(resp1.GetETag(), resp2.GetETag()); + } + + [TestMethod] + public async Task TestSameContentSameChecksum() + { + using var runner = TestHost.Run(Content.From(Resource.FromString("Hello World!"))); + + using var resp1 = await runner.GetResponseAsync(); + using var resp2 = await runner.GetResponseAsync(); + + Assert.IsNotNull(resp1.GetETag()); + + Assert.AreEqual(resp1.GetETag(), resp2.GetETag()); + } + + } + +} diff --git a/Testing/Engine/CompanionTests.cs b/Testing/Acceptance/Engine/CompanionTests.cs similarity index 84% rename from Testing/Engine/CompanionTests.cs rename to Testing/Acceptance/Engine/CompanionTests.cs index 9796ec0f..64206bf2 100644 --- a/Testing/Engine/CompanionTests.cs +++ b/Testing/Acceptance/Engine/CompanionTests.cs @@ -1,70 +1,71 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Api.Infrastructure; -using GenHTTP.Api.Protocol; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public sealed class CompanionTests - { - - private class CustomCompanion : IServerCompanion - { - - public bool Called { get; private set; } - - public void OnRequestHandled(IRequest request, IResponse response) - { - Called = true; - } - - public void OnServerError(ServerErrorScope scope, Exception error) - { - Called = true; - } - - } - - /// - /// As a developer, I want to configure the server to easily log to the console. - /// - [TestMethod] - public async Task TestConsole() - { - using var runner = new TestRunner(); - - runner.Host.Console().Start(); - - using var __ = await runner.GetResponse(); - } - - /// - /// As a developer, I want to add custom companions to get notified by server actions. - /// - [TestMethod] - public async Task TestCustom() - { - using var runner = new TestRunner(); - - var companion = new CustomCompanion(); - - runner.Host.Companion(companion).Start(); - - using var __ = await runner.GetResponse(); - - // the companion is called _after_ the response has been sent - // bad hack, reconsider - Thread.Sleep(50); - - Assert.IsTrue(companion.Called); - } - - } - -} +using System; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Api.Infrastructure; +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Layouting; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public sealed class CompanionTests + { + + private class CustomCompanion : IServerCompanion + { + + public bool Called { get; private set; } + + public void OnRequestHandled(IRequest request, IResponse response) + { + Called = true; + } + + public void OnServerError(ServerErrorScope scope, Exception error) + { + Called = true; + } + + } + + /// + /// As a developer, I want to configure the server to easily log to the console. + /// + [TestMethod] + public async Task TestConsole() + { + using var runner = new TestHost(Layout.Create()); + + runner.Host.Console().Start(); + + using var __ = await runner.GetResponseAsync(); + } + + /// + /// As a developer, I want to add custom companions to get notified by server actions. + /// + [TestMethod] + public async Task TestCustom() + { + using var runner = new TestHost(Layout.Create()); + + var companion = new CustomCompanion(); + + runner.Host.Companion(companion).Start(); + + using var __ = await runner.GetResponseAsync(); + + // the companion is called _after_ the response has been sent + // bad hack, reconsider + Thread.Sleep(50); + + Assert.IsTrue(companion.Called); + } + + } + +} diff --git a/Testing/Engine/CompressionTests.cs b/Testing/Acceptance/Engine/CompressionTests.cs similarity index 79% rename from Testing/Engine/CompressionTests.cs rename to Testing/Acceptance/Engine/CompressionTests.cs index c930a60c..caaa9196 100644 --- a/Testing/Engine/CompressionTests.cs +++ b/Testing/Acceptance/Engine/CompressionTests.cs @@ -1,185 +1,185 @@ -using System.IO.Compression; -using System.Linq; -using System.Threading.Tasks; - -using GenHTTP.Api.Content.IO; -using GenHTTP.Api.Infrastructure; -using GenHTTP.Api.Protocol; -using GenHTTP.Modules.Basics; -using GenHTTP.Modules.Compression; -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Layouting; -using GenHTTP.Testing.Acceptance.Utilities; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public sealed class CompressionTests - { - - private class CustomAlgorithm : ICompressionAlgorithm - { - - public string Name => "custom"; - - public Priority Priority => Priority.High; - - public IResponseContent Compress(IResponseContent content, CompressionLevel level) - { - return content; - } - - } - - private class CustomAlgorithmBuilder : IBuilder - { - - public ICompressionAlgorithm Build() - { - return new CustomAlgorithm(); - } - - } - - /// - /// As a developer, I expect responses to be compressed out of the box. - /// - [TestMethod] - public async Task TestCompression() - { - using var runner = TestRunner.Run(); - - var request = runner.GetRequest(); - request.Headers.Add("Accept-Encoding", "gzip, br"); - - using var response = await runner.GetResponse(request); - - Assert.AreEqual("br", response.Content.Headers.ContentEncoding.First()); - } - - /// - /// As a browser, I expect only supported compression algorithms to be used - /// to generate my response. - /// - [TestMethod] - public async Task TestSpecficAlgorithm() - { - using var runner = TestRunner.Run(); - - var request = runner.GetRequest(); - request.Headers.Add("Accept-Encoding", "gzip"); - - using var response = await runner.GetResponse(request); - - Assert.AreEqual("gzip", response.Content.Headers.ContentEncoding.First()); - } - - /// - /// As a developer, I want to be able to disable compression. - /// - [TestMethod] - public async Task TestCompressionDisabled() - { - using var runner = TestRunner.Run(false); - - using var response = await runner.GetResponse(); - - Assert.IsFalse(response.Content.Headers.ContentEncoding.Any()); - } - - /// - /// As a developer, I want to be able to add custom compression algorithms. - /// - [TestMethod] - public async Task TestCustomCompression() - { - using var runner = new TestRunner(); - - runner.Host.Compression(CompressedContent.Default().Add(new CustomAlgorithm()).Level(CompressionLevel.Optimal)).Start(); - - var request = runner.GetRequest(); - request.Headers.Add("Accept-Encoding", "custom"); - - using var response = await runner.GetResponse(request); - - Assert.AreEqual("custom", response.Content.Headers.ContentEncoding.First()); - } - - /// - /// As a developer, I want already compressed content not to be compressed again. - /// - [TestMethod] - public async Task TestNoAdditionalCompression() - { - var image = Resource.FromString("Image!").Type(ContentType.ImageJpg); - - using var runner = TestRunner.Run(Layout.Create().Add("uncompressed", Content.From(image))); - - using var response = await runner.GetResponse("/uncompressed"); - - Assert.IsFalse(response.Content.Headers.ContentEncoding.Any()); - } - - [TestMethod] - public async Task TestVariyHeaderAdded() - { - using var runner = TestRunner.Run(); - - var request = runner.GetRequest(); - request.Headers.Add("Accept-Encoding", "gzip"); - - using var response = await runner.GetResponse(request); - - Assert.AreEqual("Accept-Encoding", response.GetHeader("Vary")); - } - - [TestMethod] - public async Task TestVariyHeaderExtendedAdded() - { - var handler = new FunctionalHandler(responseProvider: (r) => - { - return r.Respond() - .Header("Vary", "Host") - .Content(Resource.FromString("Hello World").Build()) - .Type(ContentType.TextHtml) - .Build(); - }); - - using var runner = TestRunner.Run(handler.Wrap()); - - var request = runner.GetRequest(); - request.Headers.Add("Accept-Encoding", "gzip"); - - using var response = await runner.GetResponse(request); - - Assert.IsTrue(response.Headers.Vary.Contains("Host")); - Assert.IsTrue(response.Headers.Vary.Contains("Accept-Encoding")); - } - - [TestMethod] - public async Task TestContentType() - { - var handler = new FunctionalHandler(responseProvider: (r) => - { - return r.Respond() - .Content(Resource.FromString("Hello World").Build()) - .Type("application/json; charset=utf-8") - .Build(); - }); - - using var runner = TestRunner.Run(handler.Wrap()); - - var request = runner.GetRequest(); - request.Headers.Add("Accept-Encoding", "gzip, deflate, br"); - - using var response = await runner.GetResponse(request); - - Assert.AreEqual("br", response.Content.Headers.ContentEncoding.First()); - } - - } - -} +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; + +using GenHTTP.Api.Content.IO; +using GenHTTP.Api.Infrastructure; +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Basics; +using GenHTTP.Modules.Compression; +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Layouting; +using GenHTTP.Testing.Acceptance.Utilities; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public sealed class CompressionTests + { + + private class CustomAlgorithm : ICompressionAlgorithm + { + + public string Name => "custom"; + + public Priority Priority => Priority.High; + + public IResponseContent Compress(IResponseContent content, CompressionLevel level) + { + return content; + } + + } + + private class CustomAlgorithmBuilder : IBuilder + { + + public ICompressionAlgorithm Build() + { + return new CustomAlgorithm(); + } + + } + + /// + /// As a developer, I expect responses to be compressed out of the box. + /// + [TestMethod] + public async Task TestCompression() + { + using var runner = TestHost.Run(Layout.Create()); + + var request = runner.GetRequest(); + request.Headers.Add("Accept-Encoding", "gzip, br"); + + using var response = await runner.GetResponseAsync(request); + + Assert.AreEqual("br", response.Content.Headers.ContentEncoding.First()); + } + + /// + /// As a browser, I expect only supported compression algorithms to be used + /// to generate my response. + /// + [TestMethod] + public async Task TestSpecficAlgorithm() + { + using var runner = TestHost.Run(Layout.Create()); + + var request = runner.GetRequest(); + request.Headers.Add("Accept-Encoding", "gzip"); + + using var response = await runner.GetResponseAsync(request); + + Assert.AreEqual("gzip", response.Content.Headers.ContentEncoding.First()); + } + + /// + /// As a developer, I want to be able to disable compression. + /// + [TestMethod] + public async Task TestCompressionDisabled() + { + using var runner = TestHost.Run(Layout.Create(), false); + + using var response = await runner.GetResponseAsync(); + + Assert.IsFalse(response.Content.Headers.ContentEncoding.Any()); + } + + /// + /// As a developer, I want to be able to add custom compression algorithms. + /// + [TestMethod] + public async Task TestCustomCompression() + { + using var runner = new TestHost(Layout.Create()); + + runner.Host.Compression(CompressedContent.Default().Add(new CustomAlgorithm()).Level(CompressionLevel.Optimal)).Start(); + + var request = runner.GetRequest(); + request.Headers.Add("Accept-Encoding", "custom"); + + using var response = await runner.GetResponseAsync(request); + + Assert.AreEqual("custom", response.Content.Headers.ContentEncoding.First()); + } + + /// + /// As a developer, I want already compressed content not to be compressed again. + /// + [TestMethod] + public async Task TestNoAdditionalCompression() + { + var image = Resource.FromString("Image!").Type(ContentType.ImageJpg); + + using var runner = TestHost.Run(Layout.Create().Add("uncompressed", Content.From(image))); + + using var response = await runner.GetResponseAsync("/uncompressed"); + + Assert.IsFalse(response.Content.Headers.ContentEncoding.Any()); + } + + [TestMethod] + public async Task TestVariyHeaderAdded() + { + using var runner = TestHost.Run(Layout.Create()); + + var request = runner.GetRequest(); + request.Headers.Add("Accept-Encoding", "gzip"); + + using var response = await runner.GetResponseAsync(request); + + Assert.AreEqual("Accept-Encoding", response.GetHeader("Vary")); + } + + [TestMethod] + public async Task TestVariyHeaderExtendedAdded() + { + var handler = new FunctionalHandler(responseProvider: (r) => + { + return r.Respond() + .Header("Vary", "Host") + .Content(Resource.FromString("Hello World").Build()) + .Type(ContentType.TextHtml) + .Build(); + }); + + using var runner = TestHost.Run(handler.Wrap()); + + var request = runner.GetRequest(); + request.Headers.Add("Accept-Encoding", "gzip"); + + using var response = await runner.GetResponseAsync(request); + + Assert.IsTrue(response.Headers.Vary.Contains("Host")); + Assert.IsTrue(response.Headers.Vary.Contains("Accept-Encoding")); + } + + [TestMethod] + public async Task TestContentType() + { + var handler = new FunctionalHandler(responseProvider: (r) => + { + return r.Respond() + .Content(Resource.FromString("Hello World").Build()) + .Type("application/json; charset=utf-8") + .Build(); + }); + + using var runner = TestHost.Run(handler.Wrap()); + + var request = runner.GetRequest(); + request.Headers.Add("Accept-Encoding", "gzip, deflate, br"); + + using var response = await runner.GetResponseAsync(request); + + Assert.AreEqual("br", response.Content.Headers.ContentEncoding.First()); + } + + } + +} diff --git a/Testing/Engine/ContentTests.cs b/Testing/Acceptance/Engine/ContentTests.cs similarity index 81% rename from Testing/Engine/ContentTests.cs rename to Testing/Acceptance/Engine/ContentTests.cs index c6f9a44a..f64fabd9 100644 --- a/Testing/Engine/ContentTests.cs +++ b/Testing/Acceptance/Engine/ContentTests.cs @@ -1,125 +1,125 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Threading.Tasks; - -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.Basics; -using GenHTTP.Modules.Conversion.Providers.Json; -using GenHTTP.Modules.DirectoryBrowsing; -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Robots; -using GenHTTP.Modules.SinglePageApplications; -using GenHTTP.Modules.Sitemaps; - -using GenHTTP.Testing.Acceptance.Providers; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public sealed class ContentTests - { - - #region Supporting data structures - - public sealed class ContentPrinter : IConcern - { - - public IHandler Content { get; } - - public IHandler Parent { get; } - - public ContentPrinter(IHandler parent, Func contentFactory) - { - Parent = parent; - Content = contentFactory(this); - } - - public ValueTask PrepareAsync() => Content.PrepareAsync(); - - public IAsyncEnumerable GetContentAsync(IRequest request) => Content.GetContentAsync(request); - - public ValueTask HandleAsync(IRequest request) - { - var response = request.Respond() - .Content(new JsonContent(Content.GetContentAsync(request), new JsonSerializerOptions())) - .Type(ContentType.ApplicationJson) - .Build(); - - return new ValueTask(response); - } - - } - - public sealed class ContentPrinterBuilder : IConcernBuilder - { - - public IConcern Build(IHandler parent, Func contentFactory) - { - return new ContentPrinter(parent, contentFactory); - } - - } - - #endregion - - [TestMethod] - public async Task TestResources() - { - using var runner = TestRunner.Run(Resources.From(ResourceTree.FromAssembly("Resources")) - .Add(new ContentPrinterBuilder())); - - using var response = await runner.GetResponse(); - - Assert.IsTrue((await response.GetContent()).Contains("Error.html")); - } - - [TestMethod] - public async Task TestDirectoryListing() - { - using var runner = TestRunner.Run(Listing.From(ResourceTree.FromAssembly("Resources")) - .Add(new ContentPrinterBuilder())); - - using var response = await runner.GetResponse(); - - Assert.IsTrue((await response.GetContent()).Contains("Error.html")); - } - - [TestMethod] - public async Task TestSinglePageApplication() - { - using var runner = TestRunner.Run(SinglePageApplication.From(ResourceTree.FromAssembly("Resources")) - .Add(new ContentPrinterBuilder())); - - using var response = await runner.GetResponse(); - - Assert.IsTrue((await response.GetContent()).Contains("Error.html")); - } - - [TestMethod] - public async Task TestWebsite() - { - using var runner = TestRunner.Run(WebsiteTests.GetWebsite().Add(new ContentPrinterBuilder())); - - using var response = await runner.GetResponse(); - - var content = await response.GetContent(); - - AssertX.Contains("custom.js", content); - AssertX.Contains("custom.css", content); - - AssertX.Contains(Sitemap.FILE_NAME, content); - AssertX.Contains(BotInstructions.FILE_NAME, content); - - AssertX.Contains("some.txt", content); - AssertX.Contains("favicon.ico", content); - } - - } - -} +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Basics; +using GenHTTP.Modules.Conversion.Providers.Json; +using GenHTTP.Modules.DirectoryBrowsing; +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Robots; +using GenHTTP.Modules.SinglePageApplications; +using GenHTTP.Modules.Sitemaps; + +using GenHTTP.Testing.Acceptance.Providers; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public sealed class ContentTests + { + + #region Supporting data structures + + public sealed class ContentPrinter : IConcern + { + + public IHandler Content { get; } + + public IHandler Parent { get; } + + public ContentPrinter(IHandler parent, Func contentFactory) + { + Parent = parent; + Content = contentFactory(this); + } + + public ValueTask PrepareAsync() => Content.PrepareAsync(); + + public IAsyncEnumerable GetContentAsync(IRequest request) => Content.GetContentAsync(request); + + public ValueTask HandleAsync(IRequest request) + { + var response = request.Respond() + .Content(new JsonContent(Content.GetContentAsync(request), new JsonSerializerOptions())) + .Type(ContentType.ApplicationJson) + .Build(); + + return new ValueTask(response); + } + + } + + public sealed class ContentPrinterBuilder : IConcernBuilder + { + + public IConcern Build(IHandler parent, Func contentFactory) + { + return new ContentPrinter(parent, contentFactory); + } + + } + + #endregion + + [TestMethod] + public async Task TestResources() + { + using var runner = TestHost.Run(Resources.From(ResourceTree.FromAssembly("Resources")) + .Add(new ContentPrinterBuilder())); + + using var response = await runner.GetResponseAsync(); + + Assert.IsTrue((await response.GetContent()).Contains("Error.html")); + } + + [TestMethod] + public async Task TestDirectoryListing() + { + using var runner = TestHost.Run(Listing.From(ResourceTree.FromAssembly("Resources")) + .Add(new ContentPrinterBuilder())); + + using var response = await runner.GetResponseAsync(); + + Assert.IsTrue((await response.GetContent()).Contains("Error.html")); + } + + [TestMethod] + public async Task TestSinglePageApplication() + { + using var runner = TestHost.Run(SinglePageApplication.From(ResourceTree.FromAssembly("Resources")) + .Add(new ContentPrinterBuilder())); + + using var response = await runner.GetResponseAsync(); + + Assert.IsTrue((await response.GetContent()).Contains("Error.html")); + } + + [TestMethod] + public async Task TestWebsite() + { + using var runner = TestHost.Run(WebsiteTests.GetWebsite().Add(new ContentPrinterBuilder())); + + using var response = await runner.GetResponseAsync(); + + var content = await response.GetContent(); + + AssertX.Contains("custom.js", content); + AssertX.Contains("custom.css", content); + + AssertX.Contains(Sitemap.FILE_NAME, content); + AssertX.Contains(BotInstructions.FILE_NAME, content); + + AssertX.Contains("some.txt", content); + AssertX.Contains("favicon.ico", content); + } + + } + +} diff --git a/Testing/Engine/CookieTests.cs b/Testing/Acceptance/Engine/CookieTests.cs similarity index 86% rename from Testing/Engine/CookieTests.cs rename to Testing/Acceptance/Engine/CookieTests.cs index 262f9d6d..80c2f6a6 100644 --- a/Testing/Engine/CookieTests.cs +++ b/Testing/Acceptance/Engine/CookieTests.cs @@ -1,81 +1,81 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.Basics; -using GenHTTP.Modules.IO; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public sealed class CookieTests - { - - private class TestProvider : IHandler - { - - public ICookieCollection? Cookies { get; private set; } - - public ValueTask PrepareAsync() => ValueTask.CompletedTask; - - public IHandler Parent => throw new NotSupportedException(); - - public IAsyncEnumerable GetContentAsync(IRequest request) - { - throw new NotImplementedException(); - } - - public ValueTask HandleAsync(IRequest request) - { - Cookies = request.Cookies; - - return request.Respond() - .Cookie(new Cookie("TestCookie", "TestValue", 86400)) - .Content("I ❤ Cookies!") - .Type(ContentType.TextHtml) - .BuildTask(); - - } - - } - - /// - /// As a developer, I want to be able to set cookies to be accepted by the browser. - /// - [TestMethod] - public async Task TestCookiesCanBeReturned() - { - using var runner = TestRunner.Run(new TestProvider()); - - using var response = await runner.GetResponse(); - - Assert.AreEqual("TestCookie=TestValue; Max-Age=86400; Path=/", response.GetHeader("Set-Cookie")); - } - - /// - /// As a developer, I want to be able to read cookies from the client. - /// - [TestMethod] - public async Task TestCookiesCanBeRead() - { - var provider = new TestProvider(); - - using var runner = TestRunner.Run(provider); - - var request = runner.GetRequest(); - request.Headers.Add("Cookie", "1=2; 3=4"); - - using var _ = await runner.GetResponse(request); - - Assert.AreEqual("4", provider.Cookies?["3"].Value); - } - - } - -} +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Basics; +using GenHTTP.Modules.IO; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public sealed class CookieTests + { + + private class TestProvider : IHandler + { + + public ICookieCollection? Cookies { get; private set; } + + public ValueTask PrepareAsync() => ValueTask.CompletedTask; + + public IHandler Parent => throw new NotSupportedException(); + + public IAsyncEnumerable GetContentAsync(IRequest request) + { + throw new NotImplementedException(); + } + + public ValueTask HandleAsync(IRequest request) + { + Cookies = request.Cookies; + + return request.Respond() + .Cookie(new Cookie("TestCookie", "TestValue", 86400)) + .Content("I ❤ Cookies!") + .Type(ContentType.TextHtml) + .BuildTask(); + + } + + } + + /// + /// As a developer, I want to be able to set cookies to be accepted by the browser. + /// + [TestMethod] + public async Task TestCookiesCanBeReturned() + { + using var runner = TestHost.Run(new TestProvider().Wrap()); + + using var response = await runner.GetResponseAsync(); + + Assert.AreEqual("TestCookie=TestValue; Max-Age=86400; Path=/", response.GetHeader("Set-Cookie")); + } + + /// + /// As a developer, I want to be able to read cookies from the client. + /// + [TestMethod] + public async Task TestCookiesCanBeRead() + { + var provider = new TestProvider(); + + using var runner = TestHost.Run(provider.Wrap()); + + var request = runner.GetRequest(); + request.Headers.Add("Cookie", "1=2; 3=4"); + + using var _ = await runner.GetResponseAsync(request); + + Assert.AreEqual("4", provider.Cookies?["3"].Value); + } + + } + +} diff --git a/Testing/Engine/DeveloperModeTests.cs b/Testing/Acceptance/Engine/DeveloperModeTests.cs similarity index 86% rename from Testing/Engine/DeveloperModeTests.cs rename to Testing/Acceptance/Engine/DeveloperModeTests.cs index 04cde178..bd724bcf 100644 --- a/Testing/Engine/DeveloperModeTests.cs +++ b/Testing/Acceptance/Engine/DeveloperModeTests.cs @@ -1,74 +1,74 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.Layouting; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public sealed class DeveloperModeTests - { - - private class ThrowingProvider : IHandler - { - - public ValueTask PrepareAsync() => ValueTask.CompletedTask; - - public IHandler Parent => throw new NotImplementedException(); - - public IAsyncEnumerable GetContentAsync(IRequest request) - { - throw new NotImplementedException(); - } - - public ValueTask HandleAsync(IRequest request) - { - throw new InvalidOperationException("Nope!"); - } - - } - - /// - /// As a developer of a web project, I would like to see exceptions rendered - /// in the browser, so that I can trace an error more quickly - /// - [TestMethod] - public async Task TestExceptionsWithTrace() - { - using var runner = new TestRunner(); - - var router = Layout.Create().Index(new ThrowingProvider().Wrap()); - - runner.Host.Handler(router).Development().Start(); - - using var response = await runner.GetResponse(); - - Assert.IsTrue((await response.GetContent()).Contains("Exception")); - } - - /// - /// As a devops member, I do not want an web application to leak internal - /// implementation detail with exception messages - /// - [TestMethod] - public async Task TestExceptionsWithNoTrace() - { - var router = Layout.Create().Index(new ThrowingProvider().Wrap()); - - using var runner = TestRunner.Run(router); - - using var response = await runner.GetResponse(); - - Assert.IsFalse((await response.GetContent()).Contains("Exception")); - } - - } - -} +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Layouting; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public sealed class DeveloperModeTests + { + + private class ThrowingProvider : IHandler + { + + public ValueTask PrepareAsync() => ValueTask.CompletedTask; + + public IHandler Parent => throw new NotImplementedException(); + + public IAsyncEnumerable GetContentAsync(IRequest request) + { + throw new NotImplementedException(); + } + + public ValueTask HandleAsync(IRequest request) + { + throw new InvalidOperationException("Nope!"); + } + + } + + /// + /// As a developer of a web project, I would like to see exceptions rendered + /// in the browser, so that I can trace an error more quickly + /// + [TestMethod] + public async Task TestExceptionsWithTrace() + { + using var runner = new TestHost(Layout.Create()); + + var router = Layout.Create().Index(new ThrowingProvider().Wrap()); + + runner.Host.Handler(router).Development().Start(); + + using var response = await runner.GetResponseAsync(); + + Assert.IsTrue((await response.GetContent()).Contains("Exception")); + } + + /// + /// As a devops member, I do not want an web application to leak internal + /// implementation detail with exception messages + /// + [TestMethod] + public async Task TestExceptionsWithNoTrace() + { + var router = Layout.Create().Index(new ThrowingProvider().Wrap()); + + using var runner = TestHost.Run(router, development: false); + + using var response = await runner.GetResponseAsync(); + + Assert.IsFalse((await response.GetContent()).Contains("Exception")); + } + + } + +} diff --git a/Testing/Engine/EncodingTests.cs b/Testing/Acceptance/Engine/EncodingTests.cs similarity index 81% rename from Testing/Engine/EncodingTests.cs rename to Testing/Acceptance/Engine/EncodingTests.cs index 08f667fd..93c05e6d 100644 --- a/Testing/Engine/EncodingTests.cs +++ b/Testing/Acceptance/Engine/EncodingTests.cs @@ -1,32 +1,32 @@ -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Layouting; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public sealed class EncodingTests - { - - /// - /// As a developer, I want UTF-8 to be my default encoding. - /// - [TestMethod] - public async Task TestUtf8DefaultEncoding() - { - var layout = Layout.Create().Add("utf8", Content.From(Resource.FromString("From GenHTTP with ❤"))); - - using var runner = TestRunner.Run(layout); - - using var response = await runner.GetResponse("/utf8"); - - Assert.AreEqual("From GenHTTP with ❤", await response.GetContent()); - } - - } - -} +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Layouting; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public sealed class EncodingTests + { + + /// + /// As a developer, I want UTF-8 to be my default encoding. + /// + [TestMethod] + public async Task TestUtf8DefaultEncoding() + { + var layout = Layout.Create().Add("utf8", Content.From(Resource.FromString("From GenHTTP with ❤"))); + + using var runner = TestHost.Run(layout); + + using var response = await runner.GetResponseAsync("/utf8"); + + Assert.AreEqual("From GenHTTP with ❤", await response.GetContent()); + } + + } + +} diff --git a/Testing/Engine/ErrorHandlingTest.cs b/Testing/Acceptance/Engine/ErrorHandlingTest.cs similarity index 80% rename from Testing/Engine/ErrorHandlingTest.cs rename to Testing/Acceptance/Engine/ErrorHandlingTest.cs index ee84c62c..0f51246b 100644 --- a/Testing/Engine/ErrorHandlingTest.cs +++ b/Testing/Acceptance/Engine/ErrorHandlingTest.cs @@ -1,33 +1,33 @@ -using System; -using System.Net; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Testing.Acceptance.Utilities; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public sealed class ErrorHandlingTest - { - - [TestMethod] - public async Task TestGenericError() - { - var handler = new FunctionalHandler(responseProvider: (r) => - { - throw new NotImplementedException(); - }); - - using var runner = TestRunner.Run(handler); - - using var response = await runner.GetResponse(); - - await response.AssertStatusAsync(HttpStatusCode.InternalServerError); - } - - } - -} +using System; +using System.Net; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Testing.Acceptance.Utilities; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public sealed class ErrorHandlingTest + { + + [TestMethod] + public async Task TestGenericError() + { + var handler = new FunctionalHandler(responseProvider: (r) => + { + throw new NotImplementedException(); + }); + + using var runner = TestHost.Run(handler.Wrap()); + + using var response = await runner.GetResponseAsync(); + + await response.AssertStatusAsync(HttpStatusCode.InternalServerError); + } + + } + +} diff --git a/Testing/Engine/FlexibleTypeTests.cs b/Testing/Acceptance/Engine/FlexibleTypeTests.cs similarity index 90% rename from Testing/Engine/FlexibleTypeTests.cs rename to Testing/Acceptance/Engine/FlexibleTypeTests.cs index 0b71a5b4..94d8abc3 100644 --- a/Testing/Engine/FlexibleTypeTests.cs +++ b/Testing/Acceptance/Engine/FlexibleTypeTests.cs @@ -1,64 +1,64 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Basics; -using GenHTTP.Modules.Layouting; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public sealed class FlexibleTypeTests - { - - private class Provider : IHandler - { - - public ValueTask PrepareAsync() => ValueTask.CompletedTask; - - public IHandler Parent => throw new System.NotImplementedException(); - - public IAsyncEnumerable GetContentAsync(IRequest request) - { - throw new System.NotImplementedException(); - } - - public ValueTask HandleAsync(IRequest request) - { - return request.Respond() - .Content("Hello World!") - .Type("application/x-custom") - .Status(256, "Custom Status") - .BuildTask(); - } - - } - - /// - /// As a developer I would like to use status codes and content types - /// not supported by the server. - /// - [TestMethod] - public async Task TestFlexibleStatus() - { - var content = Layout.Create().Index(new Provider().Wrap()); - - using var runner = TestRunner.Run(content); - - using var response = await runner.GetResponse(); - - Assert.AreEqual(256, (int)response.StatusCode); - Assert.AreEqual("Custom Status", response.ReasonPhrase); - - Assert.AreEqual("application/x-custom", response.GetContentHeader("Content-Type")); - } - - } - -} +using System.Collections.Generic; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Basics; +using GenHTTP.Modules.Layouting; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public sealed class FlexibleTypeTests + { + + private class Provider : IHandler + { + + public ValueTask PrepareAsync() => ValueTask.CompletedTask; + + public IHandler Parent => throw new System.NotImplementedException(); + + public IAsyncEnumerable GetContentAsync(IRequest request) + { + throw new System.NotImplementedException(); + } + + public ValueTask HandleAsync(IRequest request) + { + return request.Respond() + .Content("Hello World!") + .Type("application/x-custom") + .Status(256, "Custom Status") + .BuildTask(); + } + + } + + /// + /// As a developer I would like to use status codes and content types + /// not supported by the server. + /// + [TestMethod] + public async Task TestFlexibleStatus() + { + var content = Layout.Create().Index(new Provider().Wrap()); + + using var runner = TestHost.Run(content); + + using var response = await runner.GetResponseAsync(); + + Assert.AreEqual(256, (int)response.StatusCode); + Assert.AreEqual("Custom Status", response.ReasonPhrase); + + Assert.AreEqual("application/x-custom", response.GetContentHeader("Content-Type")); + } + + } + +} diff --git a/Testing/Engine/HeaderTests.cs b/Testing/Acceptance/Engine/HeaderTests.cs similarity index 78% rename from Testing/Engine/HeaderTests.cs rename to Testing/Acceptance/Engine/HeaderTests.cs index ccabc097..87a77a5c 100644 --- a/Testing/Engine/HeaderTests.cs +++ b/Testing/Acceptance/Engine/HeaderTests.cs @@ -1,51 +1,51 @@ -using System.Net; -using System.Threading.Tasks; - -using GenHTTP.Testing.Acceptance.Utilities; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public class HeaderTests - { - - [TestMethod] - public async Task TestServerHeaderCanBeSet() - { - var handler = new FunctionalHandler(responseProvider: (r) => - { - return r.Respond() - .Header("Server", "TFB") - .Build(); - }); - - using var runner = TestRunner.Run(handler); - - using var response = await runner.GetResponse(); - - Assert.AreEqual("TFB", response.GetHeader("Server")); - } - - [TestMethod] - public async Task TestReservedHeaderCannotBeSet() - { - var handler = new FunctionalHandler(responseProvider: (r) => - { - return r.Respond() - .Header("Date", "123") - .Build(); - }); - - using var runner = TestRunner.Run(handler); - - using var response = await runner.GetResponse(); - - await response.AssertStatusAsync(HttpStatusCode.InternalServerError); - } - - } - -} +using System.Net; +using System.Threading.Tasks; + +using GenHTTP.Testing.Acceptance.Utilities; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public class HeaderTests + { + + [TestMethod] + public async Task TestServerHeaderCanBeSet() + { + var handler = new FunctionalHandler(responseProvider: (r) => + { + return r.Respond() + .Header("Server", "TFB") + .Build(); + }); + + using var runner = TestHost.Run(handler.Wrap()); + + using var response = await runner.GetResponseAsync(); + + Assert.AreEqual("TFB", response.GetHeader("Server")); + } + + [TestMethod] + public async Task TestReservedHeaderCannotBeSet() + { + var handler = new FunctionalHandler(responseProvider: (r) => + { + return r.Respond() + .Header("Date", "123") + .Build(); + }); + + using var runner = TestHost.Run(handler.Wrap()); + + using var response = await runner.GetResponseAsync(); + + await response.AssertStatusAsync(HttpStatusCode.InternalServerError); + } + + } + +} diff --git a/Testing/Engine/HostTests.cs b/Testing/Acceptance/Engine/HostTests.cs similarity index 67% rename from Testing/Engine/HostTests.cs rename to Testing/Acceptance/Engine/HostTests.cs index f942ec7f..ff87d7eb 100644 --- a/Testing/Engine/HostTests.cs +++ b/Testing/Acceptance/Engine/HostTests.cs @@ -1,39 +1,41 @@ -using System.Net; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public sealed class HostTests - { - - [TestMethod] - public async Task TestStart() - { - using var runner = new TestRunner(); - - runner.Host.Start(); - - using var response = await runner.GetResponse(); - - await response.AssertStatusAsync(HttpStatusCode.NotFound); - } - - [TestMethod] - public async Task TestRestart() - { - using var runner = new TestRunner(); - - runner.Host.Restart(); - - using var response = await runner.GetResponse(); - - await response.AssertStatusAsync(HttpStatusCode.NotFound); - } - - } - -} +using System.Net; +using System.Threading.Tasks; + +using GenHTTP.Modules.Layouting; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public sealed class HostTests + { + + [TestMethod] + public async Task TestStart() + { + using var runner = new TestHost(Layout.Create()); + + runner.Host.Start(); + + using var response = await runner.GetResponseAsync(); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + [TestMethod] + public async Task TestRestart() + { + using var runner = new TestHost(Layout.Create()); + + runner.Host.Restart(); + + using var response = await runner.GetResponseAsync(); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + } + +} diff --git a/Testing/Engine/ParserTests.cs b/Testing/Acceptance/Engine/ParserTests.cs similarity index 70% rename from Testing/Engine/ParserTests.cs rename to Testing/Acceptance/Engine/ParserTests.cs index fe188199..4d6c696a 100644 --- a/Testing/Engine/ParserTests.cs +++ b/Testing/Acceptance/Engine/ParserTests.cs @@ -1,138 +1,138 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.IO; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public sealed class ParserTests - { - - #region Supporting data structures - - private class PathReturner : IHandler - { - - public ValueTask PrepareAsync() => ValueTask.CompletedTask; - - public IHandler Parent => throw new NotImplementedException(); - - public IAsyncEnumerable GetContentAsync(IRequest request) - { - throw new NotImplementedException(); - } - - public ValueTask HandleAsync(IRequest request) - { - return request.Respond() - .Content(request.Target.Path.ToString()) - .BuildTask(); - } - - } - - private class QueryReturner : IHandler - { - - public ValueTask PrepareAsync() => ValueTask.CompletedTask; - - public IHandler Parent => throw new NotImplementedException(); - - public IAsyncEnumerable GetContentAsync(IRequest request) - { - throw new NotImplementedException(); - } - - public ValueTask HandleAsync(IRequest request) - { - return request.Respond() - .Content(string.Join('|', request.Query.Select(kv => kv.Key + "=" + kv.Value))) - .BuildTask(); - } - - } - - #endregion - - [TestMethod] - public async Task TestEndodedUri() - { - using var runner = TestRunner.Run(new PathReturner().Wrap()); - - using var respose = await runner.GetResponse("/söme/ürl/with specialities/"); - - Assert.AreEqual("/söme/ürl/with specialities/", await respose.GetContent()); - } - - [TestMethod] - public async Task TestEncodedQuery() - { - using var runner = TestRunner.Run(new QueryReturner().Wrap()); - - using var respose = await runner.GetResponse("/?söme key=💕"); - - Assert.AreEqual("söme key=💕", await respose.GetContent()); - } - - [TestMethod] - public async Task TestMultipleSlashes() - { - using var runner = TestRunner.Run(new PathReturner().Wrap()); - - using var respose = await runner.GetResponse("//one//two//three//"); - - Assert.AreEqual("//one//two//three//", await respose.GetContent()); - } - - [TestMethod] - public async Task TestEmptyQuery() - { - using var runner = TestRunner.Run(new QueryReturner().Wrap()); - - using var respose = await runner.GetResponse("/?"); - - Assert.AreEqual(string.Empty, await respose.GetContent()); - } - - [TestMethod] - public async Task TestUnkeyedQuery() - { - using var runner = TestRunner.Run(new QueryReturner().Wrap()); - - using var respose = await runner.GetResponse("/?query"); - - Assert.AreEqual("query=", await respose.GetContent()); - } - - [TestMethod] - public async Task TestQueryWithSlashes() - { - using var runner = TestRunner.Run(new QueryReturner().Wrap()); - - using var respose = await runner.GetResponse("/?key=/one/two"); - - Assert.AreEqual("key=/one/two", await respose.GetContent()); - } - - [TestMethod] - public async Task TestQueryWithSpaces() - { - using var runner = TestRunner.Run(new QueryReturner().Wrap()); - - using var respose = await runner.GetResponse("/?path=/Some+Folder/With%20Subfolders/"); - - Assert.AreEqual("path=/Some+Folder/With Subfolders/", await respose.GetContent()); - } - - } - -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.IO; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public sealed class ParserTests + { + + #region Supporting data structures + + private class PathReturner : IHandler + { + + public ValueTask PrepareAsync() => ValueTask.CompletedTask; + + public IHandler Parent => throw new NotImplementedException(); + + public IAsyncEnumerable GetContentAsync(IRequest request) + { + throw new NotImplementedException(); + } + + public ValueTask HandleAsync(IRequest request) + { + return request.Respond() + .Content(request.Target.Path.ToString()) + .BuildTask(); + } + + } + + private class QueryReturner : IHandler + { + + public ValueTask PrepareAsync() => ValueTask.CompletedTask; + + public IHandler Parent => throw new NotImplementedException(); + + public IAsyncEnumerable GetContentAsync(IRequest request) + { + throw new NotImplementedException(); + } + + public ValueTask HandleAsync(IRequest request) + { + return request.Respond() + .Content(string.Join('|', request.Query.Select(kv => kv.Key + "=" + kv.Value))) + .BuildTask(); + } + + } + + #endregion + + [TestMethod] + public async Task TestEndodedUri() + { + using var runner = TestHost.Run(new PathReturner().Wrap()); + + using var respose = await runner.GetResponseAsync("/söme/ürl/with specialities/"); + + Assert.AreEqual("/söme/ürl/with specialities/", await respose.GetContent()); + } + + [TestMethod] + public async Task TestEncodedQuery() + { + using var runner = TestHost.Run(new QueryReturner().Wrap()); + + using var respose = await runner.GetResponseAsync("/?söme key=💕"); + + Assert.AreEqual("söme key=💕", await respose.GetContent()); + } + + [TestMethod] + public async Task TestMultipleSlashes() + { + using var runner = TestHost.Run(new PathReturner().Wrap()); + + using var respose = await runner.GetResponseAsync("//one//two//three//"); + + Assert.AreEqual("//one//two//three//", await respose.GetContent()); + } + + [TestMethod] + public async Task TestEmptyQuery() + { + using var runner = TestHost.Run(new QueryReturner().Wrap()); + + using var respose = await runner.GetResponseAsync("/?"); + + Assert.AreEqual(string.Empty, await respose.GetContent()); + } + + [TestMethod] + public async Task TestUnkeyedQuery() + { + using var runner = TestHost.Run(new QueryReturner().Wrap()); + + using var respose = await runner.GetResponseAsync("/?query"); + + Assert.AreEqual("query=", await respose.GetContent()); + } + + [TestMethod] + public async Task TestQueryWithSlashes() + { + using var runner = TestHost.Run(new QueryReturner().Wrap()); + + using var respose = await runner.GetResponseAsync("/?key=/one/two"); + + Assert.AreEqual("key=/one/two", await respose.GetContent()); + } + + [TestMethod] + public async Task TestQueryWithSpaces() + { + using var runner = TestHost.Run(new QueryReturner().Wrap()); + + using var respose = await runner.GetResponseAsync("/?path=/Some+Folder/With%20Subfolders/"); + + Assert.AreEqual("path=/Some+Folder/With Subfolders/", await respose.GetContent()); + } + + } + +} diff --git a/Testing/Engine/PipelineTests.cs b/Testing/Acceptance/Engine/PipelineTests.cs similarity index 91% rename from Testing/Engine/PipelineTests.cs rename to Testing/Acceptance/Engine/PipelineTests.cs index 1d8a571d..a792a95b 100644 --- a/Testing/Engine/PipelineTests.cs +++ b/Testing/Acceptance/Engine/PipelineTests.cs @@ -1,82 +1,82 @@ -using System.IO; -using System.Net.Sockets; -using System.Text; - -using GenHTTP.Modules.IO; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public sealed class PipelineTests - { - - [TestMethod] - public void ServerSupportsPipelining() - { - using var runner = TestRunner.Run(Content.From(Resource.FromString("Hello World!"))); - - using var client = new TcpClient("127.0.0.1", runner.Port) - { - ReceiveTimeout = 1000 - }; - - var stream = client.GetStream(); - - var count = 10; - - WriteRequests(stream, count); - - ReadRequests(stream, count, "Hello World!"); - } - - private static void WriteRequests(Stream stream, int count) - { - using var writer = new StreamWriter(stream, leaveOpen: true); - - var builder = new StringBuilder(); - - for (int i = 0; i < count; i++) - { - builder.Append("GET / HTTP/1.1\r\n"); - builder.Append("Host: 128.0.0.1\r\n"); - builder.Append("Connection: Keep-Alive\r\n\r\n"); - } - - writer.Write(builder.ToString()); - - writer.Flush(); - } - - private static void ReadRequests(Stream stream, int count, string searchFor) - { - using var reader = new StreamReader(stream, leaveOpen: true); - - string? line; - - int found = 0; - - var result = new StringBuilder(); - - try - { - while ((line = reader.ReadLine()) is not null) - { - if (line.Contains(searchFor)) - { - found++; - } - - result.AppendLine(line); - } - } - catch (IOException) { } - - Assert.AreEqual(count - 1, found); // last body does not end with \r\n - } - - } - -} +using System.IO; +using System.Net.Sockets; +using System.Text; + +using GenHTTP.Modules.IO; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public sealed class PipelineTests + { + + [TestMethod] + public void ServerSupportsPipelining() + { + using var runner = TestHost.Run(Content.From(Resource.FromString("Hello World!"))); + + using var client = new TcpClient("127.0.0.1", runner.Port) + { + ReceiveTimeout = 1000 + }; + + var stream = client.GetStream(); + + var count = 10; + + WriteRequests(stream, count); + + ReadRequests(stream, count, "Hello World!"); + } + + private static void WriteRequests(Stream stream, int count) + { + using var writer = new StreamWriter(stream, leaveOpen: true); + + var builder = new StringBuilder(); + + for (int i = 0; i < count; i++) + { + builder.Append("GET / HTTP/1.1\r\n"); + builder.Append("Host: 128.0.0.1\r\n"); + builder.Append("Connection: Keep-Alive\r\n\r\n"); + } + + writer.Write(builder.ToString()); + + writer.Flush(); + } + + private static void ReadRequests(Stream stream, int count, string searchFor) + { + using var reader = new StreamReader(stream, leaveOpen: true); + + string? line; + + int found = 0; + + var result = new StringBuilder(); + + try + { + while ((line = reader.ReadLine()) is not null) + { + if (line.Contains(searchFor)) + { + found++; + } + + result.AppendLine(line); + } + } + catch (IOException) { } + + Assert.AreEqual(count - 1, found); // last body does not end with \r\n + } + + } + +} diff --git a/Testing/Engine/PooledDictionaryTests.cs b/Testing/Acceptance/Engine/PooledDictionaryTests.cs similarity index 96% rename from Testing/Engine/PooledDictionaryTests.cs rename to Testing/Acceptance/Engine/PooledDictionaryTests.cs index 31452827..2e8f5410 100644 --- a/Testing/Engine/PooledDictionaryTests.cs +++ b/Testing/Acceptance/Engine/PooledDictionaryTests.cs @@ -1,102 +1,102 @@ -using System; -using System.Collections.Generic; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Engine.Utilities; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public sealed class PooledDictionaryTests - { - - [TestMethod] - public void TestReplace() - { - using var dict = new PooledDictionary(); - - dict["one"] = "One"; - dict["one"] = "Two"; - - Assert.IsTrue(dict.ContainsKey("one")); - Assert.IsTrue(dict.Contains(new KeyValuePair("one", "Two"))); - - Assert.AreEqual("Two", dict["one"]); - } - - [TestMethod] - public void TestNotFound() - { - using var dict = new PooledDictionary(); - - Assert.ThrowsException(() => dict["nope"]); - } - - [TestMethod] - public void TestCounts() - { - using var dict = new PooledDictionary(); - - Assert.AreEqual(0, dict.Keys.Count); - Assert.AreEqual(0, dict.Values.Count); - - AssertX.Empty(dict); - - dict["one"] = "one"; - - Assert.AreEqual(1, dict.Keys.Count); - Assert.AreEqual(1, dict.Values.Count); - - AssertX.Single(dict); - } - - [TestMethod] - public void TestClear() - { - using var dict = new PooledDictionary(); - dict["one"] = "One"; - - dict.Clear(); - - Assert.IsFalse(dict.ContainsKey("one")); - } - - [TestMethod] - public void TestResize() - { - using var dict = new PooledDictionary(); - - for (int i = 0; i < 25; i++) - { - dict.Add(new KeyValuePair(i, i)); - } - - Assert.IsTrue(dict.Capacity > 25); - - for (int i = 0; i < 25; i++) - { - Assert.AreEqual(i, dict[i]); - } - } - - [TestMethod] - public void TestNoRemove() - { - using var dict = new PooledDictionary(); - - Assert.ThrowsException(() => dict.Remove("")); - } - - [TestMethod] - public void TestNoRemove2() - { - using var dict = new PooledDictionary(); - - Assert.ThrowsException(() => dict.Remove(new KeyValuePair("", ""))); - } - - } - -} +using System; +using System.Collections.Generic; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Engine.Utilities; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public sealed class PooledDictionaryTests + { + + [TestMethod] + public void TestReplace() + { + using var dict = new PooledDictionary(); + + dict["one"] = "One"; + dict["one"] = "Two"; + + Assert.IsTrue(dict.ContainsKey("one")); + Assert.IsTrue(dict.Contains(new KeyValuePair("one", "Two"))); + + Assert.AreEqual("Two", dict["one"]); + } + + [TestMethod] + public void TestNotFound() + { + using var dict = new PooledDictionary(); + + Assert.ThrowsException(() => dict["nope"]); + } + + [TestMethod] + public void TestCounts() + { + using var dict = new PooledDictionary(); + + Assert.AreEqual(0, dict.Keys.Count); + Assert.AreEqual(0, dict.Values.Count); + + AssertX.Empty(dict); + + dict["one"] = "one"; + + Assert.AreEqual(1, dict.Keys.Count); + Assert.AreEqual(1, dict.Values.Count); + + AssertX.Single(dict); + } + + [TestMethod] + public void TestClear() + { + using var dict = new PooledDictionary(); + dict["one"] = "One"; + + dict.Clear(); + + Assert.IsFalse(dict.ContainsKey("one")); + } + + [TestMethod] + public void TestResize() + { + using var dict = new PooledDictionary(); + + for (int i = 0; i < 25; i++) + { + dict.Add(new KeyValuePair(i, i)); + } + + Assert.IsTrue(dict.Capacity > 25); + + for (int i = 0; i < 25; i++) + { + Assert.AreEqual(i, dict[i]); + } + } + + [TestMethod] + public void TestNoRemove() + { + using var dict = new PooledDictionary(); + + Assert.ThrowsException(() => dict.Remove("")); + } + + [TestMethod] + public void TestNoRemove2() + { + using var dict = new PooledDictionary(); + + Assert.ThrowsException(() => dict.Remove(new KeyValuePair("", ""))); + } + + } + +} diff --git a/Testing/Engine/ProtocolTests.cs b/Testing/Acceptance/Engine/ProtocolTests.cs similarity index 89% rename from Testing/Engine/ProtocolTests.cs rename to Testing/Acceptance/Engine/ProtocolTests.cs index b9a65ba1..88785825 100644 --- a/Testing/Engine/ProtocolTests.cs +++ b/Testing/Acceptance/Engine/ProtocolTests.cs @@ -1,128 +1,128 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; - -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; -using GenHTTP.Modules.Basics; -using GenHTTP.Modules.IO; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public sealed class ProtocolTests - { - - private class ValueRecorder : IHandler - { - - public string? Value { get; private set; } - - public ValueTask PrepareAsync() => ValueTask.CompletedTask; - - public IHandler Parent => throw new NotImplementedException(); - - public IAsyncEnumerable GetContentAsync(IRequest request) - { - throw new NotImplementedException(); - } - - public ValueTask HandleAsync(IRequest request) - { - if (request.Content is not null) - { - using var reader = new StreamReader(request.Content); - Value = reader.ReadToEnd(); - } - - return new ValueTask(request.Respond().Build()); - } - - } - - private class ContentLengthResponder : IHandler - { - - public ValueTask PrepareAsync() => ValueTask.CompletedTask; - - public IHandler Parent => throw new NotImplementedException(); - - public IAsyncEnumerable GetContentAsync(IRequest request) - { - throw new NotImplementedException(); - } - - public ValueTask HandleAsync(IRequest request) - { - var content = request.Content?.Length.ToString() ?? "No Content"; - - return request.Respond() - .Content(content) - .Type(ContentType.TextPlain) - .BuildTask(); - } - - } - - /// - /// As a client I can stream data to the server. - /// - [TestMethod] - public async Task TestPost() - { - var recorder = new ValueRecorder(); - - var str = "From client with ❤"; - - using var runner = TestRunner.Run(recorder.Wrap()); - - var request = runner.GetRequest(); - - request.Method = HttpMethod.Post; - request.Content = new StringContent(str); - - using var _ = await runner.GetResponse(request); - - Assert.AreEqual(str, recorder.Value); - } - - /// - /// As a client I can submit large data. - /// - [TestMethod] - public async Task TestPutLarge() - { - using var runner = TestRunner.Run(new ContentLengthResponder()); - - using var content = new MemoryStream(); - - var random = new Random(); - - var buffer = new byte[65536]; - - for (int i = 0; i < 20; i++) // 1.3 MB - { - random.NextBytes(buffer); - content.Write(buffer, 0, buffer.Length); - } - - content.Seek(0, SeekOrigin.Begin); - - var request = runner.GetRequest(); - - request.Method = HttpMethod.Put; - request.Content = new StreamContent(content); - - using var response = await runner.GetResponse(request); - - Assert.AreEqual("1310720", await response.GetContent()); - } - - } - -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Basics; +using GenHTTP.Modules.IO; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public sealed class ProtocolTests + { + + private class ValueRecorder : IHandler + { + + public string? Value { get; private set; } + + public ValueTask PrepareAsync() => ValueTask.CompletedTask; + + public IHandler Parent => throw new NotImplementedException(); + + public IAsyncEnumerable GetContentAsync(IRequest request) + { + throw new NotImplementedException(); + } + + public ValueTask HandleAsync(IRequest request) + { + if (request.Content is not null) + { + using var reader = new StreamReader(request.Content); + Value = reader.ReadToEnd(); + } + + return new ValueTask(request.Respond().Build()); + } + + } + + private class ContentLengthResponder : IHandler + { + + public ValueTask PrepareAsync() => ValueTask.CompletedTask; + + public IHandler Parent => throw new NotImplementedException(); + + public IAsyncEnumerable GetContentAsync(IRequest request) + { + throw new NotImplementedException(); + } + + public ValueTask HandleAsync(IRequest request) + { + var content = request.Content?.Length.ToString() ?? "No Content"; + + return request.Respond() + .Content(content) + .Type(ContentType.TextPlain) + .BuildTask(); + } + + } + + /// + /// As a client I can stream data to the server. + /// + [TestMethod] + public async Task TestPost() + { + var recorder = new ValueRecorder(); + + var str = "From client with ❤"; + + using var runner = TestHost.Run(recorder.Wrap()); + + var request = runner.GetRequest(); + + request.Method = HttpMethod.Post; + request.Content = new StringContent(str); + + using var _ = await runner.GetResponseAsync(request); + + Assert.AreEqual(str, recorder.Value); + } + + /// + /// As a client I can submit large data. + /// + [TestMethod] + public async Task TestPutLarge() + { + using var runner = TestHost.Run(new ContentLengthResponder().Wrap()); + + using var content = new MemoryStream(); + + var random = new Random(); + + var buffer = new byte[65536]; + + for (int i = 0; i < 20; i++) // 1.3 MB + { + random.NextBytes(buffer); + content.Write(buffer, 0, buffer.Length); + } + + content.Seek(0, SeekOrigin.Begin); + + var request = runner.GetRequest(); + + request.Method = HttpMethod.Put; + request.Content = new StreamContent(content); + + using var response = await runner.GetResponseAsync(request); + + Assert.AreEqual("1310720", await response.GetContent()); + } + + } + +} diff --git a/Testing/Engine/ResponseTests.cs b/Testing/Acceptance/Engine/ResponseTests.cs similarity index 90% rename from Testing/Engine/ResponseTests.cs rename to Testing/Acceptance/Engine/ResponseTests.cs index 0354d4af..1a9b2d02 100644 --- a/Testing/Engine/ResponseTests.cs +++ b/Testing/Acceptance/Engine/ResponseTests.cs @@ -1,111 +1,111 @@ -using System; -using System.Net.Http; -using System.Net; -using System.Threading.Tasks; -using System.Collections.Generic; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Basics; -using GenHTTP.Modules.Layouting; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public sealed class ResponseTests - { - - private class ResponseProvider : IHandler - { - - public DateTime Modified { get; } - - public ValueTask PrepareAsync() => ValueTask.CompletedTask; - - public IHandler Parent => throw new NotImplementedException(); - - public ResponseProvider() - { - Modified = DateTime.Now.AddDays(-10); - } - - public ValueTask HandleAsync(IRequest request) - { - return request.Method.KnownMethod switch - { - RequestMethod.POST => request.Respond() - .Content("") - .Type("") - .BuildTask(), - _ => request.Respond() - .Content("Hello World") - .Type("text/x-custom") - .Expires(DateTime.Now.AddYears(1)) - .Modified(Modified) - .Header("X-Powered-By", "Test Runner") - .BuildTask(), - }; - } - - public IAsyncEnumerable GetContentAsync(IRequest request) - { - throw new NotImplementedException(); - } - - } - - /// - /// As a developer, I'd like to use all of the response builders methods. - /// - [TestMethod] - public async Task TestProperties() - { - var provider = new ResponseProvider(); - - var router = Layout.Create().Index(provider.Wrap()); - - using var runner = TestRunner.Run(router); - - using var response = await runner.GetResponse(); - - await response.AssertStatusAsync(HttpStatusCode.OK); - - Assert.AreEqual("Hello World", await response.GetContent()); - Assert.AreEqual("text/x-custom", response.GetContentHeader("Content-Type")); - - Assert.AreEqual(provider.Modified.WithoutMS(), response.Content.Headers.LastModified); - Assert.IsNotNull(response.GetContentHeader("Expires")); - - Assert.AreEqual("Test Runner", response.GetHeader("X-Powered-By")); - } - - /// - /// As a client, I'd like a response containing an empty body to return a Content-Length of 0. - /// - [TestMethod] - public async Task TestEmptyBody() - { - var provider = new ResponseProvider(); - - var router = Layout.Create().Index(provider.Wrap()); - - using var runner = TestRunner.Run(router); - - var request = runner.GetRequest(); - request.Method = HttpMethod.Post; - - using var response = await runner.GetResponse(request); - - AssertX.IsNullOrEmpty(response.GetContentHeader("Content-Type")); - - Assert.AreEqual("0", response.GetContentHeader("Content-Length")); - } - - } - -} +using System; +using System.Net.Http; +using System.Net; +using System.Threading.Tasks; +using System.Collections.Generic; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Basics; +using GenHTTP.Modules.Layouting; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public sealed class ResponseTests + { + + private class ResponseProvider : IHandler + { + + public DateTime Modified { get; } + + public ValueTask PrepareAsync() => ValueTask.CompletedTask; + + public IHandler Parent => throw new NotImplementedException(); + + public ResponseProvider() + { + Modified = DateTime.Now.AddDays(-10); + } + + public ValueTask HandleAsync(IRequest request) + { + return request.Method.KnownMethod switch + { + RequestMethod.POST => request.Respond() + .Content("") + .Type("") + .BuildTask(), + _ => request.Respond() + .Content("Hello World") + .Type("text/x-custom") + .Expires(DateTime.Now.AddYears(1)) + .Modified(Modified) + .Header("X-Powered-By", "Test Runner") + .BuildTask(), + }; + } + + public IAsyncEnumerable GetContentAsync(IRequest request) + { + throw new NotImplementedException(); + } + + } + + /// + /// As a developer, I'd like to use all of the response builders methods. + /// + [TestMethod] + public async Task TestProperties() + { + var provider = new ResponseProvider(); + + var router = Layout.Create().Index(provider.Wrap()); + + using var runner = TestHost.Run(router); + + using var response = await runner.GetResponseAsync(); + + await response.AssertStatusAsync(HttpStatusCode.OK); + + Assert.AreEqual("Hello World", await response.GetContent()); + Assert.AreEqual("text/x-custom", response.GetContentHeader("Content-Type")); + + Assert.AreEqual(provider.Modified.WithoutMS(), response.Content.Headers.LastModified); + Assert.IsNotNull(response.GetContentHeader("Expires")); + + Assert.AreEqual("Test Runner", response.GetHeader("X-Powered-By")); + } + + /// + /// As a client, I'd like a response containing an empty body to return a Content-Length of 0. + /// + [TestMethod] + public async Task TestEmptyBody() + { + var provider = new ResponseProvider(); + + var router = Layout.Create().Index(provider.Wrap()); + + using var runner = TestHost.Run(router); + + var request = runner.GetRequest(); + request.Method = HttpMethod.Post; + + using var response = await runner.GetResponseAsync(request); + + AssertX.IsNullOrEmpty(response.GetContentHeader("Content-Type")); + + Assert.AreEqual("0", response.GetContentHeader("Content-Length")); + } + + } + +} diff --git a/Testing/Engine/RoutingTests.cs b/Testing/Acceptance/Engine/RoutingTests.cs similarity index 76% rename from Testing/Engine/RoutingTests.cs rename to Testing/Acceptance/Engine/RoutingTests.cs index 4c9ea6a8..8f36e7fb 100644 --- a/Testing/Engine/RoutingTests.cs +++ b/Testing/Acceptance/Engine/RoutingTests.cs @@ -1,27 +1,29 @@ -using System.Net; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public sealed class RoutingTests - { - - /// - /// As a client, I expect the server to return 404 for non-existing files. - /// - [TestMethod] - public async Task NotFoundForUnknownRoute() - { - using var runner = TestRunner.Run(); - - using var response = await runner.GetResponse(); - await response.AssertStatusAsync(HttpStatusCode.NotFound); - } - - } - -} +using System.Net; +using System.Threading.Tasks; + +using GenHTTP.Modules.Layouting; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public sealed class RoutingTests + { + + /// + /// As a client, I expect the server to return 404 for non-existing files. + /// + [TestMethod] + public async Task NotFoundForUnknownRoute() + { + using var runner = TestHost.Run(Layout.Create()); + + using var response = await runner.GetResponseAsync(); + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + } + +} diff --git a/Testing/Engine/SecurityTests.cs b/Testing/Acceptance/Engine/SecurityTests.cs similarity index 87% rename from Testing/Engine/SecurityTests.cs rename to Testing/Acceptance/Engine/SecurityTests.cs index 3b44ea6f..180dcdaa 100644 --- a/Testing/Engine/SecurityTests.cs +++ b/Testing/Acceptance/Engine/SecurityTests.cs @@ -1,224 +1,224 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Security.Authentication; -using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; - -using GenHTTP.Api.Infrastructure; -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Layouting; -using GenHTTP.Modules.Security; -using GenHTTP.Modules.Security.Providers; -using System.Net.Http; - -namespace GenHTTP.Testing.Acceptance.Engine -{ - - [TestClass] - public sealed class SecurityTests - { - - /// - /// As a developer I would like to serve my application in a secure manner. - /// - [TestMethod] - public Task TestSecure() - { - return RunSecure(async (insec, sec) => - { - using var client = TestRunner.GetClient(ignoreSecurityErrors: true); - - using var response = await client.GetAsync($"https://localhost:{sec}"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("Hello Alice!", await response.Content.ReadAsStringAsync()); - }); - } - - /// - /// As a developer, I expect the server to redirect to a secure endpoint - /// by default. - /// - [TestMethod] - public Task TestDefaultRedirection() - { - return RunSecure(async (insec, sec) => - { - using var client = TestRunner.GetClient(followRedirects: false); - - using var response = await client.GetAsync($"http://localhost:{insec}"); - - await response.AssertStatusAsync(HttpStatusCode.MovedPermanently); - Assert.AreEqual($"https://localhost:{sec}/", response.Headers.GetValues("Location").First()); - }); - } - - /// - /// As a developer, I expect HTTP requests not to be redirected if - /// upgrades are allowed but not requested by the client. - /// - [TestMethod] - public Task TestNoRedirectionWithAllowed() - { - return RunSecure(async (insec, sec) => - { - using var client = TestRunner.GetClient(followRedirects: false); - - using var response = await client.GetAsync($"http://localhost:{insec}"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - }, mode: SecureUpgrade.Allow); - } - - /// - /// As I developer, I expect requests to be upgraded if requested - /// by the client. - /// - [TestMethod] - public Task TestRedirectionWhenRequested() - { - return RunSecure(async (insec, sec) => - { - using var client = TestRunner.GetClient(followRedirects: false); - - var request = new HttpRequestMessage(HttpMethod.Get, $"http://localhost:{insec}"); - request.Headers.Add("Upgrade-Insecure-Requests", "1"); - - using var response = await client.SendAsync(request); - - await response.AssertStatusAsync(HttpStatusCode.TemporaryRedirect); - - Assert.AreEqual($"https://localhost:{sec}/", response.Headers.GetValues("Location").First()); - Assert.AreEqual($"Upgrade-Insecure-Requests", response.Headers.GetValues("Vary").First()); - }, mode: SecureUpgrade.Allow); - } - - /// - /// As the hoster of a web application, I want my application to enforce strict - /// transport security, so that man-in-the-middle attacks can be avoided to some extend. - /// - [TestMethod] - public Task TestTransportPolicy() - { - return RunSecure(async (insec, sec) => - { - using var client = TestRunner.GetClient(ignoreSecurityErrors: true); - - using var insecureResponse = await client.GetAsync($"http://localhost:{insec}"); - - await insecureResponse.AssertStatusAsync(HttpStatusCode.OK); - Assert.IsFalse(insecureResponse.Headers.Contains("Strict-Transport-Security")); - - using var secureResponse = await client.GetAsync($"https://localhost:{sec}"); - - await secureResponse.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("max-age=31536000; includeSubDomains; preload", secureResponse.Headers.GetValues("Strict-Transport-Security").First()); - - }, mode: SecureUpgrade.None); - } - - /// - /// As the operator of the server, I expect the server to resume - /// normal operation after a security error has happened. - /// - [TestMethod] - public Task TestSecurityError() - { - return RunSecure(async (insec, sec) => - { - await Assert.ThrowsExceptionAsync(async () => - { - using var client = TestRunner.GetClient(); - - using var failedResponse = await client.GetAsync($"https://localhost:{sec}"); - }); - - using var client = TestRunner.GetClient(ignoreSecurityErrors: true); - using var response = await client.GetAsync($"https://localhost:{sec}"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - }); - } - - /// - /// As a web developer, I can decide not to return a certificate which will - /// abort the server SSL handshake. - /// - [TestMethod] - public Task TestNoCertificate() - { - return RunSecure(async (insec, sec) => - { - await Assert.ThrowsExceptionAsync(async () => - { - using var client = TestRunner.GetClient(ignoreSecurityErrors: false); - - using var failedResponse = await client.GetAsync($"https://localhost:{sec}"); - }); - }, host: "myserver"); - } - - private static async Task RunSecure(Func logic, SecureUpgrade? mode = null, string host = "localhost") - { - var content = Layout.Create().Index(Content.From(Resource.FromString("Hello Alice!"))); - - using var runner = new TestRunner(mode is null); - - var port = TestRunner.NextPort(); - - using var cert = await GetCertificate(); - - runner.Host.Handler(content) - .Bind(IPAddress.Any, (ushort)runner.Port) - .Bind(IPAddress.Any, (ushort)port, new PickyCertificateProvider(host, cert), SslProtocols.Tls12); - - if (mode is not null) - { - runner.Host.SecureUpgrade(mode.Value); - runner.Host.StrictTransport(new StrictTransportPolicy(TimeSpan.FromDays(365), true, true)); - } - - runner.Start(); - - await logic((ushort)runner.Port, (ushort)port); - } - - private static async ValueTask GetCertificate() - { - using var stream = await Resource.FromAssembly("Certificate.pfx").Build().GetContentAsync(); - - using var mem = new MemoryStream(); - - await stream.CopyToAsync(mem); - - return new X509Certificate2(mem.ToArray()); - } - - private class PickyCertificateProvider : ICertificateProvider - { - - private string Host { get; } - - private X509Certificate2 Certificate { get; } - - public PickyCertificateProvider(string host, X509Certificate2 certificate) - { - Host = host; - Certificate = certificate; - } - - public X509Certificate2? Provide(string? host) - { - return host == Host ? Certificate : null; - } - - } - - } - -} - +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; + +using GenHTTP.Api.Infrastructure; +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Security; +using GenHTTP.Modules.Security.Providers; +using System.Net.Http; + +namespace GenHTTP.Testing.Acceptance.Engine +{ + + [TestClass] + public sealed class SecurityTests + { + + /// + /// As a developer I would like to serve my application in a secure manner. + /// + [TestMethod] + public Task TestSecure() + { + return RunSecure(async (insec, sec) => + { + using var client = TestHost.GetClient(ignoreSecurityErrors: true); + + using var response = await client.GetAsync($"https://localhost:{sec}"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("Hello Alice!", await response.Content.ReadAsStringAsync()); + }); + } + + /// + /// As a developer, I expect the server to redirect to a secure endpoint + /// by default. + /// + [TestMethod] + public Task TestDefaultRedirection() + { + return RunSecure(async (insec, sec) => + { + using var client = TestHost.GetClient(followRedirects: false); + + using var response = await client.GetAsync($"http://localhost:{insec}"); + + await response.AssertStatusAsync(HttpStatusCode.MovedPermanently); + Assert.AreEqual($"https://localhost:{sec}/", response.Headers.GetValues("Location").First()); + }); + } + + /// + /// As a developer, I expect HTTP requests not to be redirected if + /// upgrades are allowed but not requested by the client. + /// + [TestMethod] + public Task TestNoRedirectionWithAllowed() + { + return RunSecure(async (insec, sec) => + { + using var client = TestHost.GetClient(followRedirects: false); + + using var response = await client.GetAsync($"http://localhost:{insec}"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + }, mode: SecureUpgrade.Allow); + } + + /// + /// As I developer, I expect requests to be upgraded if requested + /// by the client. + /// + [TestMethod] + public Task TestRedirectionWhenRequested() + { + return RunSecure(async (insec, sec) => + { + using var client = TestHost.GetClient(followRedirects: false); + + var request = new HttpRequestMessage(HttpMethod.Get, $"http://localhost:{insec}"); + request.Headers.Add("Upgrade-Insecure-Requests", "1"); + + using var response = await client.SendAsync(request); + + await response.AssertStatusAsync(HttpStatusCode.TemporaryRedirect); + + Assert.AreEqual($"https://localhost:{sec}/", response.Headers.GetValues("Location").First()); + Assert.AreEqual($"Upgrade-Insecure-Requests", response.Headers.GetValues("Vary").First()); + }, mode: SecureUpgrade.Allow); + } + + /// + /// As the hoster of a web application, I want my application to enforce strict + /// transport security, so that man-in-the-middle attacks can be avoided to some extend. + /// + [TestMethod] + public Task TestTransportPolicy() + { + return RunSecure(async (insec, sec) => + { + using var client = TestHost.GetClient(ignoreSecurityErrors: true); + + using var insecureResponse = await client.GetAsync($"http://localhost:{insec}"); + + await insecureResponse.AssertStatusAsync(HttpStatusCode.OK); + Assert.IsFalse(insecureResponse.Headers.Contains("Strict-Transport-Security")); + + using var secureResponse = await client.GetAsync($"https://localhost:{sec}"); + + await secureResponse.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("max-age=31536000; includeSubDomains; preload", secureResponse.Headers.GetValues("Strict-Transport-Security").First()); + + }, mode: SecureUpgrade.None); + } + + /// + /// As the operator of the server, I expect the server to resume + /// normal operation after a security error has happened. + /// + [TestMethod] + public Task TestSecurityError() + { + return RunSecure(async (insec, sec) => + { + await Assert.ThrowsExceptionAsync(async () => + { + using var client = TestHost.GetClient(); + + using var failedResponse = await client.GetAsync($"https://localhost:{sec}"); + }); + + using var client = TestHost.GetClient(ignoreSecurityErrors: true); + using var response = await client.GetAsync($"https://localhost:{sec}"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + }); + } + + /// + /// As a web developer, I can decide not to return a certificate which will + /// abort the server SSL handshake. + /// + [TestMethod] + public Task TestNoCertificate() + { + return RunSecure(async (insec, sec) => + { + await Assert.ThrowsExceptionAsync(async () => + { + using var client = TestHost.GetClient(ignoreSecurityErrors: false); + + using var failedResponse = await client.GetAsync($"https://localhost:{sec}"); + }); + }, host: "myserver"); + } + + private static async Task RunSecure(Func logic, SecureUpgrade? mode = null, string host = "localhost") + { + var content = Layout.Create().Index(Content.From(Resource.FromString("Hello Alice!"))); + + using var runner = new TestHost(Layout.Create(), mode is null); + + var port = TestHost.NextPort(); + + using var cert = await GetCertificate(); + + runner.Host.Handler(content) + .Bind(IPAddress.Any, (ushort)runner.Port) + .Bind(IPAddress.Any, (ushort)port, new PickyCertificateProvider(host, cert), SslProtocols.Tls12); + + if (mode is not null) + { + runner.Host.SecureUpgrade(mode.Value); + runner.Host.StrictTransport(new StrictTransportPolicy(TimeSpan.FromDays(365), true, true)); + } + + runner.Start(); + + await logic((ushort)runner.Port, (ushort)port); + } + + private static async ValueTask GetCertificate() + { + using var stream = await Resource.FromAssembly("Certificate.pfx").Build().GetContentAsync(); + + using var mem = new MemoryStream(); + + await stream.CopyToAsync(mem); + + return new X509Certificate2(mem.ToArray()); + } + + private class PickyCertificateProvider : ICertificateProvider + { + + private string Host { get; } + + private X509Certificate2 Certificate { get; } + + public PickyCertificateProvider(string host, X509Certificate2 certificate) + { + Host = host; + Certificate = certificate; + } + + public X509Certificate2? Provide(string? host) + { + return host == Host ? Certificate : null; + } + + } + + } + +} + diff --git a/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj b/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj new file mode 100644 index 00000000..1ba23140 --- /dev/null +++ b/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj @@ -0,0 +1,93 @@ + + + + + net6.0;net7.0;net8.0 + + 10.0 + enable + true + + false + + + + + + + + + + + + + + + Never + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Testing/Modules/Authentication/ApiKeyAuthenticationTests.cs b/Testing/Acceptance/Modules/Authentication/ApiKeyAuthenticationTests.cs similarity index 82% rename from Testing/Modules/Authentication/ApiKeyAuthenticationTests.cs rename to Testing/Acceptance/Modules/Authentication/ApiKeyAuthenticationTests.cs index d5773891..4d01a851 100644 --- a/Testing/Modules/Authentication/ApiKeyAuthenticationTests.cs +++ b/Testing/Acceptance/Modules/Authentication/ApiKeyAuthenticationTests.cs @@ -1,143 +1,143 @@ -using System.Net; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Api.Protocol; -using GenHTTP.Api.Content.Authentication; - -using GenHTTP.Modules.Layouting; -using GenHTTP.Modules.Layouting.Provider; -using GenHTTP.Modules.Authentication; -using GenHTTP.Modules.Authentication.ApiKey; -using GenHTTP.Modules.IO; - -namespace GenHTTP.Testing.Acceptance.Modules.Authentication -{ - - [TestClass] - public sealed class ApiKeyAuthenticationTests - { - - [TestMethod] - public async Task TestNoKey() - { - using var runner = GetRunnerWithKeys("123"); - - using var response = await runner.GetResponse(); - - await response.AssertStatusAsync(HttpStatusCode.Unauthorized); - } - - [TestMethod] - public async Task TestInvalidKey() - { - using var runner = GetRunnerWithKeys("123"); - - var request = runner.GetRequest(); - request.Headers.Add("X-API-Key", "124"); - - using var response = await runner.GetResponse(request); - - Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); - } - - [TestMethod] - public async Task TestValidKey() - { - using var runner = GetRunnerWithKeys("123"); - - var request = runner.GetRequest(); - request.Headers.Add("X-API-Key", "123"); - - using var response = await runner.GetResponse(request); - - await response.AssertStatusAsync(HttpStatusCode.OK); - } - - [TestMethod] - public async Task TestValidKeyFromQuery() - { - var auth = ApiKeyAuthentication.Create() - .WithQueryParameter("key") - .Keys("123"); - - using var runner = GetRunnerWithAuth(auth); - - using var response = await runner.GetResponse("/?key=123"); - - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); - } - - [TestMethod] - public async Task TestValidKeyFromHeader() - { - var auth = ApiKeyAuthentication.Create() - .WithHeader("key") - .Keys("123"); - - using var runner = GetRunnerWithAuth(auth); - - var request = runner.GetRequest(); - request.Headers.Add("key", "123"); - - using var response = await runner.GetResponse(request); - - await response.AssertStatusAsync(HttpStatusCode.OK); - } - - [TestMethod] - public async Task TestCustomExtractor() - { - var auth = ApiKeyAuthentication.Create() - .Extractor((r) => r.UserAgent) - .Keys("123"); - - using var runner = GetRunnerWithAuth(auth); - - var request = runner.GetRequest(); - request.Headers.Add("User-Agent", "123"); - - using var response = await runner.GetResponse(request); - - await response.AssertStatusAsync(HttpStatusCode.OK); - } - - [TestMethod] - public async Task TestCustomAuthenticator() - { - static ValueTask authenticator(IRequest r, string k) => (k.Length == 5) ? new ValueTask(new ApiKeyUser(k)) : new ValueTask(); - - var auth = ApiKeyAuthentication.Create() - .Authenticator(authenticator); - - using var runner = GetRunnerWithAuth(auth); - - var request = runner.GetRequest(); - request.Headers.Add("X-API-Key", "12345"); - - using var response = await runner.GetResponse(request); - - await response.AssertStatusAsync(HttpStatusCode.OK); - } - - private static TestRunner GetRunnerWithKeys(params string[] keys) - { - var auth = ApiKeyAuthentication.Create() - .Keys(keys); - - return GetRunnerWithAuth(auth); - } - - private static TestRunner GetRunnerWithAuth(ApiKeyConcernBuilder auth) - { - var content = GetContent().Authentication(auth); - - return TestRunner.Run(content); - } - - private static LayoutBuilder GetContent() => Layout.Create().Add(Content.From(Resource.FromString("Hello World!"))); - - } - -} +using System.Net; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Api.Protocol; +using GenHTTP.Api.Content.Authentication; + +using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Layouting.Provider; +using GenHTTP.Modules.Authentication; +using GenHTTP.Modules.Authentication.ApiKey; +using GenHTTP.Modules.IO; + +namespace GenHTTP.Testing.Acceptance.Modules.Authentication +{ + + [TestClass] + public sealed class ApiKeyAuthenticationTests + { + + [TestMethod] + public async Task TestNoKey() + { + using var runner = GetRunnerWithKeys("123"); + + using var response = await runner.GetResponseAsync(); + + await response.AssertStatusAsync(HttpStatusCode.Unauthorized); + } + + [TestMethod] + public async Task TestInvalidKey() + { + using var runner = GetRunnerWithKeys("123"); + + var request = runner.GetRequest(); + request.Headers.Add("X-API-Key", "124"); + + using var response = await runner.GetResponseAsync(request); + + Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); + } + + [TestMethod] + public async Task TestValidKey() + { + using var runner = GetRunnerWithKeys("123"); + + var request = runner.GetRequest(); + request.Headers.Add("X-API-Key", "123"); + + using var response = await runner.GetResponseAsync(request); + + await response.AssertStatusAsync(HttpStatusCode.OK); + } + + [TestMethod] + public async Task TestValidKeyFromQuery() + { + var auth = ApiKeyAuthentication.Create() + .WithQueryParameter("key") + .Keys("123"); + + using var runner = GetRunnerWithAuth(auth); + + using var response = await runner.GetResponseAsync("/?key=123"); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + } + + [TestMethod] + public async Task TestValidKeyFromHeader() + { + var auth = ApiKeyAuthentication.Create() + .WithHeader("key") + .Keys("123"); + + using var runner = GetRunnerWithAuth(auth); + + var request = runner.GetRequest(); + request.Headers.Add("key", "123"); + + using var response = await runner.GetResponseAsync(request); + + await response.AssertStatusAsync(HttpStatusCode.OK); + } + + [TestMethod] + public async Task TestCustomExtractor() + { + var auth = ApiKeyAuthentication.Create() + .Extractor((r) => r.UserAgent) + .Keys("123"); + + using var runner = GetRunnerWithAuth(auth); + + var request = runner.GetRequest(); + request.Headers.Add("User-Agent", "123"); + + using var response = await runner.GetResponseAsync(request); + + await response.AssertStatusAsync(HttpStatusCode.OK); + } + + [TestMethod] + public async Task TestCustomAuthenticator() + { + static ValueTask authenticator(IRequest r, string k) => (k.Length == 5) ? new ValueTask(new ApiKeyUser(k)) : new ValueTask(); + + var auth = ApiKeyAuthentication.Create() + .Authenticator(authenticator); + + using var runner = GetRunnerWithAuth(auth); + + var request = runner.GetRequest(); + request.Headers.Add("X-API-Key", "12345"); + + using var response = await runner.GetResponseAsync(request); + + await response.AssertStatusAsync(HttpStatusCode.OK); + } + + private static TestHost GetRunnerWithKeys(params string[] keys) + { + var auth = ApiKeyAuthentication.Create() + .Keys(keys); + + return GetRunnerWithAuth(auth); + } + + private static TestHost GetRunnerWithAuth(ApiKeyConcernBuilder auth) + { + var content = GetContent().Authentication(auth); + + return TestHost.Run(content); + } + + private static LayoutBuilder GetContent() => Layout.Create().Add(Content.From(Resource.FromString("Hello World!"))); + + } + +} diff --git a/Testing/Modules/Authentication/BasicAuthenticationTests.cs b/Testing/Acceptance/Modules/Authentication/BasicAuthenticationTests.cs similarity index 78% rename from Testing/Modules/Authentication/BasicAuthenticationTests.cs rename to Testing/Acceptance/Modules/Authentication/BasicAuthenticationTests.cs index 17cf06b9..a97000ce 100644 --- a/Testing/Modules/Authentication/BasicAuthenticationTests.cs +++ b/Testing/Acceptance/Modules/Authentication/BasicAuthenticationTests.cs @@ -1,140 +1,140 @@ -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; - -using GenHTTP.Api.Content.Authentication; - -using GenHTTP.Modules.Authentication; -using GenHTTP.Modules.Authentication.Basic; -using GenHTTP.Modules.Layouting; -using GenHTTP.Modules.Layouting.Provider; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Modules.Authentication -{ - - [TestClass] - public sealed class BasicAuthenticationTests - { - - [TestMethod] - public async Task TestNoUser() - { - var content = GetContent().Authentication(BasicAuthentication.Create()); - - using var runner = TestRunner.Run(content); - - using var response = await runner.GetResponse(); - - await response.AssertStatusAsync(HttpStatusCode.Unauthorized); - } - - [TestMethod] - public async Task TestValidUser() - { - var content = GetContent().Authentication(BasicAuthentication.Create() - .Add("user", "password")); - - using var runner = TestRunner.Run(content); - - using var response = await GetResponse(runner, "user", "password"); - - Assert.AreEqual("user", await response.GetContent()); - } - - [TestMethod] - public async Task TestInvalidPassword() - { - var content = GetContent().Authentication(BasicAuthentication.Create() - .Add("user", "password")); - - using var runner = TestRunner.Run(content); - - using var response = await GetResponse(runner, "user", "p"); - - Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode); - } - - [TestMethod] - public async Task TestInvalidUser() - { - var content = GetContent().Authentication(BasicAuthentication.Create()); - - using var runner = TestRunner.Run(content); - - using var response = await GetResponse(runner, "u", "password"); - - await response.AssertStatusAsync(HttpStatusCode.Unauthorized); - } - - [TestMethod] - public async Task TestCustomUser() - { - var content = GetContent().Authentication(BasicAuthentication.Create((u, p) => new ValueTask(new BasicAuthenticationUser("my")))); - - using var runner = TestRunner.Run(content); - - using var response = await GetResponse(runner, "_", "_"); - - Assert.AreEqual("my", await response.GetContent()); - } - - [TestMethod] - public async Task TestNoCustomUser() - { - var content = GetContent().Authentication(BasicAuthentication.Create((u, p) => new ValueTask())); - - using var runner = TestRunner.Run(content); - - using var response = await GetResponse(runner, "_", "_"); - - await response.AssertStatusAsync(HttpStatusCode.Unauthorized); - } - - [TestMethod] - public async Task TestOtherAuthenticationIsNotAccepted() - { - var content = GetContent().Authentication(BasicAuthentication.Create()); - - using var runner = TestRunner.Run(content); - - var request = runner.GetRequest(); - request.Headers.Add("Authorization", "Bearer 123"); - - using var response = await runner.GetResponse(request); - - await response.AssertStatusAsync(HttpStatusCode.Unauthorized); - } - - [TestMethod] - public async Task TestNoValidBase64() - { - var content = GetContent().Authentication(BasicAuthentication.Create()); - - using var runner = TestRunner.Run(content); - - var request = runner.GetRequest(); - request.Headers.Add("Authorization", "Basic 123"); - - using var response = await runner.GetResponse(request); - - await response.AssertStatusAsync(HttpStatusCode.Unauthorized); - } - - private static async Task GetResponse(TestRunner runner, string user, string password) - { - using var client = TestRunner.GetClient(creds: new NetworkCredential(user, password)); - - return await runner.GetResponse(client: client); - } - - private static LayoutBuilder GetContent() - { - return Layout.Create() - .Index(new UserReturningHandlerBuilder()); - } - - } - -} +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +using GenHTTP.Api.Content.Authentication; + +using GenHTTP.Modules.Authentication; +using GenHTTP.Modules.Authentication.Basic; +using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Layouting.Provider; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.Authentication +{ + + [TestClass] + public sealed class BasicAuthenticationTests + { + + [TestMethod] + public async Task TestNoUser() + { + var content = GetContent().Authentication(BasicAuthentication.Create()); + + using var runner = TestHost.Run(content); + + using var response = await runner.GetResponseAsync(); + + await response.AssertStatusAsync(HttpStatusCode.Unauthorized); + } + + [TestMethod] + public async Task TestValidUser() + { + var content = GetContent().Authentication(BasicAuthentication.Create() + .Add("user", "password")); + + using var runner = TestHost.Run(content); + + using var response = await GetResponse(runner, "user", "password"); + + Assert.AreEqual("user", await response.GetContent()); + } + + [TestMethod] + public async Task TestInvalidPassword() + { + var content = GetContent().Authentication(BasicAuthentication.Create() + .Add("user", "password")); + + using var runner = TestHost.Run(content); + + using var response = await GetResponse(runner, "user", "p"); + + Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [TestMethod] + public async Task TestInvalidUser() + { + var content = GetContent().Authentication(BasicAuthentication.Create()); + + using var runner = TestHost.Run(content); + + using var response = await GetResponse(runner, "u", "password"); + + await response.AssertStatusAsync(HttpStatusCode.Unauthorized); + } + + [TestMethod] + public async Task TestCustomUser() + { + var content = GetContent().Authentication(BasicAuthentication.Create((u, p) => new ValueTask(new BasicAuthenticationUser("my")))); + + using var runner = TestHost.Run(content); + + using var response = await GetResponse(runner, "_", "_"); + + Assert.AreEqual("my", await response.GetContent()); + } + + [TestMethod] + public async Task TestNoCustomUser() + { + var content = GetContent().Authentication(BasicAuthentication.Create((u, p) => new ValueTask())); + + using var runner = TestHost.Run(content); + + using var response = await GetResponse(runner, "_", "_"); + + await response.AssertStatusAsync(HttpStatusCode.Unauthorized); + } + + [TestMethod] + public async Task TestOtherAuthenticationIsNotAccepted() + { + var content = GetContent().Authentication(BasicAuthentication.Create()); + + using var runner = TestHost.Run(content); + + var request = runner.GetRequest(); + request.Headers.Add("Authorization", "Bearer 123"); + + using var response = await runner.GetResponseAsync(request); + + await response.AssertStatusAsync(HttpStatusCode.Unauthorized); + } + + [TestMethod] + public async Task TestNoValidBase64() + { + var content = GetContent().Authentication(BasicAuthentication.Create()); + + using var runner = TestHost.Run(content); + + var request = runner.GetRequest(); + request.Headers.Add("Authorization", "Basic 123"); + + using var response = await runner.GetResponseAsync(request); + + await response.AssertStatusAsync(HttpStatusCode.Unauthorized); + } + + private static async Task GetResponse(TestHost runner, string user, string password) + { + using var client = TestHost.GetClient(creds: new NetworkCredential(user, password)); + + return await runner.GetResponseAsync(client: client); + } + + private static LayoutBuilder GetContent() + { + return Layout.Create() + .Index(new UserReturningHandlerBuilder()); + } + + } + +} diff --git a/Testing/Modules/Authentication/UserInjectionTests.cs b/Testing/Acceptance/Modules/Authentication/UserInjectionTests.cs similarity index 79% rename from Testing/Modules/Authentication/UserInjectionTests.cs rename to Testing/Acceptance/Modules/Authentication/UserInjectionTests.cs index f2f8a516..a6eef0c3 100644 --- a/Testing/Modules/Authentication/UserInjectionTests.cs +++ b/Testing/Acceptance/Modules/Authentication/UserInjectionTests.cs @@ -1,68 +1,68 @@ -using System.Net; -using System.Threading.Tasks; - -using GenHTTP.Modules.Authentication; -using GenHTTP.Modules.Authentication.Basic; -using GenHTTP.Modules.Functional; -using GenHTTP.Modules.Reflection; -using GenHTTP.Modules.Reflection.Injectors; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Modules.Authentication -{ - - [TestClass] - public class UserInjectionTests - { - - #region Tests - - [TestMethod] - public async Task TestUserInjected() - { - using var runner = GetRunner(); - - using var client = TestRunner.GetClient(creds: new NetworkCredential("abc", "def")); - - using var response = await runner.GetResponse(client: client); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("abc", await response.GetContent()); - } - - [TestMethod] - public async Task TestNoUser() - { - using var runner = GetRunner(); - - using var response = await runner.GetResponse(); - - await response.AssertStatusAsync(HttpStatusCode.Unauthorized); - } - - #endregion - - #region Helpers - - private static TestRunner GetRunner() - { - var auth = BasicAuthentication.Create() - .Add("abc", "def"); - - var injection = Injection.Empty() - .Add(new UserInjector()); - - var content = Inline.Create() - .Get((BasicAuthenticationUser user) => user.DisplayName) - .Injectors(injection) - .Authentication(auth); - - return TestRunner.Run(content); - } - - #endregion - - } - -} +using System.Net; +using System.Threading.Tasks; + +using GenHTTP.Modules.Authentication; +using GenHTTP.Modules.Authentication.Basic; +using GenHTTP.Modules.Functional; +using GenHTTP.Modules.Reflection; +using GenHTTP.Modules.Reflection.Injectors; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.Authentication +{ + + [TestClass] + public class UserInjectionTests + { + + #region Tests + + [TestMethod] + public async Task TestUserInjected() + { + using var runner = GetRunner(); + + using var client = TestHost.GetClient(creds: new NetworkCredential("abc", "def")); + + using var response = await runner.GetResponseAsync(client: client); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("abc", await response.GetContent()); + } + + [TestMethod] + public async Task TestNoUser() + { + using var runner = GetRunner(); + + using var response = await runner.GetResponseAsync(); + + await response.AssertStatusAsync(HttpStatusCode.Unauthorized); + } + + #endregion + + #region Helpers + + private static TestHost GetRunner() + { + var auth = BasicAuthentication.Create() + .Add("abc", "def"); + + var injection = Injection.Empty() + .Add(new UserInjector()); + + var content = Inline.Create() + .Get((BasicAuthenticationUser user) => user.DisplayName) + .Injectors(injection) + .Authentication(auth); + + return TestHost.Run(content); + } + + #endregion + + } + +} diff --git a/Testing/Modules/Authentication/UserReturningHandler.cs b/Testing/Acceptance/Modules/Authentication/UserReturningHandler.cs similarity index 96% rename from Testing/Modules/Authentication/UserReturningHandler.cs rename to Testing/Acceptance/Modules/Authentication/UserReturningHandler.cs index 5a48070c..654cc49a 100644 --- a/Testing/Modules/Authentication/UserReturningHandler.cs +++ b/Testing/Acceptance/Modules/Authentication/UserReturningHandler.cs @@ -1,47 +1,47 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -using GenHTTP.Api.Content; -using GenHTTP.Api.Content.Authentication; -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.Authentication; -using GenHTTP.Modules.IO; - -namespace GenHTTP.Testing.Acceptance.Modules.Authentication -{ - - public class UserReturningHandlerBuilder : IHandlerBuilder - { - - public IHandler Build(IHandler parent) => new UserReturningHandler(parent); - - } - - public class UserReturningHandler : IHandler - { - - public ValueTask PrepareAsync() => ValueTask.CompletedTask; - - public IHandler Parent { get; } - - public UserReturningHandler(IHandler parent) - { - Parent = parent; - } - - public IAsyncEnumerable GetContentAsync(IRequest request) => AsyncEnumerable.Empty(); - - public ValueTask HandleAsync(IRequest request) - { - var content = request.GetUser()?.DisplayName ?? throw new ProviderException(ResponseStatus.BadRequest, "No user!"); - - return request.Respond() - .Content(content) - .BuildTask(); - } - - } - -} +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Content.Authentication; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Authentication; +using GenHTTP.Modules.IO; + +namespace GenHTTP.Testing.Acceptance.Modules.Authentication +{ + + public class UserReturningHandlerBuilder : IHandlerBuilder + { + + public IHandler Build(IHandler parent) => new UserReturningHandler(parent); + + } + + public class UserReturningHandler : IHandler + { + + public ValueTask PrepareAsync() => ValueTask.CompletedTask; + + public IHandler Parent { get; } + + public UserReturningHandler(IHandler parent) + { + Parent = parent; + } + + public IAsyncEnumerable GetContentAsync(IRequest request) => AsyncEnumerable.Empty(); + + public ValueTask HandleAsync(IRequest request) + { + var content = request.GetUser()?.DisplayName ?? throw new ProviderException(ResponseStatus.BadRequest, "No user!"); + + return request.Respond() + .Content(content) + .BuildTask(); + } + + } + +} diff --git a/Testing/Modules/CacheValidationTests.cs b/Testing/Acceptance/Modules/CacheValidationTests.cs similarity index 66% rename from Testing/Modules/CacheValidationTests.cs rename to Testing/Acceptance/Modules/CacheValidationTests.cs index 8d20d78e..037db444 100644 --- a/Testing/Modules/CacheValidationTests.cs +++ b/Testing/Acceptance/Modules/CacheValidationTests.cs @@ -1,97 +1,97 @@ -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; - -using GenHTTP.Modules.IO; -using GenHTTP.Testing.Acceptance.Utilities; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Modules -{ - - [TestClass] - public sealed class CacheValidationTests - { - - [TestMethod] - public async Task TestETagIsGenerated() - { - using var runner = TestRunner.Run(Content.From(Resource.FromString("Hello World!"))); - - using var response = await runner.GetResponse(); - - var eTag = response.GetHeader("ETag"); - - Assert.IsNotNull(eTag); - - AssertX.StartsWith("\"", eTag); - AssertX.EndsWith("\"", eTag); - } - - [TestMethod] - public async Task TestServerReturnsUnmodified() - { - using var runner = TestRunner.Run(Content.From(Resource.FromString("Hello World!"))); - - using var response = await runner.GetResponse(); - - var eTag = response.GetHeader("ETag"); - - var request = runner.GetRequest(); - - request.Headers.Add("If-None-Match", eTag); - - using var cached = await runner.GetResponse(request); - - await cached.AssertStatusAsync(HttpStatusCode.NotModified); - - Assert.AreEqual("0", cached.GetContentHeader("Content-Length")); - } - - [TestMethod] - public async Task TestServerReturnsModified() - { - using var runner = TestRunner.Run(Content.From(Resource.FromString("Hello World!"))); - - var request = runner.GetRequest(); - - request.Headers.Add("If-None-Match", "\"123\""); - - using var reloaded = await runner.GetResponse(request); - - await reloaded.AssertStatusAsync(HttpStatusCode.OK); - } - - [TestMethod] - public async Task TestNoContentNoEtag() - { - var noContent = new FunctionalHandler(responseProvider: (r) => - { - return r.Respond().Status(Api.Protocol.ResponseStatus.NoContent).Build(); - }); - - using var runner = TestRunner.Run(noContent.Wrap()); - - using var response = await runner.GetResponse(); - - Assert.IsFalse(response.Headers.Contains("ETag")); - } - - [TestMethod] - public async Task TestOtherMethodNoETag() - { - using var runner = TestRunner.Run(Content.From(Resource.FromString("Hello World!"))); - - var request = runner.GetRequest(); - - request.Method = HttpMethod.Delete; - - using var response = await runner.GetResponse(request); - - Assert.IsFalse(response.Headers.Contains("ETag")); - } - - } - -} +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +using GenHTTP.Modules.IO; +using GenHTTP.Testing.Acceptance.Utilities; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules +{ + + [TestClass] + public sealed class CacheValidationTests + { + + [TestMethod] + public async Task TestETagIsGenerated() + { + using var runner = TestHost.Run(Content.From(Resource.FromString("Hello World!"))); + + using var response = await runner.GetResponseAsync(); + + var eTag = response.GetHeader("ETag"); + + Assert.IsNotNull(eTag); + + AssertX.StartsWith("\"", eTag); + AssertX.EndsWith("\"", eTag); + } + + [TestMethod] + public async Task TestServerReturnsUnmodified() + { + using var runner = TestHost.Run(Content.From(Resource.FromString("Hello World!"))); + + using var response = await runner.GetResponseAsync(); + + var eTag = response.GetHeader("ETag"); + + var request = runner.GetRequest(); + + request.Headers.Add("If-None-Match", eTag); + + using var cached = await runner.GetResponseAsync(request); + + await cached.AssertStatusAsync(HttpStatusCode.NotModified); + + Assert.AreEqual("0", cached.GetContentHeader("Content-Length")); + } + + [TestMethod] + public async Task TestServerReturnsModified() + { + using var runner = TestHost.Run(Content.From(Resource.FromString("Hello World!"))); + + var request = runner.GetRequest(); + + request.Headers.Add("If-None-Match", "\"123\""); + + using var reloaded = await runner.GetResponseAsync(request); + + await reloaded.AssertStatusAsync(HttpStatusCode.OK); + } + + [TestMethod] + public async Task TestNoContentNoEtag() + { + var noContent = new FunctionalHandler(responseProvider: (r) => + { + return r.Respond().Status(Api.Protocol.ResponseStatus.NoContent).Build(); + }); + + using var runner = TestHost.Run(noContent.Wrap()); + + using var response = await runner.GetResponseAsync(); + + Assert.IsFalse(response.Headers.Contains("ETag")); + } + + [TestMethod] + public async Task TestOtherMethodNoETag() + { + using var runner = TestHost.Run(Content.From(Resource.FromString("Hello World!"))); + + var request = runner.GetRequest(); + + request.Method = HttpMethod.Delete; + + using var response = await runner.GetResponseAsync(request); + + Assert.IsFalse(response.Headers.Contains("ETag")); + } + + } + +} diff --git a/Testing/Modules/Caching/CacheTests.cs b/Testing/Acceptance/Modules/Caching/CacheTests.cs similarity index 96% rename from Testing/Modules/Caching/CacheTests.cs rename to Testing/Acceptance/Modules/Caching/CacheTests.cs index feb927c2..2909b4a7 100644 --- a/Testing/Modules/Caching/CacheTests.cs +++ b/Testing/Acceptance/Modules/Caching/CacheTests.cs @@ -1,158 +1,158 @@ -using System; -using System.IO; -using System.Threading.Tasks; - -using GenHTTP.Api.Content.Caching; -using GenHTTP.Modules.Caching; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Modules.Caching -{ - - public record CachedEntry(string Data, DateTime? Date = null, int? Int = null); - - [TestClass] - public class CacheTests - { - - [TestMethod] - public async Task TestHit() - { - foreach (var cache in GetCaches()) - { - var now = DateTime.Now; - - await cache.StoreAsync("k", "v", new CachedEntry("1", now, 42)); - - Assert.AreEqual(1, (await cache.GetEntriesAsync("k")).Length); - - var hit = (await cache.GetEntryAsync("k", "v"))!; - - Assert.AreEqual("1", hit.Data); - Assert.AreEqual(now, hit.Date); - Assert.AreEqual(42, hit.Int); - } - } - - [TestMethod] - public async Task TestMiss() - { - foreach (var cache in GetCaches()) - { - Assert.AreEqual(0, (await cache.GetEntriesAsync("k")).Length); - - Assert.IsNull(await cache.GetEntryAsync("k", "v")); - } - } - - [TestMethod] - public async Task TestVariantMiss() - { - foreach (var cache in GetCaches()) - { - await cache.StoreAsync("k", "v1", new CachedEntry("1")); - - Assert.IsNull(await cache.GetEntryAsync("k", "v2")); - } - } - - [TestMethod] - public async Task TestRemoval() - { - foreach (var cache in GetCaches()) - { - await cache.StoreAsync("k", "v", new CachedEntry("1")); - - await cache.StoreAsync("k", "v", null); - - Assert.AreEqual(0, (await cache.GetEntriesAsync("k")).Length); - - Assert.IsNull(await cache.GetEntryAsync("k", "v")); - } - } - - [TestMethod] - public async Task TestStreaming() - { - foreach (var cache in GetCaches()) - { - using var stream = new MemoryStream(new byte[] { 1 }); - - await cache.StoreAsync("k", "v", stream); - - Assert.AreEqual(1, (await cache.GetEntriesAsync("k")).Length); - - using var resultStream = (await cache.GetEntryAsync("k", "v"))!; - - Assert.AreNotEqual(stream, resultStream); - Assert.AreEqual(1, resultStream.Length); - } - } - - [TestMethod] - public async Task TestStreamingOverwrite() - { - foreach (var cache in GetCaches()) - { - using var stream = new MemoryStream(new byte[] { 1 }); - - await cache.StoreAsync("k", "v", stream); - - await cache.StoreAsync("k", "v", stream); - - Assert.AreEqual(1, (await cache.GetEntriesAsync("k")).Length); - } - } - - [TestMethod] - public async Task TestDirectStreaming() - { - foreach (var cache in GetCaches()) - { - await cache.StoreDirectAsync("k", "v", (s) => s.WriteAsync(new byte[] { 1 })); - - Assert.AreEqual(1, (await cache.GetEntriesAsync("k")).Length); - - using var resultStream = (await cache.GetEntryAsync("k", "v"))!; - - Assert.AreEqual(1, resultStream.Length); - } - } - - [TestMethod] - public async Task TestDirectStreamingOverwrite() - { - foreach (var cache in GetCaches()) - { - await cache.StoreDirectAsync("k", "v", (s) => s.WriteAsync(new byte[] { 1 })); - - await cache.StoreDirectAsync("k", "v", (s) => s.WriteAsync(new byte[] { 1 })); - - Assert.AreEqual(1, (await cache.GetEntriesAsync("k")).Length); - } - } - - [TestMethod] - public async Task TestStreamingMiss() - { - foreach (var cache in GetCaches()) - { - Assert.AreEqual(0, (await cache.GetEntriesAsync("k")).Length); - - Assert.IsNull(await cache.GetEntryAsync("k", "v")); - } - } - - private static ICache[] GetCaches() - { - return new ICache[] - { - Cache.Memory().Build(), - Cache.TemporaryFiles().Build() - }; - } - - } - -} +using System; +using System.IO; +using System.Threading.Tasks; + +using GenHTTP.Api.Content.Caching; +using GenHTTP.Modules.Caching; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.Caching +{ + + public record CachedEntry(string Data, DateTime? Date = null, int? Int = null); + + [TestClass] + public class CacheTests + { + + [TestMethod] + public async Task TestHit() + { + foreach (var cache in GetCaches()) + { + var now = DateTime.Now; + + await cache.StoreAsync("k", "v", new CachedEntry("1", now, 42)); + + Assert.AreEqual(1, (await cache.GetEntriesAsync("k")).Length); + + var hit = (await cache.GetEntryAsync("k", "v"))!; + + Assert.AreEqual("1", hit.Data); + Assert.AreEqual(now, hit.Date); + Assert.AreEqual(42, hit.Int); + } + } + + [TestMethod] + public async Task TestMiss() + { + foreach (var cache in GetCaches()) + { + Assert.AreEqual(0, (await cache.GetEntriesAsync("k")).Length); + + Assert.IsNull(await cache.GetEntryAsync("k", "v")); + } + } + + [TestMethod] + public async Task TestVariantMiss() + { + foreach (var cache in GetCaches()) + { + await cache.StoreAsync("k", "v1", new CachedEntry("1")); + + Assert.IsNull(await cache.GetEntryAsync("k", "v2")); + } + } + + [TestMethod] + public async Task TestRemoval() + { + foreach (var cache in GetCaches()) + { + await cache.StoreAsync("k", "v", new CachedEntry("1")); + + await cache.StoreAsync("k", "v", null); + + Assert.AreEqual(0, (await cache.GetEntriesAsync("k")).Length); + + Assert.IsNull(await cache.GetEntryAsync("k", "v")); + } + } + + [TestMethod] + public async Task TestStreaming() + { + foreach (var cache in GetCaches()) + { + using var stream = new MemoryStream(new byte[] { 1 }); + + await cache.StoreAsync("k", "v", stream); + + Assert.AreEqual(1, (await cache.GetEntriesAsync("k")).Length); + + using var resultStream = (await cache.GetEntryAsync("k", "v"))!; + + Assert.AreNotEqual(stream, resultStream); + Assert.AreEqual(1, resultStream.Length); + } + } + + [TestMethod] + public async Task TestStreamingOverwrite() + { + foreach (var cache in GetCaches()) + { + using var stream = new MemoryStream(new byte[] { 1 }); + + await cache.StoreAsync("k", "v", stream); + + await cache.StoreAsync("k", "v", stream); + + Assert.AreEqual(1, (await cache.GetEntriesAsync("k")).Length); + } + } + + [TestMethod] + public async Task TestDirectStreaming() + { + foreach (var cache in GetCaches()) + { + await cache.StoreDirectAsync("k", "v", (s) => s.WriteAsync(new byte[] { 1 })); + + Assert.AreEqual(1, (await cache.GetEntriesAsync("k")).Length); + + using var resultStream = (await cache.GetEntryAsync("k", "v"))!; + + Assert.AreEqual(1, resultStream.Length); + } + } + + [TestMethod] + public async Task TestDirectStreamingOverwrite() + { + foreach (var cache in GetCaches()) + { + await cache.StoreDirectAsync("k", "v", (s) => s.WriteAsync(new byte[] { 1 })); + + await cache.StoreDirectAsync("k", "v", (s) => s.WriteAsync(new byte[] { 1 })); + + Assert.AreEqual(1, (await cache.GetEntriesAsync("k")).Length); + } + } + + [TestMethod] + public async Task TestStreamingMiss() + { + foreach (var cache in GetCaches()) + { + Assert.AreEqual(0, (await cache.GetEntriesAsync("k")).Length); + + Assert.IsNull(await cache.GetEntryAsync("k", "v")); + } + } + + private static ICache[] GetCaches() + { + return new ICache[] + { + Cache.Memory().Build(), + Cache.TemporaryFiles().Build() + }; + } + + } + +} diff --git a/Testing/Modules/ClientCaching/PolicyTests.cs b/Testing/Acceptance/Modules/ClientCaching/PolicyTests.cs similarity index 76% rename from Testing/Modules/ClientCaching/PolicyTests.cs rename to Testing/Acceptance/Modules/ClientCaching/PolicyTests.cs index ad1b1467..ef0aa14c 100644 --- a/Testing/Modules/ClientCaching/PolicyTests.cs +++ b/Testing/Acceptance/Modules/ClientCaching/PolicyTests.cs @@ -1,92 +1,92 @@ -using System.Net.Http; -using System.Threading.Tasks; - -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.ClientCaching; -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Layouting; -using GenHTTP.Modules.Sitemaps; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Modules.ClientCaching -{ - - [TestClass] - public sealed class PolicyTests - { - - [TestMethod] - public async Task TestExpireHeaderSet() - { - var content = Content.From(Resource.FromString("Content")) - .Add(ClientCache.Policy().Duration(1)); - - using var runner = TestRunner.Run(content); - - using var response = await runner.GetResponse(); - - Assert.IsNotNull(response.GetContentHeader("Expires")); - } - - [TestMethod] - public async Task TestExpireHeaderNotSetForOtherMethods() - { - var content = Content.From(Resource.FromString("Content")) - .Add(ClientCache.Policy().Duration(1)); - - using var runner = TestRunner.Run(content); - - var request = runner.GetRequest(); - request.Method = HttpMethod.Head; - - using var response = await runner.GetResponse(request); - - AssertX.IsNullOrEmpty(response.GetContentHeader("Expires")); - } - - [TestMethod] - public async Task TestExpireHeaderNotSetForOtherStatus() - { - var content = Layout.Create() - .Add(ClientCache.Policy().Duration(1)); - - using var runner = TestRunner.Run(content); - - using var response = await runner.GetResponse(); - - AssertX.IsNullOrEmpty(response.GetContentHeader("Expires")); - } - - [TestMethod] - public async Task TestPredicate() - { - var content = Content.From(Resource.FromString("Content")) - .Add(ClientCache.Policy().Duration(1).Predicate((_, r) => r.ContentType?.RawType != "text/plain")); - - using var runner = TestRunner.Run(content); - - using var response = await runner.GetResponse(); - - AssertX.IsNullOrEmpty(response.GetContentHeader("Expires")); - } - - [TestMethod] - public async Task TestContent() - { - var content = Layout.Create() - .Index(Content.From(Resource.FromString("Index").Type(new FlexibleContentType(ContentType.TextHtml)))) - .Add(Sitemap.FILE_NAME, Sitemap.Create()) - .Add(ClientCache.Policy().Duration(1)); - - using var runner = TestRunner.Run(content); - - using var response = await runner.GetResponse("/" + Sitemap.FILE_NAME); - - Assert.AreEqual(1, (await response.GetSitemap()).Count); - } - - } - -} +using System.Net.Http; +using System.Threading.Tasks; + +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.ClientCaching; +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Sitemaps; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.ClientCaching +{ + + [TestClass] + public sealed class PolicyTests + { + + [TestMethod] + public async Task TestExpireHeaderSet() + { + var content = Content.From(Resource.FromString("Content")) + .Add(ClientCache.Policy().Duration(1)); + + using var runner = TestHost.Run(content); + + using var response = await runner.GetResponseAsync(); + + Assert.IsNotNull(response.GetContentHeader("Expires")); + } + + [TestMethod] + public async Task TestExpireHeaderNotSetForOtherMethods() + { + var content = Content.From(Resource.FromString("Content")) + .Add(ClientCache.Policy().Duration(1)); + + using var runner = TestHost.Run(content); + + var request = runner.GetRequest(); + request.Method = HttpMethod.Head; + + using var response = await runner.GetResponseAsync(request); + + AssertX.IsNullOrEmpty(response.GetContentHeader("Expires")); + } + + [TestMethod] + public async Task TestExpireHeaderNotSetForOtherStatus() + { + var content = Layout.Create() + .Add(ClientCache.Policy().Duration(1)); + + using var runner = TestHost.Run(content); + + using var response = await runner.GetResponseAsync(); + + AssertX.IsNullOrEmpty(response.GetContentHeader("Expires")); + } + + [TestMethod] + public async Task TestPredicate() + { + var content = Content.From(Resource.FromString("Content")) + .Add(ClientCache.Policy().Duration(1).Predicate((_, r) => r.ContentType?.RawType != "text/plain")); + + using var runner = TestHost.Run(content); + + using var response = await runner.GetResponseAsync(); + + AssertX.IsNullOrEmpty(response.GetContentHeader("Expires")); + } + + [TestMethod] + public async Task TestContent() + { + var content = Layout.Create() + .Index(Content.From(Resource.FromString("Index").Type(new FlexibleContentType(ContentType.TextHtml)))) + .Add(Sitemap.FILE_NAME, Sitemap.Create()) + .Add(ClientCache.Policy().Duration(1)); + + using var runner = TestHost.Run(content); + + using var response = await runner.GetResponseAsync("/" + Sitemap.FILE_NAME); + + Assert.AreEqual(1, (await response.GetSitemap()).Count); + } + + } + +} diff --git a/Testing/Modules/Controllers/ActionTests.cs b/Testing/Acceptance/Modules/Controllers/ActionTests.cs similarity index 80% rename from Testing/Modules/Controllers/ActionTests.cs rename to Testing/Acceptance/Modules/Controllers/ActionTests.cs index 5409800c..87fdd0a8 100644 --- a/Testing/Modules/Controllers/ActionTests.cs +++ b/Testing/Acceptance/Modules/Controllers/ActionTests.cs @@ -1,241 +1,241 @@ -using System.Net; -using System.IO; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.Controllers; -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Layouting; -using System.Collections.Generic; -using System.Net.Http.Headers; - -namespace GenHTTP.Testing.Acceptance.Modules.Controllers -{ - - [TestClass] - public sealed class ActionTests - { - - #region Supporting data structures - - public sealed class Model - { - - public string? Field { get; set; } - - } - - public sealed class TestController - { - - public IHandlerBuilder Index() - { - return Content.From(Resource.FromString("Hello World!")); - } - - public IHandlerBuilder Action(int? query) - { - return Content.From(Resource.FromString(query?.ToString() ?? "Action")); - } - - [ControllerAction(RequestMethod.PUT)] - public IHandlerBuilder Action(int? value1, string value2) - { - return Content.From(Resource.FromString((value1?.ToString() ?? "Action") + $" {value2}")); - } - - public IHandlerBuilder SimpleAction([FromPath] int id) - { - return Content.From(Resource.FromString(id.ToString())); - } - - public IHandlerBuilder ComplexAction(int three, [FromPath] int one, [FromPath] int two) - { - return Content.From(Resource.FromString((one + two + three).ToString())); - } - - [ControllerAction(RequestMethod.POST)] - public IHandlerBuilder Action(Model data) - { - return Content.From(Resource.FromString(data.Field ?? "no content")); - } - - public IHandlerBuilder HypenCAsing99() - { - return Content.From(Resource.FromString("OK")); - } - - public void Void() { } - - } - - #endregion - - #region Tests - - [TestMethod] - public async Task TestIndex() - { - using var runner = GetRunner(); - - using var response = await runner.GetResponse("/t/"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("Hello World!", await response.GetContent()); - } - - [TestMethod] - public async Task TestAction() - { - using var runner = GetRunner(); - - using var response = await runner.GetResponse("/t/action/"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("Action", await response.GetContent()); - } - - [TestMethod] - public async Task TestActionWithQuery() - { - using var runner = GetRunner(); - - using var response = await runner.GetResponse("/t/action/?query=0815"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("815", await response.GetContent()); - } - - [TestMethod] - public async Task TestActionWithQueryFromBody() - { - using var runner = GetRunner(); - - var dict = new Dictionary() - { - { "value2", "test" } - }; - - var request = runner.GetRequest("/t/action/"); - - request.Method = HttpMethod.Put; - request.Content = new FormUrlEncodedContent(dict); - - using var response = await runner.GetResponse(request); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("Action test", await response.GetContent()); - } - - [TestMethod] - public async Task TestActionWithBody() - { - using var runner = GetRunner(); - - var request = runner.GetRequest("/t/action/"); - - request.Method = HttpMethod.Post; - - request.Content = new StringContent("{ \"field\": \"FieldData\" }", null, "application/json"); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - - using var response = await runner.GetResponse(request); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("FieldData", await response.GetContent()); - } - - [TestMethod] - public async Task TestActionWithParameter() - { - using var runner = GetRunner(); - - using var response = await runner.GetResponse("/t/simple-action/4711/"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("4711", await response.GetContent()); - } - - [TestMethod] - public async Task TestActionWithBadParameter() - { - using var runner = GetRunner(); - - using var response = await runner.GetResponse("/t/simple-action/string/"); - - await response.AssertStatusAsync(HttpStatusCode.BadRequest); - } - - [TestMethod] - public async Task TestActionWithMixedParameters() - { - using var runner = GetRunner(); - - using var response = await runner.GetResponse("/t/complex-action/1/2/?three=3"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("6", await response.GetContent()); - } - - [TestMethod] - public async Task TestActionWithNoResult() - { - using var runner = GetRunner(); - - using var response = await runner.GetResponse("/t/void/"); - - await response.AssertStatusAsync(HttpStatusCode.NoContent); - } - - [TestMethod] - public async Task TestNonExistingAction() - { - using var runner = GetRunner(); - - using var response = await runner.GetResponse("/t/nope/"); - - await response.AssertStatusAsync(HttpStatusCode.NotFound); - } - - [TestMethod] - public async Task TestHypenCasing() - { - using var runner = GetRunner(); - - using var response = await runner.GetResponse("/t/hypen-casing-99/"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("OK", await response.GetContent()); - } - - [TestMethod] - public async Task TestIndexController() - { - using var runner = TestRunner.Run(Layout.Create().IndexController()); - - using var response = await runner.GetResponse("/simple-action/4711/"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("4711", await response.GetContent()); - } - - #endregion - - #region Helpers - - private TestRunner GetRunner() - { - return TestRunner.Run(Layout.Create().AddController("t")); - } - - #endregion - - } - -} +using System.Net; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Controllers; +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Layouting; +using System.Collections.Generic; +using System.Net.Http.Headers; + +namespace GenHTTP.Testing.Acceptance.Modules.Controllers +{ + + [TestClass] + public sealed class ActionTests + { + + #region Supporting data structures + + public sealed class Model + { + + public string? Field { get; set; } + + } + + public sealed class TestController + { + + public IHandlerBuilder Index() + { + return Content.From(Resource.FromString("Hello World!")); + } + + public IHandlerBuilder Action(int? query) + { + return Content.From(Resource.FromString(query?.ToString() ?? "Action")); + } + + [ControllerAction(RequestMethod.PUT)] + public IHandlerBuilder Action(int? value1, string value2) + { + return Content.From(Resource.FromString((value1?.ToString() ?? "Action") + $" {value2}")); + } + + public IHandlerBuilder SimpleAction([FromPath] int id) + { + return Content.From(Resource.FromString(id.ToString())); + } + + public IHandlerBuilder ComplexAction(int three, [FromPath] int one, [FromPath] int two) + { + return Content.From(Resource.FromString((one + two + three).ToString())); + } + + [ControllerAction(RequestMethod.POST)] + public IHandlerBuilder Action(Model data) + { + return Content.From(Resource.FromString(data.Field ?? "no content")); + } + + public IHandlerBuilder HypenCAsing99() + { + return Content.From(Resource.FromString("OK")); + } + + public void Void() { } + + } + + #endregion + + #region Tests + + [TestMethod] + public async Task TestIndex() + { + using var runner = GetRunner(); + + using var response = await runner.GetResponseAsync("/t/"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("Hello World!", await response.GetContent()); + } + + [TestMethod] + public async Task TestAction() + { + using var runner = GetRunner(); + + using var response = await runner.GetResponseAsync("/t/action/"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("Action", await response.GetContent()); + } + + [TestMethod] + public async Task TestActionWithQuery() + { + using var runner = GetRunner(); + + using var response = await runner.GetResponseAsync("/t/action/?query=0815"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("815", await response.GetContent()); + } + + [TestMethod] + public async Task TestActionWithQueryFromBody() + { + using var runner = GetRunner(); + + var dict = new Dictionary() + { + { "value2", "test" } + }; + + var request = runner.GetRequest("/t/action/"); + + request.Method = HttpMethod.Put; + request.Content = new FormUrlEncodedContent(dict); + + using var response = await runner.GetResponseAsync(request); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("Action test", await response.GetContent()); + } + + [TestMethod] + public async Task TestActionWithBody() + { + using var runner = GetRunner(); + + var request = runner.GetRequest("/t/action/"); + + request.Method = HttpMethod.Post; + + request.Content = new StringContent("{ \"field\": \"FieldData\" }", null, "application/json"); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + using var response = await runner.GetResponseAsync(request); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("FieldData", await response.GetContent()); + } + + [TestMethod] + public async Task TestActionWithParameter() + { + using var runner = GetRunner(); + + using var response = await runner.GetResponseAsync("/t/simple-action/4711/"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("4711", await response.GetContent()); + } + + [TestMethod] + public async Task TestActionWithBadParameter() + { + using var runner = GetRunner(); + + using var response = await runner.GetResponseAsync("/t/simple-action/string/"); + + await response.AssertStatusAsync(HttpStatusCode.BadRequest); + } + + [TestMethod] + public async Task TestActionWithMixedParameters() + { + using var runner = GetRunner(); + + using var response = await runner.GetResponseAsync("/t/complex-action/1/2/?three=3"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("6", await response.GetContent()); + } + + [TestMethod] + public async Task TestActionWithNoResult() + { + using var runner = GetRunner(); + + using var response = await runner.GetResponseAsync("/t/void/"); + + await response.AssertStatusAsync(HttpStatusCode.NoContent); + } + + [TestMethod] + public async Task TestNonExistingAction() + { + using var runner = GetRunner(); + + using var response = await runner.GetResponseAsync("/t/nope/"); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + [TestMethod] + public async Task TestHypenCasing() + { + using var runner = GetRunner(); + + using var response = await runner.GetResponseAsync("/t/hypen-casing-99/"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("OK", await response.GetContent()); + } + + [TestMethod] + public async Task TestIndexController() + { + using var runner = TestHost.Run(Layout.Create().IndexController()); + + using var response = await runner.GetResponseAsync("/simple-action/4711/"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("4711", await response.GetContent()); + } + + #endregion + + #region Helpers + + private TestHost GetRunner() + { + return TestHost.Run(Layout.Create().AddController("t")); + } + + #endregion + + } + +} diff --git a/Testing/Modules/Controllers/ChecksumTests.cs b/Testing/Acceptance/Modules/Controllers/ChecksumTests.cs similarity index 75% rename from Testing/Modules/Controllers/ChecksumTests.cs rename to Testing/Acceptance/Modules/Controllers/ChecksumTests.cs index e87c1ef4..0a97a598 100644 --- a/Testing/Modules/Controllers/ChecksumTests.cs +++ b/Testing/Acceptance/Modules/Controllers/ChecksumTests.cs @@ -1,54 +1,54 @@ -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; - -using GenHTTP.Api.Content.Templating; -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Scriban; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Modules.Controllers -{ - - [TestClass] - public sealed class ChecksumTests - { - - [TestMethod] - public async Task TestEnumerableViewModel() - { - var vm = new List() { 4711 }; - - var page = ModScriban.Page>>(Resource.FromString("Hello World!"), (r, h) => new(new ViewModel>(r, h, vm))); - - using var runner = TestRunner.Run(page); - - using var r1 = await runner.GetResponse(); - - vm.Add(0815); - - using var r2 = await runner.GetResponse(); - - Assert.AreNotEqual(r1.GetHeader("ETag"), r2.GetHeader("ETag")); - } - - [TestMethod] - public async Task TestNonEnumerableViewModel() - { - var vm = new StringBuilder(); - - var page = ModScriban.Page>(Resource.FromString("Hello World!"), (r, h) => new(new ViewModel(r, h, vm))); - - using var runner = TestRunner.Run(page); - - using var r1 = await runner.GetResponse(); - - using var r2 = await runner.GetResponse(); - - Assert.AreEqual(r1.GetHeader("ETag"), r2.GetHeader("ETag")); - } - - } - -} +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +using GenHTTP.Api.Content.Templating; +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Scriban; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.Controllers +{ + + [TestClass] + public sealed class ChecksumTests + { + + [TestMethod] + public async Task TestEnumerableViewModel() + { + var vm = new List() { 4711 }; + + var page = ModScriban.Page>>(Resource.FromString("Hello World!"), (r, h) => new(new ViewModel>(r, h, vm))); + + using var runner = TestHost.Run(page); + + using var r1 = await runner.GetResponseAsync(); + + vm.Add(0815); + + using var r2 = await runner.GetResponseAsync(); + + Assert.AreNotEqual(r1.GetHeader("ETag"), r2.GetHeader("ETag")); + } + + [TestMethod] + public async Task TestNonEnumerableViewModel() + { + var vm = new StringBuilder(); + + var page = ModScriban.Page>(Resource.FromString("Hello World!"), (r, h) => new(new ViewModel(r, h, vm))); + + using var runner = TestHost.Run(page); + + using var r1 = await runner.GetResponseAsync(); + + using var r2 = await runner.GetResponseAsync(); + + Assert.AreEqual(r1.GetHeader("ETag"), r2.GetHeader("ETag")); + } + + } + +} diff --git a/Testing/Modules/Controllers/ContentTests.cs b/Testing/Acceptance/Modules/Controllers/ContentTests.cs similarity index 93% rename from Testing/Modules/Controllers/ContentTests.cs rename to Testing/Acceptance/Modules/Controllers/ContentTests.cs index 502abd3f..cc7e7e67 100644 --- a/Testing/Modules/Controllers/ContentTests.cs +++ b/Testing/Acceptance/Modules/Controllers/ContentTests.cs @@ -1,116 +1,116 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.Controllers; -using GenHTTP.Modules.Layouting; -using GenHTTP.Modules.Placeholders; -using GenHTTP.Modules.Sitemaps; -using GenHTTP.Modules.Reflection; - -namespace GenHTTP.Testing.Acceptance.Modules.Controllers -{ - - [TestClass] - public sealed class ContentTests - { - - #region Supporting data structures - - public sealed class TestController - { - private static readonly IHandlerBuilder _Page = Page.From(string.Empty); - - public IHandlerBuilder Index() => _Page; - - public IHandlerBuilder Complex() => Layout.Create().Add("page", _Page); - - public IHandler WithParent(IHandler parent, IRequest _) => _Page.Build(parent); - - public void Ignored() { } - - [ControllerAction(RequestMethod.GET, IgnoreContent = true)] - public IHandlerBuilder IgnoredContent() => _Page; - - [ControllerAction(RequestMethod.POST)] - public IHandlerBuilder Post(string _) => _Page; - - public IHandlerBuilder NoHints([FromPath] string path) => _Page; - - [ControllerAction(RequestMethod.GET, ContentHints = typeof(Hints))] - public IHandlerBuilder WithHints([FromPath] int number, int query) => _Page; - - } - - public sealed class Hints : IContentHints - { - public List GetHints(IRequest request) - { - return new List() - { - new ContentHint() { { "number", 10 }, { "query", 11 } }, - new ContentHint() { { "number", 12 } } - }; - } - } - - #endregion - - #region Tests - - [TestMethod] - public async Task TestIndex() => AssertX.Contains("/c/", await GetSitemap()); - - [TestMethod] - public async Task TestComplex() => AssertX.Contains("/c/complex/page", await GetSitemap()); - - [TestMethod] - public async Task TestWithParent() => AssertX.Contains("/c/with-parent/", await GetSitemap()); - - [TestMethod] - public async Task TestOthersIgnored() => AssertX.DoesNotContain("/c/ignored/", await GetSitemap()); - - [TestMethod] - public async Task TestIgnoredContent() => AssertX.DoesNotContain("/c/ignored-content/", await GetSitemap()); - - [TestMethod] - public async Task TestPostIsIgnored() => AssertX.DoesNotContain("/c/post/", await GetSitemap()); - - [TestMethod] - public async Task TestNoHintsNoContent() => AssertX.DoesNotContain("/c/no-hints/", await GetSitemap()); - - [TestMethod] - public async Task TestHints() - { - var sitemap = await GetSitemap(); - - AssertX.Contains("/c/with-hints/10/", sitemap); - AssertX.Contains("/c/with-hints/12/", sitemap); - } - - #endregion - - #region Helpers - - private async Task> GetSitemap() - { - var root = Layout.Create() - .AddController("c") - .Add("sitemap", Sitemap.Create()); - - using var runner = TestRunner.Run(root); - - using var response = await runner.GetResponse("/sitemap"); - - return await response.GetSitemap(); - } - - #endregion - - } - -} +using System.Collections.Generic; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Controllers; +using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Placeholders; +using GenHTTP.Modules.Sitemaps; +using GenHTTP.Modules.Reflection; + +namespace GenHTTP.Testing.Acceptance.Modules.Controllers +{ + + [TestClass] + public sealed class ContentTests + { + + #region Supporting data structures + + public sealed class TestController + { + private static readonly IHandlerBuilder _Page = Page.From(string.Empty); + + public IHandlerBuilder Index() => _Page; + + public IHandlerBuilder Complex() => Layout.Create().Add("page", _Page); + + public IHandler WithParent(IHandler parent, IRequest _) => _Page.Build(parent); + + public void Ignored() { } + + [ControllerAction(RequestMethod.GET, IgnoreContent = true)] + public IHandlerBuilder IgnoredContent() => _Page; + + [ControllerAction(RequestMethod.POST)] + public IHandlerBuilder Post(string _) => _Page; + + public IHandlerBuilder NoHints([FromPath] string path) => _Page; + + [ControllerAction(RequestMethod.GET, ContentHints = typeof(Hints))] + public IHandlerBuilder WithHints([FromPath] int number, int query) => _Page; + + } + + public sealed class Hints : IContentHints + { + public List GetHints(IRequest request) + { + return new List() + { + new ContentHint() { { "number", 10 }, { "query", 11 } }, + new ContentHint() { { "number", 12 } } + }; + } + } + + #endregion + + #region Tests + + [TestMethod] + public async Task TestIndex() => AssertX.Contains("/c/", await GetSitemap()); + + [TestMethod] + public async Task TestComplex() => AssertX.Contains("/c/complex/page", await GetSitemap()); + + [TestMethod] + public async Task TestWithParent() => AssertX.Contains("/c/with-parent/", await GetSitemap()); + + [TestMethod] + public async Task TestOthersIgnored() => AssertX.DoesNotContain("/c/ignored/", await GetSitemap()); + + [TestMethod] + public async Task TestIgnoredContent() => AssertX.DoesNotContain("/c/ignored-content/", await GetSitemap()); + + [TestMethod] + public async Task TestPostIsIgnored() => AssertX.DoesNotContain("/c/post/", await GetSitemap()); + + [TestMethod] + public async Task TestNoHintsNoContent() => AssertX.DoesNotContain("/c/no-hints/", await GetSitemap()); + + [TestMethod] + public async Task TestHints() + { + var sitemap = await GetSitemap(); + + AssertX.Contains("/c/with-hints/10/", sitemap); + AssertX.Contains("/c/with-hints/12/", sitemap); + } + + #endregion + + #region Helpers + + private async Task> GetSitemap() + { + var root = Layout.Create() + .AddController("c") + .Add("sitemap", Sitemap.Create()); + + using var runner = TestHost.Run(root); + + using var response = await runner.GetResponseAsync("/sitemap"); + + return await response.GetSitemap(); + } + + #endregion + + } + +} diff --git a/Testing/Modules/Controllers/ErrorHandlingTests.cs b/Testing/Acceptance/Modules/Controllers/ErrorHandlingTests.cs similarity index 89% rename from Testing/Modules/Controllers/ErrorHandlingTests.cs rename to Testing/Acceptance/Modules/Controllers/ErrorHandlingTests.cs index b8e6954a..f2f4bb7d 100644 --- a/Testing/Modules/Controllers/ErrorHandlingTests.cs +++ b/Testing/Acceptance/Modules/Controllers/ErrorHandlingTests.cs @@ -1,13 +1,13 @@ using System; using System.Net; -using System.Net.Http; +using System.Net.Http; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using GenHTTP.Modules.Controllers; -using GenHTTP.Modules.Layouting; - +using GenHTTP.Modules.Layouting; + namespace GenHTTP.Testing.Acceptance.Modules.Controllers { @@ -58,7 +58,7 @@ public void TestNoNullablePathArguments() Assert.ThrowsException(() => { var controller = Controller.From(); - using var _ = TestRunner.Run(controller); + using var _ = TestHost.Run(controller); }); } @@ -68,7 +68,7 @@ public void TestNoComplexPathArguments() Assert.ThrowsException(() => { var controller = Controller.From(); - using var _ = TestRunner.Run(controller); + using var _ = TestHost.Run(controller); }); } @@ -81,9 +81,9 @@ private async Task Run(string uri) var layout = Layout.Create() .AddController("c"); - using var runner = TestRunner.Run(layout); + using var runner = TestHost.Run(layout); - return await runner.GetResponse(uri); + return await runner.GetResponseAsync(uri); } #endregion diff --git a/Testing/Modules/Controllers/ResultTypeTests.cs b/Testing/Acceptance/Modules/Controllers/ResultTypeTests.cs similarity index 83% rename from Testing/Modules/Controllers/ResultTypeTests.cs rename to Testing/Acceptance/Modules/Controllers/ResultTypeTests.cs index 05d0840d..a9e43930 100644 --- a/Testing/Modules/Controllers/ResultTypeTests.cs +++ b/Testing/Acceptance/Modules/Controllers/ResultTypeTests.cs @@ -1,114 +1,114 @@ -using System.Net; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.Controllers; -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Layouting; -using GenHTTP.Modules.Conversion; -using GenHTTP.Modules.Reflection; - -namespace GenHTTP.Testing.Acceptance.Modules.Controllers -{ - - [TestClass] - public sealed class ResultTypeTests - { - - #region Supporting data structures - - public sealed class TestController - { - - public IHandlerBuilder HandlerBuilder() - { - return Content.From(Resource.FromString("HandlerBuilder")); - } - - public IHandler Handler(IHandler parent) - { - return Content.From(Resource.FromString("Handler")).Build(parent); - } - - public IResponseBuilder ResponseBuilder(IRequest request) - { - return request.Respond().Content("ResponseBuilder"); - } - - public IResponse Response(IRequest request) - { - return request.Respond().Content("Response").Build(); - } - - } - - #endregion - - #region Tests - - [TestMethod] - public async Task ControllerMayReturnHandlerBuilder() - { - using var runner = GetRunner(); - - using var response = await runner.GetResponse("/t/handler-builder/"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("HandlerBuilder", await response.GetContent()); - } - - [TestMethod] - public async Task ControllerMayReturnHandler() - { - using var runner = GetRunner(); - - using var response = await runner.GetResponse("/t/handler/"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("Handler", await response.GetContent()); - } - - [TestMethod] - public async Task ControllerMayReturnResponseBuilder() - { - using var runner = GetRunner(); - - using var response = await runner.GetResponse("/t/response-builder/"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("ResponseBuilder", await response.GetContent()); - } - - [TestMethod] - public async Task ControllerMayReturnResponse() - { - using var runner = GetRunner(); - - using var response = await runner.GetResponse("/t/response/"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("Response", await response.GetContent()); - } - - #endregion - - #region Helpers - - private static TestRunner GetRunner() - { - var controller = Controller.From() - .Formats(Serialization.Default()) - .Injectors(Injection.Default()); - - return TestRunner.Run(Layout.Create().Add("t", controller)); - } - - #endregion - - } - -} +using System.Net; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Controllers; +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Conversion; +using GenHTTP.Modules.Reflection; + +namespace GenHTTP.Testing.Acceptance.Modules.Controllers +{ + + [TestClass] + public sealed class ResultTypeTests + { + + #region Supporting data structures + + public sealed class TestController + { + + public IHandlerBuilder HandlerBuilder() + { + return Content.From(Resource.FromString("HandlerBuilder")); + } + + public IHandler Handler(IHandler parent) + { + return Content.From(Resource.FromString("Handler")).Build(parent); + } + + public IResponseBuilder ResponseBuilder(IRequest request) + { + return request.Respond().Content("ResponseBuilder"); + } + + public IResponse Response(IRequest request) + { + return request.Respond().Content("Response").Build(); + } + + } + + #endregion + + #region Tests + + [TestMethod] + public async Task ControllerMayReturnHandlerBuilder() + { + using var runner = GetRunner(); + + using var response = await runner.GetResponseAsync("/t/handler-builder/"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("HandlerBuilder", await response.GetContent()); + } + + [TestMethod] + public async Task ControllerMayReturnHandler() + { + using var runner = GetRunner(); + + using var response = await runner.GetResponseAsync("/t/handler/"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("Handler", await response.GetContent()); + } + + [TestMethod] + public async Task ControllerMayReturnResponseBuilder() + { + using var runner = GetRunner(); + + using var response = await runner.GetResponseAsync("/t/response-builder/"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("ResponseBuilder", await response.GetContent()); + } + + [TestMethod] + public async Task ControllerMayReturnResponse() + { + using var runner = GetRunner(); + + using var response = await runner.GetResponseAsync("/t/response/"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("Response", await response.GetContent()); + } + + #endregion + + #region Helpers + + private static TestHost GetRunner() + { + var controller = Controller.From() + .Formats(Serialization.Default()) + .Injectors(Injection.Default()); + + return TestHost.Run(Layout.Create().Add("t", controller)); + } + + #endregion + + } + +} diff --git a/Testing/Modules/Controllers/RoutingTests.cs b/Testing/Acceptance/Modules/Controllers/RoutingTests.cs similarity index 84% rename from Testing/Modules/Controllers/RoutingTests.cs rename to Testing/Acceptance/Modules/Controllers/RoutingTests.cs index e9c40e4b..514b8cf1 100644 --- a/Testing/Modules/Controllers/RoutingTests.cs +++ b/Testing/Acceptance/Modules/Controllers/RoutingTests.cs @@ -1,206 +1,206 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.Basics; -using GenHTTP.Modules.Controllers; -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Layouting; -using System.Threading.Tasks; - -namespace GenHTTP.Testing.Acceptance.Modules.Controllers -{ - - [TestClass] - public sealed class RoutingTests - { - - #region Supporting data structures - - public sealed class RouteController - { - - public IHandlerBuilder Appenders([FromPath] int one, [FromPath] string two) - { - return new AppenderDependentHandlerBuilder(); - } - - public IHandlerBuilder Nested([FromPath] int one, [FromPath] string two) - { - return Layout.Create() - .Add("inner", new AppenderDependentHandlerBuilder()); - } - - public IHandlerBuilder InnerController([FromPath] int i) - { - return Layout.Create() - .Add((i + 1).ToString(), Controller.From()); - } - - public IHandlerBuilder Index() => Content.From(Resource.FromString("Index")); - - public IHandlerBuilder DoSomethingWithController() - { - return Redirect.To("{controller}/", true); - } - - public IHandlerBuilder DoSomethingWithIndex() - { - return Redirect.To("{index}/", true); - } - - public IHandlerBuilder DoSomethingWithParent() - { - return Redirect.To("{layout}/test", true); - } - - public IHandlerBuilder DoSomethingWithAppenders() - { - return Redirect.To("appenders/1/2/", true); - } - - } - - public sealed class AppenderDependentHandlerBuilder : IHandlerBuilder - { - - public IHandler Build(IHandler parent) => new AppenderDependentHandler(parent); - - } - - public sealed class AppenderDependentHandler : IHandler - { - - public IHandler Parent { get; } - - public ValueTask PrepareAsync() => ValueTask.CompletedTask; - - public AppenderDependentHandler(IHandler parent) - { - Parent = parent; - } - - public IAsyncEnumerable GetContentAsync(IRequest request) - { - var root = this.GetRoot(request, false); - - var info = ContentInfo.Create() - .Title("My File") - .Build(); - - return new List() - { - new ContentElement(root, info, ContentType.ApplicationForceDownload) - }.ToAsyncEnumerable(); - } - - public async ValueTask HandleAsync(IRequest request) - { - return await Content.From( Resource.FromString(await (GetContentAsync(request).Select(c => c.Path).FirstAsync()))) - .Build(this) - .HandleAsync(request); - } - - } - - #endregion - - #region Tests - - [TestMethod] - public async Task TestAppenders() - { - using var runner = Setup(); - - using var response = await runner.GetResponse("/r/appenders/1/test/"); - - Assert.AreEqual("/r/appenders/1/test/", await response.GetContent()); - } - - [TestMethod] - public async Task TestNested() - { - using var runner = Setup(); - - using var response = await runner.GetResponse("/r/nested/1/test/inner"); - - Assert.AreEqual("/r/nested/1/test/inner", await response.GetContent()); - } - - /// - /// Ensure that nesting of controllers is possible and - /// routing still works as expected. - /// - [TestMethod] - public async Task TestInner() - { - using var runner = Setup(); - - using var response = await runner.GetResponse("/r/inner-controller/1/2/inner-controller/3/4/appenders/5/6/"); - - Assert.AreEqual("/r/inner-controller/1/2/inner-controller/3/4/appenders/5/6/", await response.GetContent()); - } - - [TestMethod] - public async Task TestRoutingToController() - { - using var runner = Setup(); - - using var response = await runner.GetResponse("/r/do-something-with-controller/"); - - Assert.AreEqual("/r/", new Uri(response.GetHeader("Location")!).AbsolutePath); - } - - [TestMethod] - public async Task TestRoutingToIndex() - { - using var runner = Setup(); - - using var response = await runner.GetResponse("/r/do-something-with-index/"); - - Assert.AreEqual("/r/", new Uri(response.GetHeader("Location")!).AbsolutePath); - } - - [TestMethod] - public async Task TestRoutingToParent() - { - using var runner = Setup(); - - using var response = await runner.GetResponse("/r/do-something-with-parent/"); - - Assert.AreEqual("/test", new Uri(response.GetHeader("Location")!).AbsolutePath); - } - - [TestMethod] - public async Task TestRoutingToAppender() - { - using var runner = Setup(); - - using var response = await runner.GetResponse("/r/do-something-with-appenders/"); - - Assert.AreEqual("/r/appenders/1/2/", new Uri(response.GetHeader("Location")!).AbsolutePath); - } - - #endregion - - #region Helpers - - private TestRunner Setup() - { - var layout = Layout.Create() - .AddController("r") - .Add(Content.From(Resource.FromString("Blubb"))); - - return TestRunner.Run(layout); - } - - #endregion - - } - -} +using System; +using System.Collections.Generic; +using System.Linq; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Basics; +using GenHTTP.Modules.Controllers; +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Layouting; +using System.Threading.Tasks; + +namespace GenHTTP.Testing.Acceptance.Modules.Controllers +{ + + [TestClass] + public sealed class RoutingTests + { + + #region Supporting data structures + + public sealed class RouteController + { + + public IHandlerBuilder Appenders([FromPath] int one, [FromPath] string two) + { + return new AppenderDependentHandlerBuilder(); + } + + public IHandlerBuilder Nested([FromPath] int one, [FromPath] string two) + { + return Layout.Create() + .Add("inner", new AppenderDependentHandlerBuilder()); + } + + public IHandlerBuilder InnerController([FromPath] int i) + { + return Layout.Create() + .Add((i + 1).ToString(), Controller.From()); + } + + public IHandlerBuilder Index() => Content.From(Resource.FromString("Index")); + + public IHandlerBuilder DoSomethingWithController() + { + return Redirect.To("{controller}/", true); + } + + public IHandlerBuilder DoSomethingWithIndex() + { + return Redirect.To("{index}/", true); + } + + public IHandlerBuilder DoSomethingWithParent() + { + return Redirect.To("{layout}/test", true); + } + + public IHandlerBuilder DoSomethingWithAppenders() + { + return Redirect.To("appenders/1/2/", true); + } + + } + + public sealed class AppenderDependentHandlerBuilder : IHandlerBuilder + { + + public IHandler Build(IHandler parent) => new AppenderDependentHandler(parent); + + } + + public sealed class AppenderDependentHandler : IHandler + { + + public IHandler Parent { get; } + + public ValueTask PrepareAsync() => ValueTask.CompletedTask; + + public AppenderDependentHandler(IHandler parent) + { + Parent = parent; + } + + public IAsyncEnumerable GetContentAsync(IRequest request) + { + var root = this.GetRoot(request, false); + + var info = ContentInfo.Create() + .Title("My File") + .Build(); + + return new List() + { + new ContentElement(root, info, ContentType.ApplicationForceDownload) + }.ToAsyncEnumerable(); + } + + public async ValueTask HandleAsync(IRequest request) + { + return await Content.From( Resource.FromString(await (GetContentAsync(request).Select(c => c.Path).FirstAsync()))) + .Build(this) + .HandleAsync(request); + } + + } + + #endregion + + #region Tests + + [TestMethod] + public async Task TestAppenders() + { + using var runner = Setup(); + + using var response = await runner.GetResponseAsync("/r/appenders/1/test/"); + + Assert.AreEqual("/r/appenders/1/test/", await response.GetContent()); + } + + [TestMethod] + public async Task TestNested() + { + using var runner = Setup(); + + using var response = await runner.GetResponseAsync("/r/nested/1/test/inner"); + + Assert.AreEqual("/r/nested/1/test/inner", await response.GetContent()); + } + + /// + /// Ensure that nesting of controllers is possible and + /// routing still works as expected. + /// + [TestMethod] + public async Task TestInner() + { + using var runner = Setup(); + + using var response = await runner.GetResponseAsync("/r/inner-controller/1/2/inner-controller/3/4/appenders/5/6/"); + + Assert.AreEqual("/r/inner-controller/1/2/inner-controller/3/4/appenders/5/6/", await response.GetContent()); + } + + [TestMethod] + public async Task TestRoutingToController() + { + using var runner = Setup(); + + using var response = await runner.GetResponseAsync("/r/do-something-with-controller/"); + + Assert.AreEqual("/r/", new Uri(response.GetHeader("Location")!).AbsolutePath); + } + + [TestMethod] + public async Task TestRoutingToIndex() + { + using var runner = Setup(); + + using var response = await runner.GetResponseAsync("/r/do-something-with-index/"); + + Assert.AreEqual("/r/", new Uri(response.GetHeader("Location")!).AbsolutePath); + } + + [TestMethod] + public async Task TestRoutingToParent() + { + using var runner = Setup(); + + using var response = await runner.GetResponseAsync("/r/do-something-with-parent/"); + + Assert.AreEqual("/test", new Uri(response.GetHeader("Location")!).AbsolutePath); + } + + [TestMethod] + public async Task TestRoutingToAppender() + { + using var runner = Setup(); + + using var response = await runner.GetResponseAsync("/r/do-something-with-appenders/"); + + Assert.AreEqual("/r/appenders/1/2/", new Uri(response.GetHeader("Location")!).AbsolutePath); + } + + #endregion + + #region Helpers + + private TestHost Setup() + { + var layout = Layout.Create() + .AddController("r") + .Add(Content.From(Resource.FromString("Blubb"))); + + return TestHost.Run(layout); + } + + #endregion + + } + +} diff --git a/Testing/Modules/Controllers/SeoTests.cs b/Testing/Acceptance/Modules/Controllers/SeoTests.cs similarity index 83% rename from Testing/Modules/Controllers/SeoTests.cs rename to Testing/Acceptance/Modules/Controllers/SeoTests.cs index d5fbe786..e10800b6 100644 --- a/Testing/Modules/Controllers/SeoTests.cs +++ b/Testing/Acceptance/Modules/Controllers/SeoTests.cs @@ -1,69 +1,69 @@ -using System.Net; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.Controllers; -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Layouting; - -namespace GenHTTP.Testing.Acceptance.Modules.Controllers -{ - - [TestClass] - public sealed class SeoTests - { - - #region Supporting data structures - - public sealed class TestController - { - - public IHandlerBuilder Action() - { - return Content.From(Resource.FromString("Action")); - } - - [ControllerAction(RequestMethod.DELETE)] - public IHandlerBuilder Action([FromPath] int id) - { - return Content.From(Resource.FromString(id.ToString())); - } - - } - - #endregion - - #region Tests - - /// - /// As the developer of a web application, I don't want the MCV framework to generate duplicate content - /// by accepting upper case letters in action names. - /// - [TestMethod] - public async Task TestActionCasingMatters() - { - using var runner = GetRunner(); - - using var response = await runner.GetResponse("/t/Action/"); - - await response.AssertStatusAsync(HttpStatusCode.NotFound); - } - - #endregion - - #region Helpers - - private TestRunner GetRunner() - { - return TestRunner.Run(Layout.Create().AddController("t")); - } - - #endregion - - } - -} +using System.Net; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Controllers; +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Layouting; + +namespace GenHTTP.Testing.Acceptance.Modules.Controllers +{ + + [TestClass] + public sealed class SeoTests + { + + #region Supporting data structures + + public sealed class TestController + { + + public IHandlerBuilder Action() + { + return Content.From(Resource.FromString("Action")); + } + + [ControllerAction(RequestMethod.DELETE)] + public IHandlerBuilder Action([FromPath] int id) + { + return Content.From(Resource.FromString(id.ToString())); + } + + } + + #endregion + + #region Tests + + /// + /// As the developer of a web application, I don't want the MCV framework to generate duplicate content + /// by accepting upper case letters in action names. + /// + [TestMethod] + public async Task TestActionCasingMatters() + { + using var runner = GetRunner(); + + using var response = await runner.GetResponseAsync("/t/Action/"); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + #endregion + + #region Helpers + + private TestHost GetRunner() + { + return TestHost.Run(Layout.Create().AddController("t")); + } + + #endregion + + } + +} diff --git a/Testing/Modules/Conversion/ErrorHandlingTests.cs b/Testing/Acceptance/Modules/Conversion/ErrorHandlingTests.cs similarity index 89% rename from Testing/Modules/Conversion/ErrorHandlingTests.cs rename to Testing/Acceptance/Modules/Conversion/ErrorHandlingTests.cs index 1e27e6cb..d264a7a1 100644 --- a/Testing/Modules/Conversion/ErrorHandlingTests.cs +++ b/Testing/Acceptance/Modules/Conversion/ErrorHandlingTests.cs @@ -27,7 +27,7 @@ public async Task UndeserializableBodyReturnsWithBadRequest() var inline = Inline.Create() .Post("/t", (MyEntity entity) => entity.Data); - using var runner = TestRunner.Run(inline); + using var runner = TestHost.Run(inline); using var request = runner.GetRequest("/t"); @@ -36,7 +36,7 @@ public async Task UndeserializableBodyReturnsWithBadRequest() request.Content = new StringContent("I cannot be deserialized", null, "application/json"); request.Content.Headers.ContentType = new("application/json"); - using var response = await runner.GetResponse(request); + using var response = await runner.GetResponseAsync(request); await response.AssertStatusAsync(HttpStatusCode.BadRequest); } diff --git a/Testing/Modules/ConversionTests.cs b/Testing/Acceptance/Modules/ConversionTests.cs similarity index 93% rename from Testing/Modules/ConversionTests.cs rename to Testing/Acceptance/Modules/ConversionTests.cs index 581bf9de..d9bda1c3 100644 --- a/Testing/Modules/ConversionTests.cs +++ b/Testing/Acceptance/Modules/ConversionTests.cs @@ -1,140 +1,140 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; - -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; -using GenHTTP.Modules.Conversion.Providers; -using GenHTTP.Modules.Conversion.Providers.Forms; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Modules.Conversion -{ - - [TestClass] - public sealed class ConversionTests - { - - #region Supporting data structures - -#pragma warning disable CS0649 - - private class FieldData - { - - public int field; - - } - - private class PropertyData - { - - public int Field { get; set; } - - } - - private class TypedData - { - - public bool Boolean { get; set; } - - public double Double { get; set; } - - public string? String { get; set; } - - public EnumData Enum { get; set; } - - } - - private enum EnumData { One, Two } - -#pragma warning restore CS0649 - - private class ConversionHandlerBuilder : IHandlerBuilder - { - private readonly ISerializationFormat _Format; - - public ConversionHandlerBuilder(ISerializationFormat format) - { - _Format = format; - } - - public IHandler Build(IHandler parent) - { - return new ConversionHandler(_Format, parent); - } - - } - - private class ConversionHandler : IHandler - { - - public IHandler Parent { get; } - - public ISerializationFormat Format { get; } - - public ValueTask PrepareAsync() => ValueTask.CompletedTask; - - public ConversionHandler(ISerializationFormat format, IHandler parent) - { - Parent = parent; - Format = format; - } - - public IAsyncEnumerable GetContentAsync(IRequest request) => AsyncEnumerable.Empty(); - - public async ValueTask HandleAsync(IRequest request) - { - var obj = await Format.DeserializeAsync(request.Content!, typeof(T)); - - return (await Format.SerializeAsync(request, obj!)).Build(); - } - - } - - #endregion - - #region Tests - - [TestMethod] - public async Task TestFormFieldSerialization() => await RunTest("field=20"); - - [TestMethod] - public async Task TestFormPropertySerialization() => await RunTest("Field=20"); - - [TestMethod] - public async Task TestFormTypeSerialization() => await RunTest("Boolean=1&Double=0.2&String=Test&Enum=One"); - - [TestMethod] - public async Task TestFormDefaultValueSerialization() => await RunTest("Boolean=0&Double=0&String=&Enum=One"); - - #endregion - - #region Helpers - - private static async Task RunTest(string serialized) where TFormat : ISerializationFormat, new() - { - var handler = new ConversionHandlerBuilder(new TFormat()); - - using var runner = TestRunner.Run(handler); - - var request = runner.GetRequest(); - - request.Method = HttpMethod.Post; - request.Content = new StringContent(serialized); - - using var response = await runner.GetResponse(request); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual(serialized, await response.GetContent()); - } - - #endregion - - } - -} +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Conversion.Providers; +using GenHTTP.Modules.Conversion.Providers.Forms; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.Conversion +{ + + [TestClass] + public sealed class ConversionTests + { + + #region Supporting data structures + +#pragma warning disable CS0649 + + private class FieldData + { + + public int field; + + } + + private class PropertyData + { + + public int Field { get; set; } + + } + + private class TypedData + { + + public bool Boolean { get; set; } + + public double Double { get; set; } + + public string? String { get; set; } + + public EnumData Enum { get; set; } + + } + + private enum EnumData { One, Two } + +#pragma warning restore CS0649 + + private class ConversionHandlerBuilder : IHandlerBuilder + { + private readonly ISerializationFormat _Format; + + public ConversionHandlerBuilder(ISerializationFormat format) + { + _Format = format; + } + + public IHandler Build(IHandler parent) + { + return new ConversionHandler(_Format, parent); + } + + } + + private class ConversionHandler : IHandler + { + + public IHandler Parent { get; } + + public ISerializationFormat Format { get; } + + public ValueTask PrepareAsync() => ValueTask.CompletedTask; + + public ConversionHandler(ISerializationFormat format, IHandler parent) + { + Parent = parent; + Format = format; + } + + public IAsyncEnumerable GetContentAsync(IRequest request) => AsyncEnumerable.Empty(); + + public async ValueTask HandleAsync(IRequest request) + { + var obj = await Format.DeserializeAsync(request.Content!, typeof(T)); + + return (await Format.SerializeAsync(request, obj!)).Build(); + } + + } + + #endregion + + #region Tests + + [TestMethod] + public async Task TestFormFieldSerialization() => await RunTest("field=20"); + + [TestMethod] + public async Task TestFormPropertySerialization() => await RunTest("Field=20"); + + [TestMethod] + public async Task TestFormTypeSerialization() => await RunTest("Boolean=1&Double=0.2&String=Test&Enum=One"); + + [TestMethod] + public async Task TestFormDefaultValueSerialization() => await RunTest("Boolean=0&Double=0&String=&Enum=One"); + + #endregion + + #region Helpers + + private static async Task RunTest(string serialized) where TFormat : ISerializationFormat, new() + { + var handler = new ConversionHandlerBuilder(new TFormat()); + + using var runner = TestHost.Run(handler); + + var request = runner.GetRequest(); + + request.Method = HttpMethod.Post; + request.Content = new StringContent(serialized); + + using var response = await runner.GetResponseAsync(request); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual(serialized, await response.GetContent()); + } + + #endregion + + } + +} diff --git a/Testing/Modules/ErrorHandling/CustomErrorMapperTests.cs b/Testing/Acceptance/Modules/ErrorHandling/CustomErrorMapperTests.cs similarity index 86% rename from Testing/Modules/ErrorHandling/CustomErrorMapperTests.cs rename to Testing/Acceptance/Modules/ErrorHandling/CustomErrorMapperTests.cs index 49ae7720..a3952f75 100644 --- a/Testing/Modules/ErrorHandling/CustomErrorMapperTests.cs +++ b/Testing/Acceptance/Modules/ErrorHandling/CustomErrorMapperTests.cs @@ -1,83 +1,83 @@ -using System; -using System.Threading.Tasks; - -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.ErrorHandling; -using GenHTTP.Modules.Functional; -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Layouting; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Modules.ErrorHandling -{ - - [TestClass] - public sealed class CustomErrorMapperTests - { - - #region Supporting data structures - - public class ErrorLengthMapper : IErrorMapper - { - public ValueTask GetNotFound(IRequest request, IHandler handler) - { - return new - ( - request.Respond() - .Status(ResponseStatus.NotFound) - .Content(Resource.FromString("404").Build()) - .Build() - ); - } - - public ValueTask Map(IRequest request, IHandler handler, Exception error) - { - return new - ( - request.Respond() - .Status(ResponseStatus.NotFound) - .Content(Resource.FromString($"{error.Message.Length}").Build()) - .Build() - ); - } - } - - #endregion - - #region Tests - - [TestMethod] - public async Task Test404Mapped() - { - var test = Layout.Create() - .Add(ErrorHandler.From(new ErrorLengthMapper())); - - using var runner = TestRunner.Run(test); - - using var response = await runner.GetResponse("/"); - Assert.AreEqual("404", await response.GetContent()); - } - - [TestMethod] - public async Task TestExceptionMapped() - { - Action thrower = () => throw new Exception("Nope!"); - - var test = Layout.Create() - .Add(Inline.Create().Get(thrower)) - .Add(ErrorHandler.From(new ErrorLengthMapper())); - - using var runner = TestRunner.Run(test); - - using var response = await runner.GetResponse("/"); - Assert.AreEqual("5", await response.GetContent()); - } - - #endregion - - } - -} +using System; +using System.Threading.Tasks; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.ErrorHandling; +using GenHTTP.Modules.Functional; +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Layouting; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.ErrorHandling +{ + + [TestClass] + public sealed class CustomErrorMapperTests + { + + #region Supporting data structures + + public class ErrorLengthMapper : IErrorMapper + { + public ValueTask GetNotFound(IRequest request, IHandler handler) + { + return new + ( + request.Respond() + .Status(ResponseStatus.NotFound) + .Content(Resource.FromString("404").Build()) + .Build() + ); + } + + public ValueTask Map(IRequest request, IHandler handler, Exception error) + { + return new + ( + request.Respond() + .Status(ResponseStatus.NotFound) + .Content(Resource.FromString($"{error.Message.Length}").Build()) + .Build() + ); + } + } + + #endregion + + #region Tests + + [TestMethod] + public async Task Test404Mapped() + { + var test = Layout.Create() + .Add(ErrorHandler.From(new ErrorLengthMapper())); + + using var runner = TestHost.Run(test); + + using var response = await runner.GetResponseAsync("/"); + Assert.AreEqual("404", await response.GetContent()); + } + + [TestMethod] + public async Task TestExceptionMapped() + { + Action thrower = () => throw new Exception("Nope!"); + + var test = Layout.Create() + .Add(Inline.Create().Get(thrower)) + .Add(ErrorHandler.From(new ErrorLengthMapper())); + + using var runner = TestHost.Run(test); + + using var response = await runner.GetResponseAsync("/"); + Assert.AreEqual("5", await response.GetContent()); + } + + #endregion + + } + +} diff --git a/Testing/Modules/Functional/InlineTests.cs b/Testing/Acceptance/Modules/Functional/InlineTests.cs similarity index 59% rename from Testing/Modules/Functional/InlineTests.cs rename to Testing/Acceptance/Modules/Functional/InlineTests.cs index 0ec51e1b..027378cf 100644 --- a/Testing/Modules/Functional/InlineTests.cs +++ b/Testing/Acceptance/Modules/Functional/InlineTests.cs @@ -1,218 +1,218 @@ -using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Http.Json; -using System.Text; -using System.Threading.Tasks; - -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.Basics; -using GenHTTP.Modules.Functional; -using GenHTTP.Modules.IO; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Modules.Functional -{ - - [TestClass] - public sealed class InlineTests - { - - #region Supporting data structures - - public record MyClass(string String, int Int, double Double); - - private enum EnumData { One, Two } - - #endregion - - [TestMethod] - public async Task TestGetRoot() - { - using var host = TestRunner.Run(Inline.Create().Get(() => 42)); - - using var response = await host.GetResponse(); - - Assert.AreEqual("42", await response.GetContent()); - } - - [TestMethod] - public async Task TestGetPath() - { - using var host = TestRunner.Run(Inline.Create().Get("/blubb", () => 42)); - - using var response = await host.GetResponse("/blubb"); - - Assert.AreEqual("42", await response.GetContent()); - } - - [TestMethod] - public async Task TestGetQueryParam() - { - using var host = TestRunner.Run(Inline.Create().Get((int param) => param + 1)); - - using var response = await host.GetResponse("/?param=41"); - - Assert.AreEqual("42", await response.GetContent()); - } - - [TestMethod] - public async Task TestGetEmptyBooleanQueryParam() - { - using var host = TestRunner.Run(Inline.Create().Get((bool param) => param)); - - using var response = await host.GetResponse("/?param="); - - Assert.AreEqual("False", await response.GetContent()); - } - - [TestMethod] - public async Task TestGetEmptyDoubleQueryParam() - { - using var host = TestRunner.Run(Inline.Create().Get((double param) => param)); - - using var response = await host.GetResponse("/?param="); - - Assert.AreEqual("0", await response.GetContent()); - } - - [TestMethod] - public async Task TestGetEmptyStringQueryParam() - { - using var host = TestRunner.Run(Inline.Create().Get((string param) => param)); - - using var response = await host.GetResponse("/?param="); - - Assert.AreEqual("", await response.GetContent()); - } - - [TestMethod] - public async Task TestGetEmptyEnumQueryParam() - { - using var host = TestRunner.Run(Inline.Create().Get((EnumData param) => param)); - - using var response = await host.GetResponse("/?param="); - - Assert.AreEqual("One", await response.GetContent()); - } - - [TestMethod] - public async Task TestGetPathParam() - { - using var host = TestRunner.Run(Inline.Create().Get(":param", (int param) => param + 1)); - - using var response = await host.GetResponse("/41"); - - Assert.AreEqual("42", await response.GetContent()); - } - - [TestMethod] - public async Task TestNotFound() - { - using var host = TestRunner.Run(Inline.Create().Get(() => 42)); - - using var response = await host.GetResponse("/nope"); - - await response.AssertStatusAsync(HttpStatusCode.NotFound); - } - - [TestMethod] - public async Task TestRaw() - { - using var host = TestRunner.Run(Inline.Create().Get((IRequest request) => - { - return request.Respond() - .Status(ResponseStatus.OK) - .Content("42"); - })); - - using var response = await host.GetResponse(); - - Assert.AreEqual("42", await response.GetContent()); - } - - [TestMethod] - public async Task TestStream() - { - using var host = TestRunner.Run(Inline.Create().Get(() => new MemoryStream(Encoding.UTF8.GetBytes("42")))); - - using var response = await host.GetResponse(); - - Assert.AreEqual("42", await response.GetContent()); - } - - [TestMethod] - public async Task TestJson() - { - using var host = TestRunner.Run(Inline.Create().Get(() => new MyClass("42", 42, 42.0))); - - using var response = await host.GetResponse(); - - Assert.AreEqual("{\"string\":\"42\",\"int\":42,\"double\":42}", await response.GetContent()); - } - - [TestMethod] - public async Task TestPostJson() - { - using var host = TestRunner.Run(Inline.Create().Post((MyClass input) => input)); - - var request = host.GetRequest(); - - request.Method = HttpMethod.Post; - - request.Content = new StringContent("{\"string\":\"42\",\"int\":42,\"double\":42}", Encoding.UTF8, "application/json"); - - using var response = await host.GetResponse(request); - - Assert.AreEqual("{\"string\":\"42\",\"int\":42,\"double\":42}", await response.GetContent()); - } - - [TestMethod] - public async Task TestAsync() - { - using var host = TestRunner.Run(Inline.Create().Get(async () => - { - var stream = new MemoryStream(); - - await stream.WriteAsync(Encoding.UTF8.GetBytes("42")); - - stream.Seek(0, SeekOrigin.Begin); - - return stream; - })); - - using var response = await host.GetResponse(); - - Assert.AreEqual("42", await response.GetContent()); - } - - [TestMethod] - public async Task TestHandlerBuilder() - { - var target = "https://www.google.de/"; - - using var host = TestRunner.Run(Inline.Create().Get(() => Redirect.To(target))); - - using var response = await host.GetResponse(); - - Assert.AreEqual(target, response.GetHeader("Location")); - } - - [TestMethod] - public async Task TestHandler() - { - var target = "https://www.google.de/"; - - using var host = TestRunner.Run(Inline.Create().Get((IHandler parent) => Redirect.To(target).Build(parent))); - - using var response = await host.GetResponse(); - - Assert.AreEqual(target, response.GetHeader("Location")); - } - - } - -} +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text; +using System.Threading.Tasks; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Basics; +using GenHTTP.Modules.Functional; +using GenHTTP.Modules.IO; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.Functional +{ + + [TestClass] + public sealed class InlineTests + { + + #region Supporting data structures + + public record MyClass(string String, int Int, double Double); + + private enum EnumData { One, Two } + + #endregion + + [TestMethod] + public async Task TestGetRoot() + { + using var host = TestHost.Run(Inline.Create().Get(() => 42)); + + using var response = await host.GetResponseAsync(); + + Assert.AreEqual("42", await response.GetContent()); + } + + [TestMethod] + public async Task TestGetPath() + { + using var host = TestHost.Run(Inline.Create().Get("/blubb", () => 42)); + + using var response = await host.GetResponseAsync("/blubb"); + + Assert.AreEqual("42", await response.GetContent()); + } + + [TestMethod] + public async Task TestGetQueryParam() + { + using var host = TestHost.Run(Inline.Create().Get((int param) => param + 1)); + + using var response = await host.GetResponseAsync("/?param=41"); + + Assert.AreEqual("42", await response.GetContent()); + } + + [TestMethod] + public async Task TestGetEmptyBooleanQueryParam() + { + using var host = TestHost.Run(Inline.Create().Get((bool param) => param)); + + using var response = await host.GetResponseAsync("/?param="); + + Assert.AreEqual("False", await response.GetContent()); + } + + [TestMethod] + public async Task TestGetEmptyDoubleQueryParam() + { + using var host = TestHost.Run(Inline.Create().Get((double param) => param)); + + using var response = await host.GetResponseAsync("/?param="); + + Assert.AreEqual("0", await response.GetContent()); + } + + [TestMethod] + public async Task TestGetEmptyStringQueryParam() + { + using var host = TestHost.Run(Inline.Create().Get((string param) => param)); + + using var response = await host.GetResponseAsync("/?param="); + + Assert.AreEqual("", await response.GetContent()); + } + + [TestMethod] + public async Task TestGetEmptyEnumQueryParam() + { + using var host = TestHost.Run(Inline.Create().Get((EnumData param) => param)); + + using var response = await host.GetResponseAsync("/?param="); + + Assert.AreEqual("One", await response.GetContent()); + } + + [TestMethod] + public async Task TestGetPathParam() + { + using var host = TestHost.Run(Inline.Create().Get(":param", (int param) => param + 1)); + + using var response = await host.GetResponseAsync("/41"); + + Assert.AreEqual("42", await response.GetContent()); + } + + [TestMethod] + public async Task TestNotFound() + { + using var host = TestHost.Run(Inline.Create().Get(() => 42)); + + using var response = await host.GetResponseAsync("/nope"); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + [TestMethod] + public async Task TestRaw() + { + using var host = TestHost.Run(Inline.Create().Get((IRequest request) => + { + return request.Respond() + .Status(ResponseStatus.OK) + .Content("42"); + })); + + using var response = await host.GetResponseAsync(); + + Assert.AreEqual("42", await response.GetContent()); + } + + [TestMethod] + public async Task TestStream() + { + using var host = TestHost.Run(Inline.Create().Get(() => new MemoryStream(Encoding.UTF8.GetBytes("42")))); + + using var response = await host.GetResponseAsync(); + + Assert.AreEqual("42", await response.GetContent()); + } + + [TestMethod] + public async Task TestJson() + { + using var host = TestHost.Run(Inline.Create().Get(() => new MyClass("42", 42, 42.0))); + + using var response = await host.GetResponseAsync(); + + Assert.AreEqual("{\"string\":\"42\",\"int\":42,\"double\":42}", await response.GetContent()); + } + + [TestMethod] + public async Task TestPostJson() + { + using var host = TestHost.Run(Inline.Create().Post((MyClass input) => input)); + + var request = host.GetRequest(); + + request.Method = HttpMethod.Post; + + request.Content = new StringContent("{\"string\":\"42\",\"int\":42,\"double\":42}", Encoding.UTF8, "application/json"); + + using var response = await host.GetResponseAsync(request); + + Assert.AreEqual("{\"string\":\"42\",\"int\":42,\"double\":42}", await response.GetContent()); + } + + [TestMethod] + public async Task TestAsync() + { + using var host = TestHost.Run(Inline.Create().Get(async () => + { + var stream = new MemoryStream(); + + await stream.WriteAsync(Encoding.UTF8.GetBytes("42")); + + stream.Seek(0, SeekOrigin.Begin); + + return stream; + })); + + using var response = await host.GetResponseAsync(); + + Assert.AreEqual("42", await response.GetContent()); + } + + [TestMethod] + public async Task TestHandlerBuilder() + { + var target = "https://www.google.de/"; + + using var host = TestHost.Run(Inline.Create().Get(() => Redirect.To(target))); + + using var response = await host.GetResponseAsync(); + + Assert.AreEqual(target, response.GetHeader("Location")); + } + + [TestMethod] + public async Task TestHandler() + { + var target = "https://www.google.de/"; + + using var host = TestHost.Run(Inline.Create().Get((IHandler parent) => Redirect.To(target).Build(parent))); + + using var response = await host.GetResponseAsync(); + + Assert.AreEqual(target, response.GetHeader("Location")); + } + + } + +} diff --git a/Testing/Modules/IO/ChangeTrackingTests.cs b/Testing/Acceptance/Modules/IO/ChangeTrackingTests.cs similarity index 96% rename from Testing/Modules/IO/ChangeTrackingTests.cs rename to Testing/Acceptance/Modules/IO/ChangeTrackingTests.cs index 983ad70b..c2dee169 100644 --- a/Testing/Modules/IO/ChangeTrackingTests.cs +++ b/Testing/Acceptance/Modules/IO/ChangeTrackingTests.cs @@ -1,66 +1,66 @@ -using System.IO; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Modules.IO; - -using GenHTTP.Testing.Acceptance.Utilities; - -namespace GenHTTP.Testing.Acceptance.Modules.IO -{ - - [TestClass] - public sealed class ChangeTrackingTests - { - - [TestMethod] - public async Task TestChanges() - { - var file = Path.GetTempFileName(); - - try - { - await FileUtil.WriteTextAsync(file, "One"); - - var resource = Resource.FromFile(file) - .Build() - .Track(); - - using (var _ = await resource.GetContentAsync()) { } - - Assert.IsFalse(await resource.HasChanged()); - - // modification timestamp is in seconds on unix, so we need another length - await FileUtil.WriteTextAsync(file, "Three"); - - Assert.IsTrue(await resource.HasChanged()); - } - finally - { - try { File.Delete(file); } catch { /* nop */ } - } - } - - [TestMethod] - public async Task TestBuildWithTracking() - { - Assert.IsTrue(await Resource.FromAssembly("File.txt").BuildWithTracking().HasChanged()); - } - - [TestMethod] - public void TestMetaInformation() - { - var resource = Resource.FromAssembly("File.txt").Build(); - - var tracked = resource.Track(); - - Assert.AreEqual(resource.Name, tracked.Name); - Assert.AreEqual(resource.Length, tracked.Length); - Assert.AreEqual(resource.Modified, tracked.Modified); - Assert.AreEqual(resource.ContentType, tracked.ContentType); - } - - } - -} +using System.IO; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Modules.IO; + +using GenHTTP.Testing.Acceptance.Utilities; + +namespace GenHTTP.Testing.Acceptance.Modules.IO +{ + + [TestClass] + public sealed class ChangeTrackingTests + { + + [TestMethod] + public async Task TestChanges() + { + var file = Path.GetTempFileName(); + + try + { + await FileUtil.WriteTextAsync(file, "One"); + + var resource = Resource.FromFile(file) + .Build() + .Track(); + + using (var _ = await resource.GetContentAsync()) { } + + Assert.IsFalse(await resource.HasChanged()); + + // modification timestamp is in seconds on unix, so we need another length + await FileUtil.WriteTextAsync(file, "Three"); + + Assert.IsTrue(await resource.HasChanged()); + } + finally + { + try { File.Delete(file); } catch { /* nop */ } + } + } + + [TestMethod] + public async Task TestBuildWithTracking() + { + Assert.IsTrue(await Resource.FromAssembly("File.txt").BuildWithTracking().HasChanged()); + } + + [TestMethod] + public void TestMetaInformation() + { + var resource = Resource.FromAssembly("File.txt").Build(); + + var tracked = resource.Track(); + + Assert.AreEqual(resource.Name, tracked.Name); + Assert.AreEqual(resource.Length, tracked.Length); + Assert.AreEqual(resource.Modified, tracked.Modified); + Assert.AreEqual(resource.ContentType, tracked.ContentType); + } + + } + +} diff --git a/Testing/Modules/IO/ContentTests.cs b/Testing/Acceptance/Modules/IO/ContentTests.cs similarity index 63% rename from Testing/Modules/IO/ContentTests.cs rename to Testing/Acceptance/Modules/IO/ContentTests.cs index 5a1cfd9e..d2ec1307 100644 --- a/Testing/Modules/IO/ContentTests.cs +++ b/Testing/Acceptance/Modules/IO/ContentTests.cs @@ -1,38 +1,38 @@ -using System.Net; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Modules.IO; - -namespace GenHTTP.Testing.Acceptance.Modules.IO -{ - - [TestClass] - public sealed class ContentTests - { - - [TestMethod] - public async Task TestContent() - { - using var runner = TestRunner.Run(Content.From(Resource.FromString("Hello World!"))); - - using var response = await runner.GetResponse(); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("Hello World!", await response.GetContent()); - } - - [TestMethod] - public async Task TestContentIgnoresRouting() - { - using var runner = TestRunner.Run(Content.From(Resource.FromString("Hello World!"))); - - using var response = await runner.GetResponse("/some/path"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - } - - } - -} +using System.Net; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Modules.IO; + +namespace GenHTTP.Testing.Acceptance.Modules.IO +{ + + [TestClass] + public sealed class ContentTests + { + + [TestMethod] + public async Task TestContent() + { + using var runner = TestHost.Run(Content.From(Resource.FromString("Hello World!"))); + + using var response = await runner.GetResponseAsync(); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("Hello World!", await response.GetContent()); + } + + [TestMethod] + public async Task TestContentIgnoresRouting() + { + using var runner = TestHost.Run(Content.From(Resource.FromString("Hello World!"))); + + using var response = await runner.GetResponseAsync("/some/path"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + } + + } + +} diff --git a/Testing/Modules/IO/DownloadTests.cs b/Testing/Acceptance/Modules/IO/DownloadTests.cs similarity index 72% rename from Testing/Modules/IO/DownloadTests.cs rename to Testing/Acceptance/Modules/IO/DownloadTests.cs index 1ece7c1b..2c697373 100644 --- a/Testing/Modules/IO/DownloadTests.cs +++ b/Testing/Acceptance/Modules/IO/DownloadTests.cs @@ -1,100 +1,100 @@ -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Layouting; - -namespace GenHTTP.Testing.Acceptance.Modules.IO -{ - - [TestClass] - public sealed class DownloadTests - { - - [TestMethod] - public async Task TestDownload() - { - using var runner = TestRunner.Run(Download.From(Resource.FromAssembly("File.txt"))); - - using var response = await runner.GetResponse(); - - await response.AssertStatusAsync(HttpStatusCode.OK); - - Assert.AreEqual("This is text!", await response.GetContent()); - Assert.AreEqual("text/plain", response.GetContentHeader("Content-Type")); - } - - [TestMethod] - public async Task TestDownloadDoesNotAcceptRouting() - { - var layout = Layout.Create() - .Add("file.txt", Download.From(Resource.FromAssembly("File.txt"))); - - using var runner = TestRunner.Run(layout); - - using var response = await runner.GetResponse("/file.txt/blubb"); - - await response.AssertStatusAsync(HttpStatusCode.NotFound); - } - - [TestMethod] - public async Task DownloadsCannotBeModified() - { - var download = Download.From(Resource.FromAssembly("File.txt")); - - using var runner = TestRunner.Run(download); - - var request = runner.GetRequest(); - - request.Method = HttpMethod.Put; - request.Content = new StringContent("Hello World!", Encoding.UTF8, "text/plain"); - - using var response = await runner.GetResponse(request); - - await response.AssertStatusAsync(HttpStatusCode.MethodNotAllowed); - } - - [TestMethod] - public async Task TestFileName() - { - var download = Download.From(Resource.FromAssembly("File.txt")) - .FileName("myfile.txt"); - - using var runner = TestRunner.Run(download); - - using var response = await runner.GetResponse(); - - Assert.AreEqual("attachment; filename=\"myfile.txt\"", response.GetContentHeader("Content-Disposition")); - } - - [TestMethod] - public async Task TestNoFileName() - { - var download = Download.From(Resource.FromAssembly("File.txt")); - - using var runner = TestRunner.Run(download); - - using var response = await runner.GetResponse(); - - Assert.AreEqual("attachment", response.GetContentHeader("Content-Disposition")); - } - - [TestMethod] - public async Task TestFileNameFromResource() - { - var download = Download.From(Resource.FromAssembly("File.txt").Name("myfile.txt")); - - using var runner = TestRunner.Run(download); - - using var response = await runner.GetResponse(); - - Assert.AreEqual("attachment; filename=\"myfile.txt\"", response.GetContentHeader("Content-Disposition")); - } - - } - -} +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Layouting; + +namespace GenHTTP.Testing.Acceptance.Modules.IO +{ + + [TestClass] + public sealed class DownloadTests + { + + [TestMethod] + public async Task TestDownload() + { + using var runner = TestHost.Run(Download.From(Resource.FromAssembly("File.txt"))); + + using var response = await runner.GetResponseAsync(); + + await response.AssertStatusAsync(HttpStatusCode.OK); + + Assert.AreEqual("This is text!", await response.GetContent()); + Assert.AreEqual("text/plain", response.GetContentHeader("Content-Type")); + } + + [TestMethod] + public async Task TestDownloadDoesNotAcceptRouting() + { + var layout = Layout.Create() + .Add("file.txt", Download.From(Resource.FromAssembly("File.txt"))); + + using var runner = TestHost.Run(layout); + + using var response = await runner.GetResponseAsync("/file.txt/blubb"); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + [TestMethod] + public async Task DownloadsCannotBeModified() + { + var download = Download.From(Resource.FromAssembly("File.txt")); + + using var runner = TestHost.Run(download); + + var request = runner.GetRequest(); + + request.Method = HttpMethod.Put; + request.Content = new StringContent("Hello World!", Encoding.UTF8, "text/plain"); + + using var response = await runner.GetResponseAsync(request); + + await response.AssertStatusAsync(HttpStatusCode.MethodNotAllowed); + } + + [TestMethod] + public async Task TestFileName() + { + var download = Download.From(Resource.FromAssembly("File.txt")) + .FileName("myfile.txt"); + + using var runner = TestHost.Run(download); + + using var response = await runner.GetResponseAsync(); + + Assert.AreEqual("attachment; filename=\"myfile.txt\"", response.GetContentHeader("Content-Disposition")); + } + + [TestMethod] + public async Task TestNoFileName() + { + var download = Download.From(Resource.FromAssembly("File.txt")); + + using var runner = TestHost.Run(download); + + using var response = await runner.GetResponseAsync(); + + Assert.AreEqual("attachment", response.GetContentHeader("Content-Disposition")); + } + + [TestMethod] + public async Task TestFileNameFromResource() + { + var download = Download.From(Resource.FromAssembly("File.txt").Name("myfile.txt")); + + using var runner = TestHost.Run(download); + + using var response = await runner.GetResponseAsync(); + + Assert.AreEqual("attachment; filename=\"myfile.txt\"", response.GetContentHeader("Content-Disposition")); + } + + } + +} diff --git a/Testing/Modules/IO/RangeTests.cs b/Testing/Acceptance/Modules/IO/RangeTests.cs similarity index 91% rename from Testing/Modules/IO/RangeTests.cs rename to Testing/Acceptance/Modules/IO/RangeTests.cs index 706ff4d1..bb043521 100644 --- a/Testing/Modules/IO/RangeTests.cs +++ b/Testing/Acceptance/Modules/IO/RangeTests.cs @@ -1,178 +1,180 @@ -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using GenHTTP.Modules.IO; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Modules.IO -{ - - [TestClass] - public class RangeTests - { - private const string CONTENT = "0123456789"; - - [TestMethod] - public async Task TestRangesAreOptional() - { - using var response = await GetResponse(null); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual(CONTENT, await response.GetContent()); - } - - [TestMethod] - public async Task TestFullRangeIsSatisfied() - { - using var response = await GetResponse("bytes=1-8"); - - await response.AssertStatusAsync(HttpStatusCode.PartialContent); - Assert.AreEqual("12345678", await response.GetContent()); - Assert.AreEqual("bytes 1-8/10", response.GetContentHeader("Content-Range")); - } - - [TestMethod] - public async Task TestRangeFromStartIsSatisfied() - { - using var response = await GetResponse("bytes=4-"); - - await response.AssertStatusAsync(HttpStatusCode.PartialContent); - Assert.AreEqual("456789", await response.GetContent()); - Assert.AreEqual("bytes 4-9/10", response.GetContentHeader("Content-Range")); - } - - [TestMethod] - public async Task TestRangeFromEndIsSatisfied() - { - using var response = await GetResponse("bytes=-4"); - - await response.AssertStatusAsync(HttpStatusCode.PartialContent); - Assert.AreEqual("6789", await response.GetContent()); - Assert.AreEqual("bytes 6-9/10", response.GetContentHeader("Content-Range")); - } - - [TestMethod] - public async Task TestSingleRangeIsSatisfied() - { - using var response = await GetResponse("bytes=1-1"); - - await response.AssertStatusAsync(HttpStatusCode.PartialContent); - Assert.AreEqual("1", await response.GetContent()); - Assert.AreEqual("bytes 1-1/10", response.GetContentHeader("Content-Range")); - } - - [TestMethod] - public async Task TestFullRangeNotSatisfied() - { - using var response = await GetResponse("bytes=9-13"); - - await response.AssertStatusAsync(HttpStatusCode.RequestedRangeNotSatisfiable); - Assert.AreEqual("bytes */10", response.GetContentHeader("Content-Range")); - } - - [TestMethod] - public async Task TestRangeFromStartNotSatisfied() - { - using var response = await GetResponse("bytes=12-"); - - await response.AssertStatusAsync(HttpStatusCode.RequestedRangeNotSatisfiable); - Assert.AreEqual("bytes */10", response.GetContentHeader("Content-Range")); - } - - [TestMethod] - public async Task TestRangeFromEndNotSatisfied() - { - using var response = await GetResponse("bytes=-12"); - - await response.AssertStatusAsync(HttpStatusCode.RequestedRangeNotSatisfiable); - Assert.AreEqual("bytes */10", response.GetContentHeader("Content-Range")); - } - - [TestMethod] - public async Task TestMultipleRangesNotSatisfied() - { - using var response = await GetResponse("bytes=1-2,3-4"); - - await response.AssertStatusAsync(HttpStatusCode.RequestedRangeNotSatisfiable); - Assert.AreEqual("bytes */10", response.GetContentHeader("Content-Range")); - } - - [TestMethod] - public async Task TestOneBasedIndexDoesNotWork() - { - using var response = await GetResponse("bytes=1-10"); - - await response.AssertStatusAsync(HttpStatusCode.RequestedRangeNotSatisfiable); - Assert.AreEqual("bytes */10", response.GetContentHeader("Content-Range")); - } - - [TestMethod] - public async Task TestHeadRequest() - { - using var response = await GetResponse("bytes=1-8", HttpMethod.Head); - - await response.AssertStatusAsync(HttpStatusCode.PartialContent); - - Assert.AreEqual("bytes 1-8/10", response.GetContentHeader("Content-Range")); - Assert.AreEqual("8", response.GetContentHeader("Content-Length")); - - Assert.AreEqual("bytes", response.GetHeader("Accept-Ranges")); - } - - [TestMethod] - public async Task TestRangesIgnoredOnPostRequests() - { - using var response = await GetResponse("bytes=1-8", HttpMethod.Post); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual(CONTENT, await response.GetContent()); - } - - [TestMethod] - public async Task TestRangesAreTaggedDifferently() - { - using var withRange = await GetResponse("bytes=1-8"); - using var withoutRange = await GetResponse(null); - - Assert.AreNotEqual(withRange.GetHeader("ETag"), withoutRange.GetHeader("ETag")); - } - - [TestMethod] - public async Task TestAddSupportForSingleFile() - { - var download = Download.From(Resource.FromString("Hello World!")) - .AddRangeSupport(); - - using var runner = TestRunner.Run(download); - - using var response = await runner.GetResponse(); - - Assert.AreEqual("bytes", response.GetHeader("Accept-Ranges")); - } - - private static async Task GetResponse(string? requestedRange, HttpMethod? method = null) - { - using var runner = GetRunner(); - - var request = runner.GetRequest(method: method ?? HttpMethod.Get); - - if (requestedRange != null) - { - request.Headers.Add("Range", requestedRange); - } - - return await runner.GetResponse(request); - } - - private static TestRunner GetRunner() - { - var content = Content.From(Resource.FromString(CONTENT)); - - return TestRunner.Run(content, rangeSupport: true); - } - - } - -} +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +using GenHTTP.Modules.IO; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.IO +{ + + [TestClass] + public class RangeTests + { + private const string CONTENT = "0123456789"; + + [TestMethod] + public async Task TestRangesAreOptional() + { + using var response = await GetResponse(null); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual(CONTENT, await response.GetContent()); + } + + [TestMethod] + public async Task TestFullRangeIsSatisfied() + { + using var response = await GetResponse("bytes=1-8"); + + await response.AssertStatusAsync(HttpStatusCode.PartialContent); + Assert.AreEqual("12345678", await response.GetContent()); + Assert.AreEqual("bytes 1-8/10", response.GetContentHeader("Content-Range")); + } + + [TestMethod] + public async Task TestRangeFromStartIsSatisfied() + { + using var response = await GetResponse("bytes=4-"); + + await response.AssertStatusAsync(HttpStatusCode.PartialContent); + Assert.AreEqual("456789", await response.GetContent()); + Assert.AreEqual("bytes 4-9/10", response.GetContentHeader("Content-Range")); + } + + [TestMethod] + public async Task TestRangeFromEndIsSatisfied() + { + using var response = await GetResponse("bytes=-4"); + + await response.AssertStatusAsync(HttpStatusCode.PartialContent); + Assert.AreEqual("6789", await response.GetContent()); + Assert.AreEqual("bytes 6-9/10", response.GetContentHeader("Content-Range")); + } + + [TestMethod] + public async Task TestSingleRangeIsSatisfied() + { + using var response = await GetResponse("bytes=1-1"); + + await response.AssertStatusAsync(HttpStatusCode.PartialContent); + Assert.AreEqual("1", await response.GetContent()); + Assert.AreEqual("bytes 1-1/10", response.GetContentHeader("Content-Range")); + } + + [TestMethod] + public async Task TestFullRangeNotSatisfied() + { + using var response = await GetResponse("bytes=9-13"); + + await response.AssertStatusAsync(HttpStatusCode.RequestedRangeNotSatisfiable); + Assert.AreEqual("bytes */10", response.GetContentHeader("Content-Range")); + } + + [TestMethod] + public async Task TestRangeFromStartNotSatisfied() + { + using var response = await GetResponse("bytes=12-"); + + await response.AssertStatusAsync(HttpStatusCode.RequestedRangeNotSatisfiable); + Assert.AreEqual("bytes */10", response.GetContentHeader("Content-Range")); + } + + [TestMethod] + public async Task TestRangeFromEndNotSatisfied() + { + using var response = await GetResponse("bytes=-12"); + + await response.AssertStatusAsync(HttpStatusCode.RequestedRangeNotSatisfiable); + Assert.AreEqual("bytes */10", response.GetContentHeader("Content-Range")); + } + + [TestMethod] + public async Task TestMultipleRangesNotSatisfied() + { + using var response = await GetResponse("bytes=1-2,3-4"); + + await response.AssertStatusAsync(HttpStatusCode.RequestedRangeNotSatisfiable); + Assert.AreEqual("bytes */10", response.GetContentHeader("Content-Range")); + } + + [TestMethod] + public async Task TestOneBasedIndexDoesNotWork() + { + using var response = await GetResponse("bytes=1-10"); + + await response.AssertStatusAsync(HttpStatusCode.RequestedRangeNotSatisfiable); + Assert.AreEqual("bytes */10", response.GetContentHeader("Content-Range")); + } + + [TestMethod] + public async Task TestHeadRequest() + { + using var response = await GetResponse("bytes=1-8", HttpMethod.Head); + + await response.AssertStatusAsync(HttpStatusCode.PartialContent); + + Assert.AreEqual("bytes 1-8/10", response.GetContentHeader("Content-Range")); + Assert.AreEqual("8", response.GetContentHeader("Content-Length")); + + Assert.AreEqual("bytes", response.GetHeader("Accept-Ranges")); + } + + [TestMethod] + public async Task TestRangesIgnoredOnPostRequests() + { + using var response = await GetResponse("bytes=1-8", HttpMethod.Post); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual(CONTENT, await response.GetContent()); + } + + [TestMethod] + public async Task TestRangesAreTaggedDifferently() + { + using var withRange = await GetResponse("bytes=1-8"); + using var withoutRange = await GetResponse(null); + + Assert.AreNotEqual(withRange.GetHeader("ETag"), withoutRange.GetHeader("ETag")); + } + + [TestMethod] + public async Task TestAddSupportForSingleFile() + { + var download = Download.From(Resource.FromString("Hello World!")) + .AddRangeSupport(); + + using var runner = TestHost.Run(download); + + using var response = await runner.GetResponseAsync(); + + Assert.AreEqual("bytes", response.GetHeader("Accept-Ranges")); + } + + private static async Task GetResponse(string? requestedRange, HttpMethod? method = null) + { + using var runner = GetRunner(); + + var request = runner.GetRequest(method: method ?? HttpMethod.Get); + + if (requestedRange != null) + { + request.Headers.Add("Range", requestedRange); + } + + return await runner.GetResponseAsync(request); + } + + private static TestHost GetRunner() + { + var content = Content.From(Resource.FromString(CONTENT)); + + content.AddRangeSupport(); + + return TestHost.Run(content); + } + + } + +} diff --git a/Testing/Modules/IO/RangedStreamTests.cs b/Testing/Acceptance/Modules/IO/RangedStreamTests.cs similarity index 96% rename from Testing/Modules/IO/RangedStreamTests.cs rename to Testing/Acceptance/Modules/IO/RangedStreamTests.cs index ad00dad8..0042f50a 100644 --- a/Testing/Modules/IO/RangedStreamTests.cs +++ b/Testing/Acceptance/Modules/IO/RangedStreamTests.cs @@ -1,93 +1,93 @@ -using System; -using System.IO; -using System.Text; - -using GenHTTP.Modules.IO.Ranges; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Modules.IO -{ - - [TestClass] - public class RangedStreamTests - { - - [TestMethod] - public void TestFullRange() - { - Assert.AreEqual("0123456789", GetRange(0, 9, 0, 10)); - } - - [TestMethod] - public void TestNotAllWritten() - { - Assert.AreEqual("23", GetRange(0, 9, 2, 2)); - } - - [TestMethod] - public void TestRangeExtracted() - { - Assert.AreEqual("3456", GetRange(3, 6, 0, 10)); - } - - [TestMethod] - public void TestNothingToWrite() - { - Assert.AreEqual("", GetRange(12, 14, 0, 10)); - } - - [TestMethod] - public void TestEndOfLargeFile() - { - Assert.AreEqual("12345", GetRange(10_001, 10_005, 0, 10, position: 10_000)); - } - - [TestMethod] - public void TestSomewhereInLargeFile() - { - Assert.AreEqual("0123456789", GetRange(0, 10_000, 0, 10, position: 5000)); - } - - [TestMethod] - public void TestOutOfLargeFile() - { - Assert.AreEqual("", GetRange(0, 10_000, 0, 10, position: 15_000)); - } - - [TestMethod] - public void TestBasics() - { - using var stream = new RangedStream(new MemoryStream(), 0, 10); - - Assert.AreEqual(0, stream.Position); - Assert.AreEqual(10, stream.Length); - - Assert.IsTrue(stream.CanWrite); - - Assert.IsFalse(stream.CanRead); - Assert.IsFalse(stream.CanSeek); - - Assert.ThrowsException(() => stream.Read(Array.Empty(), 0, 1)); - - Assert.ThrowsException(() => stream.Seek(0, SeekOrigin.Begin)); - - Assert.ThrowsException(() => stream.SetLength(0)); - } - - private static string GetRange(ulong start, ulong end, int offset, int count, int position = 0) - { - using var target = new MemoryStream(); - - using var stream = new RangedStream(target, start, end); - - stream.Position = position; - - stream.Write(Encoding.ASCII.GetBytes("0123456789"), offset, count); - - return Encoding.ASCII.GetString(target.ToArray()); - } - - } - -} +using System; +using System.IO; +using System.Text; + +using GenHTTP.Modules.IO.Ranges; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.IO +{ + + [TestClass] + public class RangedStreamTests + { + + [TestMethod] + public void TestFullRange() + { + Assert.AreEqual("0123456789", GetRange(0, 9, 0, 10)); + } + + [TestMethod] + public void TestNotAllWritten() + { + Assert.AreEqual("23", GetRange(0, 9, 2, 2)); + } + + [TestMethod] + public void TestRangeExtracted() + { + Assert.AreEqual("3456", GetRange(3, 6, 0, 10)); + } + + [TestMethod] + public void TestNothingToWrite() + { + Assert.AreEqual("", GetRange(12, 14, 0, 10)); + } + + [TestMethod] + public void TestEndOfLargeFile() + { + Assert.AreEqual("12345", GetRange(10_001, 10_005, 0, 10, position: 10_000)); + } + + [TestMethod] + public void TestSomewhereInLargeFile() + { + Assert.AreEqual("0123456789", GetRange(0, 10_000, 0, 10, position: 5000)); + } + + [TestMethod] + public void TestOutOfLargeFile() + { + Assert.AreEqual("", GetRange(0, 10_000, 0, 10, position: 15_000)); + } + + [TestMethod] + public void TestBasics() + { + using var stream = new RangedStream(new MemoryStream(), 0, 10); + + Assert.AreEqual(0, stream.Position); + Assert.AreEqual(10, stream.Length); + + Assert.IsTrue(stream.CanWrite); + + Assert.IsFalse(stream.CanRead); + Assert.IsFalse(stream.CanSeek); + + Assert.ThrowsException(() => stream.Read(Array.Empty(), 0, 1)); + + Assert.ThrowsException(() => stream.Seek(0, SeekOrigin.Begin)); + + Assert.ThrowsException(() => stream.SetLength(0)); + } + + private static string GetRange(ulong start, ulong end, int offset, int count, int position = 0) + { + using var target = new MemoryStream(); + + using var stream = new RangedStream(target, start, end); + + stream.Position = position; + + stream.Write(Encoding.ASCII.GetBytes("0123456789"), offset, count); + + return Encoding.ASCII.GetString(target.ToArray()); + } + + } + +} diff --git a/Testing/Modules/IO/ResourceTest.cs b/Testing/Acceptance/Modules/IO/ResourceTest.cs similarity index 92% rename from Testing/Modules/IO/ResourceTest.cs rename to Testing/Acceptance/Modules/IO/ResourceTest.cs index 013722bd..c41ac9c4 100644 --- a/Testing/Modules/IO/ResourceTest.cs +++ b/Testing/Acceptance/Modules/IO/ResourceTest.cs @@ -1,161 +1,161 @@ -using System; -using System.IO; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Api.Content.IO; -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Layouting; - -using GenHTTP.Testing.Acceptance.Utilities; - -namespace GenHTTP.Testing.Acceptance.Modules.IO -{ - - [TestClass] - public sealed class ResourceTest - { - - [TestMethod] - public async Task TestStringResource() - { - var resource = Resource.FromString("Hello World") - .Build(); - - Assert.AreEqual(ContentType.TextPlain, resource.ContentType?.KnownType); - - using var content = await resource.GetContentAsync(); - - Assert.AreEqual(11, content.Length); - - Assert.AreEqual((ulong)11, resource.Length!); - - Assert.IsNull(resource.Modified); - Assert.IsNull(resource.Name); - } - - [TestMethod] - public async Task TestFileResource() - { - var file = Path.GetTempFileName(); - - try - { - await FileUtil.WriteTextAsync(file, "Hello World"); - - var resource = Resource.FromFile(file) - .Build(); - - using var content = await resource.GetContentAsync(); - - Assert.AreEqual(11, content.Length); - - Assert.AreEqual((ulong)11, resource.Length!); - - Assert.IsNotNull(resource.Modified); - Assert.IsNotNull(resource.Name); - } - finally - { - try { File.Delete(file); } catch { /* nop */ } - } - } - - [TestMethod] - public async Task TestAssemblyResource() - { - var resource = Resource.FromAssembly("File.txt") - .Build(); - - Assert.AreEqual(ContentType.TextPlain, resource.ContentType?.KnownType); - - using var content = await resource.GetContentAsync(); - - Assert.AreEqual(16, content.Length); - - Assert.AreEqual((ulong)16, resource.Length!); - - Assert.IsNotNull(resource.Modified); - } - - [TestMethod] - public async Task TestAssemblyResourceRouting() - { - var layout = Layout.Create() - .Add("1", Content.From(Resource.FromAssembly("File.txt"))) - .Add("2", Content.From(Resource.FromAssembly("OtherFile.txt"))); - - using var runner = TestRunner.Run(layout); - - using var f1 = await runner.GetResponse("/1"); - Assert.AreEqual("This is text!", await f1.GetContent()); - - using var f2 = await runner.GetResponse("/2"); - Assert.AreEqual("This is other text!", await f2.GetContent()); - } - - [TestMethod] - public void TestStringMetaData() - { - TestMetaData(Resource.FromString("Hello World")); - } - - [TestMethod] - public void TestEmbeddedMetaData() - { - TestMetaData(Resource.FromAssembly("File.txt")); - } - - [TestMethod] - public void TestFileMetaData() - { - var file = Path.GetTempFileName(); - - FileUtil.WriteText(file, "blubb"); - - try - { - TestMetaData(Resource.FromFile(file), false); - } - finally - { - try { File.Delete(file); } catch { /* nop */ } - } - } - - private static void TestMetaData(IResourceBuilder builder, bool modified = true) where T : IResourceBuilder - { - var now = DateTime.UtcNow; - - builder.Name("MyFile.txt") - .Type(ContentType.VideoH264) - .Type(new FlexibleContentType(ContentType.VideoH264)); - - if (modified) - { - builder.Modified(now); - } - - var resource = builder.Build(); - - Assert.AreEqual("MyFile.txt", resource.Name); - Assert.AreEqual(ContentType.VideoH264, resource.ContentType?.KnownType); - - if (modified) - { - Assert.AreEqual(now, resource.Modified); - } - } - - [TestMethod] - public void TestNonExistentFile() - { - Assert.ThrowsException(() => Resource.FromFile("blubb.txt").Build()); - } - - } - -} +using System; +using System.IO; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Api.Content.IO; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Layouting; + +using GenHTTP.Testing.Acceptance.Utilities; + +namespace GenHTTP.Testing.Acceptance.Modules.IO +{ + + [TestClass] + public sealed class ResourceTest + { + + [TestMethod] + public async Task TestStringResource() + { + var resource = Resource.FromString("Hello World") + .Build(); + + Assert.AreEqual(ContentType.TextPlain, resource.ContentType?.KnownType); + + using var content = await resource.GetContentAsync(); + + Assert.AreEqual(11, content.Length); + + Assert.AreEqual((ulong)11, resource.Length!); + + Assert.IsNull(resource.Modified); + Assert.IsNull(resource.Name); + } + + [TestMethod] + public async Task TestFileResource() + { + var file = Path.GetTempFileName(); + + try + { + await FileUtil.WriteTextAsync(file, "Hello World"); + + var resource = Resource.FromFile(file) + .Build(); + + using var content = await resource.GetContentAsync(); + + Assert.AreEqual(11, content.Length); + + Assert.AreEqual((ulong)11, resource.Length!); + + Assert.IsNotNull(resource.Modified); + Assert.IsNotNull(resource.Name); + } + finally + { + try { File.Delete(file); } catch { /* nop */ } + } + } + + [TestMethod] + public async Task TestAssemblyResource() + { + var resource = Resource.FromAssembly("File.txt") + .Build(); + + Assert.AreEqual(ContentType.TextPlain, resource.ContentType?.KnownType); + + using var content = await resource.GetContentAsync(); + + Assert.AreEqual(16, content.Length); + + Assert.AreEqual((ulong)16, resource.Length!); + + Assert.IsNotNull(resource.Modified); + } + + [TestMethod] + public async Task TestAssemblyResourceRouting() + { + var layout = Layout.Create() + .Add("1", Content.From(Resource.FromAssembly("File.txt"))) + .Add("2", Content.From(Resource.FromAssembly("OtherFile.txt"))); + + using var runner = TestHost.Run(layout); + + using var f1 = await runner.GetResponseAsync("/1"); + Assert.AreEqual("This is text!", await f1.GetContent()); + + using var f2 = await runner.GetResponseAsync("/2"); + Assert.AreEqual("This is other text!", await f2.GetContent()); + } + + [TestMethod] + public void TestStringMetaData() + { + TestMetaData(Resource.FromString("Hello World")); + } + + [TestMethod] + public void TestEmbeddedMetaData() + { + TestMetaData(Resource.FromAssembly("File.txt")); + } + + [TestMethod] + public void TestFileMetaData() + { + var file = Path.GetTempFileName(); + + FileUtil.WriteText(file, "blubb"); + + try + { + TestMetaData(Resource.FromFile(file), false); + } + finally + { + try { File.Delete(file); } catch { /* nop */ } + } + } + + private static void TestMetaData(IResourceBuilder builder, bool modified = true) where T : IResourceBuilder + { + var now = DateTime.UtcNow; + + builder.Name("MyFile.txt") + .Type(ContentType.VideoH264) + .Type(new FlexibleContentType(ContentType.VideoH264)); + + if (modified) + { + builder.Modified(now); + } + + var resource = builder.Build(); + + Assert.AreEqual("MyFile.txt", resource.Name); + Assert.AreEqual(ContentType.VideoH264, resource.ContentType?.KnownType); + + if (modified) + { + Assert.AreEqual(now, resource.Modified); + } + } + + [TestMethod] + public void TestNonExistentFile() + { + Assert.ThrowsException(() => Resource.FromFile("blubb.txt").Build()); + } + + } + +} diff --git a/Testing/Modules/IO/ResourceTreeTests.cs b/Testing/Acceptance/Modules/IO/ResourceTreeTests.cs similarity index 95% rename from Testing/Modules/IO/ResourceTreeTests.cs rename to Testing/Acceptance/Modules/IO/ResourceTreeTests.cs index dcb577b5..c0c9ee76 100644 --- a/Testing/Modules/IO/ResourceTreeTests.cs +++ b/Testing/Acceptance/Modules/IO/ResourceTreeTests.cs @@ -1,31 +1,31 @@ -using System.Linq; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Modules.IO; - -namespace GenHTTP.Testing.Acceptance.Modules.IO -{ - - [TestClass] - public sealed class ResourceTreeTests - { - - [TestMethod] - public async Task TestAssembly() - { - var tree = ResourceTree.FromAssembly("Resources").Build(); - - Assert.IsNotNull(await tree.TryGetNodeAsync("Subdirectory")); - - Assert.IsNotNull(await tree.TryGetResourceAsync("File.txt")); - - Assert.AreEqual(1, await tree.GetNodes().CountAsync()); - - Assert.AreEqual(5, await tree.GetResources().CountAsync()); - } - - } - -} +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Modules.IO; + +namespace GenHTTP.Testing.Acceptance.Modules.IO +{ + + [TestClass] + public sealed class ResourceTreeTests + { + + [TestMethod] + public async Task TestAssembly() + { + var tree = ResourceTree.FromAssembly("Resources").Build(); + + Assert.IsNotNull(await tree.TryGetNodeAsync("Subdirectory")); + + Assert.IsNotNull(await tree.TryGetResourceAsync("File.txt")); + + Assert.AreEqual(1, await tree.GetNodes().CountAsync()); + + Assert.AreEqual(5, await tree.GetResources().CountAsync()); + } + + } + +} diff --git a/Testing/Modules/IO/ResourcesTests.cs b/Testing/Acceptance/Modules/IO/ResourcesTests.cs similarity index 58% rename from Testing/Modules/IO/ResourcesTests.cs rename to Testing/Acceptance/Modules/IO/ResourcesTests.cs index 3b0cd602..c887d655 100644 --- a/Testing/Modules/IO/ResourcesTests.cs +++ b/Testing/Acceptance/Modules/IO/ResourcesTests.cs @@ -1,108 +1,108 @@ -using System.Net; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Layouting; -using GenHTTP.Modules.Sitemaps; - -namespace GenHTTP.Testing.Acceptance.Modules.IO -{ - - [TestClass] - public sealed class ResourcesTests - { - - [TestMethod] - public async Task TestFileDownload() - { - using var runner = TestRunner.Run(Resources.From(ResourceTree.FromAssembly())); - - using var response = await runner.GetResponse("/Resources/File.txt"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("This is text!", await response.GetContent()); - } - - [TestMethod] - public async Task TestSubdirectoryFileDownload() - { - using var runner = TestRunner.Run(Resources.From(ResourceTree.FromAssembly())); - - using var response = await runner.GetResponse("/Resources/Subdirectory/AnotherFile.txt"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("This is another text!", await response.GetContent()); - } - - [TestMethod] - public async Task TestNoFileDownload() - { - using var runner = TestRunner.Run(Resources.From(ResourceTree.FromAssembly())); - - using var response = await runner.GetResponse("/Resources/nah.txt"); - - await response.AssertStatusAsync(HttpStatusCode.NotFound); - } - - [TestMethod] - public async Task TestNoSubdirectoryFileDownload() - { - using var runner = TestRunner.Run(Resources.From(ResourceTree.FromAssembly())); - - using var response = await runner.GetResponse("/Resources/nah/File.txt"); - - await response.AssertStatusAsync(HttpStatusCode.NotFound); - } - - [TestMethod] - public async Task TestRootDownload() - { - using var runner = TestRunner.Run(Resources.From(ResourceTree.FromAssembly("Resources"))); - - using var response = await runner.GetResponse("/File.txt"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("This is text!", await response.GetContent()); - } - - [TestMethod] - public async Task TestDirectory() - { - using var runner = TestRunner.Run(Resources.From(ResourceTree.FromAssembly())); - - using var response = await runner.GetResponse("/Resources/nah/"); - - await response.AssertStatusAsync(HttpStatusCode.NotFound); - } - - [TestMethod] - public async Task TestNonExistingDirectory() - { - using var runner = TestRunner.Run(Resources.From(ResourceTree.FromAssembly())); - - using var response = await runner.GetResponse("/Resources/nah/"); - - await response.AssertStatusAsync(HttpStatusCode.NotFound); - } - - [TestMethod] - public async Task TestContent() - { - var layout = Layout.Create() - .Add("sitemap", Sitemap.Create()) - .Add("resources", Resources.From(ResourceTree.FromAssembly("Resources"))); - - using var runner = TestRunner.Run(layout); - - using var response = await runner.GetResponse("/sitemap"); - - var sitemap = await response.GetSitemap(); - - AssertX.Contains("/resources/Error.html", sitemap); - } - - } - -} +using System.Net; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Sitemaps; + +namespace GenHTTP.Testing.Acceptance.Modules.IO +{ + + [TestClass] + public sealed class ResourcesTests + { + + [TestMethod] + public async Task TestFileDownload() + { + using var runner = TestHost.Run(Resources.From(ResourceTree.FromAssembly())); + + using var response = await runner.GetResponseAsync("/Resources/File.txt"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("This is text!", await response.GetContent()); + } + + [TestMethod] + public async Task TestSubdirectoryFileDownload() + { + using var runner = TestHost.Run(Resources.From(ResourceTree.FromAssembly())); + + using var response = await runner.GetResponseAsync("/Resources/Subdirectory/AnotherFile.txt"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("This is another text!", await response.GetContent()); + } + + [TestMethod] + public async Task TestNoFileDownload() + { + using var runner = TestHost.Run(Resources.From(ResourceTree.FromAssembly())); + + using var response = await runner.GetResponseAsync("/Resources/nah.txt"); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + [TestMethod] + public async Task TestNoSubdirectoryFileDownload() + { + using var runner = TestHost.Run(Resources.From(ResourceTree.FromAssembly())); + + using var response = await runner.GetResponseAsync("/Resources/nah/File.txt"); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + [TestMethod] + public async Task TestRootDownload() + { + using var runner = TestHost.Run(Resources.From(ResourceTree.FromAssembly("Resources"))); + + using var response = await runner.GetResponseAsync("/File.txt"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("This is text!", await response.GetContent()); + } + + [TestMethod] + public async Task TestDirectory() + { + using var runner = TestHost.Run(Resources.From(ResourceTree.FromAssembly())); + + using var response = await runner.GetResponseAsync("/Resources/nah/"); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + [TestMethod] + public async Task TestNonExistingDirectory() + { + using var runner = TestHost.Run(Resources.From(ResourceTree.FromAssembly())); + + using var response = await runner.GetResponseAsync("/Resources/nah/"); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + [TestMethod] + public async Task TestContent() + { + var layout = Layout.Create() + .Add("sitemap", Sitemap.Create()) + .Add("resources", Resources.From(ResourceTree.FromAssembly("Resources"))); + + using var runner = TestHost.Run(layout); + + using var response = await runner.GetResponseAsync("/sitemap"); + + var sitemap = await response.GetSitemap(); + + AssertX.Contains("/resources/Error.html", sitemap); + } + + } + +} diff --git a/Testing/Modules/IO/VirtualTreeTests.cs b/Testing/Acceptance/Modules/IO/VirtualTreeTests.cs similarity index 96% rename from Testing/Modules/IO/VirtualTreeTests.cs rename to Testing/Acceptance/Modules/IO/VirtualTreeTests.cs index 1e7fea58..20c49ce5 100644 --- a/Testing/Modules/IO/VirtualTreeTests.cs +++ b/Testing/Acceptance/Modules/IO/VirtualTreeTests.cs @@ -1,53 +1,53 @@ -using System.Threading.Tasks; - -using GenHTTP.Api.Content.IO; -using GenHTTP.Api.Routing; - -using GenHTTP.Modules.IO; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Modules.IO -{ - - [TestClass] - public sealed class VirtualTreeTests - { - - [TestMethod] - public async Task TestNestedTree() - { - var tree = ResourceTree.FromAssembly("Resources"); - - var virt = VirtualTree.Create() - .Add("r", tree) - .Build(); - - var (node, file) = await virt.Find(GetTarget("/r/File.txt")); - - Assert.IsNotNull(node); - Assert.IsNotNull(file); - - Assert.IsTrue((node as IResourceNode)?.Parent == virt); - - Assert.IsNotNull(virt.Modified); - } - - [TestMethod] - public async Task TestResource() - { - var virt = VirtualTree.Create() - .Add("res.txt", Resource.FromString("Blubb")) - .Build(); - - var (node, file) = await virt.Find(GetTarget("/res.txt")); - - Assert.IsNotNull(node); - Assert.IsNotNull(file); - } - - private static RoutingTarget GetTarget(string path) => new(new PathBuilder(path).Build()); - - } - -} +using System.Threading.Tasks; + +using GenHTTP.Api.Content.IO; +using GenHTTP.Api.Routing; + +using GenHTTP.Modules.IO; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.IO +{ + + [TestClass] + public sealed class VirtualTreeTests + { + + [TestMethod] + public async Task TestNestedTree() + { + var tree = ResourceTree.FromAssembly("Resources"); + + var virt = VirtualTree.Create() + .Add("r", tree) + .Build(); + + var (node, file) = await virt.Find(GetTarget("/r/File.txt")); + + Assert.IsNotNull(node); + Assert.IsNotNull(file); + + Assert.IsTrue((node as IResourceNode)?.Parent == virt); + + Assert.IsNotNull(virt.Modified); + } + + [TestMethod] + public async Task TestResource() + { + var virt = VirtualTree.Create() + .Add("res.txt", Resource.FromString("Blubb")) + .Build(); + + var (node, file) = await virt.Find(GetTarget("/res.txt")); + + Assert.IsNotNull(node); + Assert.IsNotNull(file); + } + + private static RoutingTarget GetTarget(string path) => new(new PathBuilder(path).Build()); + + } + +} diff --git a/Testing/Modules/LayoutTests.cs b/Testing/Acceptance/Modules/LayoutTests.cs similarity index 79% rename from Testing/Modules/LayoutTests.cs rename to Testing/Acceptance/Modules/LayoutTests.cs index 9728af6f..909e91d3 100644 --- a/Testing/Modules/LayoutTests.cs +++ b/Testing/Acceptance/Modules/LayoutTests.cs @@ -1,89 +1,89 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using System.Net; -using System.Threading.Tasks; - -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Layouting; -using System; - -namespace GenHTTP.Testing.Acceptance.Modules -{ - - [TestClass] - public sealed class LayoutTests - { - - /// - /// As a developer I can define the default route to be devlivered. - /// - [TestMethod] - public async Task TestGetIndex() - { - var layout = Layout.Create() - .Index(Content.From(Resource.FromString("Hello World!"))); - - using var runner = TestRunner.Run(layout); - - using var response = await runner.GetResponse(); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("Hello World!", await response.GetContent()); - - using var notFound = await runner.GetResponse("/notfound"); - - await notFound.AssertStatusAsync(HttpStatusCode.NotFound); - } - - /// - /// As a developer I can set a default handler to be used for requests. - /// - [TestMethod] - public async Task TestDefaultContent() - { - var layout = Layout.Create().Add(Content.From(Resource.FromString("Hello World!"))); - - using var runner = TestRunner.Run(layout); - - foreach (var path in new string[] { "/something", "/" }) - { - using var response = await runner.GetResponse(path); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("Hello World!", await response.GetContent()); - } - } - - /// - /// As the developer of a web application, I don't want my application - /// to produce duplicate content for missing trailing slashes. - /// - [TestMethod] - public async Task TestRedirect() - { - var layout = Layout.Create() - .Add("section", Layout.Create().Index(Content.From(Resource.FromString("Hello World!")))); - - using var runner = TestRunner.Run(layout); - - using var response = await runner.GetResponse("/section/"); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("Hello World!", await response.GetContent()); - - using var redirected = await runner.GetResponse("/section"); - - await redirected.AssertStatusAsync(HttpStatusCode.MovedPermanently); - AssertX.EndsWith("/section/", redirected.GetHeader("Location")!); - } - - [TestMethod] - [ExpectedException(typeof(ArgumentException))] - public void TestIllegalPathCharacters() - { - Layout.Create().Add("some/path", Content.From(Resource.FromString("Hello World"))); - } - - } - -} +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System.Net; +using System.Threading.Tasks; + +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Layouting; +using System; + +namespace GenHTTP.Testing.Acceptance.Modules +{ + + [TestClass] + public sealed class LayoutTests + { + + /// + /// As a developer I can define the default route to be devlivered. + /// + [TestMethod] + public async Task TestGetIndex() + { + var layout = Layout.Create() + .Index(Content.From(Resource.FromString("Hello World!"))); + + using var runner = TestHost.Run(layout); + + using var response = await runner.GetResponseAsync(); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("Hello World!", await response.GetContent()); + + using var notFound = await runner.GetResponseAsync("/notfound"); + + await notFound.AssertStatusAsync(HttpStatusCode.NotFound); + } + + /// + /// As a developer I can set a default handler to be used for requests. + /// + [TestMethod] + public async Task TestDefaultContent() + { + var layout = Layout.Create().Add(Content.From(Resource.FromString("Hello World!"))); + + using var runner = TestHost.Run(layout); + + foreach (var path in new string[] { "/something", "/" }) + { + using var response = await runner.GetResponseAsync(path); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("Hello World!", await response.GetContent()); + } + } + + /// + /// As the developer of a web application, I don't want my application + /// to produce duplicate content for missing trailing slashes. + /// + [TestMethod] + public async Task TestRedirect() + { + var layout = Layout.Create() + .Add("section", Layout.Create().Index(Content.From(Resource.FromString("Hello World!")))); + + using var runner = TestHost.Run(layout); + + using var response = await runner.GetResponseAsync("/section/"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("Hello World!", await response.GetContent()); + + using var redirected = await runner.GetResponseAsync("/section"); + + await redirected.AssertStatusAsync(HttpStatusCode.MovedPermanently); + AssertX.EndsWith("/section/", redirected.GetHeader("Location")!); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void TestIllegalPathCharacters() + { + Layout.Create().Add("some/path", Content.From(Resource.FromString("Hello World"))); + } + + } + +} diff --git a/Testing/Modules/ListingTests.cs b/Testing/Acceptance/Modules/ListingTests.cs similarity index 81% rename from Testing/Modules/ListingTests.cs rename to Testing/Acceptance/Modules/ListingTests.cs index 43b9f6f2..1882d1f7 100644 --- a/Testing/Modules/ListingTests.cs +++ b/Testing/Acceptance/Modules/ListingTests.cs @@ -1,113 +1,113 @@ -using System.IO; -using System.Net; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using GenHTTP.Modules.DirectoryBrowsing; -using GenHTTP.Modules.IO; - -using GenHTTP.Testing.Acceptance.Utilities; - -namespace GenHTTP.Testing.Acceptance.Providers -{ - - [TestClass] - public sealed class ListingTests - { - - /// - /// As an user of a web application, I can view the folders and files available - /// on root level of a listed directory. - /// - [TestMethod] - public async Task TestGetMainListing() - { - using var runner = GetEnvironment(); - - using var response = await runner.GetResponse("/"); - - var content = await response.GetContent(); - - AssertX.Contains("Subdirectory", content); - AssertX.Contains("With%20Spaces", content); - - AssertX.Contains("my.txt", content); - - AssertX.DoesNotContain("..", content); - } - - /// - /// As an user of a web application, I can view the folders and files available - /// within a subdirectory of a listed directory. - /// - [TestMethod] - public async Task TestGetSubdirectory() - { - using var runner = GetEnvironment(); - - using var response = await runner.GetResponse("/Subdirectory/"); - - var content = await response.GetContent(); - - AssertX.Contains("..", content); - } - - - /// - /// As an user of a web application, I can download the files listed by the - /// directory listing feature. - /// - [TestMethod] - public async Task TestDownload() - { - using var runner = GetEnvironment(); - - using var response = await runner.GetResponse("/my.txt"); - - Assert.AreEqual("Hello World!", await response.GetContent()); - } - - [TestMethod] - public async Task TestNonExistingFolder() - { - using var runner = GetEnvironment(); - - using var response = await runner.GetResponse("/idonotexist/"); - - await response.AssertStatusAsync(HttpStatusCode.NotFound); - } - - [TestMethod] - public async Task TestSameListingSameChecksum() - { - using var runner = GetEnvironment(); - - using var resp1 = await runner.GetResponse(); - using var resp2 = await runner.GetResponse(); - - Assert.IsNotNull(resp1.GetETag()); - - Assert.AreEqual(resp1.GetETag(), resp2.GetETag()); - } - - private static TestRunner GetEnvironment() - { - var tempFolder = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - - Directory.CreateDirectory(tempFolder); - - Directory.CreateDirectory(Path.Combine(tempFolder, "Subdirectory")); - - Directory.CreateDirectory(Path.Combine(tempFolder, "With Spaces")); - - FileUtil.WriteText(Path.Combine(tempFolder, "my.txt"), "Hello World!"); - - var listing = Listing.From(ResourceTree.FromDirectory(tempFolder)); - - return TestRunner.Run(listing); - } - - } - -} +using System.IO; +using System.Net; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using GenHTTP.Modules.DirectoryBrowsing; +using GenHTTP.Modules.IO; + +using GenHTTP.Testing.Acceptance.Utilities; + +namespace GenHTTP.Testing.Acceptance.Providers +{ + + [TestClass] + public sealed class ListingTests + { + + /// + /// As an user of a web application, I can view the folders and files available + /// on root level of a listed directory. + /// + [TestMethod] + public async Task TestGetMainListing() + { + using var runner = GetEnvironment(); + + using var response = await runner.GetResponseAsync("/"); + + var content = await response.GetContent(); + + AssertX.Contains("Subdirectory", content); + AssertX.Contains("With%20Spaces", content); + + AssertX.Contains("my.txt", content); + + AssertX.DoesNotContain("..", content); + } + + /// + /// As an user of a web application, I can view the folders and files available + /// within a subdirectory of a listed directory. + /// + [TestMethod] + public async Task TestGetSubdirectory() + { + using var runner = GetEnvironment(); + + using var response = await runner.GetResponseAsync("/Subdirectory/"); + + var content = await response.GetContent(); + + AssertX.Contains("..", content); + } + + + /// + /// As an user of a web application, I can download the files listed by the + /// directory listing feature. + /// + [TestMethod] + public async Task TestDownload() + { + using var runner = GetEnvironment(); + + using var response = await runner.GetResponseAsync("/my.txt"); + + Assert.AreEqual("Hello World!", await response.GetContent()); + } + + [TestMethod] + public async Task TestNonExistingFolder() + { + using var runner = GetEnvironment(); + + using var response = await runner.GetResponseAsync("/idonotexist/"); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + [TestMethod] + public async Task TestSameListingSameChecksum() + { + using var runner = GetEnvironment(); + + using var resp1 = await runner.GetResponseAsync(); + using var resp2 = await runner.GetResponseAsync(); + + Assert.IsNotNull(resp1.GetETag()); + + Assert.AreEqual(resp1.GetETag(), resp2.GetETag()); + } + + private static TestHost GetEnvironment() + { + var tempFolder = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + Directory.CreateDirectory(tempFolder); + + Directory.CreateDirectory(Path.Combine(tempFolder, "Subdirectory")); + + Directory.CreateDirectory(Path.Combine(tempFolder, "With Spaces")); + + FileUtil.WriteText(Path.Combine(tempFolder, "my.txt"), "Hello World!"); + + var listing = Listing.From(ResourceTree.FromDirectory(tempFolder)); + + return TestHost.Run(listing); + } + + } + +} diff --git a/Testing/Modules/LoadBalancerTests.cs b/Testing/Acceptance/Modules/LoadBalancerTests.cs similarity index 73% rename from Testing/Modules/LoadBalancerTests.cs rename to Testing/Acceptance/Modules/LoadBalancerTests.cs index 1e3fb17d..4253e40c 100644 --- a/Testing/Modules/LoadBalancerTests.cs +++ b/Testing/Acceptance/Modules/LoadBalancerTests.cs @@ -1,103 +1,103 @@ -using System.Net; -using System.Threading.Tasks; - -using GenHTTP.Api.Infrastructure; - -using GenHTTP.Modules.IO; -using GenHTTP.Modules.LoadBalancing; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Providers -{ - - [TestClass] - public sealed class LoadBalancerTests - { - - [TestMethod] - public async Task TestProxy() - { - using var upstream = TestRunner.Run(Content.From(Resource.FromString("Proxy!"))); - - var loadbalancer = LoadBalancer.Create() - .Proxy($"http://localhost:{upstream.Port}"); - - using var runner = TestRunner.Run(loadbalancer); - - using var response = await runner.GetResponse(); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("Proxy!", await response.GetContent()); - } - - [TestMethod] - public async Task TestRedirect() - { - var loadbalancer = LoadBalancer.Create() - .Redirect($"http://node"); - - using var runner = TestRunner.Run(loadbalancer); - - using var response = await runner.GetResponse("/page"); - - await response.AssertStatusAsync(HttpStatusCode.TemporaryRedirect); - Assert.AreEqual("http://node/page", response.GetHeader("Location")); - } - - [TestMethod] - public async Task TestCustomHandler() - { - var loadbalancer = LoadBalancer.Create() - .Add(Content.From(Resource.FromString("My Content!"))); - - using var runner = TestRunner.Run(loadbalancer); - - using var response = await runner.GetResponse(); - - await response.AssertStatusAsync(HttpStatusCode.OK); - Assert.AreEqual("My Content!", await response.GetContent()); - } - - [TestMethod] - public async Task TestPriorities() - { - var loadbalancer = LoadBalancer.Create() - .Add(Content.From(Resource.FromString("Prio A")), r => Priority.High) - .Add(Content.From(Resource.FromString("Prio B")), r => Priority.Low); - - using var runner = TestRunner.Run(loadbalancer); - - using var response = await runner.GetResponse(); - - Assert.AreEqual("Prio A", await response.GetContent()); - } - - [TestMethod] - public async Task TestMultiplePriorities() - { - var loadbalancer = LoadBalancer.Create() - .Add(Content.From(Resource.FromString("Prio A1")), r => Priority.High) - .Add(Content.From(Resource.FromString("Prio A2")), r => Priority.High) - .Add(Content.From(Resource.FromString("Prio A3")), r => Priority.High); - - using var runner = TestRunner.Run(loadbalancer); - - using var response = await runner.GetResponse(); - - AssertX.StartsWith("Prio A", await response.GetContent()); - } - - [TestMethod] - public async Task TestNoNodes() - { - using var runner = TestRunner.Run(LoadBalancer.Create()); - - using var response = await runner.GetResponse(); - - await response.AssertStatusAsync(HttpStatusCode.NotFound); - } - - } - -} +using System.Net; +using System.Threading.Tasks; + +using GenHTTP.Api.Infrastructure; + +using GenHTTP.Modules.IO; +using GenHTTP.Modules.LoadBalancing; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Providers +{ + + [TestClass] + public sealed class LoadBalancerTests + { + + [TestMethod] + public async Task TestProxy() + { + using var upstream = TestHost.Run(Content.From(Resource.FromString("Proxy!"))); + + var loadbalancer = LoadBalancer.Create() + .Proxy($"http://localhost:{upstream.Port}"); + + using var runner = TestHost.Run(loadbalancer); + + using var response = await runner.GetResponseAsync(); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("Proxy!", await response.GetContent()); + } + + [TestMethod] + public async Task TestRedirect() + { + var loadbalancer = LoadBalancer.Create() + .Redirect($"http://node"); + + using var runner = TestHost.Run(loadbalancer); + + using var response = await runner.GetResponseAsync("/page"); + + await response.AssertStatusAsync(HttpStatusCode.TemporaryRedirect); + Assert.AreEqual("http://node/page", response.GetHeader("Location")); + } + + [TestMethod] + public async Task TestCustomHandler() + { + var loadbalancer = LoadBalancer.Create() + .Add(Content.From(Resource.FromString("My Content!"))); + + using var runner = TestHost.Run(loadbalancer); + + using var response = await runner.GetResponseAsync(); + + await response.AssertStatusAsync(HttpStatusCode.OK); + Assert.AreEqual("My Content!", await response.GetContent()); + } + + [TestMethod] + public async Task TestPriorities() + { + var loadbalancer = LoadBalancer.Create() + .Add(Content.From(Resource.FromString("Prio A")), r => Priority.High) + .Add(Content.From(Resource.FromString("Prio B")), r => Priority.Low); + + using var runner = TestHost.Run(loadbalancer); + + using var response = await runner.GetResponseAsync(); + + Assert.AreEqual("Prio A", await response.GetContent()); + } + + [TestMethod] + public async Task TestMultiplePriorities() + { + var loadbalancer = LoadBalancer.Create() + .Add(Content.From(Resource.FromString("Prio A1")), r => Priority.High) + .Add(Content.From(Resource.FromString("Prio A2")), r => Priority.High) + .Add(Content.From(Resource.FromString("Prio A3")), r => Priority.High); + + using var runner = TestHost.Run(loadbalancer); + + using var response = await runner.GetResponseAsync(); + + AssertX.StartsWith("Prio A", await response.GetContent()); + } + + [TestMethod] + public async Task TestNoNodes() + { + using var runner = TestHost.Run(LoadBalancer.Create()); + + using var response = await runner.GetResponseAsync(); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + } + +} diff --git a/Testing/Modules/PageTests.cs b/Testing/Acceptance/Modules/PageTests.cs similarity index 85% rename from Testing/Modules/PageTests.cs rename to Testing/Acceptance/Modules/PageTests.cs index 381017b2..e4df784d 100644 --- a/Testing/Modules/PageTests.cs +++ b/Testing/Acceptance/Modules/PageTests.cs @@ -1,217 +1,219 @@ -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; - -using GenHTTP.Api.Content; -using GenHTTP.Api.Content.Templating; -using GenHTTP.Api.Protocol; -using GenHTTP.Api.Routing; - -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Layouting; -using GenHTTP.Modules.Markdown; -using GenHTTP.Modules.Placeholders; -using GenHTTP.Modules.Razor; -using GenHTTP.Modules.Scriban; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GenHTTP.Testing.Acceptance.Providers -{ - - #region Supporting data structures - - public sealed class CustomModel : AbstractModel - { - - public string World => "World"; - - public CustomModel(IRequest request, IHandler handler) : base(request, handler) { } - - public override ValueTask CalculateChecksumAsync() => new(17); - - } - - public sealed class PathModel : AbstractModel - { - - public WebPath Path => new PathBuilder("/test/1").Build(); - - public PathModel(IRequest r, IHandler h) : base(r, h) { } - - public override ValueTask CalculateChecksumAsync() => new(17); - - } - - #endregion - - [TestClass] - public sealed class PageTests - { - - [TestMethod] - public async Task TestStringPage() - { - var layout = Page.From("Hello World!") - .Title("My Page") - .Description("My Description"); - - using var runner = TestRunner.Run(layout); - - using var response = await runner.GetResponse(); - - var content = await response.GetContent(); - - Assert.AreNotEqual("Hello World!", content); - AssertX.Contains("Hello World!", content); - - Assert.AreEqual("text/html", response.GetContentHeader("Content-Type")); - } - - [TestMethod] - public async Task TestMarkdownPage() - { - var md = @"# Hello World! -```csharp -// code -```"; - - var page = ModMarkdown.Page(Resource.FromString(md)) - .Title("Markdown Page") - .Description("A page rendered with markdown"); - - using var runner = TestRunner.Run(page); - - using var response = await runner.GetResponse(); - - var content = await response.GetContent(); - - AssertX.Contains("

Hello World!

", content); - - AssertX.Contains("
// code", content);
-        }
-
-        [TestMethod]
-        public async Task TestRendering()
-        {
-            static ValueTask modelProvider(IRequest r, IHandler h) => new ValueTask(new CustomModel(r, h));
-
-            var providers = new List()
-            {
-                ModScriban.Page(Resource.FromString("Hello {{ world }}!"), modelProvider).Title("1").Description("2"),
-                ModRazor.Page(Resource.FromString("Hello @Model.World!"), modelProvider).Title("1").Description("2"),
-                Placeholders.Page(Resource.FromString("Hello [World]!"), modelProvider).Title("1").Description("2")
-            };
-
-            foreach (var provider in providers)
-            {
-                var layout = Layout.Create().Add("page", provider);
-
-                using var runner = TestRunner.Run(layout);
-
-                using var response = await runner.GetResponse("/page");
-
-                var content = await response.GetContent();
-
-                Assert.AreNotEqual("Hello World!", content);
-                AssertX.Contains("Hello World!", content);
-            }
-        }
-
-        [TestMethod]
-        public async Task TestContentInfo()
-        {
-            var page = Page.From("Hello world!")
-                           .Title("My Title")
-                           .Description("My Description");
-
-            using var runner = TestRunner.Run(page);
-
-            using var response = await runner.GetResponse();
-
-            var content = await response.GetContent();
-
-            await response.AssertStatusAsync(HttpStatusCode.OK);
-
-            AssertX.Contains("My Title", content);
-            AssertX.Contains("", content);
-        }
-
-        [TestMethod]
-        public async Task TestNoContentInfo()
-        {
-            var page = Page.From("Hello world!");
-
-            using var runner = TestRunner.Run(page);
-
-            using var response = await runner.GetResponse();
-
-            var content = await response.GetContent();
-
-            await response.AssertStatusAsync(HttpStatusCode.OK);
-
-            AssertX.Contains("Untitled Page", content);
-            AssertX.Contains("", content);
-        }
-
-        [TestMethod]
-        public async Task TestRouting()
-        {
-            var providers = new List()
-            {
-                ModScriban.Page(Resource.FromString("{{ route 'https://google.de' }}|{{ route 'res/123' }}|{{ route 'other/456/' }}|{{ route './relative' }}")),
-                ModRazor.Page(Resource.FromString("@Model.Route(\"https://google.de\")|@Model.Route(\"res/123\")|@Model.Route(\"other/456/\")|@Model.Route(\"./relative\")")),
-            };
-
-            foreach (var provider in providers)
-            {
-                var inner = Layout.Create()
-                                  .Add("page", provider);
-
-                var outer = Layout.Create()
-                                  .Add("res", Layout.Create())
-                                  .Add("inner", inner);
-
-                var layout = Layout.Create()
-                                   .Add("other", Layout.Create())
-                                   .Add("outer", outer);
-
-                using var runner = TestRunner.Run(layout);
-
-                using var response = await runner.GetResponse("/outer/inner/page");
-
-                var content = await response.GetContent();
-
-                AssertX.Contains("https://google.de|../res/123|../../other/456/|./relative", content);
-            }
-        }
-
-        [TestMethod]
-        public async Task TestRoutingToPath()
-        {
-            var providers = new List()
-            {
-                ModScriban.Page(Resource.FromString("{{ route path }}"), (IRequest r, IHandler h) => new(new PathModel(r, h))),
-                ModRazor.Page(Resource.FromString("@Model.Route(Model.Path)"), (IRequest r, IHandler h) => new PathModel(r, h)),
-            };
-
-            foreach (var provider in providers)
-            {
-                var layout = Layout.Create()
-                                   .Add("page", provider)
-                                   .Add("test", Content.From(Resource.FromString("test")));
-                
-                using var runner = TestRunner.Run(layout);
-
-                using var response = await runner.GetResponse("/page");
-
-                var content = await response.GetContent();
-
-                await response.AssertStatusAsync(HttpStatusCode.OK);
-                AssertX.Contains("/test/1", content);
-            }
-        }
-
-    }
-
-}
+using System.Collections.Generic;
+using System.Net;
+using System.Threading.Tasks;
+
+using GenHTTP.Api.Content;
+using GenHTTP.Api.Content.Templating;
+using GenHTTP.Api.Protocol;
+using GenHTTP.Api.Routing;
+
+using GenHTTP.Modules.IO;
+using GenHTTP.Modules.Layouting;
+using GenHTTP.Modules.Markdown;
+using GenHTTP.Modules.Placeholders;
+using GenHTTP.Modules.Razor;
+using GenHTTP.Modules.Scriban;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace GenHTTP.Testing.Acceptance.Providers
+{
+
+    #region Supporting data structures
+
+    public sealed class CustomModel : AbstractModel
+    {
+
+        public string World => "World";
+
+        public CustomModel(IRequest request, IHandler handler) : base(request, handler) { }
+
+        public override ValueTask CalculateChecksumAsync() => new(17);
+
+    }
+
+    public sealed class PathModel : AbstractModel
+    {
+
+        public WebPath Path => new PathBuilder("/test/1").Build();
+
+        public PathModel(IRequest r, IHandler h) : base(r, h) { }
+
+        public override ValueTask CalculateChecksumAsync() => new(17);
+
+    }
+
+    #endregion
+
+    [TestClass]
+    public sealed class PageTests
+    {
+        
+        [TestMethod]
+        public async Task TestStringPage()
+        {
+            var layout = Page.From("Hello World!")
+                             .Title("My Page")
+                             .Description("My Description");
+
+            using var runner = TestHost.Run(layout);
+
+            using var response = await runner.GetResponseAsync();
+
+            var content = await response.GetContent();
+
+            Assert.AreNotEqual("Hello World!", content);
+            AssertX.Contains("Hello World!", content);
+
+            Assert.AreEqual("text/html", response.GetContentHeader("Content-Type"));
+        }
+
+        [TestMethod]
+        public async Task TestMarkdownPage()
+        {
+            var md = @"# Hello World!
+```csharp
+// code
+```";
+
+            var page = ModMarkdown.Page(Resource.FromString(md))
+                                  .Title("Markdown Page")
+                                  .Description("A page rendered with markdown");
+
+            using var runner = TestHost.Run(page);
+
+            using var response = await runner.GetResponseAsync();
+
+            var content = await response.GetContent();
+
+            AssertX.Contains("

Hello World!

", content); + + AssertX.Contains("
// code", content);
+        }
+
+        [TestMethod]
+        public async Task TestRendering()
+        {
+            static ValueTask modelProvider(IRequest r, IHandler h) => new ValueTask(new CustomModel(r, h));
+
+            var providers = new List()
+            {
+                ModScriban.Page(Resource.FromString("Hello {{ world }}!"), modelProvider).Title("1").Description("2"),
+                ModRazor.Page(Resource.FromString("Hello @Model.World!"), modelProvider).Title("1").Description("2"),
+                Placeholders.Page(Resource.FromString("Hello [World]!"), modelProvider).Title("1").Description("2")
+            };
+
+            foreach (var provider in providers)
+            {
+                var layout = Layout.Create().Add("page", provider);
+
+                using var runner = TestHost.Run(layout);
+
+                using var response = await runner.GetResponseAsync("/page");
+
+                var content = await response.GetContent();
+
+                Assert.AreNotEqual("Hello World!", content);
+                AssertX.Contains("Hello World!", content);
+            }
+        }
+
+        [TestMethod]
+        public async Task TestContentInfo()
+        {
+            var page = Page.From("Hello world!")
+                           .Title("My Title")
+                           .Description("My Description");
+
+            using var runner = TestHost.Run(page);
+
+            using var response = await runner.GetResponseAsync();
+
+            var content = await response.GetContent();
+
+            await response.AssertStatusAsync(HttpStatusCode.OK);
+
+            AssertX.Contains("My Title", content);
+            AssertX.Contains("", content);
+        }
+
+        [TestMethod]
+        public async Task TestNoContentInfo()
+        {
+            var page = Page.From("Hello world!");
+
+            using var runner = TestHost.Run(page);
+
+            using var response = await runner.GetResponseAsync();
+
+            var content = await response.GetContent();
+
+            await response.AssertStatusAsync(HttpStatusCode.OK);
+
+            AssertX.Contains("Untitled Page", content);
+            AssertX.Contains("", content);
+        }
+
+        [TestMethod]
+        public async Task TestRouting()
+        {
+            var providers = new List()
+            {
+                ModScriban.Page(Resource.FromString("{{ route 'https://google.de' }}|{{ route 'res/123' }}|{{ route 'other/456/' }}|{{ route './relative' }}")),
+                ModRazor.Page(Resource.FromString("@Model.Route(\"https://google.de\")|@Model.Route(\"res/123\")|@Model.Route(\"other/456/\")|@Model.Route(\"./relative\")")),
+            };
+
+            foreach (var provider in providers)
+            {
+                var inner = Layout.Create()
+                                  .Add("page", provider);
+
+                var outer = Layout.Create()
+                                  .Add("res", Layout.Create())
+                                  .Add("inner", inner);
+
+                var layout = Layout.Create()
+                                   .Add("other", Layout.Create())
+                                   .Add("outer", outer);
+
+                using var runner = TestHost.Run(layout);
+
+                using var response = await runner.GetResponseAsync("/outer/inner/page");
+
+                await response.AssertStatusAsync(HttpStatusCode.OK);
+
+                var content = await response.GetContent();
+
+                AssertX.Contains("https://google.de|../res/123|../../other/456/|./relative", content);
+            }
+        }
+
+        [TestMethod]
+        public async Task TestRoutingToPath()
+        {
+            var providers = new List()
+            {
+                ModScriban.Page(Resource.FromString("{{ route path }}"), (IRequest r, IHandler h) => new(new PathModel(r, h))),
+                ModRazor.Page(Resource.FromString("@Model.Route(Model.Path)"), (IRequest r, IHandler h) => new PathModel(r, h)),
+            };
+
+            foreach (var provider in providers)
+            {
+                var layout = Layout.Create()
+                                   .Add("page", provider)
+                                   .Add("test", Content.From(Resource.FromString("test")));
+                
+                using var runner = TestHost.Run(layout);
+
+                using var response = await runner.GetResponseAsync("/page");
+
+                var content = await response.GetContent();
+
+                await response.AssertStatusAsync(HttpStatusCode.OK);
+                AssertX.Contains("/test/1", content);
+            }
+        }
+
+    }
+
+}
diff --git a/Testing/Modules/Pages/CombinedPageTest.cs b/Testing/Acceptance/Modules/Pages/CombinedPageTest.cs
similarity index 79%
rename from Testing/Modules/Pages/CombinedPageTest.cs
rename to Testing/Acceptance/Modules/Pages/CombinedPageTest.cs
index 56f9783a..c0406e9a 100644
--- a/Testing/Modules/Pages/CombinedPageTest.cs
+++ b/Testing/Acceptance/Modules/Pages/CombinedPageTest.cs
@@ -1,92 +1,92 @@
-using System.Threading.Tasks;
-
-using GenHTTP.Api.Content.Templating;
-using GenHTTP.Modules.IO;
-using GenHTTP.Modules.Markdown;
-using GenHTTP.Modules.Pages;
-using GenHTTP.Modules.Razor;
-using GenHTTP.Modules.Scriban;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-namespace GenHTTP.Testing.Acceptance.Modules.Pages
-{
-
-    [TestClass]
-    public class CombinedPageTest
-    {
-
-        [TestMethod]
-        public async Task TestMetaData()
-        {
-            var page = CombinedPage.Create()
-                                   .Title("My Page")
-                                   .Description("My Description");
-
-            using var runner = TestRunner.Run(page);
-
-            using var response = await runner.GetResponse();
-
-            var content = await response.GetContent();
-
-            AssertX.Contains("My Page", content);
-            AssertX.Contains("My Description", content);
-
-            Assert.AreEqual("text/html", response.GetContentHeader("Content-Type"));
-        }
-
-        [TestMethod]
-        public async Task TestPlainText()
-        {
-            var page = CombinedPage.Create()
-                                   .Add("Static Content");
-
-            using var runner = TestRunner.Run(page);
-
-            using var response = await runner.GetResponse();
-
-            var content = await response.GetContent();
-
-            AssertX.Contains("Static Content", content);
-        }
-
-        [TestMethod]
-        public async Task TestRenderingEngines()
-        {
-            static ValueTask model(Api.Protocol.IRequest r, Api.Content.IHandler h) => new(new BasicModel(r, h));
-
-            var page = CombinedPage.Create()
-                                   .AddScriban(Resource.FromString("Scriban at {{ request.target.path }}"), model)
-                                   .AddRazor(Resource.FromString("Razor at @Model.Request.Target.Path"), model)
-                                   .AddMarkdown(Resource.FromString("Mark*down*"));
-
-            using var runner = TestRunner.Run(page);
-
-            using var response = await runner.GetResponse("/page");
-
-            var content = await response.GetContent();
-
-            AssertX.Contains("Scriban at /page", content);
-            AssertX.Contains("Razor at /page", content);
-            AssertX.Contains("Markdown", content);
-        }
-
-        [TestMethod]
-        public async Task TestStableChecksum()
-        {
-            var page = CombinedPage.Create()
-                                   .Add("Static Content");
-
-            using var runner = TestRunner.Run(page);
-
-            using var r1 = await runner.GetResponse();
-            using var r2 = await runner.GetResponse();
-
-            Assert.IsNotNull(r1.GetHeader("ETag"));
-
-            Assert.AreEqual(r1.GetHeader("ETag"), r2.GetHeader("ETag"));
-        }
-
-    }
-
-}
+using System.Threading.Tasks;
+
+using GenHTTP.Api.Content.Templating;
+using GenHTTP.Modules.IO;
+using GenHTTP.Modules.Markdown;
+using GenHTTP.Modules.Pages;
+using GenHTTP.Modules.Razor;
+using GenHTTP.Modules.Scriban;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace GenHTTP.Testing.Acceptance.Modules.Pages
+{
+
+    [TestClass]
+    public class CombinedPageTest
+    {
+
+        [TestMethod]
+        public async Task TestMetaData()
+        {
+            var page = CombinedPage.Create()
+                                   .Title("My Page")
+                                   .Description("My Description");
+
+            using var runner = TestHost.Run(page);
+
+            using var response = await runner.GetResponseAsync();
+
+            var content = await response.GetContent();
+
+            AssertX.Contains("My Page", content);
+            AssertX.Contains("My Description", content);
+
+            Assert.AreEqual("text/html", response.GetContentHeader("Content-Type"));
+        }
+
+        [TestMethod]
+        public async Task TestPlainText()
+        {
+            var page = CombinedPage.Create()
+                                   .Add("Static Content");
+
+            using var runner = TestHost.Run(page);
+
+            using var response = await runner.GetResponseAsync();
+
+            var content = await response.GetContent();
+
+            AssertX.Contains("Static Content", content);
+        }
+
+        [TestMethod]
+        public async Task TestRenderingEngines()
+        {
+            static ValueTask model(Api.Protocol.IRequest r, Api.Content.IHandler h) => new(new BasicModel(r, h));
+
+            var page = CombinedPage.Create()
+                                   .AddScriban(Resource.FromString("Scriban at {{ request.target.path }}"), model)
+                                   .AddRazor(Resource.FromString("Razor at @Model.Request.Target.Path"), model)
+                                   .AddMarkdown(Resource.FromString("Mark*down*"));
+
+            using var runner = TestHost.Run(page);
+
+            using var response = await runner.GetResponseAsync("/page");
+
+            var content = await response.GetContent();
+
+            AssertX.Contains("Scriban at /page", content);
+            AssertX.Contains("Razor at /page", content);
+            AssertX.Contains("Markdown", content);
+        }
+
+        [TestMethod]
+        public async Task TestStableChecksum()
+        {
+            var page = CombinedPage.Create()
+                                   .Add("Static Content");
+
+            using var runner = TestHost.Run(page);
+
+            using var r1 = await runner.GetResponseAsync();
+            using var r2 = await runner.GetResponseAsync();
+
+            Assert.IsNotNull(r1.GetHeader("ETag"));
+
+            Assert.AreEqual(r1.GetHeader("ETag"), r2.GetHeader("ETag"));
+        }
+
+    }
+
+}
diff --git a/Testing/Modules/Pages/ErrorHandlingTests.cs b/Testing/Acceptance/Modules/Pages/ErrorHandlingTests.cs
similarity index 78%
rename from Testing/Modules/Pages/ErrorHandlingTests.cs
rename to Testing/Acceptance/Modules/Pages/ErrorHandlingTests.cs
index cc22ceb1..700acf5a 100644
--- a/Testing/Modules/Pages/ErrorHandlingTests.cs
+++ b/Testing/Acceptance/Modules/Pages/ErrorHandlingTests.cs
@@ -1,30 +1,30 @@
-using System.Net;
-using System.Threading.Tasks;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using GenHTTP.Modules.IO;
-using GenHTTP.Modules.Scriban;
-
-namespace GenHTTP.Testing.Acceptance.Modules.Pages
-{
-
-    [TestClass]
-    public class ErrorHandlingTests
-    {
-
-        [TestMethod]
-        public async Task TestErrorPage()
-        {
-            var page = ModScriban.Page(Resource.FromString("{{i.will.fail}}"));
-
-            using var runner = TestRunner.Run(page);
-
-            using var response = await runner.GetResponse();
-
-            await response.AssertStatusAsync(HttpStatusCode.InternalServerError);
-        }
-
-    }
-
-}
+using System.Net;
+using System.Threading.Tasks;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using GenHTTP.Modules.IO;
+using GenHTTP.Modules.Scriban;
+
+namespace GenHTTP.Testing.Acceptance.Modules.Pages
+{
+
+    [TestClass]
+    public class ErrorHandlingTests
+    {
+
+        [TestMethod]
+        public async Task TestErrorPage()
+        {
+            var page = ModScriban.Page(Resource.FromString("{{i.will.fail}}"));
+
+            using var runner = TestHost.Run(page);
+
+            using var response = await runner.GetResponseAsync();
+
+            await response.AssertStatusAsync(HttpStatusCode.InternalServerError);
+        }
+
+    }
+
+}
diff --git a/Testing/Modules/ProtobufTests.cs b/Testing/Acceptance/Modules/ProtobufTests.cs
similarity index 95%
rename from Testing/Modules/ProtobufTests.cs
rename to Testing/Acceptance/Modules/ProtobufTests.cs
index 8842a73d..66763366 100644
--- a/Testing/Modules/ProtobufTests.cs
+++ b/Testing/Acceptance/Modules/ProtobufTests.cs
@@ -139,18 +139,18 @@ private async Task WithResponse(string uri, HttpMethod method, byte[]? body, str
                 }
             }
 
-            using var response = await service.GetResponse(request);
+            using var response = await service.GetResponseAsync(request);
 
             await logic(response);
         }
 
-        private static TestRunner GetService()
+        private static TestHost GetService()
         {
             var service = ServiceResource.From()
                                          .Formats(Serialization.Default().AddProtobuf())
                                          .Injectors(Injection.Default());
 
-            return TestRunner.Run(Layout.Create().Add("t", service));
+            return TestHost.Run(Layout.Create().Add("t", service));
         }
 
         #endregion
diff --git a/Testing/Modules/Razor/ConfigurationTests.cs b/Testing/Acceptance/Modules/Razor/ConfigurationTests.cs
similarity index 82%
rename from Testing/Modules/Razor/ConfigurationTests.cs
rename to Testing/Acceptance/Modules/Razor/ConfigurationTests.cs
index e62b7872..a27c9abf 100644
--- a/Testing/Modules/Razor/ConfigurationTests.cs
+++ b/Testing/Acceptance/Modules/Razor/ConfigurationTests.cs
@@ -1,33 +1,33 @@
-using System.Threading.Tasks;
-
-using GenHTTP.Modules.IO;
-using GenHTTP.Modules.Razor;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-namespace GenHTTP.Testing.Acceptance.Modules.Razor
-{
-
-    [TestClass]
-    public sealed class ConfigurationTests
-    {
-
-        [TestMethod]
-        public async Task TestLinq()
-        {
-            var template = Resource.FromString("100 = @Enumerable.Range(0, 100).Count()");
-
-            var page = ModRazor.Page(template)
-                               .AddAssemblyReference("System.Linq")
-                               .AddUsing("System.Linq");
-
-            using var runner = TestRunner.Run(page);
-
-            using var response = await runner.GetResponse();
-
-            AssertX.Contains("100 = 100", await response.GetContent());
-        }
-
-    }
-
-}
+using System.Threading.Tasks;
+
+using GenHTTP.Modules.IO;
+using GenHTTP.Modules.Razor;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace GenHTTP.Testing.Acceptance.Modules.Razor
+{
+
+    [TestClass]
+    public sealed class ConfigurationTests
+    {
+
+        [TestMethod]
+        public async Task TestLinq()
+        {
+            var template = Resource.FromString("100 = @Enumerable.Range(0, 100).Count()");
+
+            var page = ModRazor.Page(template)
+                               .AddAssemblyReference("System.Linq")
+                               .AddUsing("System.Linq");
+
+            using var runner = TestHost.Run(page);
+
+            using var response = await runner.GetResponseAsync();
+
+            AssertX.Contains("100 = 100", await response.GetContent());
+        }
+
+    }
+
+}
diff --git a/Testing/Modules/RedirectTests.cs b/Testing/Acceptance/Modules/RedirectTests.cs
similarity index 73%
rename from Testing/Modules/RedirectTests.cs
rename to Testing/Acceptance/Modules/RedirectTests.cs
index 3a0044a7..5e0cdee4 100644
--- a/Testing/Modules/RedirectTests.cs
+++ b/Testing/Acceptance/Modules/RedirectTests.cs
@@ -1,132 +1,132 @@
-using System;
-using System.Net;
-using System.Net.Http;
-using System.Threading.Tasks;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using GenHTTP.Modules.Basics;
-using GenHTTP.Modules.Layouting;
-using GenHTTP.Modules.IO;
-
-namespace GenHTTP.Testing.Acceptance.Providers
-{
-
-    [TestClass]
-    public sealed class RedirectTests
-    {
-
-        [TestMethod]
-        public async Task TestTemporary()
-        {
-            var redirect = Redirect.To("https://google.de/", true);
-
-            using var runner = TestRunner.Run(redirect);
-
-            using var response = await runner.GetResponse();
-
-            await response.AssertStatusAsync(HttpStatusCode.TemporaryRedirect);
-            Assert.AreEqual("https://google.de/", response.GetHeader("Location"));
-        }
-
-        [TestMethod]
-        public async Task TestTemporaryPost()
-        {
-            var redirect = Redirect.To("https://google.de/", true);
-
-            using var runner = TestRunner.Run(redirect);
-
-            var request = runner.GetRequest();
-            request.Method = HttpMethod.Post;
-
-            using var response = await runner.GetResponse(request);
-
-            await response.AssertStatusAsync(HttpStatusCode.SeeOther);
-            Assert.AreEqual("https://google.de/", response.GetHeader("Location"));
-        }
-
-        [TestMethod]
-        public async Task TestPermanent()
-        {
-            var redirect = Redirect.To("https://google.de/");
-
-            using var runner = TestRunner.Run(redirect);
-
-            using var response = await runner.GetResponse();
-
-            await response.AssertStatusAsync(HttpStatusCode.MovedPermanently);
-            Assert.AreEqual("https://google.de/", response.GetHeader("Location"));
-        }
-
-        [TestMethod]
-        public async Task TestPermanentPost()
-        {
-            var redirect = Redirect.To("https://google.de/", false);
-
-            using var runner = TestRunner.Run(redirect);
-
-            var request = runner.GetRequest();
-            request.Method = HttpMethod.Post;
-
-            using var response = await runner.GetResponse(request);
-
-            await response.AssertStatusAsync(HttpStatusCode.PermanentRedirect);
-            Assert.AreEqual("https://google.de/", response.GetHeader("Location"));
-        }
-
-        [TestMethod]
-        public async Task TestSimpleRoute()
-        {
-            var layout = Layout.Create()
-                               .Add("redirect", Redirect.To("{index}"))
-                               .Index(Content.From(Resource.FromString("Hello World")));
-
-            using var runner = TestRunner.Run(layout);
-
-            using var response = await runner.GetResponse("/redirect");
-
-            Assert.AreEqual("/", new Uri(response.GetHeader("Location")!).AbsolutePath);
-        }
-
-        [TestMethod]
-        public async Task TestSimpleRelativeRoute()
-        {
-            var layout = Layout.Create()
-                               .Add("redirect", Redirect.To("./me/to"));
-
-            using var runner = TestRunner.Run(layout);
-
-            using var response = await runner.GetResponse("/redirect/");
-
-            Assert.AreEqual("/redirect/me/to", new Uri(response.GetHeader("Location")!).AbsolutePath);
-        }
-
-        [TestMethod]
-        public async Task TestNavigatedRelativeRoute()
-        {
-            var layout = Layout.Create()
-                               .Add("redirect", Redirect.To("../me/to"));
-
-            using var runner = TestRunner.Run(layout);
-
-            using var response = await runner.GetResponse("/redirect/");
-
-            Assert.AreEqual("/me/to", new Uri(response.GetHeader("Location")!).AbsolutePath);
-        }
-
-        [TestMethod]
-        public async Task TestAbsoluteRoute()
-        {
-            var layout = Layout.Create()
-                               .Add("redirect", Redirect.To("/me/to/"));
-
-            using var runner = TestRunner.Run(layout);
-
-            using var response = await runner.GetResponse("/redirect/");
-
-            Assert.AreEqual("/me/to/", new Uri(response.GetHeader("Location")!).AbsolutePath);
-        }
-
-    }
-
-}
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using GenHTTP.Modules.Basics;
+using GenHTTP.Modules.Layouting;
+using GenHTTP.Modules.IO;
+
+namespace GenHTTP.Testing.Acceptance.Providers
+{
+
+    [TestClass]
+    public sealed class RedirectTests
+    {
+
+        [TestMethod]
+        public async Task TestTemporary()
+        {
+            var redirect = Redirect.To("https://google.de/", true);
+
+            using var runner = TestHost.Run(redirect);
+
+            using var response = await runner.GetResponseAsync();
+
+            await response.AssertStatusAsync(HttpStatusCode.TemporaryRedirect);
+            Assert.AreEqual("https://google.de/", response.GetHeader("Location"));
+        }
+
+        [TestMethod]
+        public async Task TestTemporaryPost()
+        {
+            var redirect = Redirect.To("https://google.de/", true);
+
+            using var runner = TestHost.Run(redirect);
+
+            var request = runner.GetRequest();
+            request.Method = HttpMethod.Post;
+
+            using var response = await runner.GetResponseAsync(request);
+
+            await response.AssertStatusAsync(HttpStatusCode.SeeOther);
+            Assert.AreEqual("https://google.de/", response.GetHeader("Location"));
+        }
+
+        [TestMethod]
+        public async Task TestPermanent()
+        {
+            var redirect = Redirect.To("https://google.de/");
+
+            using var runner = TestHost.Run(redirect);
+
+            using var response = await runner.GetResponseAsync();
+
+            await response.AssertStatusAsync(HttpStatusCode.MovedPermanently);
+            Assert.AreEqual("https://google.de/", response.GetHeader("Location"));
+        }
+
+        [TestMethod]
+        public async Task TestPermanentPost()
+        {
+            var redirect = Redirect.To("https://google.de/", false);
+
+            using var runner = TestHost.Run(redirect);
+
+            var request = runner.GetRequest();
+            request.Method = HttpMethod.Post;
+
+            using var response = await runner.GetResponseAsync(request);
+
+            await response.AssertStatusAsync(HttpStatusCode.PermanentRedirect);
+            Assert.AreEqual("https://google.de/", response.GetHeader("Location"));
+        }
+
+        [TestMethod]
+        public async Task TestSimpleRoute()
+        {
+            var layout = Layout.Create()
+                               .Add("redirect", Redirect.To("{index}"))
+                               .Index(Content.From(Resource.FromString("Hello World")));
+
+            using var runner = TestHost.Run(layout);
+
+            using var response = await runner.GetResponseAsync("/redirect");
+
+            Assert.AreEqual("/", new Uri(response.GetHeader("Location")!).AbsolutePath);
+        }
+
+        [TestMethod]
+        public async Task TestSimpleRelativeRoute()
+        {
+            var layout = Layout.Create()
+                               .Add("redirect", Redirect.To("./me/to"));
+
+            using var runner = TestHost.Run(layout);
+
+            using var response = await runner.GetResponseAsync("/redirect/");
+
+            Assert.AreEqual("/redirect/me/to", new Uri(response.GetHeader("Location")!).AbsolutePath);
+        }
+
+        [TestMethod]
+        public async Task TestNavigatedRelativeRoute()
+        {
+            var layout = Layout.Create()
+                               .Add("redirect", Redirect.To("../me/to"));
+
+            using var runner = TestHost.Run(layout);
+
+            using var response = await runner.GetResponseAsync("/redirect/");
+
+            Assert.AreEqual("/me/to", new Uri(response.GetHeader("Location")!).AbsolutePath);
+        }
+
+        [TestMethod]
+        public async Task TestAbsoluteRoute()
+        {
+            var layout = Layout.Create()
+                               .Add("redirect", Redirect.To("/me/to/"));
+
+            using var runner = TestHost.Run(layout);
+
+            using var response = await runner.GetResponseAsync("/redirect/");
+
+            Assert.AreEqual("/me/to/", new Uri(response.GetHeader("Location")!).AbsolutePath);
+        }
+
+    }
+
+}
diff --git a/Testing/Modules/ReverseProxyTests.cs b/Testing/Acceptance/Modules/ReverseProxyTests.cs
similarity index 85%
rename from Testing/Modules/ReverseProxyTests.cs
rename to Testing/Acceptance/Modules/ReverseProxyTests.cs
index 3944f4b5..823e332f 100644
--- a/Testing/Modules/ReverseProxyTests.cs
+++ b/Testing/Acceptance/Modules/ReverseProxyTests.cs
@@ -1,422 +1,422 @@
-using System;
-using System.IO;
-using System.Collections.Generic;
-using System.Net;
-using System.Net.Http;
-using System.Threading.Tasks;
-using System.Linq;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using GenHTTP.Api.Content;
-using GenHTTP.Api.Protocol;
-
-using GenHTTP.Modules.IO;
-using GenHTTP.Modules.Practices;
-using GenHTTP.Modules.ReverseProxy;
-using GenHTTP.Modules.Layouting;
-
-using Cookie = GenHTTP.Api.Protocol.Cookie;
-
-namespace GenHTTP.Testing.Acceptance.Providers
-{
-
-    [TestClass]
-    public sealed class ReverseProxyTests
-    {
-
-        #region Supporting data structures
-
-        private class TestSetup : IDisposable
-        {
-            private readonly TestRunner _Target;
-
-            public TestRunner Runner { get; }
-
-            private TestSetup(TestRunner source, TestRunner target)
-            {
-                Runner = source;
-                _Target = target;
-            }
-
-            public static TestSetup Create(Func response)
-            {
-                // server hosting the actuall web app
-                var testServer = new TestRunner();
-
-                testServer.Host.Handler(new ProxiedRouter(response).Wrap())
-                               .Development()
-                               .Start();
-
-                // proxying server
-                var proxy = Proxy.Create()
-                                 .ConnectTimeout(TimeSpan.FromSeconds(2))
-                                 .ReadTimeout(TimeSpan.FromSeconds(5))
-                                 .Upstream("http://localhost:" + testServer.Port);
-
-                var runner = new TestRunner();
-
-                runner.Host.Handler(proxy)
-                           .Development()
-                           .Defaults()
-                           .Start();
-
-                return new TestSetup(runner, testServer);
-            }
-
-            #region IDisposable Support
-
-            private bool disposedValue = false;
-
-            protected virtual void Dispose(bool disposing)
-            {
-                if (!disposedValue)
-                {
-                    if (disposing)
-                    {
-                        Runner.Dispose();
-                        _Target.Dispose();
-                    }
-
-                    disposedValue = true;
-                }
-            }
-
-            public void Dispose()
-            {
-                Dispose(true);
-            }
-
-            #endregion
-
-        }
-
-        private class ProxiedRouter : IHandler
-        {
-            private readonly Func _Response;
-
-            public ProxiedRouter(Func response)
-            {
-                _Response = response;
-            }
-
-            public ValueTask PrepareAsync() => ValueTask.CompletedTask;
-
-            public IHandler Parent => throw new NotImplementedException();
-
-            public IAsyncEnumerable GetContentAsync(IRequest request) => AsyncEnumerable.Empty();
-
-            public ValueTask HandleAsync(IRequest request)
-            {
-                return new ProxiedProvider(_Response).HandleAsync(request);
-            }
-
-        }
-
-        private class ProxiedProvider : IHandler
-        {
-            private readonly Func _Response;
-
-            public ProxiedProvider(Func response)
-            {
-                _Response = response;
-            }
-
-            public IHandler Parent => throw new NotImplementedException();
-            
-            public ValueTask PrepareAsync() => ValueTask.CompletedTask;
-
-            public IAsyncEnumerable GetContentAsync(IRequest request)
-            {
-                throw new NotImplementedException();
-            }
-
-            public ValueTask HandleAsync(IRequest request)
-            {
-                Assert.AreNotEqual(request.Client, request.LocalClient);
-                Assert.IsTrue(request.Forwardings.Count > 0);
-
-                var response = _Response.Invoke(request);
-
-                if (response is not null)
-                {
-                    return new ValueTask(response);
-                }
-
-                return request.Respond()
-                              .Status(ResponseStatus.InternalServerError)
-                              .BuildTask();
-            }
-
-        }
-
-        #endregion
-
-        [TestMethod]
-        public async Task TestBasics()
-        {
-            using var setup = TestSetup.Create((r) =>
-            {
-                return r.Respond().Content("Hello World!").Build();
-            });
-
-            var runner = setup.Runner;
-
-            using var response = await runner.GetResponse();
-            Assert.AreEqual("Hello World!", await response.GetContent());
-        }
-
-        [TestMethod]
-        public async Task TestRedirection()
-        {
-            using var setup = TestSetup.Create((r) =>
-            {
-                return r.Respond().Header("Location", $"http://localhost:{r.EndPoint.Port}/target").Status(ResponseStatus.TemporaryRedirect).Build();
-            });
-
-            var runner = setup.Runner;
-
-            using var redirected = await runner.GetResponse("/");
-
-            Assert.AreEqual($"http://localhost:{runner.Port}/target", redirected.GetHeader("Location"));
-        }
-
-        [TestMethod]
-        public async Task TestHead()
-        {
-            using var setup = TestSetup.Create((r) =>
-            {
-                return r.Respond().Content("Hello World!").Build();
-            });
-
-            var runner = setup.Runner;
-
-            var headRequest = runner.GetRequest();
-            headRequest.Method = HttpMethod.Head;
-
-            using var headed = await runner.GetResponse(headRequest);
-
-            await headed.AssertStatusAsync(HttpStatusCode.OK);
-        }
-
-        [TestMethod]
-        public async Task TestCookies()
-        {
-            using var setup = TestSetup.Create((r) =>
-            {
-                Assert.AreEqual("World", r.Cookies["Hello"].Value);
-
-                return r.Respond()
-                        .Content("Hello World!")
-                        .Cookie(new Cookie("One", "1"))
-                        .Cookie(new Cookie("Two", "2"))
-                        .Build();
-            });
-
-            var runner = setup.Runner;
-
-            var cookies = new CookieContainer();
-            cookies.Add(new System.Net.Cookie("Hello", "World", "/", "localhost"));
-
-            using var client = TestRunner.GetClient(cookies: cookies);
-
-            var cookieRequest = runner.GetRequest();
-
-            using var cookied = await client.SendAsync(cookieRequest);
-
-            await cookied.AssertStatusAsync(HttpStatusCode.OK);
-
-            var returned = cookies.GetCookies(new Uri(runner.GetUrl()));
-
-            Assert.AreEqual("1", returned["One"]!.Value);
-            Assert.AreEqual("2", returned["Two"]!.Value);
-        }
-
-        [TestMethod]
-        public async Task TestHeaders()
-        {
-            var now = DateTime.UtcNow;
-
-            using var setup = TestSetup.Create((r) =>
-            {
-                return r.Respond()
-                        .Content("Hello World")
-                        .Header("X-Custom-Header", r.Headers["X-Custom-Header"])
-                        .Expires(now)
-                        .Modified(now)
-                        .Build();
-            });
-
-            var runner = setup.Runner;
-
-            var request = runner.GetRequest();
-
-            request.Headers.Add("X-Custom-Header", "Custom Value");
-
-            using var response = await runner.GetResponse(request);
-
-            Assert.AreEqual("Custom Value", response.GetHeader("X-Custom-Header"));
-
-            Assert.AreEqual(now.ToString("r"), response.GetContentHeader("Expires"));
-            Assert.AreEqual(now.ToString("r"), response.GetContentHeader("Last-Modified"));
-        }
-
-        [TestMethod]
-        public async Task TestPost()
-        {
-            using var setup = TestSetup.Create((r) =>
-            {
-                using var reader = new StreamReader(r.Content!);
-                return r.Respond().Content(reader.ReadToEnd()).Build();
-            });
-
-            var runner = setup.Runner;
-
-            var request = runner.GetRequest();
-
-            request.Method = HttpMethod.Post;
-            request.Content = new StringContent("Input");
-
-            using var response = await runner.GetResponse(request);
-
-            await response.AssertStatusAsync(HttpStatusCode.OK);
-            Assert.AreEqual("Input", await response.GetContent());
-        }
-
-        [TestMethod]
-        public async Task TestPathing()
-        {
-            using var setup = TestSetup.Create((r) =>
-            {
-                return r.Respond().Content(r.Target.Path.ToString()).Build();
-            });
-
-            var runner = setup.Runner;
-
-            using var r1 = await runner.GetResponse("/");
-            Assert.AreEqual("/", await r1.GetContent());
-
-            using var r2 = await runner.GetResponse("/login/");
-            Assert.AreEqual("/login/", await r2.GetContent());
-
-            using var r3 = await runner.GetResponse("/login");
-            Assert.AreEqual("/login", await r3.GetContent());
-        }
-
-        [TestMethod]
-        public async Task TestQuery()
-        {
-            using var setup = TestSetup.Create((r) =>
-            {
-                var result = string.Join('|', r.Query.Select(kv => $"{kv.Key}={kv.Value}"));
-                return r.Respond().Content(result).Build();
-            });
-
-            var runner = setup.Runner;
-
-            using var r2 = await runner.GetResponse("/?one=two");
-            Assert.AreEqual("one=two", await r2.GetContent());
-
-            using var r3 = await runner.GetResponse("/?one=two&three=four");
-            Assert.AreEqual("one=two|three=four", await r3.GetContent());
-
-            using var r1 = await runner.GetResponse("/");
-            Assert.AreEqual("", await r1.GetContent());
-        }
-
-        [TestMethod]
-        public async Task TestQuerySpecialChars()
-        {
-            using var setup = TestSetup.Create((r) =>
-            {
-                var result = string.Join('|', r.Query.Select(kv => $"{kv.Key}={kv.Value}"));
-                return r.Respond().Content(result).Build();
-            });
-
-            var runner = setup.Runner;
-
-            using var r = await runner.GetResponse("/?key=%20%3C+");
-            Assert.AreEqual("key= <+", await r.GetContent());
-        }
-
-        [TestMethod]
-        public async Task TestPathSpecialChars()
-        {
-            using var setup = TestSetup.Create((r) =>
-            {
-                return r.Respond().Content(r.Target.Path.ToString(true)).Build();
-            });
-
-            var runner = setup.Runner;
-
-            using var r = await runner.GetResponse("/%3F%23%26%2F %20");
-            Assert.AreEqual("/%3F%23%26%2F%20%20", await r.GetContent());
-        }
-
-        [TestMethod]
-        public async Task TestPathPreservesSpecialChars()
-        {
-            using var setup = TestSetup.Create((r) =>
-            {
-                return r.Respond().Content(r.Target.Path.ToString(true)).Build();
-            });
-
-            var runner = setup.Runner;
-
-            using var r = await runner.GetResponse("/$@:");
-            Assert.AreEqual("/$@:", await r.GetContent());
-        }
-
-        [TestMethod]
-        public async Task TestContentLengthPreserved()
-        {
-            using var setup = TestSetup.Create((r) =>
-            {
-                return r.Respond()
-                        .Content("Hello World")
-                        .Type(new(ContentType.ImageJpg))
-                        .Build();
-            });
-
-            using var response = await setup.Runner.GetResponse();
-
-            Assert.AreEqual(11, response.Content.Headers.ContentLength);
-            AssertX.IsNullOrEmpty(response.GetHeader("Transfer-Encoding"));
-        }
-
-        [TestMethod]
-        public async Task TestBadGateway()
-        {
-            var proxy = Proxy.Create()
-                             .Upstream("http://icertainlydonotexistasadomain");
-            
-            using var runner = TestRunner.Run(proxy);
-
-            using var response = await runner.GetResponse();
-
-            await response.AssertStatusAsync(HttpStatusCode.BadGateway);
-        }
-
-
-        [TestMethod]
-        public async Task TestCompression()
-        {
-            using var setup = TestSetup.Create((r) =>
-            {
-                return r.Respond().Content("Hello World!").Build();
-            });
-
-            var runner = setup.Runner;
-
-            var request = runner.GetRequest();
-
-            request.Headers.Add("Accept-Encoding", "br, gzip, deflate");
-
-            using var response = await runner.GetResponse(request);
-
-            Assert.AreEqual("br", response.GetContentHeader("Content-Encoding"));
-        }
-
-    }
-
-}
+using System;
+using System.IO;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using System.Linq;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using GenHTTP.Api.Content;
+using GenHTTP.Api.Protocol;
+
+using GenHTTP.Modules.IO;
+using GenHTTP.Modules.Practices;
+using GenHTTP.Modules.ReverseProxy;
+using GenHTTP.Modules.Layouting;
+
+using Cookie = GenHTTP.Api.Protocol.Cookie;
+
+namespace GenHTTP.Testing.Acceptance.Providers
+{
+
+    [TestClass]
+    public sealed class ReverseProxyTests
+    {
+
+        #region Supporting data structures
+
+        private class TestSetup : IDisposable
+        {
+            private readonly TestHost _Target;
+
+            public TestHost Runner { get; }
+
+            private TestSetup(TestHost source, TestHost target)
+            {
+                Runner = source;
+                _Target = target;
+            }
+
+            public static TestSetup Create(Func response)
+            {
+                // server hosting the actuall web app
+                var testServer = new TestHost(Layout.Create());
+
+                testServer.Host.Handler(new ProxiedRouter(response).Wrap())
+                               .Development()
+                               .Start();
+
+                // proxying server
+                var proxy = Proxy.Create()
+                                 .ConnectTimeout(TimeSpan.FromSeconds(2))
+                                 .ReadTimeout(TimeSpan.FromSeconds(5))
+                                 .Upstream("http://localhost:" + testServer.Port);
+
+                var runner = new TestHost(Layout.Create());
+
+                runner.Host.Handler(proxy)
+                           .Development()
+                           .Defaults()
+                           .Start();
+
+                return new TestSetup(runner, testServer);
+            }
+
+            #region IDisposable Support
+
+            private bool disposedValue = false;
+
+            protected virtual void Dispose(bool disposing)
+            {
+                if (!disposedValue)
+                {
+                    if (disposing)
+                    {
+                        Runner.Dispose();
+                        _Target.Dispose();
+                    }
+
+                    disposedValue = true;
+                }
+            }
+
+            public void Dispose()
+            {
+                Dispose(true);
+            }
+
+            #endregion
+
+        }
+
+        private class ProxiedRouter : IHandler
+        {
+            private readonly Func _Response;
+
+            public ProxiedRouter(Func response)
+            {
+                _Response = response;
+            }
+
+            public ValueTask PrepareAsync() => ValueTask.CompletedTask;
+
+            public IHandler Parent => throw new NotImplementedException();
+
+            public IAsyncEnumerable GetContentAsync(IRequest request) => AsyncEnumerable.Empty();
+
+            public ValueTask HandleAsync(IRequest request)
+            {
+                return new ProxiedProvider(_Response).HandleAsync(request);
+            }
+
+        }
+
+        private class ProxiedProvider : IHandler
+        {
+            private readonly Func _Response;
+
+            public ProxiedProvider(Func response)
+            {
+                _Response = response;
+            }
+
+            public IHandler Parent => throw new NotImplementedException();
+            
+            public ValueTask PrepareAsync() => ValueTask.CompletedTask;
+
+            public IAsyncEnumerable GetContentAsync(IRequest request)
+            {
+                throw new NotImplementedException();
+            }
+
+            public ValueTask HandleAsync(IRequest request)
+            {
+                Assert.AreNotEqual(request.Client, request.LocalClient);
+                Assert.IsTrue(request.Forwardings.Count > 0);
+
+                var response = _Response.Invoke(request);
+
+                if (response is not null)
+                {
+                    return new ValueTask(response);
+                }
+
+                return request.Respond()
+                              .Status(ResponseStatus.InternalServerError)
+                              .BuildTask();
+            }
+
+        }
+
+        #endregion
+
+        [TestMethod]
+        public async Task TestBasics()
+        {
+            using var setup = TestSetup.Create((r) =>
+            {
+                return r.Respond().Content("Hello World!").Build();
+            });
+
+            var runner = setup.Runner;
+
+            using var response = await runner.GetResponseAsync();
+            Assert.AreEqual("Hello World!", await response.GetContent());
+        }
+
+        [TestMethod]
+        public async Task TestRedirection()
+        {
+            using var setup = TestSetup.Create((r) =>
+            {
+                return r.Respond().Header("Location", $"http://localhost:{r.EndPoint.Port}/target").Status(ResponseStatus.TemporaryRedirect).Build();
+            });
+
+            var runner = setup.Runner;
+
+            using var redirected = await runner.GetResponseAsync("/");
+
+            Assert.AreEqual($"http://localhost:{runner.Port}/target", redirected.GetHeader("Location"));
+        }
+
+        [TestMethod]
+        public async Task TestHead()
+        {
+            using var setup = TestSetup.Create((r) =>
+            {
+                return r.Respond().Content("Hello World!").Build();
+            });
+
+            var runner = setup.Runner;
+
+            var headRequest = runner.GetRequest();
+            headRequest.Method = HttpMethod.Head;
+
+            using var headed = await runner.GetResponseAsync(headRequest);
+
+            await headed.AssertStatusAsync(HttpStatusCode.OK);
+        }
+
+        [TestMethod]
+        public async Task TestCookies()
+        {
+            using var setup = TestSetup.Create((r) =>
+            {
+                Assert.AreEqual("World", r.Cookies["Hello"].Value);
+
+                return r.Respond()
+                        .Content("Hello World!")
+                        .Cookie(new Cookie("One", "1"))
+                        .Cookie(new Cookie("Two", "2"))
+                        .Build();
+            });
+
+            var runner = setup.Runner;
+
+            var cookies = new CookieContainer();
+            cookies.Add(new System.Net.Cookie("Hello", "World", "/", "localhost"));
+
+            using var client = TestHost.GetClient(cookies: cookies);
+
+            var cookieRequest = runner.GetRequest();
+
+            using var cookied = await client.SendAsync(cookieRequest);
+
+            await cookied.AssertStatusAsync(HttpStatusCode.OK);
+
+            var returned = cookies.GetCookies(new Uri(runner.GetUrl()));
+
+            Assert.AreEqual("1", returned["One"]!.Value);
+            Assert.AreEqual("2", returned["Two"]!.Value);
+        }
+
+        [TestMethod]
+        public async Task TestHeaders()
+        {
+            var now = DateTime.UtcNow;
+
+            using var setup = TestSetup.Create((r) =>
+            {
+                return r.Respond()
+                        .Content("Hello World")
+                        .Header("X-Custom-Header", r.Headers["X-Custom-Header"])
+                        .Expires(now)
+                        .Modified(now)
+                        .Build();
+            });
+
+            var runner = setup.Runner;
+
+            var request = runner.GetRequest();
+
+            request.Headers.Add("X-Custom-Header", "Custom Value");
+
+            using var response = await runner.GetResponseAsync(request);
+
+            Assert.AreEqual("Custom Value", response.GetHeader("X-Custom-Header"));
+
+            Assert.AreEqual(now.ToString("r"), response.GetContentHeader("Expires"));
+            Assert.AreEqual(now.ToString("r"), response.GetContentHeader("Last-Modified"));
+        }
+
+        [TestMethod]
+        public async Task TestPost()
+        {
+            using var setup = TestSetup.Create((r) =>
+            {
+                using var reader = new StreamReader(r.Content!);
+                return r.Respond().Content(reader.ReadToEnd()).Build();
+            });
+
+            var runner = setup.Runner;
+
+            var request = runner.GetRequest();
+
+            request.Method = HttpMethod.Post;
+            request.Content = new StringContent("Input");
+
+            using var response = await runner.GetResponseAsync(request);
+
+            await response.AssertStatusAsync(HttpStatusCode.OK);
+            Assert.AreEqual("Input", await response.GetContent());
+        }
+
+        [TestMethod]
+        public async Task TestPathing()
+        {
+            using var setup = TestSetup.Create((r) =>
+            {
+                return r.Respond().Content(r.Target.Path.ToString()).Build();
+            });
+
+            var runner = setup.Runner;
+
+            using var r1 = await runner.GetResponseAsync("/");
+            Assert.AreEqual("/", await r1.GetContent());
+
+            using var r2 = await runner.GetResponseAsync("/login/");
+            Assert.AreEqual("/login/", await r2.GetContent());
+
+            using var r3 = await runner.GetResponseAsync("/login");
+            Assert.AreEqual("/login", await r3.GetContent());
+        }
+
+        [TestMethod]
+        public async Task TestQuery()
+        {
+            using var setup = TestSetup.Create((r) =>
+            {
+                var result = string.Join('|', r.Query.Select(kv => $"{kv.Key}={kv.Value}"));
+                return r.Respond().Content(result).Build();
+            });
+
+            var runner = setup.Runner;
+
+            using var r2 = await runner.GetResponseAsync("/?one=two");
+            Assert.AreEqual("one=two", await r2.GetContent());
+
+            using var r3 = await runner.GetResponseAsync("/?one=two&three=four");
+            Assert.AreEqual("one=two|three=four", await r3.GetContent());
+
+            using var r1 = await runner.GetResponseAsync("/");
+            Assert.AreEqual("", await r1.GetContent());
+        }
+
+        [TestMethod]
+        public async Task TestQuerySpecialChars()
+        {
+            using var setup = TestSetup.Create((r) =>
+            {
+                var result = string.Join('|', r.Query.Select(kv => $"{kv.Key}={kv.Value}"));
+                return r.Respond().Content(result).Build();
+            });
+
+            var runner = setup.Runner;
+
+            using var r = await runner.GetResponseAsync("/?key=%20%3C+");
+            Assert.AreEqual("key= <+", await r.GetContent());
+        }
+
+        [TestMethod]
+        public async Task TestPathSpecialChars()
+        {
+            using var setup = TestSetup.Create((r) =>
+            {
+                return r.Respond().Content(r.Target.Path.ToString(true)).Build();
+            });
+
+            var runner = setup.Runner;
+
+            using var r = await runner.GetResponseAsync("/%3F%23%26%2F %20");
+            Assert.AreEqual("/%3F%23%26%2F%20%20", await r.GetContent());
+        }
+
+        [TestMethod]
+        public async Task TestPathPreservesSpecialChars()
+        {
+            using var setup = TestSetup.Create((r) =>
+            {
+                return r.Respond().Content(r.Target.Path.ToString(true)).Build();
+            });
+
+            var runner = setup.Runner;
+
+            using var r = await runner.GetResponseAsync("/$@:");
+            Assert.AreEqual("/$@:", await r.GetContent());
+        }
+
+        [TestMethod]
+        public async Task TestContentLengthPreserved()
+        {
+            using var setup = TestSetup.Create((r) =>
+            {
+                return r.Respond()
+                        .Content("Hello World")
+                        .Type(new(ContentType.ImageJpg))
+                        .Build();
+            });
+
+            using var response = await setup.Runner.GetResponseAsync();
+
+            Assert.AreEqual(11, response.Content.Headers.ContentLength);
+            AssertX.IsNullOrEmpty(response.GetHeader("Transfer-Encoding"));
+        }
+
+        [TestMethod]
+        public async Task TestBadGateway()
+        {
+            var proxy = Proxy.Create()
+                             .Upstream("http://icertainlydonotexistasadomain");
+            
+            using var runner = TestHost.Run(proxy);
+
+            using var response = await runner.GetResponseAsync();
+
+            await response.AssertStatusAsync(HttpStatusCode.BadGateway);
+        }
+
+
+        [TestMethod]
+        public async Task TestCompression()
+        {
+            using var setup = TestSetup.Create((r) =>
+            {
+                return r.Respond().Content("Hello World!").Build();
+            });
+
+            var runner = setup.Runner;
+
+            var request = runner.GetRequest();
+
+            request.Headers.Add("Accept-Encoding", "br, gzip, deflate");
+
+            using var response = await runner.GetResponseAsync(request);
+
+            Assert.AreEqual("br", response.GetContentHeader("Content-Encoding"));
+        }
+
+    }
+
+}
diff --git a/Testing/Modules/RobotsTests.cs b/Testing/Acceptance/Modules/RobotsTests.cs
similarity index 77%
rename from Testing/Modules/RobotsTests.cs
rename to Testing/Acceptance/Modules/RobotsTests.cs
index f7406f67..bd00ef85 100644
--- a/Testing/Modules/RobotsTests.cs
+++ b/Testing/Acceptance/Modules/RobotsTests.cs
@@ -1,99 +1,99 @@
-using System.Threading.Tasks;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using GenHTTP.Api.Content;
-
-using GenHTTP.Modules.Layouting;
-using GenHTTP.Modules.Robots;
-using GenHTTP.Modules.Robots.Provider;
-using GenHTTP.Modules.Sitemaps;
-
-namespace GenHTTP.Testing.Acceptance.Providers
-{
-
-    [TestClass]
-    public sealed class RobotsTests
-    {
-
-        [TestMethod]
-        public async Task TestDefault()
-        {
-            using var runner = TestRunner.Run(GetTest(BotInstructions.Default()));
-
-            var result = await GetRobots(runner);
-
-            AssertX.Contains("User-agent: *", result);
-            AssertX.Contains("Allow: /", result);
-
-            AssertX.DoesNotContain("Sitemap", result);
-        }
-
-        [TestMethod]
-        public async Task TestDirective()
-        {
-            var robots = BotInstructions.Empty()
-                               .Directive(new string[] { "MyAgent 1", "MyAgent 2" },
-                                          new string[] { "/allowed", "/alsoallowed" },
-                                          new string[] { "/disallowed/", "/alsodisallowed" });
-
-            using var runner = TestRunner.Run(GetTest(robots));
-
-            var result = await GetRobots(runner);
-
-            AssertX.Contains("User-agent: MyAgent 1", result);
-            AssertX.Contains("User-agent: MyAgent 2", result);
-
-            AssertX.Contains("Allow: /allowed", result);
-            AssertX.Contains("Allow: /alsoallowed", result);
-
-            AssertX.Contains("Disallow: /disallowed/", result);
-            AssertX.Contains("Disallow: /alsodisallowed", result);
-        }
-
-        [TestMethod]
-        public async Task TestSitemap()
-        {
-            using var runner = TestRunner.Run(GetTest(BotInstructions.Default().Sitemap()));
-
-            var result = await GetRobots(runner);
-
-            AssertX.Contains("Sitemap: http://localhost/" + Sitemap.FILE_NAME, result);
-        }
-
-        [TestMethod]
-        public async Task TestCustomSitemap()
-        {
-            using var runner = TestRunner.Run(GetTest(BotInstructions.Default().Sitemap(Sitemap.FILE_NAME)));
-
-            var result = await GetRobots(runner);
-
-            AssertX.Contains("Sitemap: http://localhost/" + Sitemap.FILE_NAME, result);
-        }
-
-        [TestMethod]
-        public async Task TestAbsoluteSitemap()
-        {
-            using var runner = TestRunner.Run(GetTest(BotInstructions.Default().Sitemap("http://my/" + Sitemap.FILE_NAME)));
-
-            var result = await GetRobots(runner);
-
-            AssertX.Contains("Sitemap: http://my/" + Sitemap.FILE_NAME, result);
-        }
-
-        private static async Task GetRobots(TestRunner runner)
-        {
-            using var response = await runner.GetResponse("/" + BotInstructions.FILE_NAME);
-
-            return (await response.GetContent()).Replace($":{runner.Port}", string.Empty);
-        }
-
-        private static IHandlerBuilder GetTest(RobotsProviderBuilder robots)
-        {
-            return Layout.Create()
-                         .Add(BotInstructions.FILE_NAME, robots);
-        }
-
-    }
-
-}
+using System.Threading.Tasks;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using GenHTTP.Api.Content;
+
+using GenHTTP.Modules.Layouting;
+using GenHTTP.Modules.Robots;
+using GenHTTP.Modules.Robots.Provider;
+using GenHTTP.Modules.Sitemaps;
+
+namespace GenHTTP.Testing.Acceptance.Providers
+{
+
+    [TestClass]
+    public sealed class RobotsTests
+    {
+
+        [TestMethod]
+        public async Task TestDefault()
+        {
+            using var runner = TestHost.Run(GetTest(BotInstructions.Default()));
+
+            var result = await GetRobots(runner);
+
+            AssertX.Contains("User-agent: *", result);
+            AssertX.Contains("Allow: /", result);
+
+            AssertX.DoesNotContain("Sitemap", result);
+        }
+
+        [TestMethod]
+        public async Task TestDirective()
+        {
+            var robots = BotInstructions.Empty()
+                               .Directive(new string[] { "MyAgent 1", "MyAgent 2" },
+                                          new string[] { "/allowed", "/alsoallowed" },
+                                          new string[] { "/disallowed/", "/alsodisallowed" });
+
+            using var runner = TestHost.Run(GetTest(robots));
+
+            var result = await GetRobots(runner);
+
+            AssertX.Contains("User-agent: MyAgent 1", result);
+            AssertX.Contains("User-agent: MyAgent 2", result);
+
+            AssertX.Contains("Allow: /allowed", result);
+            AssertX.Contains("Allow: /alsoallowed", result);
+
+            AssertX.Contains("Disallow: /disallowed/", result);
+            AssertX.Contains("Disallow: /alsodisallowed", result);
+        }
+
+        [TestMethod]
+        public async Task TestSitemap()
+        {
+            using var runner = TestHost.Run(GetTest(BotInstructions.Default().Sitemap()));
+
+            var result = await GetRobots(runner);
+
+            AssertX.Contains("Sitemap: http://localhost/" + Sitemap.FILE_NAME, result);
+        }
+
+        [TestMethod]
+        public async Task TestCustomSitemap()
+        {
+            using var runner = TestHost.Run(GetTest(BotInstructions.Default().Sitemap(Sitemap.FILE_NAME)));
+
+            var result = await GetRobots(runner);
+
+            AssertX.Contains("Sitemap: http://localhost/" + Sitemap.FILE_NAME, result);
+        }
+
+        [TestMethod]
+        public async Task TestAbsoluteSitemap()
+        {
+            using var runner = TestHost.Run(GetTest(BotInstructions.Default().Sitemap("http://my/" + Sitemap.FILE_NAME)));
+
+            var result = await GetRobots(runner);
+
+            AssertX.Contains("Sitemap: http://my/" + Sitemap.FILE_NAME, result);
+        }
+
+        private static async Task GetRobots(TestHost runner)
+        {
+            using var response = await runner.GetResponseAsync("/" + BotInstructions.FILE_NAME);
+
+            return (await response.GetContent()).Replace($":{runner.Port}", string.Empty);
+        }
+
+        private static IHandlerBuilder GetTest(RobotsProviderBuilder robots)
+        {
+            return Layout.Create()
+                         .Add(BotInstructions.FILE_NAME, robots);
+        }
+
+    }
+
+}
diff --git a/Testing/Modules/Security/CorsTests.cs b/Testing/Acceptance/Modules/Security/CorsTests.cs
similarity index 88%
rename from Testing/Modules/Security/CorsTests.cs
rename to Testing/Acceptance/Modules/Security/CorsTests.cs
index 495ea685..81382414 100644
--- a/Testing/Modules/Security/CorsTests.cs
+++ b/Testing/Acceptance/Modules/Security/CorsTests.cs
@@ -1,133 +1,133 @@
-using System.Collections.Generic;
-using System.Net;
-using System.Net.Http;
-using System.Threading.Tasks;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using GenHTTP.Api.Protocol;
-
-using GenHTTP.Modules.IO;
-using GenHTTP.Modules.Layouting;
-using GenHTTP.Modules.Security;
-using GenHTTP.Modules.Security.Cors;
-
-namespace GenHTTP.Testing.Acceptance.Modules.Security
-{
-
-    [TestClass]
-    public sealed class CorsTests
-    {
-
-        [TestMethod]
-        public async Task TestPreflight()
-        {
-            using var runner = GetRunner(CorsPolicy.Permissive());
-
-            var request = new HttpRequestMessage(HttpMethod.Options, runner.GetUrl("/t"));
-
-            using var response = await runner.GetResponse(request);
-
-            await response.AssertStatusAsync(HttpStatusCode.NoContent);
-        }
-
-        [TestMethod]
-        public async Task TestPermissive()
-        {
-            using var runner = GetRunner(CorsPolicy.Permissive());
-
-            using var response = await runner.GetResponse("/t");
-
-            await response.AssertStatusAsync(HttpStatusCode.OK);
-
-            Assert.AreEqual("*", response.GetHeader("Access-Control-Allow-Origin"));
-
-            Assert.AreEqual("*", response.GetHeader("Access-Control-Allow-Methods"));
-            Assert.AreEqual("*, Authorization", response.GetHeader("Access-Control-Allow-Headers"));
-            Assert.AreEqual("*", response.GetHeader("Access-Control-Expose-Headers"));
-
-            Assert.AreEqual("true", response.GetHeader("Access-Control-Allow-Credentials"));
-
-            Assert.AreEqual("86400", response.GetHeader("Access-Control-Max-Age"));
-
-            Assert.AreEqual("Hello World", await response.GetContent());
-        }
-
-        [TestMethod]
-        public async Task TestPermissiveWithoutDefaultAuthorizationHeader()
-        {
-            using var runner = GetRunner(CorsPolicy.Permissive(false));
-
-            using var response = await runner.GetResponse("/t");
-
-            await response.AssertStatusAsync(HttpStatusCode.OK);
-
-            Assert.AreEqual("*", response.GetHeader("Access-Control-Allow-Origin"));
-
-            Assert.AreEqual("*", response.GetHeader("Access-Control-Allow-Methods"));
-            Assert.AreEqual("*", response.GetHeader("Access-Control-Allow-Headers"));
-            Assert.AreEqual("*", response.GetHeader("Access-Control-Expose-Headers"));
-
-            Assert.AreEqual("true", response.GetHeader("Access-Control-Allow-Credentials"));
-
-            Assert.AreEqual("86400", response.GetHeader("Access-Control-Max-Age"));
-
-            Assert.AreEqual("Hello World", await response.GetContent());
-        }
-
-        [TestMethod]
-        public async Task TestRestrictive()
-        {
-            using var runner = GetRunner(CorsPolicy.Restrictive());
-
-            using var response = await runner.GetResponse("/t");
-
-            await response.AssertStatusAsync(HttpStatusCode.OK);
-
-            Assert.IsFalse(response.Headers.Contains("Access-Control-Allow-Origin"));
-
-            Assert.IsFalse(response.Headers.Contains("Access-Control-Allow-Methods"));
-            Assert.IsFalse(response.Headers.Contains("Access-Control-Allow-Headers"));
-            Assert.IsFalse(response.Headers.Contains("Access-Control-Expose-Headers"));
-
-            Assert.IsFalse(response.Headers.Contains("Access-Control-Allow-Credentials"));
-
-            Assert.IsFalse(response.Headers.Contains("Access-Control-Max-Age"));
-        }
-
-        [TestMethod]
-        public async Task TestCustom()
-        {
-            var policy = CorsPolicy.Restrictive()
-                                   .Add("http://google.de", new List() { new FlexibleRequestMethod(RequestMethod.GET) }, null, new List() { "Accept" }, false);
-
-            using var runner = GetRunner(policy);
-
-            var request = runner.GetRequest("/t");
-            request.Headers.Add("Origin", "http://google.de");
-
-            using var response = await runner.GetResponse(request);
-
-            await response.AssertStatusAsync(HttpStatusCode.OK);
-
-            Assert.AreEqual("http://google.de", response.GetHeader("Access-Control-Allow-Origin"));
-
-            Assert.AreEqual("GET", response.GetHeader("Access-Control-Allow-Methods"));
-
-            Assert.AreEqual("Accept", response.GetHeader("Access-Control-Expose-Headers"));
-
-            Assert.AreEqual("Origin", response.GetHeader("Vary"));
-        }
-
-        private static TestRunner GetRunner(CorsPolicyBuilder policy)
-        {
-            var handler = Layout.Create()
-                                .Add("t", Content.From(Resource.FromString("Hello World")))
-                                .Add(policy);
-
-            return TestRunner.Run(handler);
-        }
-
-    }
-
-}
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using GenHTTP.Api.Protocol;
+
+using GenHTTP.Modules.IO;
+using GenHTTP.Modules.Layouting;
+using GenHTTP.Modules.Security;
+using GenHTTP.Modules.Security.Cors;
+
+namespace GenHTTP.Testing.Acceptance.Modules.Security
+{
+
+    [TestClass]
+    public sealed class CorsTests
+    {
+
+        [TestMethod]
+        public async Task TestPreflight()
+        {
+            using var runner = GetRunner(CorsPolicy.Permissive());
+
+            var request = new HttpRequestMessage(HttpMethod.Options, runner.GetUrl("/t"));
+
+            using var response = await runner.GetResponseAsync(request);
+
+            await response.AssertStatusAsync(HttpStatusCode.NoContent);
+        }
+
+        [TestMethod]
+        public async Task TestPermissive()
+        {
+            using var runner = GetRunner(CorsPolicy.Permissive());
+
+            using var response = await runner.GetResponseAsync("/t");
+
+            await response.AssertStatusAsync(HttpStatusCode.OK);
+
+            Assert.AreEqual("*", response.GetHeader("Access-Control-Allow-Origin"));
+
+            Assert.AreEqual("*", response.GetHeader("Access-Control-Allow-Methods"));
+            Assert.AreEqual("*, Authorization", response.GetHeader("Access-Control-Allow-Headers"));
+            Assert.AreEqual("*", response.GetHeader("Access-Control-Expose-Headers"));
+
+            Assert.AreEqual("true", response.GetHeader("Access-Control-Allow-Credentials"));
+
+            Assert.AreEqual("86400", response.GetHeader("Access-Control-Max-Age"));
+
+            Assert.AreEqual("Hello World", await response.GetContent());
+        }
+
+        [TestMethod]
+        public async Task TestPermissiveWithoutDefaultAuthorizationHeader()
+        {
+            using var runner = GetRunner(CorsPolicy.Permissive(false));
+
+            using var response = await runner.GetResponseAsync("/t");
+
+            await response.AssertStatusAsync(HttpStatusCode.OK);
+
+            Assert.AreEqual("*", response.GetHeader("Access-Control-Allow-Origin"));
+
+            Assert.AreEqual("*", response.GetHeader("Access-Control-Allow-Methods"));
+            Assert.AreEqual("*", response.GetHeader("Access-Control-Allow-Headers"));
+            Assert.AreEqual("*", response.GetHeader("Access-Control-Expose-Headers"));
+
+            Assert.AreEqual("true", response.GetHeader("Access-Control-Allow-Credentials"));
+
+            Assert.AreEqual("86400", response.GetHeader("Access-Control-Max-Age"));
+
+            Assert.AreEqual("Hello World", await response.GetContent());
+        }
+
+        [TestMethod]
+        public async Task TestRestrictive()
+        {
+            using var runner = GetRunner(CorsPolicy.Restrictive());
+
+            using var response = await runner.GetResponseAsync("/t");
+
+            await response.AssertStatusAsync(HttpStatusCode.OK);
+
+            Assert.IsFalse(response.Headers.Contains("Access-Control-Allow-Origin"));
+
+            Assert.IsFalse(response.Headers.Contains("Access-Control-Allow-Methods"));
+            Assert.IsFalse(response.Headers.Contains("Access-Control-Allow-Headers"));
+            Assert.IsFalse(response.Headers.Contains("Access-Control-Expose-Headers"));
+
+            Assert.IsFalse(response.Headers.Contains("Access-Control-Allow-Credentials"));
+
+            Assert.IsFalse(response.Headers.Contains("Access-Control-Max-Age"));
+        }
+
+        [TestMethod]
+        public async Task TestCustom()
+        {
+            var policy = CorsPolicy.Restrictive()
+                                   .Add("http://google.de", new List() { new FlexibleRequestMethod(RequestMethod.GET) }, null, new List() { "Accept" }, false);
+
+            using var runner = GetRunner(policy);
+
+            var request = runner.GetRequest("/t");
+            request.Headers.Add("Origin", "http://google.de");
+
+            using var response = await runner.GetResponseAsync(request);
+
+            await response.AssertStatusAsync(HttpStatusCode.OK);
+
+            Assert.AreEqual("http://google.de", response.GetHeader("Access-Control-Allow-Origin"));
+
+            Assert.AreEqual("GET", response.GetHeader("Access-Control-Allow-Methods"));
+
+            Assert.AreEqual("Accept", response.GetHeader("Access-Control-Expose-Headers"));
+
+            Assert.AreEqual("Origin", response.GetHeader("Vary"));
+        }
+
+        private static TestHost GetRunner(CorsPolicyBuilder policy)
+        {
+            var handler = Layout.Create()
+                                .Add("t", Content.From(Resource.FromString("Hello World")))
+                                .Add(policy);
+
+            return TestHost.Run(handler);
+        }
+
+    }
+
+}
diff --git a/Testing/Modules/Security/ExtensionTests.cs b/Testing/Acceptance/Modules/Security/ExtensionTests.cs
similarity index 78%
rename from Testing/Modules/Security/ExtensionTests.cs
rename to Testing/Acceptance/Modules/Security/ExtensionTests.cs
index 66600056..9990631f 100644
--- a/Testing/Modules/Security/ExtensionTests.cs
+++ b/Testing/Acceptance/Modules/Security/ExtensionTests.cs
@@ -1,30 +1,31 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using System.Threading.Tasks;
-
-using GenHTTP.Modules.Security;
-using GenHTTP.Modules.IO;
-
-namespace GenHTTP.Testing.Acceptance.Modules.Security
-{
-
-    [TestClass]
-    public sealed class ExtensionTests
-    {
-
-        [TestMethod]
-        public async Task ServerCanBeHardened()
-        {
-            using var runner = new TestRunner();
-
-            runner.Host.Handler(Content.From(Resource.FromString("Hello Eve!")))
-                       .Harden()
-                       .Start();
-
-            using var response = await runner.GetResponse();
-
-            Assert.AreEqual("nosniff", response.GetHeader("X-Content-Type-Options"));
-        }
-
-    }
-
-}
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.Threading.Tasks;
+
+using GenHTTP.Modules.Security;
+using GenHTTP.Modules.IO;
+using GenHTTP.Modules.Layouting;
+
+namespace GenHTTP.Testing.Acceptance.Modules.Security
+{
+
+    [TestClass]
+    public sealed class ExtensionTests
+    {
+
+        [TestMethod]
+        public async Task ServerCanBeHardened()
+        {
+            using var runner = new TestHost(Layout.Create());
+
+            runner.Host.Handler(Content.From(Resource.FromString("Hello Eve!")))
+                       .Harden()
+                       .Start();
+
+            using var response = await runner.GetResponseAsync();
+
+            Assert.AreEqual("nosniff", response.GetHeader("X-Content-Type-Options"));
+        }
+
+    }
+
+}
diff --git a/Testing/Modules/ServerCaching/PrecompressedContentTest.cs b/Testing/Acceptance/Modules/ServerCaching/PrecompressedContentTest.cs
similarity index 85%
rename from Testing/Modules/ServerCaching/PrecompressedContentTest.cs
rename to Testing/Acceptance/Modules/ServerCaching/PrecompressedContentTest.cs
index d7615a33..93d094b3 100644
--- a/Testing/Modules/ServerCaching/PrecompressedContentTest.cs
+++ b/Testing/Acceptance/Modules/ServerCaching/PrecompressedContentTest.cs
@@ -1,39 +1,39 @@
-using System.IO.Compression;
-using System.Net;
-using System.Threading.Tasks;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using GenHTTP.Modules.Compression;
-using GenHTTP.Modules.IO;
-using GenHTTP.Modules.ServerCaching;
-
-namespace GenHTTP.Testing.Acceptance.Modules.ServerCaching
-{
-
-    [TestClass]
-    public class PrecompressedContentTest
-    {
-
-        [TestMethod]
-        public async Task TestContentCanBePreCompressed()
-        {
-            var content = Resources.From(ResourceTree.FromAssembly("Resources"))
-                                   .Add(CompressedContent.Default().Level(CompressionLevel.Optimal))
-                                   .Add(ServerCache.Memory());
-
-            using var runner = TestRunner.Run(content, false);
-
-            var request = runner.GetRequest("/Template.html");
-
-            request.Headers.Add("Accept-Encoding", "br");
-
-            using var response = await runner.GetResponse(request);
-
-            await response.AssertStatusAsync(HttpStatusCode.OK);
-            Assert.AreEqual("br", response.GetContentHeader("Content-Encoding"));
-        }
-
-    }
-
-}
+using System.IO.Compression;
+using System.Net;
+using System.Threading.Tasks;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using GenHTTP.Modules.Compression;
+using GenHTTP.Modules.IO;
+using GenHTTP.Modules.ServerCaching;
+
+namespace GenHTTP.Testing.Acceptance.Modules.ServerCaching
+{
+
+    [TestClass]
+    public class PrecompressedContentTest
+    {
+
+        [TestMethod]
+        public async Task TestContentCanBePreCompressed()
+        {
+            var content = Resources.From(ResourceTree.FromAssembly("Resources"))
+                                   .Add(CompressedContent.Default().Level(CompressionLevel.Optimal))
+                                   .Add(ServerCache.Memory());
+
+            using var runner = TestHost.Run(content, false);
+
+            var request = runner.GetRequest("/Template.html");
+
+            request.Headers.Add("Accept-Encoding", "br");
+
+            using var response = await runner.GetResponseAsync(request);
+
+            await response.AssertStatusAsync(HttpStatusCode.OK);
+            Assert.AreEqual("br", response.GetContentHeader("Content-Encoding"));
+        }
+
+    }
+
+}
diff --git a/Testing/Modules/ServerCaching/ServerCacheTests.cs b/Testing/Acceptance/Modules/ServerCaching/ServerCacheTests.cs
similarity index 75%
rename from Testing/Modules/ServerCaching/ServerCacheTests.cs
rename to Testing/Acceptance/Modules/ServerCaching/ServerCacheTests.cs
index 4cc768ca..cb66d77e 100644
--- a/Testing/Modules/ServerCaching/ServerCacheTests.cs
+++ b/Testing/Acceptance/Modules/ServerCaching/ServerCacheTests.cs
@@ -1,455 +1,455 @@
-using System;
-using System.IO;
-using System.Net;
-using System.Net.Http;
-using System.Threading.Tasks;
-
-using GenHTTP.Api.Protocol;
-using GenHTTP.Modules.Caching;
-using GenHTTP.Modules.Compression;
-using GenHTTP.Modules.IO;
-using GenHTTP.Modules.Layouting;
-using GenHTTP.Modules.ServerCaching;
-using GenHTTP.Modules.ServerCaching.Provider;
-using GenHTTP.Modules.Sitemaps;
-
-using GenHTTP.Testing.Acceptance.Utilities;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Cookie = GenHTTP.Api.Protocol.Cookie;
-
-namespace GenHTTP.Testing.Acceptance.Modules.ServerCaching
-{
-
-    [TestClass]
-    public class ServerCacheTests
-    {
-
-        [TestMethod]
-        public async Task TestContentIsInvalidated()
-        {
-            var file = Path.GetTempFileName();
-
-            try
-            {
-                using var runner = TestRunner.Run(Content.From(Resource.FromFile(file))
-                                                         .Add(ServerCache.Memory()));
-
-                FileUtil.WriteText(file, "1");
-
-                using var first = await runner.GetResponse();
-
-                await first.AssertStatusAsync(HttpStatusCode.OK);
-                Assert.AreEqual("1", await first.GetContent());
-
-                FileUtil.WriteText(file, "12");
-
-                using var second = await runner.GetResponse();
-
-                await second.AssertStatusAsync(HttpStatusCode.OK);
-                Assert.AreEqual("12", await second.GetContent());
-            }
-            finally
-            {
-                try { File.Delete(file); } catch { /* nop */ }
-            }
-        }
-
-        [TestMethod]
-        public async Task TestContentNotInvalidated()
-        {
-            var file = Path.GetTempFileName();
-
-            try
-            {
-                using var runner = TestRunner.Run(Content.From(Resource.FromFile(file))
-                                                         .Add(ServerCache.Memory().Invalidate(false)));
-
-                FileUtil.WriteText(file, "1");
-
-                using var first = await runner.GetResponse();
-
-                await first.AssertStatusAsync(HttpStatusCode.OK);
-                Assert.AreEqual("1", await first.GetContent());
-
-                FileUtil.WriteText(file, "12");
-
-                using var second = await runner.GetResponse();
-
-                await second.AssertStatusAsync(HttpStatusCode.OK);
-                Assert.AreEqual("1", await second.GetContent());
-            }
-            finally
-            {
-                try { File.Delete(file); } catch { /* nop */ }
-            }
-        }
-
-        [TestMethod]
-        public async Task TestVariationRespected()
-        {
-            var file = Path.GetTempFileName();
-
-            FileUtil.WriteText(file, "This is some content!");
-
-            try
-            {
-                using var runner = TestRunner.Run(Content.From(Resource.FromFile(file).Type(new FlexibleContentType(ContentType.TextPlain)))
-                                                         .Add(CompressedContent.Default())
-                                                         .Add(ServerCache.Memory().Invalidate(false)), false);
-
-                var gzipRequest = runner.GetRequest();
-
-                gzipRequest.Headers.Add("Accept-Encoding", "gzip");
-
-                using var gzipResponse = await runner.GetResponse(gzipRequest);
-
-                await gzipResponse.AssertStatusAsync(HttpStatusCode.OK);
-                Assert.AreEqual("gzip", gzipResponse.GetContentHeader("Content-Encoding"));
-
-                var brRequest = runner.GetRequest();
-
-                brRequest.Headers.Add("Accept-Encoding", "br");
-
-                using var brResponse = await runner.GetResponse(brRequest);
-
-                await brResponse.AssertStatusAsync(HttpStatusCode.OK);
-                Assert.AreEqual("br", brResponse.GetContentHeader("Content-Encoding"));
-
-                using var uncompressedResponse = await runner.GetResponse();
-
-                await uncompressedResponse.AssertStatusAsync(HttpStatusCode.OK);
-                AssertX.IsNullOrEmpty(uncompressedResponse.GetContentHeader("Content-Encoding"));
-                Assert.AreEqual("This is some content!", await uncompressedResponse.GetContent());
-            }
-            finally
-            {
-                try { File.Delete(file); } catch { /* nop */ }
-            }
-        }
-
-        [TestMethod]
-        public async Task TestHeadersPreserved()
-        {
-            var now = DateTime.UtcNow;
-
-            var handler = new FunctionalHandler(responseProvider: (r) =>
-            {
-                return r.Respond()
-                        .Cookie(new Cookie("CKey", "CValue"))
-                        .Header("HKey", "HValue")
-                        .Content(Resource.FromString("0123456789").Type(new(ContentType.AudioWav)).Build())
-                        .Length(10)
-                        .Encoding("some-encoding")
-                        .Expires(now)
-                        .Modified(now)
-                        .Build();
-            });
-
-            using var runner = TestRunner.Run(handler.Wrap().Add(ServerCache.Memory().Invalidate(false)), false);
-
-            using var _ = await runner.GetResponse();
-
-            using var cached = await runner.GetResponse();
-
-            Assert.AreEqual("audio/wav", cached.GetContentHeader("Content-Type"));
-
-            Assert.AreEqual("HValue", cached.GetHeader("HKey"));
-            Assert.AreEqual("10", cached.GetContentHeader("Content-Length"));
-            Assert.AreEqual("some-encoding", cached.GetContentHeader("Content-Encoding"));
-
-            Assert.AreEqual(now.ToString(), cached.Content.Headers.LastModified.GetValueOrDefault().UtcDateTime.ToString());
-            Assert.IsTrue(cached.GetContentHeader("Expires") != null);
-
-            Assert.AreEqual("0123456789", await cached.GetContent());
-        }
-
-        [TestMethod]
-        public async Task TestNoContentCached()
-        {
-            var i = 0;
-
-            var handler = new FunctionalHandler(responseProvider: (r) =>
-            {
-                i++;
-
-                return r.Respond()
-                        .Status(ResponseStatus.NoContent)
-                        .Build();
-            });
-
-            using var runner = TestRunner.Run(handler.Wrap().Add(ServerCache.Memory().Invalidate(false)), false);
-
-            using var _ = await runner.GetResponse();
-
-            using var __ = await runner.GetResponse();
-
-            Assert.AreEqual(1, i);
-        }
-
-        [TestMethod]
-        public async Task TestNotOkNotCached()
-        {
-            var i = 0;
-
-            var handler = new FunctionalHandler(responseProvider: (r) =>
-            {
-                i++;
-
-                return r.Respond()
-                        .Status(ResponseStatus.InternalServerError)
-                        .Build();
-            });
-
-            using var runner = TestRunner.Run(handler.Wrap().Add(ServerCache.Memory().Invalidate(false)), false);
-
-            using var _ = await runner.GetResponse();
-
-            using var __ = await runner.GetResponse();
-
-            Assert.AreEqual(2, i);
-        }
-
-        [TestMethod]
-        public async Task TestPredicateNoMatchNoCache()
-        {
-            var i = 0;
-
-            var handler = new FunctionalHandler(responseProvider: (r) =>
-            {
-                i++;
-
-                return r.Respond()
-                        .Content(Resource.FromString("0123456789").Build())
-                        .Type(new FlexibleContentType(ContentType.TextHtml))
-                        .Build();
-            });
-
-            var cache = ServerCache.Memory()
-                                   .Predicate((_, r) => r.ContentType?.KnownType != ContentType.TextHtml)
-                                   .Invalidate(false);
-
-            using var runner = TestRunner.Run(handler.Wrap().Add(cache), false);
-
-            using var _ = await runner.GetResponse();
-
-            using var __ = await runner.GetResponse();
-
-            Assert.AreEqual(2, i);
-        }
-
-        [TestMethod]
-        public async Task TestPredicateMatchCached()
-        {
-            var i = 0;
-
-            var handler = new FunctionalHandler(responseProvider: (r) =>
-            {
-                i++;
-
-                return r.Respond()
-                        .Content(Resource.FromString("0123456789").Build())
-                        .Type(new FlexibleContentType(ContentType.TextCss))
-                        .Build();
-            });
-
-            var cache = ServerCache.Memory()
-                                   .Predicate((_, r) => r.ContentType?.KnownType != ContentType.TextHtml)
-                                   .Invalidate(false);
-
-            using var runner = TestRunner.Run(handler.Wrap().Add(cache), false);
-
-            using var _ = await runner.GetResponse();
-
-            using var __ = await runner.GetResponse();
-
-            Assert.AreEqual(1, i);
-        }
-
-        [TestMethod]
-        public async Task TestQueryDifferenceNotCached()
-        {
-            var i = 0;
-
-            var handler = new FunctionalHandler(responseProvider: (r) =>
-            {
-                i++;
-
-                return r.Respond()
-                        .Content(Resource.FromString("0123456789").Build())
-                        .Build();
-            });
-
-            var cache = ServerCache.Memory()
-                                   .Invalidate(false);
-
-            using var runner = TestRunner.Run(handler.Wrap().Add(cache), false);
-
-            using var _ = await runner.GetResponse("/?a=1");
-
-            using var __ = await runner.GetResponse("/?a=2");
-
-            Assert.AreEqual(2, i);
-        }
-
-        [TestMethod]
-        public async Task TestPostNotCached()
-        {
-            var i = 0;
-
-            var handler = new FunctionalHandler(responseProvider: (r) =>
-            {
-                i++;
-
-                return r.Respond()
-                        .Build();
-            });
-
-            var cache = ServerCache.Memory()
-                                   .Invalidate(false);
-
-            using var runner = TestRunner.Run(handler.Wrap().Add(cache), false);
-
-            using var _ = await runner.GetResponse(GetPostRequest(runner));
-
-            using var __ = await runner.GetResponse(GetPostRequest(runner));
-
-            Assert.AreEqual(2, i);
-        }
-
-        private static HttpRequestMessage GetPostRequest(TestRunner runner)
-        {
-            var request = runner.GetRequest();
-
-            request.Method = HttpMethod.Post;
-            request.Content = new StringContent("Hello World!");
-
-            return request;
-        }
-
-        [TestMethod]
-        public async Task TestVariationCached()
-        {
-            var i = 0;
-
-            var handler = new FunctionalHandler(responseProvider: (r) =>
-            {
-                i++;
-
-                return r.Respond()
-                        .Header("Vary", "Key")
-                        .Build();
-            });
-
-            var cache = ServerCache.Memory()
-                                   .Invalidate(false);
-
-            using var runner = TestRunner.Run(handler.Wrap().Add(cache), false);
-
-            using var _ = await runner.GetResponse(GetVaryRequest(runner));
-
-            using var __ = await runner.GetResponse(GetVaryRequest(runner));
-
-            Assert.AreEqual(1, i);
-        }
-
-        private static HttpRequestMessage GetVaryRequest(TestRunner runner)
-        {
-            var request = runner.GetRequest();
-
-            request.Headers.Add("Key", "Value");
-
-            return request;
-        }
-
-        [TestMethod]
-        public async Task TestContent()
-        {
-            var cache = ServerCache.Memory();
-
-            var content = Resources.From(ResourceTree.FromAssembly("Resources"))
-                                   .Add(cache);
-
-            var app = Layout.Create()
-                            .Add(Sitemap.FILE_NAME, Sitemap.Create())
-                            .Add("app", content);
-
-            using var runner = TestRunner.Run(app, false);
-
-            using var response = await runner.GetResponse("/sitemap.xml");
-
-            var sitemap = await response.GetSitemap();
-
-            AssertX.Contains("/app/Template.html", sitemap);
-        }
-
-        [TestMethod]
-        public async Task TestDifferentStorageBackends()
-        {
-            foreach (var serverCache in GetBackends())
-            {
-                var i = 0;
-
-                var handler = new FunctionalHandler(responseProvider: (r) =>
-                {
-                    i++;
-
-                    return r.Respond()
-                            .Content(Resource.FromString("0123456789").Build())
-                            .Build();
-                });
-
-                using var runner = TestRunner.Run(handler.Wrap().Add(serverCache.Invalidate(false)), false);
-
-                using var _ = await runner.GetResponse();
-
-                using var __ = await runner.GetResponse();
-
-                Assert.AreEqual(1, i);
-            }
-        }
-
-        [TestMethod]
-        public async Task TestAccessExpiration()
-        {
-            var i = 0;
-
-            var handler = new FunctionalHandler(responseProvider: (r) =>
-            {
-                i++;
-
-                return r.Respond()
-                        .Content(Resource.FromString(Guid.NewGuid().ToString()).Build())
-                        .Build();
-            });
-
-            var meta = Cache.Memory();
-
-            var data = Cache.TemporaryFiles()
-                            .AccessExpiration(TimeSpan.FromMilliseconds(0));
-
-            using var runner = TestRunner.Run(handler.Wrap().Add(ServerCache.Create(meta, data)), false);
-
-            using var _ = await runner.GetResponse();
-
-            using var __ = await runner.GetResponse();
-
-            Assert.AreEqual(2, i);
-        }
-
-        private static ServerCacheHandlerBuilder[] GetBackends()
-        {
-            var tempDir = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()));
-
-            return new ServerCacheHandlerBuilder[]
-            {
-                ServerCache.Memory(), 
-                ServerCache.TemporaryFiles(), 
-                ServerCache.Persistent(tempDir)
-            };
-        }
-
-    }
-
-}
+using System;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+using GenHTTP.Api.Protocol;
+using GenHTTP.Modules.Caching;
+using GenHTTP.Modules.Compression;
+using GenHTTP.Modules.IO;
+using GenHTTP.Modules.Layouting;
+using GenHTTP.Modules.ServerCaching;
+using GenHTTP.Modules.ServerCaching.Provider;
+using GenHTTP.Modules.Sitemaps;
+
+using GenHTTP.Testing.Acceptance.Utilities;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using Cookie = GenHTTP.Api.Protocol.Cookie;
+
+namespace GenHTTP.Testing.Acceptance.Modules.ServerCaching
+{
+
+    [TestClass]
+    public class ServerCacheTests
+    {
+
+        [TestMethod]
+        public async Task TestContentIsInvalidated()
+        {
+            var file = Path.GetTempFileName();
+
+            try
+            {
+                using var runner = TestHost.Run(Content.From(Resource.FromFile(file))
+                                                         .Add(ServerCache.Memory()));
+
+                FileUtil.WriteText(file, "1");
+
+                using var first = await runner.GetResponseAsync();
+
+                await first.AssertStatusAsync(HttpStatusCode.OK);
+                Assert.AreEqual("1", await first.GetContent());
+
+                FileUtil.WriteText(file, "12");
+
+                using var second = await runner.GetResponseAsync();
+
+                await second.AssertStatusAsync(HttpStatusCode.OK);
+                Assert.AreEqual("12", await second.GetContent());
+            }
+            finally
+            {
+                try { File.Delete(file); } catch { /* nop */ }
+            }
+        }
+
+        [TestMethod]
+        public async Task TestContentNotInvalidated()
+        {
+            var file = Path.GetTempFileName();
+
+            try
+            {
+                using var runner = TestHost.Run(Content.From(Resource.FromFile(file))
+                                                         .Add(ServerCache.Memory().Invalidate(false)));
+
+                FileUtil.WriteText(file, "1");
+
+                using var first = await runner.GetResponseAsync();
+
+                await first.AssertStatusAsync(HttpStatusCode.OK);
+                Assert.AreEqual("1", await first.GetContent());
+
+                FileUtil.WriteText(file, "12");
+
+                using var second = await runner.GetResponseAsync();
+
+                await second.AssertStatusAsync(HttpStatusCode.OK);
+                Assert.AreEqual("1", await second.GetContent());
+            }
+            finally
+            {
+                try { File.Delete(file); } catch { /* nop */ }
+            }
+        }
+
+        [TestMethod]
+        public async Task TestVariationRespected()
+        {
+            var file = Path.GetTempFileName();
+
+            FileUtil.WriteText(file, "This is some content!");
+
+            try
+            {
+                using var runner = TestHost.Run(Content.From(Resource.FromFile(file).Type(new FlexibleContentType(ContentType.TextPlain)))
+                                                         .Add(CompressedContent.Default())
+                                                         .Add(ServerCache.Memory().Invalidate(false)), false);
+
+                var gzipRequest = runner.GetRequest();
+
+                gzipRequest.Headers.Add("Accept-Encoding", "gzip");
+
+                using var gzipResponse = await runner.GetResponseAsync(gzipRequest);
+
+                await gzipResponse.AssertStatusAsync(HttpStatusCode.OK);
+                Assert.AreEqual("gzip", gzipResponse.GetContentHeader("Content-Encoding"));
+
+                var brRequest = runner.GetRequest();
+
+                brRequest.Headers.Add("Accept-Encoding", "br");
+
+                using var brResponse = await runner.GetResponseAsync(brRequest);
+
+                await brResponse.AssertStatusAsync(HttpStatusCode.OK);
+                Assert.AreEqual("br", brResponse.GetContentHeader("Content-Encoding"));
+
+                using var uncompressedResponse = await runner.GetResponseAsync();
+
+                await uncompressedResponse.AssertStatusAsync(HttpStatusCode.OK);
+                AssertX.IsNullOrEmpty(uncompressedResponse.GetContentHeader("Content-Encoding"));
+                Assert.AreEqual("This is some content!", await uncompressedResponse.GetContent());
+            }
+            finally
+            {
+                try { File.Delete(file); } catch { /* nop */ }
+            }
+        }
+
+        [TestMethod]
+        public async Task TestHeadersPreserved()
+        {
+            var now = DateTime.UtcNow;
+
+            var handler = new FunctionalHandler(responseProvider: (r) =>
+            {
+                return r.Respond()
+                        .Cookie(new Cookie("CKey", "CValue"))
+                        .Header("HKey", "HValue")
+                        .Content(Resource.FromString("0123456789").Type(new(ContentType.AudioWav)).Build())
+                        .Length(10)
+                        .Encoding("some-encoding")
+                        .Expires(now)
+                        .Modified(now)
+                        .Build();
+            });
+
+            using var runner = TestHost.Run(handler.Wrap().Add(ServerCache.Memory().Invalidate(false)), false);
+
+            using var _ = await runner.GetResponseAsync();
+
+            using var cached = await runner.GetResponseAsync();
+
+            Assert.AreEqual("audio/wav", cached.GetContentHeader("Content-Type"));
+
+            Assert.AreEqual("HValue", cached.GetHeader("HKey"));
+            Assert.AreEqual("10", cached.GetContentHeader("Content-Length"));
+            Assert.AreEqual("some-encoding", cached.GetContentHeader("Content-Encoding"));
+
+            Assert.AreEqual(now.ToString(), cached.Content.Headers.LastModified.GetValueOrDefault().UtcDateTime.ToString());
+            Assert.IsTrue(cached.GetContentHeader("Expires") != null);
+
+            Assert.AreEqual("0123456789", await cached.GetContent());
+        }
+
+        [TestMethod]
+        public async Task TestNoContentCached()
+        {
+            var i = 0;
+
+            var handler = new FunctionalHandler(responseProvider: (r) =>
+            {
+                i++;
+
+                return r.Respond()
+                        .Status(ResponseStatus.NoContent)
+                        .Build();
+            });
+
+            using var runner = TestHost.Run(handler.Wrap().Add(ServerCache.Memory().Invalidate(false)), false);
+
+            using var _ = await runner.GetResponseAsync();
+
+            using var __ = await runner.GetResponseAsync();
+
+            Assert.AreEqual(1, i);
+        }
+
+        [TestMethod]
+        public async Task TestNotOkNotCached()
+        {
+            var i = 0;
+
+            var handler = new FunctionalHandler(responseProvider: (r) =>
+            {
+                i++;
+
+                return r.Respond()
+                        .Status(ResponseStatus.InternalServerError)
+                        .Build();
+            });
+
+            using var runner = TestHost.Run(handler.Wrap().Add(ServerCache.Memory().Invalidate(false)), false);
+
+            using var _ = await runner.GetResponseAsync();
+
+            using var __ = await runner.GetResponseAsync();
+
+            Assert.AreEqual(2, i);
+        }
+
+        [TestMethod]
+        public async Task TestPredicateNoMatchNoCache()
+        {
+            var i = 0;
+
+            var handler = new FunctionalHandler(responseProvider: (r) =>
+            {
+                i++;
+
+                return r.Respond()
+                        .Content(Resource.FromString("0123456789").Build())
+                        .Type(new FlexibleContentType(ContentType.TextHtml))
+                        .Build();
+            });
+
+            var cache = ServerCache.Memory()
+                                   .Predicate((_, r) => r.ContentType?.KnownType != ContentType.TextHtml)
+                                   .Invalidate(false);
+
+            using var runner = TestHost.Run(handler.Wrap().Add(cache), false);
+
+            using var _ = await runner.GetResponseAsync();
+
+            using var __ = await runner.GetResponseAsync();
+
+            Assert.AreEqual(2, i);
+        }
+
+        [TestMethod]
+        public async Task TestPredicateMatchCached()
+        {
+            var i = 0;
+
+            var handler = new FunctionalHandler(responseProvider: (r) =>
+            {
+                i++;
+
+                return r.Respond()
+                        .Content(Resource.FromString("0123456789").Build())
+                        .Type(new FlexibleContentType(ContentType.TextCss))
+                        .Build();
+            });
+
+            var cache = ServerCache.Memory()
+                                   .Predicate((_, r) => r.ContentType?.KnownType != ContentType.TextHtml)
+                                   .Invalidate(false);
+
+            using var runner = TestHost.Run(handler.Wrap().Add(cache), false);
+
+            using var _ = await runner.GetResponseAsync();
+
+            using var __ = await runner.GetResponseAsync();
+
+            Assert.AreEqual(1, i);
+        }
+
+        [TestMethod]
+        public async Task TestQueryDifferenceNotCached()
+        {
+            var i = 0;
+
+            var handler = new FunctionalHandler(responseProvider: (r) =>
+            {
+                i++;
+
+                return r.Respond()
+                        .Content(Resource.FromString("0123456789").Build())
+                        .Build();
+            });
+
+            var cache = ServerCache.Memory()
+                                   .Invalidate(false);
+
+            using var runner = TestHost.Run(handler.Wrap().Add(cache), false);
+
+            using var _ = await runner.GetResponseAsync("/?a=1");
+
+            using var __ = await runner.GetResponseAsync("/?a=2");
+
+            Assert.AreEqual(2, i);
+        }
+
+        [TestMethod]
+        public async Task TestPostNotCached()
+        {
+            var i = 0;
+
+            var handler = new FunctionalHandler(responseProvider: (r) =>
+            {
+                i++;
+
+                return r.Respond()
+                        .Build();
+            });
+
+            var cache = ServerCache.Memory()
+                                   .Invalidate(false);
+
+            using var runner = TestHost.Run(handler.Wrap().Add(cache), false);
+
+            using var _ = await runner.GetResponseAsync(GetPostRequest(runner));
+
+            using var __ = await runner.GetResponseAsync(GetPostRequest(runner));
+
+            Assert.AreEqual(2, i);
+        }
+
+        private static HttpRequestMessage GetPostRequest(TestHost runner)
+        {
+            var request = runner.GetRequest();
+
+            request.Method = HttpMethod.Post;
+            request.Content = new StringContent("Hello World!");
+
+            return request;
+        }
+
+        [TestMethod]
+        public async Task TestVariationCached()
+        {
+            var i = 0;
+
+            var handler = new FunctionalHandler(responseProvider: (r) =>
+            {
+                i++;
+
+                return r.Respond()
+                        .Header("Vary", "Key")
+                        .Build();
+            });
+
+            var cache = ServerCache.Memory()
+                                   .Invalidate(false);
+
+            using var runner = TestHost.Run(handler.Wrap().Add(cache), false);
+
+            using var _ = await runner.GetResponseAsync(GetVaryRequest(runner));
+
+            using var __ = await runner.GetResponseAsync(GetVaryRequest(runner));
+
+            Assert.AreEqual(1, i);
+        }
+
+        private static HttpRequestMessage GetVaryRequest(TestHost runner)
+        {
+            var request = runner.GetRequest();
+
+            request.Headers.Add("Key", "Value");
+
+            return request;
+        }
+
+        [TestMethod]
+        public async Task TestContent()
+        {
+            var cache = ServerCache.Memory();
+
+            var content = Resources.From(ResourceTree.FromAssembly("Resources"))
+                                   .Add(cache);
+
+            var app = Layout.Create()
+                            .Add(Sitemap.FILE_NAME, Sitemap.Create())
+                            .Add("app", content);
+
+            using var runner = TestHost.Run(app, false);
+
+            using var response = await runner.GetResponseAsync("/sitemap.xml");
+
+            var sitemap = await response.GetSitemap();
+
+            AssertX.Contains("/app/Template.html", sitemap);
+        }
+
+        [TestMethod]
+        public async Task TestDifferentStorageBackends()
+        {
+            foreach (var serverCache in GetBackends())
+            {
+                var i = 0;
+
+                var handler = new FunctionalHandler(responseProvider: (r) =>
+                {
+                    i++;
+
+                    return r.Respond()
+                            .Content(Resource.FromString("0123456789").Build())
+                            .Build();
+                });
+
+                using var runner = TestHost.Run(handler.Wrap().Add(serverCache.Invalidate(false)), false);
+
+                using var _ = await runner.GetResponseAsync();
+
+                using var __ = await runner.GetResponseAsync();
+
+                Assert.AreEqual(1, i);
+            }
+        }
+
+        [TestMethod]
+        public async Task TestAccessExpiration()
+        {
+            var i = 0;
+
+            var handler = new FunctionalHandler(responseProvider: (r) =>
+            {
+                i++;
+
+                return r.Respond()
+                        .Content(Resource.FromString(Guid.NewGuid().ToString()).Build())
+                        .Build();
+            });
+
+            var meta = Cache.Memory();
+
+            var data = Cache.TemporaryFiles()
+                            .AccessExpiration(TimeSpan.FromMilliseconds(0));
+
+            using var runner = TestHost.Run(handler.Wrap().Add(ServerCache.Create(meta, data)), false);
+
+            using var _ = await runner.GetResponseAsync();
+
+            using var __ = await runner.GetResponseAsync();
+
+            Assert.AreEqual(2, i);
+        }
+
+        private static ServerCacheHandlerBuilder[] GetBackends()
+        {
+            var tempDir = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()));
+
+            return new ServerCacheHandlerBuilder[]
+            {
+                ServerCache.Memory(), 
+                ServerCache.TemporaryFiles(), 
+                ServerCache.Persistent(tempDir)
+            };
+        }
+
+    }
+
+}
diff --git a/Testing/Modules/SinglePageTests.cs b/Testing/Acceptance/Modules/SinglePageTests.cs
similarity index 73%
rename from Testing/Modules/SinglePageTests.cs
rename to Testing/Acceptance/Modules/SinglePageTests.cs
index 6d5421a1..e38f7fd8 100644
--- a/Testing/Modules/SinglePageTests.cs
+++ b/Testing/Acceptance/Modules/SinglePageTests.cs
@@ -1,130 +1,130 @@
-using System.IO;
-using System.Net;
-using System.Threading.Tasks;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using GenHTTP.Modules.SinglePageApplications;
-using GenHTTP.Modules.Layouting;
-using GenHTTP.Modules.Sitemaps;
-using GenHTTP.Modules.IO;
-
-using GenHTTP.Testing.Acceptance.Utilities;
-
-namespace GenHTTP.Testing.Acceptance.Providers
-{
-
-    [TestClass]
-    public sealed class SinglePageTests
-    {
-
-        [TestMethod]
-        public async Task TestIndex()
-        {
-            var root = CreateRoot();
-
-            FileUtil.WriteText(Path.Combine(root, "index.html"), "This is the index!");
-
-            using var runner = TestRunner.Run(SinglePageApplication.From(ResourceTree.FromDirectory(root)));
-
-            using var index = await runner.GetResponse("/");
-
-            await index.AssertStatusAsync(HttpStatusCode.OK);
-            Assert.AreEqual("text/html", index.GetContentHeader("Content-Type"));
-
-            var content = await index.GetContent();
-
-            Assert.AreEqual("This is the index!", content);
-        }
-
-        [TestMethod]
-        public async Task TestIndexServedWithRouting()
-        {
-            var root = CreateRoot();
-
-            FileUtil.WriteText(Path.Combine(root, "index.html"), "This is the index!");
-
-            var spa = SinglePageApplication.From(ResourceTree.FromDirectory(root))
-                                           .ServerSideRouting();
-
-            using var runner = TestRunner.Run(spa);
-
-            using var index = await runner.GetResponse("/some-route/");
-
-            await index.AssertStatusAsync(HttpStatusCode.OK);
-            Assert.AreEqual("text/html", index.GetContentHeader("Content-Type"));
-        }
-
-        [TestMethod]
-        public async Task TestNoIndex()
-        {
-            using var runner = TestRunner.Run(SinglePageApplication.From(ResourceTree.FromDirectory(CreateRoot())));
-
-            using var index = await runner.GetResponse("/");
-
-            await index.AssertStatusAsync(HttpStatusCode.NotFound);
-        }
-
-        [TestMethod]
-        public async Task TestFile()
-        {
-            var root = CreateRoot();
-
-            FileUtil.WriteText(Path.Combine(root, "some.txt"), "This is some text file :)");
-
-            using var runner = TestRunner.Run(SinglePageApplication.From(ResourceTree.FromDirectory(root)));
-
-            using var index = await runner.GetResponse("/some.txt");
-
-            await index.AssertStatusAsync(HttpStatusCode.OK);
-            Assert.AreEqual("text/plain", index.GetContentHeader("Content-Type"));
-
-            var content = await index.GetContent();
-
-            Assert.AreEqual("This is some text file :)", content);
-        }
-
-        [TestMethod]
-        public async Task TestNoFile()
-        {
-            using var runner = TestRunner.Run(SinglePageApplication.From(ResourceTree.FromDirectory(CreateRoot())));
-
-            using var index = await runner.GetResponse("/nope.txt");
-
-            await index.AssertStatusAsync(HttpStatusCode.NotFound);
-        }
-
-        [TestMethod]
-        public async Task TestContent()
-        {
-            var root = CreateRoot();
-
-            FileUtil.WriteText(Path.Combine(root, "index.html"), "Index");
-            FileUtil.WriteText(Path.Combine(root, "file.html"), "File");
-
-            var layout = Layout.Create()
-                               .Add("spa", SinglePageApplication.From(ResourceTree.FromDirectory(root)))
-                               .Add("sitemap", Sitemap.Create());
-
-            using var runner = TestRunner.Run(layout);
-
-            using var response = await runner.GetResponse("/sitemap");
-
-            var sitemap = await response.GetSitemap();
-
-            AssertX.Contains("/spa/index.html", sitemap);
-            AssertX.Contains("/spa/file.html", sitemap);
-        }
-
-        private static string CreateRoot()
-        {
-            var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
-
-            Directory.CreateDirectory(tempDirectory);
-
-            return tempDirectory;
-        }
-
-    }
-
-}
+using System.IO;
+using System.Net;
+using System.Threading.Tasks;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using GenHTTP.Modules.SinglePageApplications;
+using GenHTTP.Modules.Layouting;
+using GenHTTP.Modules.Sitemaps;
+using GenHTTP.Modules.IO;
+
+using GenHTTP.Testing.Acceptance.Utilities;
+
+namespace GenHTTP.Testing.Acceptance.Providers
+{
+
+    [TestClass]
+    public sealed class SinglePageTests
+    {
+
+        [TestMethod]
+        public async Task TestIndex()
+        {
+            var root = CreateRoot();
+
+            FileUtil.WriteText(Path.Combine(root, "index.html"), "This is the index!");
+
+            using var runner = TestHost.Run(SinglePageApplication.From(ResourceTree.FromDirectory(root)));
+
+            using var index = await runner.GetResponseAsync("/");
+
+            await index.AssertStatusAsync(HttpStatusCode.OK);
+            Assert.AreEqual("text/html", index.GetContentHeader("Content-Type"));
+
+            var content = await index.GetContent();
+
+            Assert.AreEqual("This is the index!", content);
+        }
+
+        [TestMethod]
+        public async Task TestIndexServedWithRouting()
+        {
+            var root = CreateRoot();
+
+            FileUtil.WriteText(Path.Combine(root, "index.html"), "This is the index!");
+
+            var spa = SinglePageApplication.From(ResourceTree.FromDirectory(root))
+                                           .ServerSideRouting();
+
+            using var runner = TestHost.Run(spa);
+
+            using var index = await runner.GetResponseAsync("/some-route/");
+
+            await index.AssertStatusAsync(HttpStatusCode.OK);
+            Assert.AreEqual("text/html", index.GetContentHeader("Content-Type"));
+        }
+
+        [TestMethod]
+        public async Task TestNoIndex()
+        {
+            using var runner = TestHost.Run(SinglePageApplication.From(ResourceTree.FromDirectory(CreateRoot())));
+
+            using var index = await runner.GetResponseAsync("/");
+
+            await index.AssertStatusAsync(HttpStatusCode.NotFound);
+        }
+
+        [TestMethod]
+        public async Task TestFile()
+        {
+            var root = CreateRoot();
+
+            FileUtil.WriteText(Path.Combine(root, "some.txt"), "This is some text file :)");
+
+            using var runner = TestHost.Run(SinglePageApplication.From(ResourceTree.FromDirectory(root)));
+
+            using var index = await runner.GetResponseAsync("/some.txt");
+
+            await index.AssertStatusAsync(HttpStatusCode.OK);
+            Assert.AreEqual("text/plain", index.GetContentHeader("Content-Type"));
+
+            var content = await index.GetContent();
+
+            Assert.AreEqual("This is some text file :)", content);
+        }
+
+        [TestMethod]
+        public async Task TestNoFile()
+        {
+            using var runner = TestHost.Run(SinglePageApplication.From(ResourceTree.FromDirectory(CreateRoot())));
+
+            using var index = await runner.GetResponseAsync("/nope.txt");
+
+            await index.AssertStatusAsync(HttpStatusCode.NotFound);
+        }
+
+        [TestMethod]
+        public async Task TestContent()
+        {
+            var root = CreateRoot();
+
+            FileUtil.WriteText(Path.Combine(root, "index.html"), "Index");
+            FileUtil.WriteText(Path.Combine(root, "file.html"), "File");
+
+            var layout = Layout.Create()
+                               .Add("spa", SinglePageApplication.From(ResourceTree.FromDirectory(root)))
+                               .Add("sitemap", Sitemap.Create());
+
+            using var runner = TestHost.Run(layout);
+
+            using var response = await runner.GetResponseAsync("/sitemap");
+
+            var sitemap = await response.GetSitemap();
+
+            AssertX.Contains("/spa/index.html", sitemap);
+            AssertX.Contains("/spa/file.html", sitemap);
+        }
+
+        private static string CreateRoot()
+        {
+            var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+
+            Directory.CreateDirectory(tempDirectory);
+
+            return tempDirectory;
+        }
+
+    }
+
+}
diff --git a/Testing/Modules/SitemapTests.cs b/Testing/Acceptance/Modules/SitemapTests.cs
similarity index 90%
rename from Testing/Modules/SitemapTests.cs
rename to Testing/Acceptance/Modules/SitemapTests.cs
index 96f2eab5..f4e12592 100644
--- a/Testing/Modules/SitemapTests.cs
+++ b/Testing/Acceptance/Modules/SitemapTests.cs
@@ -1,94 +1,94 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using System.Xml.Serialization;
-
-using GenHTTP.Api.Content;
-
-using GenHTTP.Modules.Layouting;
-using GenHTTP.Modules.Placeholders;
-using GenHTTP.Modules.Sitemaps;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-namespace GenHTTP.Testing.Acceptance.Providers
-{
-
-    [TestClass]
-    public sealed class SitemapTests
-    {
-
-        #region Helping data structures
-
-        [XmlRoot("urlset", Namespace = "http://www.sitemaps.org/schemas/sitemap/0.9")]
-        public sealed class UrlSet
-        {
-
-            [XmlElement("url")]
-            public List? Entries { get; set; }
-
-        }
-
-        public sealed class Url
-        {
-
-            [XmlElement("loc")]
-            public string? Loc { get; set; }
-
-        }
-
-        #endregion
-
-        [TestMethod]
-        public async Task TestKnownSitemap()
-        {
-            using var runner = TestRunner.Run(GetContent());
-
-            var sitemap = await GetSitemap(runner);
-
-            AssertX.Contains("http://localhost/", sitemap);
-            AssertX.Contains("http://localhost/other", sitemap);
-
-            AssertX.Contains("http://localhost/children/", sitemap);
-            AssertX.Contains("http://localhost/children/child-other", sitemap);
-
-            Assert.AreEqual(4, sitemap.Count);
-        }
-
-        private static async Task> GetSitemap(TestRunner runner)
-        {
-            var serializer = new XmlSerializer(typeof(UrlSet));
-
-            using var response = await runner.GetResponse("/" + Sitemap.FILE_NAME);
-
-            var sitemap = serializer.Deserialize(await response.Content.ReadAsStreamAsync()) as UrlSet;
-
-            return sitemap?.Entries?.Select(u => u.Loc!.Replace(":" + runner.Port, string.Empty)).ToHashSet() ?? new HashSet();
-        }
-
-        private static IHandlerBuilder GetContent()
-        {
-            var root = Layout.Create();
-
-            var children = Layout.Create();
-
-            children.Index(Page.From("Child Index Page", "Child Index"));
-            children.Add("child-other", Page.From("Child Other Page", "Child Other"));
-
-            var content = Layout.Create();
-
-            content.Index(Page.From("Index Page", "Index"));
-            content.Add("other", Page.From("Other Page", "Other"));
-
-            content.Add("children", children);
-
-            root.Add(Sitemap.FILE_NAME, Sitemap.Create());
-
-            root.Add(content);
-
-            return root;
-        }
-
-    }
-
-}
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Xml.Serialization;
+
+using GenHTTP.Api.Content;
+
+using GenHTTP.Modules.Layouting;
+using GenHTTP.Modules.Placeholders;
+using GenHTTP.Modules.Sitemaps;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace GenHTTP.Testing.Acceptance.Providers
+{
+
+    [TestClass]
+    public sealed class SitemapTests
+    {
+
+        #region Helping data structures
+
+        [XmlRoot("urlset", Namespace = "http://www.sitemaps.org/schemas/sitemap/0.9")]
+        public sealed class UrlSet
+        {
+
+            [XmlElement("url")]
+            public List? Entries { get; set; }
+
+        }
+
+        public sealed class Url
+        {
+
+            [XmlElement("loc")]
+            public string? Loc { get; set; }
+
+        }
+
+        #endregion
+
+        [TestMethod]
+        public async Task TestKnownSitemap()
+        {
+            using var runner = TestHost.Run(GetContent());
+
+            var sitemap = await GetSitemap(runner);
+
+            AssertX.Contains("http://localhost/", sitemap);
+            AssertX.Contains("http://localhost/other", sitemap);
+
+            AssertX.Contains("http://localhost/children/", sitemap);
+            AssertX.Contains("http://localhost/children/child-other", sitemap);
+
+            Assert.AreEqual(4, sitemap.Count);
+        }
+
+        private static async Task> GetSitemap(TestHost runner)
+        {
+            var serializer = new XmlSerializer(typeof(UrlSet));
+
+            using var response = await runner.GetResponseAsync("/" + Sitemap.FILE_NAME);
+
+            var sitemap = serializer.Deserialize(await response.Content.ReadAsStreamAsync()) as UrlSet;
+
+            return sitemap?.Entries?.Select(u => u.Loc!.Replace(":" + runner.Port, string.Empty)).ToHashSet() ?? new HashSet();
+        }
+
+        private static IHandlerBuilder GetContent()
+        {
+            var root = Layout.Create();
+
+            var children = Layout.Create();
+
+            children.Index(Page.From("Child Index Page", "Child Index"));
+            children.Add("child-other", Page.From("Child Other Page", "Child Other"));
+
+            var content = Layout.Create();
+
+            content.Index(Page.From("Index Page", "Index"));
+            content.Add("other", Page.From("Other Page", "Other"));
+
+            content.Add("children", children);
+
+            root.Add(Sitemap.FILE_NAME, Sitemap.Create());
+
+            root.Add(content);
+
+            return root;
+        }
+
+    }
+
+}
diff --git a/Testing/Modules/StaticWebsites/StaticWebsiteTests.cs b/Testing/Acceptance/Modules/StaticWebsites/StaticWebsiteTests.cs
similarity index 65%
rename from Testing/Modules/StaticWebsites/StaticWebsiteTests.cs
rename to Testing/Acceptance/Modules/StaticWebsites/StaticWebsiteTests.cs
index 8428902b..1f25c163 100644
--- a/Testing/Modules/StaticWebsites/StaticWebsiteTests.cs
+++ b/Testing/Acceptance/Modules/StaticWebsites/StaticWebsiteTests.cs
@@ -1,131 +1,131 @@
-using System.Net;
-using System.Threading.Tasks;
-
-using GenHTTP.Modules.IO;
-using GenHTTP.Modules.Robots;
-using GenHTTP.Modules.Sitemaps;
-using GenHTTP.Modules.StaticWebsites;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-namespace GenHTTP.Testing.Acceptance.Modules.StaticWebsites
-{
-
-    [TestClass]
-    public sealed class StaticWebsiteTests
-    {
-
-        [TestMethod]
-        public async Task TestWithIndex()
-        {
-            var tree = VirtualTree.Create()
-                                  .Add("index.html", Resource.FromString("Index 1"))
-                                  .Add("sub", VirtualTree.Create().Add("index.htm", Resource.FromString("Index 2")));
-
-            using var runner = TestRunner.Run(StaticWebsite.From(tree));
-
-            using var indexResponse = await runner.GetResponse();
-            Assert.AreEqual("Index 1", await indexResponse.GetContent());
-
-            using var subIndexResponse = await runner.GetResponse("/sub/");
-            Assert.AreEqual("Index 2", await subIndexResponse.GetContent());
-        }
-
-        [TestMethod]
-        public async Task TestNoIndex()
-        {
-            var tree = VirtualTree.Create()
-                                  .Add("sub", VirtualTree.Create());
-
-            using var runner = TestRunner.Run(StaticWebsite.From(tree));
-
-            using var indexResponse = await runner.GetResponse();
-            await indexResponse.AssertStatusAsync(HttpStatusCode.NotFound);
-
-            using var subIndexResponse = await runner.GetResponse("/sub/"); 
-            await subIndexResponse.AssertStatusAsync(HttpStatusCode.NotFound);
-        }
-
-        [TestMethod]
-        public async Task TestSitemap()
-        {
-            var tree = VirtualTree.Create()
-                                  .Add("index.html", Resource.FromString("Index 1"))
-                                  .Add("sub", VirtualTree.Create().Add("index.htm", Resource.FromString("Index 2")))
-                                  .Add("sub2", VirtualTree.Create());
-
-            using var runner = TestRunner.Run(StaticWebsite.From(tree));
-
-            using var response = await runner.GetResponse("/" + Sitemap.FILE_NAME);
-
-            var sitemap = await response.GetSitemap();
-
-            Assert.AreEqual(2, sitemap.Count);
-        }
-
-        [TestMethod]
-        public async Task TestNoSitemap()
-        {
-            using var runner = TestRunner.Run(StaticWebsite.From(VirtualTree.Create()).Sitemap(null));
-
-            using var response = await runner.GetResponse("/" + Sitemap.FILE_NAME);
-
-            await response.AssertStatusAsync(HttpStatusCode.NotFound);
-        }
-
-        [TestMethod]
-        public async Task TestSitemapOverride()
-        {
-            var tree = VirtualTree.Create()
-                                  .Add(Sitemap.FILE_NAME, Resource.FromString("Custom Sitemap")); 
-
-            using var runner = TestRunner.Run(StaticWebsite.From(tree));
-
-            using var response = await runner.GetResponse("/" + Sitemap.FILE_NAME);
-            Assert.AreEqual("Custom Sitemap", await response.GetContent());
-        }
-
-        [TestMethod]
-        public async Task TestRobots()
-        {
-            using var runner = TestRunner.Run(StaticWebsite.From(VirtualTree.Create()));
-
-            using var response = await runner.GetResponse("/" + BotInstructions.FILE_NAME);
-            await response.AssertStatusAsync(HttpStatusCode.OK);
-        }
-
-        [TestMethod]
-        public async Task TestNoRobots()
-        {
-            using var runner = TestRunner.Run(StaticWebsite.From(VirtualTree.Create()).Robots(null));
-
-            using var response = await runner.GetResponse("/" + BotInstructions.FILE_NAME);
-
-            await response.AssertStatusAsync(HttpStatusCode.NotFound);
-        }
-
-        [TestMethod]
-        public async Task TestRobotsOverride()
-        {
-            var tree = VirtualTree.Create()
-                                  .Add(BotInstructions.FILE_NAME, Resource.FromString("Custom Robots"));
-
-            using var runner = TestRunner.Run(StaticWebsite.From(tree));
-
-            using var response = await runner.GetResponse("/" + BotInstructions.FILE_NAME);
-            Assert.AreEqual("Custom Robots", await response.GetContent());
-        }
-
-        [TestMethod]
-        public async Task TestNoRobotsInSubdirectory()
-        {
-            using var runner = TestRunner.Run(StaticWebsite.From(VirtualTree.Create()));
-
-            using var response = await runner.GetResponse("/sub/" + BotInstructions.FILE_NAME);
-
-            await response.AssertStatusAsync(HttpStatusCode.NotFound);
-        }
-
-    }
-
-}
+using System.Net;
+using System.Threading.Tasks;
+
+using GenHTTP.Modules.IO;
+using GenHTTP.Modules.Robots;
+using GenHTTP.Modules.Sitemaps;
+using GenHTTP.Modules.StaticWebsites;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace GenHTTP.Testing.Acceptance.Modules.StaticWebsites
+{
+
+    [TestClass]
+    public sealed class StaticWebsiteTests
+    {
+
+        [TestMethod]
+        public async Task TestWithIndex()
+        {
+            var tree = VirtualTree.Create()
+                                  .Add("index.html", Resource.FromString("Index 1"))
+                                  .Add("sub", VirtualTree.Create().Add("index.htm", Resource.FromString("Index 2")));
+
+            using var runner = TestHost.Run(StaticWebsite.From(tree));
+
+            using var indexResponse = await runner.GetResponseAsync();
+            Assert.AreEqual("Index 1", await indexResponse.GetContent());
+
+            using var subIndexResponse = await runner.GetResponseAsync("/sub/");
+            Assert.AreEqual("Index 2", await subIndexResponse.GetContent());
+        }
+
+        [TestMethod]
+        public async Task TestNoIndex()
+        {
+            var tree = VirtualTree.Create()
+                                  .Add("sub", VirtualTree.Create());
+
+            using var runner = TestHost.Run(StaticWebsite.From(tree));
+
+            using var indexResponse = await runner.GetResponseAsync();
+            await indexResponse.AssertStatusAsync(HttpStatusCode.NotFound);
+
+            using var subIndexResponse = await runner.GetResponseAsync("/sub/"); 
+            await subIndexResponse.AssertStatusAsync(HttpStatusCode.NotFound);
+        }
+
+        [TestMethod]
+        public async Task TestSitemap()
+        {
+            var tree = VirtualTree.Create()
+                                  .Add("index.html", Resource.FromString("Index 1"))
+                                  .Add("sub", VirtualTree.Create().Add("index.htm", Resource.FromString("Index 2")))
+                                  .Add("sub2", VirtualTree.Create());
+
+            using var runner = TestHost.Run(StaticWebsite.From(tree));
+
+            using var response = await runner.GetResponseAsync("/" + Sitemap.FILE_NAME);
+
+            var sitemap = await response.GetSitemap();
+
+            Assert.AreEqual(2, sitemap.Count);
+        }
+
+        [TestMethod]
+        public async Task TestNoSitemap()
+        {
+            using var runner = TestHost.Run(StaticWebsite.From(VirtualTree.Create()).Sitemap(null));
+
+            using var response = await runner.GetResponseAsync("/" + Sitemap.FILE_NAME);
+
+            await response.AssertStatusAsync(HttpStatusCode.NotFound);
+        }
+
+        [TestMethod]
+        public async Task TestSitemapOverride()
+        {
+            var tree = VirtualTree.Create()
+                                  .Add(Sitemap.FILE_NAME, Resource.FromString("Custom Sitemap")); 
+
+            using var runner = TestHost.Run(StaticWebsite.From(tree));
+
+            using var response = await runner.GetResponseAsync("/" + Sitemap.FILE_NAME);
+            Assert.AreEqual("Custom Sitemap", await response.GetContent());
+        }
+
+        [TestMethod]
+        public async Task TestRobots()
+        {
+            using var runner = TestHost.Run(StaticWebsite.From(VirtualTree.Create()));
+
+            using var response = await runner.GetResponseAsync("/" + BotInstructions.FILE_NAME);
+            await response.AssertStatusAsync(HttpStatusCode.OK);
+        }
+
+        [TestMethod]
+        public async Task TestNoRobots()
+        {
+            using var runner = TestHost.Run(StaticWebsite.From(VirtualTree.Create()).Robots(null));
+
+            using var response = await runner.GetResponseAsync("/" + BotInstructions.FILE_NAME);
+
+            await response.AssertStatusAsync(HttpStatusCode.NotFound);
+        }
+
+        [TestMethod]
+        public async Task TestRobotsOverride()
+        {
+            var tree = VirtualTree.Create()
+                                  .Add(BotInstructions.FILE_NAME, Resource.FromString("Custom Robots"));
+
+            using var runner = TestHost.Run(StaticWebsite.From(tree));
+
+            using var response = await runner.GetResponseAsync("/" + BotInstructions.FILE_NAME);
+            Assert.AreEqual("Custom Robots", await response.GetContent());
+        }
+
+        [TestMethod]
+        public async Task TestNoRobotsInSubdirectory()
+        {
+            using var runner = TestHost.Run(StaticWebsite.From(VirtualTree.Create()));
+
+            using var response = await runner.GetResponseAsync("/sub/" + BotInstructions.FILE_NAME);
+
+            await response.AssertStatusAsync(HttpStatusCode.NotFound);
+        }
+
+    }
+
+}
diff --git a/Testing/Modules/VirtualHostsTests.cs b/Testing/Acceptance/Modules/VirtualHostsTests.cs
similarity index 71%
rename from Testing/Modules/VirtualHostsTests.cs
rename to Testing/Acceptance/Modules/VirtualHostsTests.cs
index 0c4684e3..8cdd3b31 100644
--- a/Testing/Modules/VirtualHostsTests.cs
+++ b/Testing/Acceptance/Modules/VirtualHostsTests.cs
@@ -1,63 +1,63 @@
-using System.Net;
-using System.Threading.Tasks;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using GenHTTP.Modules.VirtualHosting;
-using GenHTTP.Modules.IO;
-using GenHTTP.Modules.Layouting;
-
-namespace GenHTTP.Testing.Acceptance.Modules
-{
-
-    [TestClass]
-    public sealed class VirtualHostsTests
-    {
-
-        /// 
-        /// As a hoster, I would like to provide several domains using the
-        /// same server instance.
-        /// 
-        [TestMethod]
-        public async Task TestDomains()
-        {
-            var hosts = VirtualHosts.Create()
-                                    .Add("domain1.com", Content.From(Resource.FromString("domain1.com")))
-                                    .Add("domain2.com", Content.From(Resource.FromString("domain2.com")))
-                                    .Default(Layout.Create().Index(Content.From(Resource.FromString("default"))));
-
-            using var runner = TestRunner.Run(hosts);
-
-            await TestHost(runner, "domain1.com");
-            await TestHost(runner, "domain2.com");
-
-            await TestHost(runner, "localhost", "default");
-        }
-
-        /// 
-        /// As a developer, I expect the server to return no content if
-        /// no given route matches.
-        /// 
-        [TestMethod]
-        public async Task TestNoDefault()
-        {
-            using var runner = TestRunner.Run(VirtualHosts.Create());
-
-            using var response = await runner.GetResponse();
-
-            await response.AssertStatusAsync(HttpStatusCode.NotFound);
-        }
-
-        private static async Task TestHost(TestRunner runner, string host, string? expected = null)
-        {
-            var request = runner.GetRequest();
-            request.Headers.Add("Host", host);
-
-            using var response = await runner.GetResponse(request);
-
-            Assert.AreEqual(expected ?? host, await response.GetContent());
-        }
-
-    }
-
-}
+using System.Net;
+using System.Threading.Tasks;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using GenHTTP.Modules.VirtualHosting;
+using GenHTTP.Modules.IO;
+using GenHTTP.Modules.Layouting;
+
+namespace GenHTTP.Testing.Acceptance.Modules
+{
+
+    [TestClass]
+    public sealed class VirtualHostsTests
+    {
+
+        /// 
+        /// As a hoster, I would like to provide several domains using the
+        /// same server instance.
+        /// 
+        [TestMethod]
+        public async Task TestDomains()
+        {
+            var hosts = VirtualHosts.Create()
+                                    .Add("domain1.com", Content.From(Resource.FromString("domain1.com")))
+                                    .Add("domain2.com", Content.From(Resource.FromString("domain2.com")))
+                                    .Default(Layout.Create().Index(Content.From(Resource.FromString("default"))));
+
+            using var runner = TestHost.Run(hosts);
+
+            await RunTest(runner, "domain1.com");
+            await RunTest(runner, "domain2.com");
+
+            await RunTest(runner, "localhost", "default");
+        }
+
+        /// 
+        /// As a developer, I expect the server to return no content if
+        /// no given route matches.
+        /// 
+        [TestMethod]
+        public async Task TestNoDefault()
+        {
+            using var runner = TestHost.Run(VirtualHosts.Create());
+
+            using var response = await runner.GetResponseAsync();
+
+            await response.AssertStatusAsync(HttpStatusCode.NotFound);
+        }
+
+        private static async Task RunTest(TestHost runner, string host, string? expected = null)
+        {
+            var request = runner.GetRequest();
+            request.Headers.Add("Host", host);
+
+            using var response = await runner.GetResponseAsync(request);
+
+            Assert.AreEqual(expected ?? host, await response.GetContent());
+        }
+
+    }
+
+}
diff --git a/Testing/Modules/WebserviceTests.cs b/Testing/Acceptance/Modules/WebserviceTests.cs
similarity index 93%
rename from Testing/Modules/WebserviceTests.cs
rename to Testing/Acceptance/Modules/WebserviceTests.cs
index 9d0b6570..09ee98cb 100644
--- a/Testing/Modules/WebserviceTests.cs
+++ b/Testing/Acceptance/Modules/WebserviceTests.cs
@@ -1,316 +1,316 @@
-using System;
-using System.IO;
-using System.Net;
-using System.Text;
-using System.Threading.Tasks;
-using System.Net.Http;
-using System.Xml.Serialization;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using GenHTTP.Api.Content;
-using GenHTTP.Api.Protocol;
-
-using GenHTTP.Modules.IO;
-using GenHTTP.Modules.Webservices;
-using GenHTTP.Modules.Basics;
-using GenHTTP.Modules.Layouting;
-using GenHTTP.Modules.Conversion;
-using GenHTTP.Modules.Reflection;
-
-namespace GenHTTP.Testing.Acceptance.Modules.Webservices
-{
-
-    [TestClass]
-    public sealed class WebserviceTests
-    {
-
-        #region Supporting structures
-
-        public sealed class TestEntity
-        {
-
-            public int ID { get; set; }
-
-            public double? Nullable { get; set; }
-
-        }
-
-        public enum TestEnum
-        {
-            One,
-            Two
-        }
-
-        public sealed class TestResource
-        {
-
-            [ResourceMethod("nothing")]
-            public void DoNothing() { }
-
-            [ResourceMethod("primitive")]
-            public int Primitive(int input) => input;
-
-            [ResourceMethod("guid")]
-            public Guid Guid(Guid id) => id;
-
-            [ResourceMethod(RequestMethod.POST, "entity")]
-            public TestEntity Entity(TestEntity entity) => entity;
-
-            [ResourceMethod(RequestMethod.PUT, "stream")]
-            public Stream Stream(Stream input) => new MemoryStream(Encoding.UTF8.GetBytes(input.Length.ToString()));
-
-            [ResourceMethod("requestResponse")]
-            public ValueTask RequestResponse(IRequest request)
-            {
-                return request.Respond()
-                              .Content("Hello World")
-                              .Type(ContentType.TextPlain)
-                              .BuildTask();
-            }
-
-            [ResourceMethod("exception")]
-            public void Exception() => throw new ProviderException(ResponseStatus.AlreadyReported, "Already reported!");
-
-            [ResourceMethod("duplicate")]
-            public void Duplicate1() { }
-
-            [ResourceMethod("duplicate")]
-            public void Duplicate2() { }
-
-            [ResourceMethod("param/:param")]
-            public int PathParam(int param) => param;
-
-            [ResourceMethod("regex/(?[0-9]+)")]
-            public int RegexParam(int param) => param;
-
-            [ResourceMethod]
-            public void Empty() { }
-
-            [ResourceMethod("enum")]
-            public TestEnum Enum(TestEnum input) => input;
-
-            [ResourceMethod("nullable")]
-            public int? Nullable(int? input) => input;
-
-            [ResourceMethod("request")]
-            public string? Request(IHandler handler, IRequest request) => "yes";
-
-        }
-
-        #endregion
-
-        #region Tests
-
-        [TestMethod]
-        public async Task TestEmpty()
-        {
-            await WithResponse("", async r => { await r.AssertStatusAsync(HttpStatusCode.NoContent); });
-        }
-
-        [TestMethod]
-        public async Task TestVoidReturn()
-        {
-            await WithResponse("nothing", async r => { await r.AssertStatusAsync(HttpStatusCode.NoContent); });
-        }
-
-        [TestMethod]
-        public async Task TestPrimitives()
-        {
-            await WithResponse("primitive?input=42", async r => Assert.AreEqual("42", await r.GetContent()));
-        }
-
-        [TestMethod]
-        public async Task TestEnums()
-        {
-            await WithResponse("enum?input=One", async r => Assert.AreEqual("One", await r.GetContent()));
-        }
-
-        [TestMethod]
-        public async Task TestNullableSet()
-        {
-            await WithResponse("nullable?input=1", async r => Assert.AreEqual("1", await r.GetContent()));
-        }
-
-        [TestMethod]
-        public async Task TestNullableNotSet()
-        {
-            await WithResponse("nullable", async r => { await r.AssertStatusAsync(HttpStatusCode.NoContent); });
-        }
-
-        [TestMethod]
-        public async Task TestGuid()
-        {
-            var id = Guid.NewGuid().ToString();
-
-            await WithResponse($"guid?id={id}", async r => Assert.AreEqual(id, await r.GetContent()));
-        }
-
-        [TestMethod]
-        public async Task TestParam()
-        {
-            await WithResponse("param/42", async r => Assert.AreEqual("42", await r.GetContent()));
-        }
-
-        [TestMethod]
-        public async Task TestConversionFailure()
-        {
-            await WithResponse("param/abc", async r => { await r.AssertStatusAsync(HttpStatusCode.BadRequest); });
-        }
-
-        [TestMethod]
-        public async Task TestRegex()
-        {
-            await WithResponse("regex/42", async r => Assert.AreEqual("42", await r.GetContent()));
-        }
-
-        [TestMethod]
-        public async Task TestEntityWithNulls()
-        {
-            var entity = "{\"id\":42}";
-            await WithResponse("entity", HttpMethod.Post, entity, null, null, async r => Assert.AreEqual(entity, await r.GetContent()));
-        }
-
-        [TestMethod]
-        public async Task TestEntityWithNoNulls()
-        {
-            var entity = "{\"id\":42,\"nullable\":123.456}";
-            await WithResponse("entity", HttpMethod.Post, entity, null, null, async r => Assert.AreEqual(entity, await r.GetContent()));
-        }
-
-        [TestMethod]
-        public async Task TestNotSupportedUpload()
-        {
-            await WithResponse("entity", HttpMethod.Post, "123", "bla/blubb", null, async r => { await r.AssertStatusAsync(HttpStatusCode.UnsupportedMediaType); });
-        }
-
-        [TestMethod]
-        public async Task TestUnsupportedDownloadEnforcesDefault()
-        {
-            var entity = "{\"id\":42,\"nullable\":123.456}";
-            await WithResponse("entity", HttpMethod.Post, entity, null, "bla/blubb", async r => Assert.AreEqual(entity, await r.GetContent()));
-        }
-
-        [TestMethod]
-        public async Task TestWrongMethod()
-        {
-            await WithResponse("entity", HttpMethod.Put, "123", null, null, async r => { await r.AssertStatusAsync(HttpStatusCode.MethodNotAllowed); });
-        }
-
-        [TestMethod]
-        public async Task TestNoMethod()
-        {
-            await WithResponse("idonotexist", async r => { await r.AssertStatusAsync(HttpStatusCode.NotFound); });
-        }
-
-        [TestMethod]
-        public async Task TestStream()
-        {
-            await WithResponse("stream", HttpMethod.Put, "123456", null, null, async r => Assert.AreEqual("6", await r.GetContent()));
-        }
-
-        [TestMethod]
-        public async Task TestRequestResponse()
-        {
-            await WithResponse("requestResponse", async r => Assert.AreEqual("Hello World", await r.GetContent()));
-        }
-
-        [TestMethod]
-        public async Task TestRouting()
-        {
-            await WithResponse("request", async r => Assert.AreEqual("yes", await r.GetContent()));
-        }
-
-        [TestMethod]
-        public async Task TestEntityAsXML()
-        {
-            var entity = "11234.56";
-
-            await WithResponse("entity", HttpMethod.Post, entity, "text/xml", "text/xml", async r =>
-            {
-                var result = new XmlSerializer(typeof(TestEntity)).Deserialize(await r.Content.ReadAsStreamAsync()) as TestEntity;
-
-                Assert.IsNotNull(result);
-
-                Assert.AreEqual(1, result!.ID);
-                Assert.AreEqual(1234.56, result!.Nullable);
-            });
-        }
-
-        [TestMethod]
-        public async Task TestException()
-        {
-            await WithResponse("exception", async r => { await r.AssertStatusAsync(HttpStatusCode.AlreadyReported); });
-        }
-
-        [TestMethod]
-        public async Task TestDuplicate()
-        {
-            await WithResponse("duplicate", async r => { await r.AssertStatusAsync(HttpStatusCode.BadRequest); });
-        }
-
-        [TestMethod]
-        public async Task TestWithInstance()
-        {
-            var layout = Layout.Create().AddService("t", new TestResource());
-
-            using var runner = TestRunner.Run(layout);
-
-            using var response = await runner.GetResponse("/t");
-
-            await response.AssertStatusAsync(HttpStatusCode.NoContent);
-        }
-
-        #endregion
-
-        #region Helpers
-
-        private Task WithResponse(string uri, Func logic) => WithResponse(uri, HttpMethod.Get, null, null, null, logic);
-
-        private async Task WithResponse(string uri, HttpMethod method, string? body, string? contentType, string? accept, Func logic)
-        {
-            using var service = GetService();
-
-            var request = service.GetRequest($"/t/{uri}");
-
-            request.Method = method;
-
-            if (accept is not null)
-            {
-                request.Headers.Add("Accept", accept);
-            }
-
-            if (body is not null)
-            {
-                if (contentType is not null)
-                {
-                    request.Content = new StringContent(body, null, contentType);
-                    request.Content.Headers.ContentType = new(contentType);
-                }
-                else
-                {
-                    request.Content = new StringContent(body);
-                    request.Content.Headers.ContentType = null;
-                }
-            }
-
-            using var response = await service.GetResponse(request);
-
-            await logic(response);
-        }
-
-        private static TestRunner GetService()
-        {
-            var service = ServiceResource.From()
-                                         .Formats(Serialization.Default())
-                                         .Injectors(Injection.Default());
-
-            return TestRunner.Run(Layout.Create().Add("t", service));
-        }
-
-        #endregion
-
-    }
-
-
-}
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using System.Net.Http;
+using System.Xml.Serialization;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using GenHTTP.Api.Content;
+using GenHTTP.Api.Protocol;
+
+using GenHTTP.Modules.IO;
+using GenHTTP.Modules.Webservices;
+using GenHTTP.Modules.Basics;
+using GenHTTP.Modules.Layouting;
+using GenHTTP.Modules.Conversion;
+using GenHTTP.Modules.Reflection;
+
+namespace GenHTTP.Testing.Acceptance.Modules.Webservices
+{
+
+    [TestClass]
+    public sealed class WebserviceTests
+    {
+
+        #region Supporting structures
+
+        public sealed class TestEntity
+        {
+
+            public int ID { get; set; }
+
+            public double? Nullable { get; set; }
+
+        }
+
+        public enum TestEnum
+        {
+            One,
+            Two
+        }
+
+        public sealed class TestResource
+        {
+
+            [ResourceMethod("nothing")]
+            public void DoNothing() { }
+
+            [ResourceMethod("primitive")]
+            public int Primitive(int input) => input;
+
+            [ResourceMethod("guid")]
+            public Guid Guid(Guid id) => id;
+
+            [ResourceMethod(RequestMethod.POST, "entity")]
+            public TestEntity Entity(TestEntity entity) => entity;
+
+            [ResourceMethod(RequestMethod.PUT, "stream")]
+            public Stream Stream(Stream input) => new MemoryStream(Encoding.UTF8.GetBytes(input.Length.ToString()));
+
+            [ResourceMethod("requestResponse")]
+            public ValueTask RequestResponse(IRequest request)
+            {
+                return request.Respond()
+                              .Content("Hello World")
+                              .Type(ContentType.TextPlain)
+                              .BuildTask();
+            }
+
+            [ResourceMethod("exception")]
+            public void Exception() => throw new ProviderException(ResponseStatus.AlreadyReported, "Already reported!");
+
+            [ResourceMethod("duplicate")]
+            public void Duplicate1() { }
+
+            [ResourceMethod("duplicate")]
+            public void Duplicate2() { }
+
+            [ResourceMethod("param/:param")]
+            public int PathParam(int param) => param;
+
+            [ResourceMethod("regex/(?[0-9]+)")]
+            public int RegexParam(int param) => param;
+
+            [ResourceMethod]
+            public void Empty() { }
+
+            [ResourceMethod("enum")]
+            public TestEnum Enum(TestEnum input) => input;
+
+            [ResourceMethod("nullable")]
+            public int? Nullable(int? input) => input;
+
+            [ResourceMethod("request")]
+            public string? Request(IHandler handler, IRequest request) => "yes";
+
+        }
+
+        #endregion
+
+        #region Tests
+
+        [TestMethod]
+        public async Task TestEmpty()
+        {
+            await WithResponse("", async r => { await r.AssertStatusAsync(HttpStatusCode.NoContent); });
+        }
+
+        [TestMethod]
+        public async Task TestVoidReturn()
+        {
+            await WithResponse("nothing", async r => { await r.AssertStatusAsync(HttpStatusCode.NoContent); });
+        }
+
+        [TestMethod]
+        public async Task TestPrimitives()
+        {
+            await WithResponse("primitive?input=42", async r => Assert.AreEqual("42", await r.GetContent()));
+        }
+
+        [TestMethod]
+        public async Task TestEnums()
+        {
+            await WithResponse("enum?input=One", async r => Assert.AreEqual("One", await r.GetContent()));
+        }
+
+        [TestMethod]
+        public async Task TestNullableSet()
+        {
+            await WithResponse("nullable?input=1", async r => Assert.AreEqual("1", await r.GetContent()));
+        }
+
+        [TestMethod]
+        public async Task TestNullableNotSet()
+        {
+            await WithResponse("nullable", async r => { await r.AssertStatusAsync(HttpStatusCode.NoContent); });
+        }
+
+        [TestMethod]
+        public async Task TestGuid()
+        {
+            var id = Guid.NewGuid().ToString();
+
+            await WithResponse($"guid?id={id}", async r => Assert.AreEqual(id, await r.GetContent()));
+        }
+
+        [TestMethod]
+        public async Task TestParam()
+        {
+            await WithResponse("param/42", async r => Assert.AreEqual("42", await r.GetContent()));
+        }
+
+        [TestMethod]
+        public async Task TestConversionFailure()
+        {
+            await WithResponse("param/abc", async r => { await r.AssertStatusAsync(HttpStatusCode.BadRequest); });
+        }
+
+        [TestMethod]
+        public async Task TestRegex()
+        {
+            await WithResponse("regex/42", async r => Assert.AreEqual("42", await r.GetContent()));
+        }
+
+        [TestMethod]
+        public async Task TestEntityWithNulls()
+        {
+            var entity = "{\"id\":42}";
+            await WithResponse("entity", HttpMethod.Post, entity, null, null, async r => Assert.AreEqual(entity, await r.GetContent()));
+        }
+
+        [TestMethod]
+        public async Task TestEntityWithNoNulls()
+        {
+            var entity = "{\"id\":42,\"nullable\":123.456}";
+            await WithResponse("entity", HttpMethod.Post, entity, null, null, async r => Assert.AreEqual(entity, await r.GetContent()));
+        }
+
+        [TestMethod]
+        public async Task TestNotSupportedUpload()
+        {
+            await WithResponse("entity", HttpMethod.Post, "123", "bla/blubb", null, async r => { await r.AssertStatusAsync(HttpStatusCode.UnsupportedMediaType); });
+        }
+
+        [TestMethod]
+        public async Task TestUnsupportedDownloadEnforcesDefault()
+        {
+            var entity = "{\"id\":42,\"nullable\":123.456}";
+            await WithResponse("entity", HttpMethod.Post, entity, null, "bla/blubb", async r => Assert.AreEqual(entity, await r.GetContent()));
+        }
+
+        [TestMethod]
+        public async Task TestWrongMethod()
+        {
+            await WithResponse("entity", HttpMethod.Put, "123", null, null, async r => { await r.AssertStatusAsync(HttpStatusCode.MethodNotAllowed); });
+        }
+
+        [TestMethod]
+        public async Task TestNoMethod()
+        {
+            await WithResponse("idonotexist", async r => { await r.AssertStatusAsync(HttpStatusCode.NotFound); });
+        }
+
+        [TestMethod]
+        public async Task TestStream()
+        {
+            await WithResponse("stream", HttpMethod.Put, "123456", null, null, async r => Assert.AreEqual("6", await r.GetContent()));
+        }
+
+        [TestMethod]
+        public async Task TestRequestResponse()
+        {
+            await WithResponse("requestResponse", async r => Assert.AreEqual("Hello World", await r.GetContent()));
+        }
+
+        [TestMethod]
+        public async Task TestRouting()
+        {
+            await WithResponse("request", async r => Assert.AreEqual("yes", await r.GetContent()));
+        }
+
+        [TestMethod]
+        public async Task TestEntityAsXML()
+        {
+            var entity = "11234.56";
+
+            await WithResponse("entity", HttpMethod.Post, entity, "text/xml", "text/xml", async r =>
+            {
+                var result = new XmlSerializer(typeof(TestEntity)).Deserialize(await r.Content.ReadAsStreamAsync()) as TestEntity;
+
+                Assert.IsNotNull(result);
+
+                Assert.AreEqual(1, result!.ID);
+                Assert.AreEqual(1234.56, result!.Nullable);
+            });
+        }
+
+        [TestMethod]
+        public async Task TestException()
+        {
+            await WithResponse("exception", async r => { await r.AssertStatusAsync(HttpStatusCode.AlreadyReported); });
+        }
+
+        [TestMethod]
+        public async Task TestDuplicate()
+        {
+            await WithResponse("duplicate", async r => { await r.AssertStatusAsync(HttpStatusCode.BadRequest); });
+        }
+
+        [TestMethod]
+        public async Task TestWithInstance()
+        {
+            var layout = Layout.Create().AddService("t", new TestResource());
+
+            using var runner = TestHost.Run(layout);
+
+            using var response = await runner.GetResponseAsync("/t");
+
+            await response.AssertStatusAsync(HttpStatusCode.NoContent);
+        }
+
+        #endregion
+
+        #region Helpers
+
+        private Task WithResponse(string uri, Func logic) => WithResponse(uri, HttpMethod.Get, null, null, null, logic);
+
+        private async Task WithResponse(string uri, HttpMethod method, string? body, string? contentType, string? accept, Func logic)
+        {
+            using var service = GetService();
+
+            var request = service.GetRequest($"/t/{uri}");
+
+            request.Method = method;
+
+            if (accept is not null)
+            {
+                request.Headers.Add("Accept", accept);
+            }
+
+            if (body is not null)
+            {
+                if (contentType is not null)
+                {
+                    request.Content = new StringContent(body, null, contentType);
+                    request.Content.Headers.ContentType = new(contentType);
+                }
+                else
+                {
+                    request.Content = new StringContent(body);
+                    request.Content.Headers.ContentType = null;
+                }
+            }
+
+            using var response = await service.GetResponseAsync(request);
+
+            await logic(response);
+        }
+
+        private static TestHost GetService()
+        {
+            var service = ServiceResource.From()
+                                         .Formats(Serialization.Default())
+                                         .Injectors(Injection.Default());
+
+            return TestHost.Run(Layout.Create().Add("t", service));
+        }
+
+        #endregion
+
+    }
+
+
+}
diff --git a/Testing/Modules/Webservices/ResultTypeTests.cs b/Testing/Acceptance/Modules/Webservices/ResultTypeTests.cs
similarity index 79%
rename from Testing/Modules/Webservices/ResultTypeTests.cs
rename to Testing/Acceptance/Modules/Webservices/ResultTypeTests.cs
index 53250079..5a36b99e 100644
--- a/Testing/Modules/Webservices/ResultTypeTests.cs
+++ b/Testing/Acceptance/Modules/Webservices/ResultTypeTests.cs
@@ -1,94 +1,94 @@
-using System.Net;
-using System.Threading.Tasks;
-
-using GenHTTP.Modules.Layouting;
-using GenHTTP.Modules.Webservices;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-namespace GenHTTP.Testing.Acceptance.Modules.Webservices
-{
-
-    #region Supporting data structures
-
-    public class TestResource
-    {
-
-        [ResourceMethod("task")]
-        public Task AsyncTask() => Task.CompletedTask;
-
-        [ResourceMethod("value-task")]
-        public ValueTask AsyncValueTask() => ValueTask.CompletedTask;
-
-        [ResourceMethod("generic-task")]
-        public Task AsyncGenericTask() => Task.FromResult("Task result");
-
-        [ResourceMethod("generic-value-task")]
-        public ValueTask AsyncGenericValueTask() => ValueTask.FromResult("ValueTask result");
-
-    }
-
-    #endregion
-
-    [TestClass]
-    public class ResultTypeTests
-    {
-
-        #region Tests
-
-        [TestMethod]
-        public async Task ControllerMayReturnTask()
-        {
-            using var runner = GetRunner();
-
-            using var response = await runner.GetResponse("/t/task");
-
-            await response.AssertStatusAsync(HttpStatusCode.NoContent);
-        }
-
-        [TestMethod]
-        public async Task ControllerMayReturnValueTask()
-        {
-            using var runner = GetRunner();
-
-            using var response = await runner.GetResponse("/t/value-task");
-
-            await response.AssertStatusAsync(HttpStatusCode.NoContent);
-        }
-
-        [TestMethod]
-        public async Task ControllerMayReturnGenericTask()
-        {
-            using var runner = GetRunner();
-
-            using var response = await runner.GetResponse("/t/generic-task");
-
-            await response.AssertStatusAsync(HttpStatusCode.OK);
-            Assert.AreEqual("Task result", await response.GetContent());
-        }
-
-        [TestMethod]
-        public async Task ControllerMayReturnGenericValueTask()
-        {
-            using var runner = GetRunner();
-
-            using var response = await runner.GetResponse("/t/generic-value-task");
-
-            await response.AssertStatusAsync(HttpStatusCode.OK);
-            Assert.AreEqual("ValueTask result", await response.GetContent());
-        }
-
-        #endregion
-
-        #region Helpers
-
-        private TestRunner GetRunner()
-        {
-            return TestRunner.Run(Layout.Create().AddService("t"));
-        }
-
-        #endregion
-
-    }
-
-}
+using System.Net;
+using System.Threading.Tasks;
+
+using GenHTTP.Modules.Layouting;
+using GenHTTP.Modules.Webservices;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace GenHTTP.Testing.Acceptance.Modules.Webservices
+{
+
+    #region Supporting data structures
+
+    public class TestResource
+    {
+
+        [ResourceMethod("task")]
+        public Task AsyncTask() => Task.CompletedTask;
+
+        [ResourceMethod("value-task")]
+        public ValueTask AsyncValueTask() => ValueTask.CompletedTask;
+
+        [ResourceMethod("generic-task")]
+        public Task AsyncGenericTask() => Task.FromResult("Task result");
+
+        [ResourceMethod("generic-value-task")]
+        public ValueTask AsyncGenericValueTask() => ValueTask.FromResult("ValueTask result");
+
+    }
+
+    #endregion
+
+    [TestClass]
+    public class ResultTypeTests
+    {
+
+        #region Tests
+
+        [TestMethod]
+        public async Task ControllerMayReturnTask()
+        {
+            using var runner = GetRunner();
+
+            using var response = await runner.GetResponseAsync("/t/task");
+
+            await response.AssertStatusAsync(HttpStatusCode.NoContent);
+        }
+
+        [TestMethod]
+        public async Task ControllerMayReturnValueTask()
+        {
+            using var runner = GetRunner();
+
+            using var response = await runner.GetResponseAsync("/t/value-task");
+
+            await response.AssertStatusAsync(HttpStatusCode.NoContent);
+        }
+
+        [TestMethod]
+        public async Task ControllerMayReturnGenericTask()
+        {
+            using var runner = GetRunner();
+
+            using var response = await runner.GetResponseAsync("/t/generic-task");
+
+            await response.AssertStatusAsync(HttpStatusCode.OK);
+            Assert.AreEqual("Task result", await response.GetContent());
+        }
+
+        [TestMethod]
+        public async Task ControllerMayReturnGenericValueTask()
+        {
+            using var runner = GetRunner();
+
+            using var response = await runner.GetResponseAsync("/t/generic-value-task");
+
+            await response.AssertStatusAsync(HttpStatusCode.OK);
+            Assert.AreEqual("ValueTask result", await response.GetContent());
+        }
+
+        #endregion
+
+        #region Helpers
+
+        private TestHost GetRunner()
+        {
+            return TestHost.Run(Layout.Create().AddService("t"));
+        }
+
+        #endregion
+
+    }
+
+}
diff --git a/Testing/Modules/WebsiteTests.cs b/Testing/Acceptance/Modules/WebsiteTests.cs
similarity index 73%
rename from Testing/Modules/WebsiteTests.cs
rename to Testing/Acceptance/Modules/WebsiteTests.cs
index ae2cd936..44615bfc 100644
--- a/Testing/Modules/WebsiteTests.cs
+++ b/Testing/Acceptance/Modules/WebsiteTests.cs
@@ -1,297 +1,297 @@
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using System.Net;
-
-using GenHTTP.Api.Content;
-using GenHTTP.Api.Content.IO;
-using GenHTTP.Api.Content.Templating;
-using GenHTTP.Api.Content.Websites;
-using GenHTTP.Api.Protocol;
-
-using GenHTTP.Modules.Layouting;
-using GenHTTP.Modules.Layouting.Provider;
-using GenHTTP.Modules.IO;
-using GenHTTP.Modules.Scriban;
-using GenHTTP.Modules.Websites;
-using GenHTTP.Modules.Sitemaps;
-using GenHTTP.Modules.Robots;
-using GenHTTP.Modules.AutoReload;
-using GenHTTP.Modules.Websites.Sites;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-namespace GenHTTP.Testing.Acceptance.Providers
-{
-
-    [TestClass]
-    public sealed class WebsiteTests
-    {
-
-        #region Supporting data structures
-
-        public sealed class Theme : ITheme
-        {
-
-            public List