From ee4595c2dfad95bb01134ae299f1f4ce349e197e Mon Sep 17 00:00:00 2001 From: Kaliumhexacyanoferrat Date: Mon, 18 Nov 2024 14:18:17 +0100 Subject: [PATCH] Update for GenHTTP 9 --- .gitignore | 3 + MockH.Tests/MockH.Tests.csproj | 12 +- MockH.Tests/RedirectTests.cs | 53 +++--- MockH.Tests/RespondTests.cs | 29 ++- MockH.Tests/ReturnTests.cs | 47 +++-- MockH.Tests/RunTests.cs | 227 ++++++++++++------------ MockH.Tests/ServerTest.cs | 41 ++--- MockH.Tests/ServerTests.cs | 55 +++--- MockH/Builder/IncompleteRule.cs | 167 +++++++++-------- MockH/Builder/Rule.cs | 45 +++-- MockH/Builder/ServerBuilder.cs | 113 ++++++------ MockH/Environment/IPortProvider.cs | 33 ++-- MockH/Environment/Server.cs | 176 +++++++++--------- MockH/Environment/StaticPortProvider.cs | 29 ++- MockH/MockH.csproj | 15 +- MockH/MockServer.cs | 58 +++--- MockH/On.cs | 93 +++++----- README.md | 4 +- 18 files changed, 582 insertions(+), 618 deletions(-) diff --git a/.gitignore b/.gitignore index dfcfd56..8320fcd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +# Idea +.idea/ + # User-specific files *.rsuser *.suo diff --git a/MockH.Tests/MockH.Tests.csproj b/MockH.Tests/MockH.Tests.csproj index ce869d0..4bac29f 100644 --- a/MockH.Tests/MockH.Tests.csproj +++ b/MockH.Tests/MockH.Tests.csproj @@ -2,10 +2,10 @@ - net6.0;net7.0;net8.0 + net8.0;net9.0 enable - 10.0 + 13.0 enable true @@ -15,12 +15,12 @@ - + - + - - + + all diff --git a/MockH.Tests/RedirectTests.cs b/MockH.Tests/RedirectTests.cs index 2836952..51684ce 100644 --- a/MockH.Tests/RedirectTests.cs +++ b/MockH.Tests/RedirectTests.cs @@ -1,41 +1,38 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Net; -namespace MockH.Tests +namespace MockH.Tests; + +[TestClass] +public class RedirectTests : ServerTest { - [TestClass] - public class RedirectTests : ServerTest + [TestMethod] + public async Task RulesCanRedirectTemporarily() { + await using var server = await MockServer.RunAsync + ( + On.Get().Redirect("https://www.google.de") + ); - [TestMethod] - public async Task RulesCanRedirectTemporarily() - { - using var server = MockServer.Run - ( - On.Get().Redirect("https://www.google.de") - ); - - using var response = await GetAsync(server); - - Assert.AreEqual(HttpStatusCode.TemporaryRedirect, response.StatusCode); - Assert.AreEqual(new Uri("https://www.google.de"), response.Headers.Location); - } + using var response = await GetAsync(server); - [TestMethod] - public async Task RulesCanRedirectPermanently() - { - using var server = MockServer.Run - ( - On.Get().Redirect("https://www.google.de", temporary: false) - ); + Assert.AreEqual(HttpStatusCode.TemporaryRedirect, response.StatusCode); + Assert.AreEqual(new Uri("https://www.google.de"), response.Headers.Location); + } - using var response = await GetAsync(server); + [TestMethod] + public async Task RulesCanRedirectPermanently() + { + await using var server = await MockServer.RunAsync + ( + On.Get().Redirect("https://www.google.de", temporary: false) + ); - Assert.AreEqual(HttpStatusCode.Moved, response.StatusCode); - Assert.AreEqual(new Uri("https://www.google.de"), response.Headers.Location); - } + using var response = await GetAsync(server); + Assert.AreEqual(HttpStatusCode.Moved, response.StatusCode); + Assert.AreEqual(new Uri("https://www.google.de"), response.Headers.Location); } -} +} \ No newline at end of file diff --git a/MockH.Tests/RespondTests.cs b/MockH.Tests/RespondTests.cs index 612de60..6b577db 100644 --- a/MockH.Tests/RespondTests.cs +++ b/MockH.Tests/RespondTests.cs @@ -2,26 +2,23 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Net; -namespace MockH.Tests +namespace MockH.Tests; + +[TestClass] +public class RespondTests : ServerTest { - [TestClass] - public class RespondTests : ServerTest + [TestMethod] + public async Task BasicResponse() { + await using var server = await MockServer.RunAsync + ( + On.Get().Respond(ResponseStatus.InternalServerError) + ); - [TestMethod] - public async Task BasicResponse() - { - using var server = MockServer.Run - ( - On.Get().Respond(ResponseStatus.InternalServerError) - ); - - using var response = await GetAsync(server); - - Assert.AreEqual(HttpStatusCode.InternalServerError, response.StatusCode); - } + using var response = await GetAsync(server); + Assert.AreEqual(HttpStatusCode.InternalServerError, response.StatusCode); } -} +} \ No newline at end of file diff --git a/MockH.Tests/ReturnTests.cs b/MockH.Tests/ReturnTests.cs index 501ef3f..fa5b0df 100644 --- a/MockH.Tests/ReturnTests.cs +++ b/MockH.Tests/ReturnTests.cs @@ -1,40 +1,37 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace MockH.Tests -{ +namespace MockH.Tests; - [TestClass] - public class ReturnTests : ServerTest - { +[TestClass] +public class ReturnTests : ServerTest +{ #region Supporting data structures - public record Data(string Value); + public record Data(string Value); #endregion - [TestMethod] - public async Task RulesCanReturnPrimitiveTypes() - { - using var server = MockServer.Run - ( - On.Get().Return(42) - ); - - Assert.AreEqual("42", await GetStringAsync(server)); - } + [TestMethod] + public async Task RulesCanReturnPrimitiveTypes() + { + await using var server = await MockServer.RunAsync + ( + On.Get().Return(42) + ); - [TestMethod] - public async Task RulesCanReturnComplexTypes() - { - using var server = MockServer.Run - ( - On.Get().Return(new Data("Hello World")) - ); + Assert.AreEqual("42", await GetStringAsync(server)); + } - Assert.AreEqual("{\"value\":\"Hello World\"}", await GetStringAsync(server)); - } + [TestMethod] + public async Task RulesCanReturnComplexTypes() + { + await using var server = await MockServer.RunAsync + ( + On.Get().Return(new Data("Hello World")) + ); + Assert.AreEqual("{\"value\":\"Hello World\"}", await GetStringAsync(server)); } } \ No newline at end of file diff --git a/MockH.Tests/RunTests.cs b/MockH.Tests/RunTests.cs index 0b4eace..8512c23 100644 --- a/MockH.Tests/RunTests.cs +++ b/MockH.Tests/RunTests.cs @@ -4,125 +4,122 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Net; -namespace MockH.Tests +namespace MockH.Tests; + +[TestClass] +public class RunTests : ServerTest { - [TestClass] - public class RunTests : ServerTest + [TestMethod] + public async Task TestConstant() + { + await using var server = await MockServer.RunAsync + ( + On.Get().Run(() => 42) + ); + + using var response = await GetAsync(server); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.AreEqual("42", await response.Content.ReadAsStringAsync()); + } + + private record MyClass(int IntValue, string StringValue); + + [TestMethod] + public async Task TestJson() + { + await using var server = await MockServer.RunAsync + ( + On.Get().Run(() => new MyClass(42, "The answer")) + ); + + using var response = await GetAsync(server); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.AreEqual("{\"intValue\":42,\"stringValue\":\"The answer\"}", await response.Content.ReadAsStringAsync()); + } + + [TestMethod] + public async Task TestQuery() + { + await using var server = await MockServer.RunAsync + ( + On.Get().Run((int i) => i + 1) + ); + + using var response = await GetAsync(server, "/?i=1"); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.AreEqual("2", await response.Content.ReadAsStringAsync()); + } + + [TestMethod] + public async Task TestPath() + { + await using var server = await MockServer.RunAsync + ( + On.Get("/increment/:i").Run((int i) => i + 1) + ); + + using var response = await GetAsync(server, "/increment/1"); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.AreEqual("2", await response.Content.ReadAsStringAsync()); + } + + [TestMethod] + public async Task TestPost() + { + await using var server = await MockServer.RunAsync + ( + On.Post().Run((MyClass body) => body) + ); + + using var response = await PostAsync(server, "{\"intValue\":42,\"stringValue\":\"The answer\"}"); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.AreEqual("{\"intValue\":42,\"stringValue\":\"The answer\"}", await response.Content.ReadAsStringAsync()); + } + + [TestMethod] + public async Task TestStream() + { + await using var server = await MockServer.RunAsync + ( + On.Post().Run((Stream body) => body.Length) + ); + + using var response = await PostAsync(server, "{\"intValue\":42,\"stringValue\":\"The answer\"}"); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.AreEqual("42" /* :D */, await response.Content.ReadAsStringAsync()); + } + + [TestMethod] + public async Task TestRequest() + { + await using var server = await MockServer.RunAsync + ( + On.Get().Run((IRequest request) => request.Respond().Status(ResponseStatus.BadRequest)) + ); + + using var response = await GetAsync(server); + + Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); + } + + [TestMethod] + public async Task TestHandler() { + await using var server = await MockServer.RunAsync + ( + On.Get().Run(() => Listing.From(ResourceTree.FromDirectory("./"))) + ); - [TestMethod] - public async Task TestConstant() - { - using var server = MockServer.Run - ( - On.Get().Run(() => 42) - ); - - using var response = await GetAsync(server); - - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); - Assert.AreEqual("42", await response.Content.ReadAsStringAsync()); - } - - private record MyClass(int IntValue, string StringValue); - - [TestMethod] - public async Task TestJson() - { - using var server = MockServer.Run - ( - On.Get().Run(() => new MyClass(42, "The answer")) - ); - - using var response = await GetAsync(server); - - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); - Assert.AreEqual("{\"intValue\":42,\"stringValue\":\"The answer\"}", await response.Content.ReadAsStringAsync()); - } - - [TestMethod] - public async Task TestQuery() - { - using var server = MockServer.Run - ( - On.Get().Run((int i) => i + 1) - ); - - using var response = await GetAsync(server, "/?i=1"); - - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); - Assert.AreEqual("2", await response.Content.ReadAsStringAsync()); - } - - [TestMethod] - public async Task TestPath() - { - using var server = MockServer.Run - ( - On.Get("/increment/:i").Run((int i) => i + 1) - ); - - using var response = await GetAsync(server, "/increment/1"); - - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); - Assert.AreEqual("2", await response.Content.ReadAsStringAsync()); - } - - [TestMethod] - public async Task TestPost() - { - using var server = MockServer.Run - ( - On.Post().Run((MyClass body) => body) - ); - - using var response = await PostAsync(server, "{\"intValue\":42,\"stringValue\":\"The answer\"}"); - - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); - Assert.AreEqual("{\"intValue\":42,\"stringValue\":\"The answer\"}", await response.Content.ReadAsStringAsync()); - } - - [TestMethod] - public async Task TestStream() - { - using var server = MockServer.Run - ( - On.Post().Run((Stream body) => body.Length) - ); - - using var response = await PostAsync(server, "{\"intValue\":42,\"stringValue\":\"The answer\"}"); - - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); - Assert.AreEqual("42" /* :D */, await response.Content.ReadAsStringAsync()); - } - - [TestMethod] - public async Task TestRequest() - { - using var server = MockServer.Run - ( - On.Get().Run((IRequest request) => request.Respond().Status(ResponseStatus.BadRequest)) - ); - - using var response = await GetAsync(server); - - Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); - } - - [TestMethod] - public async Task TestHandler() - { - using var server = MockServer.Run - ( - On.Get().Run(() => Listing.From(ResourceTree.FromDirectory("./"))) - ); - - using var response = await GetAsync(server); - - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); - } + using var response = await GetAsync(server); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); } -} +} \ No newline at end of file diff --git a/MockH.Tests/ServerTest.cs b/MockH.Tests/ServerTest.cs index c152081..63cccba 100644 --- a/MockH.Tests/ServerTest.cs +++ b/MockH.Tests/ServerTest.cs @@ -2,35 +2,32 @@ using MockH.Environment; -namespace MockH.Tests +namespace MockH.Tests; + +public abstract class ServerTest { - - public abstract class ServerTest + protected HttpClient _Client = new(new HttpClientHandler() { - protected HttpClient _Client = new(new HttpClientHandler() - { - AllowAutoRedirect = false - }); + AllowAutoRedirect = false + }); - protected async ValueTask GetStringAsync(Server server, string? path = null) => await _Client.GetStringAsync(server.Url(path)); + protected async ValueTask GetStringAsync(Server server, string? path = null) => await _Client.GetStringAsync(server.Url(path)); - protected async ValueTask GetAsync(Server server, string? path = null) => await _Client.GetAsync(server.Url(path)); - - protected async ValueTask PostAsync(Server server, string value, string? path = null) - { - var content = new StringContent(value); + protected async ValueTask GetAsync(Server server, string? path = null) => await _Client.GetAsync(server.Url(path)); - content.Headers.ContentType = new("application/json"); + protected async ValueTask PostAsync(Server server, string value, string? path = null) + { + var content = new StringContent(value); - return await _Client.PostAsync(server.Url(path), content); - } + content.Headers.ContentType = new("application/json"); - [TestCleanup] - public void Cleanup() - { - _Client.Dispose(); - } + return await _Client.PostAsync(server.Url(path), content); + } + [TestCleanup] + public void Cleanup() + { + _Client.Dispose(); } -} +} \ No newline at end of file diff --git a/MockH.Tests/ServerTests.cs b/MockH.Tests/ServerTests.cs index 8143c3b..59d1f3b 100644 --- a/MockH.Tests/ServerTests.cs +++ b/MockH.Tests/ServerTests.cs @@ -1,44 +1,41 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace MockH.Tests +namespace MockH.Tests; + +[TestClass] +public class ServerTests : ServerTest { - [TestClass] - public class ServerTests : ServerTest + [TestMethod] + public async Task TestRelativeUrl() { + await using var server = await MockServer.RunAsync(); - [TestMethod] - public void TestRelativeUrl() - { - using var server = MockServer.Run(); - - var url = server.Url("/api/users"); + var url = server.Url("/api/users"); - Assert.IsTrue(url.StartsWith("http://localhost:")); - Assert.IsTrue(url.EndsWith("/api/users")); - } - - [TestMethod] - public void TestRelativeUrlWithoutSlash() - { - using var server = MockServer.Run(); + Assert.IsTrue(url.StartsWith("http://localhost:")); + Assert.IsTrue(url.EndsWith("/api/users")); + } - var url = server.Url("api/users"); + [TestMethod] + public async Task TestRelativeUrlWithoutSlash() + { + await using var server = await MockServer.RunAsync(); - Assert.IsTrue(url.StartsWith("http://localhost:")); - Assert.IsTrue(url.EndsWith("/api/users")); - } + var url = server.Url("api/users"); - [TestMethod] - public void TestAbsoluteUrl() - { - using var server = MockServer.Run(); + Assert.IsTrue(url.StartsWith("http://localhost:")); + Assert.IsTrue(url.EndsWith("/api/users")); + } - var url = server.Url("https://google.de"); + [TestMethod] + public async Task TestAbsoluteUrl() + { + await using var server = await MockServer.RunAsync(); - Assert.AreEqual("https://google.de", url); - } + var url = server.Url("https://google.de"); + Assert.AreEqual("https://google.de", url); } -} +} \ No newline at end of file diff --git a/MockH/Builder/IncompleteRule.cs b/MockH/Builder/IncompleteRule.cs index 71d467c..794a5df 100644 --- a/MockH/Builder/IncompleteRule.cs +++ b/MockH/Builder/IncompleteRule.cs @@ -2,97 +2,94 @@ using Basics = GenHTTP.Modules.Basics; -namespace MockH.Builder +namespace MockH.Builder; + +/// +/// A builder that can be used to specify the response the server +/// will return for a specified request method and path. +/// +/// +/// Use the public methods provided by the builder to create +/// a rule that can actually be executed by the server. +/// +public class IncompleteRule { + #region Get-/Setters + + private HashSet Methods { get; } + + private string? Path { get; } + + #endregion + + #region Initialization + /// - /// A builder that can be used to specify the response the server - /// will return for a specified request method and path. + /// Creates a new rule that will match the specified HTTP method + /// and the given URL path. /// + /// The method the rule should match + /// The path the rule should match (e.g. "/api/users") + public IncompleteRule(RequestMethod method, string? path) + : this(new HashSet() { new(method) }, path) { } + + /// + /// Creates a new rule that will match the specified HTTP methods + /// and the given URL path. + /// + /// The methods the rule should match + /// The path the rule should match (e.g. "/api/users") + public IncompleteRule(HashSet methods, string? path) + { + Methods = methods; + Path = path; + } + + #endregion + + #region Functionality + + /// + /// Responds with the specified payload serialized into JSON or XML, + /// depending on the "Accept" headers of the request (defaults to JSON). + /// + /// The type of the payload to be returned + /// The payload to be returned to the client + /// A rule to be used with the mock server + public Rule Return(T data) => new(Methods, Path, () => data); + + /// + /// Responds with a "Location" header and a status code indicating that + /// the resource has moved. + /// + /// The absolute URL pointing to the new location of the resource + /// true for HTTP 307, false for HTTP 301 + /// A rule to be used with the mock server + public Rule Redirect(string location, bool temporary = true) => new(Methods, Path, (IRequest request) => Basics.Redirect.To(location).Mode(temporary)); + + /// + /// Executes the given delegate to determine the response to be sent + /// to the client. + /// + /// The delegate to be invoked for matching requests + /// A rule to be used with the mock server /// - /// Use the public methods provided by the builder to create - /// a rule that can actually be executed by the server. + /// May access path or query paramters, the body of the request and the entire + /// request. Allows to return payload to be serialized, exceptions, streams or + /// an entire response to be sent to the client. + /// + /// For typical examples, see the GitHub documentation. /// - public class IncompleteRule - { + public Rule Run(Delegate action) => new(Methods, Path, action); - #region Get-/Setters - - private HashSet Methods { get; } - - private string? Path { get; } - - #endregion - - #region Initialization - - /// - /// Creates a new rule that will match the specified HTTP method - /// and the given URL path. - /// - /// The method the rule should match - /// The path the rule should match (e.g. "/api/users") - public IncompleteRule(RequestMethod method, string? path) - : this(new HashSet() { new(method) }, path) { } - - /// - /// Creates a new rule that will match the specified HTTP methods - /// and the given URL path. - /// - /// The methods the rule should match - /// The path the rule should match (e.g. "/api/users") - public IncompleteRule(HashSet methods, string? path) - { - Methods = methods; - Path = path; - } - - #endregion - - #region Functionality - - /// - /// Responds with the specified payload serialized into JSON or XML, - /// depending on the "Accept" headers of the request (defaults to JSON). - /// - /// The type of the payload to be returned - /// The payload to be returned to the client - /// A rule to be used with the mock server - public Rule Return(T data) => new(Methods, Path, () => data); - - /// - /// Responds with a "Location" header and a status code indicating that - /// the resource has moved. - /// - /// The absolute URL pointing to the new location of the resource - /// true for HTTP 307, false for HTTP 301 - /// A rule to be used with the mock server - public Rule Redirect(string location, bool temporary = true) => new(Methods, Path, (IRequest request) => Basics.Redirect.To(location).Mode(temporary)); - - /// - /// Executes the given delegate to determine the response to be sent - /// to the client. - /// - /// The delegate to be invoked for matching requests - /// A rule to be used with the mock server - /// - /// May access path or query paramters, the body of the request and the entire - /// request. Allows to return payload to be serialized, exceptions, streams or - /// an entire response to be sent to the client. - /// - /// For typical examples, see the GitHub documentation. - /// - public Rule Run(Delegate action) => new(Methods, Path, action); - - /// - /// Responds with the specified HTTP status code and no payload. - /// - /// The HTTP status to respond with - /// A rule to be used with the mock server - public Rule Respond(ResponseStatus status) => new(Methods, Path, (IRequest request) => request.Respond().Status(status).Build()); - - #endregion + /// + /// Responds with the specified HTTP status code and no payload. + /// + /// The HTTP status to respond with + /// A rule to be used with the mock server + public Rule Respond(ResponseStatus status) => new(Methods, Path, (IRequest request) => request.Respond().Status(status).Build()); - } + #endregion -} +} \ No newline at end of file diff --git a/MockH/Builder/Rule.cs b/MockH/Builder/Rule.cs index eb9b1bb..2d80c21 100644 --- a/MockH/Builder/Rule.cs +++ b/MockH/Builder/Rule.cs @@ -2,46 +2,43 @@ using GenHTTP.Modules.Functional.Provider; -namespace MockH.Builder -{ +namespace MockH.Builder; - /// - /// A rule that can be executed by the mock server to determine - /// the response to be sent to a requesting client. - /// - public class Rule - { +/// +/// A rule that can be executed by the mock server to determine +/// the response to be sent to a requesting client. +/// +public class Rule +{ #region Get-/Setters - private HashSet Methods { get; } + private HashSet Methods { get; } - private string? Path { get; } + private string? Path { get; } - private Delegate Action { get; } + private Delegate Action { get; } #endregion #region Initialization - internal Rule(HashSet methods, string? path, Delegate action) - { - Methods = methods; - Path = path; - Action = action; - } + internal Rule(HashSet methods, string? path, Delegate action) + { + Methods = methods; + Path = path; + Action = action; + } #endregion #region Functionality - internal void AddTo(InlineBuilder builder) - { - builder.On(Action, Methods, Path); - } + internal void AddTo(InlineBuilder builder) + { + builder.On(Action, Methods, Path); + } #endregion - } - -} +} \ No newline at end of file diff --git a/MockH/Builder/ServerBuilder.cs b/MockH/Builder/ServerBuilder.cs index 9d8117f..82192c9 100644 --- a/MockH/Builder/ServerBuilder.cs +++ b/MockH/Builder/ServerBuilder.cs @@ -1,71 +1,72 @@ using MockH.Environment; -namespace MockH.Builder -{ +namespace MockH.Builder; - /// - /// Allows to configure and build a new mock server instance. - /// - public class ServerBuilder - { - private static readonly IPortProvider _DefaultPortProvider = new StaticPortProvider(); +/// +/// Allows to configure and build a new mock server instance. +/// +public class ServerBuilder +{ + private static readonly IPortProvider DefaultPortProvider = new StaticPortProvider(); - private readonly List _Rules = new(); + private readonly List _rules = new(); - private IPortProvider? _PortProvider; + private IPortProvider? _portProvider; - #region Functionality + #region Functionality - /// - /// Adds the given rule to the ruleset to be evaluated on request. - /// - /// The rule to be added - /// The builder instance - public ServerBuilder Add(Rule rule) - { - _Rules.Add(rule); - return this; - } + /// + /// Adds the given rule to the ruleset to be evaluated on request. + /// + /// The rule to be added + /// The builder instance + public ServerBuilder Add(Rule rule) + { + _rules.Add(rule); + return this; + } - /// - /// Adds the given rules to the ruleset to be evaluated on request. - /// - /// The rule to be added - /// The builder instance - public ServerBuilder Add(IEnumerable rules) - { - _Rules.AddRange(rules); - return this; - } + /// + /// Adds the given rules to the ruleset to be evaluated on request. + /// + /// The rule to be added + /// The builder instance + public ServerBuilder Add(IEnumerable rules) + { + _rules.AddRange(rules); + return this; + } - /// - /// Sets the strategy used by the server to obtain a free port - /// to listen on. - /// - /// The strategy used to determine a free port to listen on - /// The builder instance - public ServerBuilder Ports(IPortProvider portProvider) - { - _PortProvider = portProvider; - return this; - } + /// + /// Sets the strategy used by the server to obtain a free port + /// to listen on. + /// + /// The strategy used to determine a free port to listen on + /// The builder instance + public ServerBuilder Ports(IPortProvider portProvider) + { + _portProvider = portProvider; + return this; + } - /// - /// Creates and starts the configured mock server instance. - /// - /// The configured and started mock server instance - /// - /// Can be called multiple times if needed. - /// - public Server Run() - { - var portProvider = _PortProvider ?? _DefaultPortProvider; + /// + /// Creates and starts the configured mock server instance. + /// + /// The configured and started mock server instance + /// + /// Can be called multiple times if needed. + /// + public async ValueTask RunAsync() + { + var portProvider = _portProvider ?? DefaultPortProvider; - return new Server(portProvider, _Rules); - } + var server = new Server(portProvider, _rules); - #endregion + await server.Host.StartAsync(); + return server; } -} + #endregion + +} \ No newline at end of file diff --git a/MockH/Environment/IPortProvider.cs b/MockH/Environment/IPortProvider.cs index e47c0f1..119cdc7 100644 --- a/MockH/Environment/IPortProvider.cs +++ b/MockH/Environment/IPortProvider.cs @@ -1,22 +1,19 @@ -namespace MockH.Environment +namespace MockH.Environment; + +/// +/// Allows the mock server to determine a free port to listen on. +/// +public interface IPortProvider { - + /// - /// Allows the mock server to determine a free port to listen on. + /// Returns the next port number that can be used to listen on. /// - public interface IPortProvider - { - - /// - /// Returns the next port number that can be used to listen on. - /// - /// The next port number to listen on - /// - /// Must be thread safe to prevent multiple server instances from listening - /// on the same port. - /// - ushort GetAvailable(); - - } + /// The next port number to listen on + /// + /// Must be thread safe to prevent multiple server instances from listening + /// on the same port. + /// + ushort GetAvailable(); -} +} \ No newline at end of file diff --git a/MockH/Environment/Server.cs b/MockH/Environment/Server.cs index 8c39c1e..d055628 100644 --- a/MockH/Environment/Server.cs +++ b/MockH/Environment/Server.cs @@ -5,129 +5,125 @@ using GenHTTP.Modules.Functional; using GenHTTP.Modules.Functional.Provider; -namespace MockH.Environment -{ +namespace MockH.Environment; - /// - /// A mock server instance serving incoming HTTP requests by - /// evaluating a given rule set. - /// - public class Server : IDisposable - { - private bool _Disposed; +/// +/// A mock server instance serving incoming HTTP requests by +/// evaluating a given rule set. +/// +public class Server : IAsyncDisposable +{ + private bool _Disposed; - #region Get-/Setters + #region Get-/Setters - private IServerHost Host { get; } + internal IServerHost Host { get; } - /// - /// The port the HTTP server is listening to. - /// - public ushort Port { get; } + /// + /// The port the HTTP server is listening to. + /// + public ushort Port { get; } #endregion #region Initialization - /// - /// Creates and starts a new server instance with the given ruleset. - /// - /// The strategy to obtain a free port with - /// The ruleset to be evaluated by the server instance - public Server(IPortProvider portProvider, List rules) - { - Port = portProvider.GetAvailable(); + /// + /// Creates and starts a new server instance with the given ruleset. + /// + /// The strategy to obtain a free port with + /// The ruleset to be evaluated by the server instance + public Server(IPortProvider portProvider, List rules) + { + Port = portProvider.GetAvailable(); - Host = GenHTTP.Engine.Host.Create() - .Port(Port) - .Handler(SetupHandler(rules)) - .Start(); - } + Host = GenHTTP.Engine.Internal.Host.Create() + .Port(Port) + .Handler(SetupHandler(rules)); + } + + private static InlineBuilder SetupHandler(List rules) + { + var builder = Inline.Create(); - private static InlineBuilder SetupHandler(List rules) + foreach (var rule in rules) { - var builder = Inline.Create(); + rule.AddTo(builder); + } - foreach (var rule in rules) - { - rule.AddTo(builder); - } + return builder; + } - return builder; - } + #endregion - #endregion + #region Functionality - #region Functionality + /// + /// Returns a fully qualified URL that can be used in a HTTP + /// request to fetch the specified, relative path. + /// + /// The requested path, e.g. "/api/users" + /// The fully qualified URL to access the specified path + public string Url(string? path) + { + string actualPath; - /// - /// Returns a fully qualified URL that can be used in a HTTP - /// request to fetch the specified, relative path. - /// - /// The requested path, e.g. "/api/users" - /// The fully qualified URL to access the specified path - public string Url(string? path) + if (path != null) { - string actualPath; + if (path.StartsWith("http")) + { + return path; + } - if (path != null) + if (path.StartsWith("/")) { - if (path.StartsWith("http")) - { - return path; - } - - if (path.StartsWith("/")) - { - actualPath = path; - } - else - { - actualPath = $"/{path}"; - } + actualPath = path; } else { - actualPath = ""; + actualPath = $"/{path}"; } - - return $"http://localhost:{Port}{actualPath}"; + } + else + { + actualPath = ""; } - #endregion + return $"http://localhost:{Port}{actualPath}"; + } + + #endregion - #region Disposal + #region Disposal - /// - /// Releases all resources held by the server instance by stopping - /// the running server instance. - /// - /// true, if managed resources should be disposed - protected virtual void Dispose(bool disposing) + /// + /// Releases all resources held by the server instance by stopping + /// the running server instance. + /// + /// true, if managed resources should be disposed + protected virtual async ValueTask DisposeAsync(bool disposing) + { + if (!_Disposed) { - if (!_Disposed) + if (disposing) { - if (disposing) - { - Host.Stop(); - } - - _Disposed = true; + await Host.StopAsync(); } - } - /// - /// Releases all resources held by the server instance by stopping - /// the running server instance. - /// - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); + _Disposed = true; } + } - #endregion - + /// + /// Releases all resources held by the server instance by stopping + /// the running server instance. + /// + public async ValueTask DisposeAsync() + { + await DisposeAsync(disposing: true); + GC.SuppressFinalize(this); } -} + #endregion + +} \ No newline at end of file diff --git a/MockH/Environment/StaticPortProvider.cs b/MockH/Environment/StaticPortProvider.cs index ab7b99d..77c0ba1 100644 --- a/MockH/Environment/StaticPortProvider.cs +++ b/MockH/Environment/StaticPortProvider.cs @@ -1,21 +1,18 @@ -namespace MockH.Environment +namespace MockH.Environment; + +/// +/// Provides port numbers by returning a number starting from 20.000 +/// and incrementing the number on each call. +/// +public class StaticPortProvider : IPortProvider { + private static int _nextPort = 20000; + /// - /// Provides port numbers by returning a number starting from 20.000 - /// and incrementing the number on each call. + /// Fetches the next available port to be used. /// - public class StaticPortProvider : IPortProvider - { - - private static int _NextPort = 20000; - - /// - /// Fetches the next available port to be used. - /// - /// The next available port to be used - public ushort GetAvailable() => (ushort)Interlocked.Increment(ref _NextPort); - - } + /// The next available port to be used + public ushort GetAvailable() => (ushort)Interlocked.Increment(ref _nextPort); -} +} \ No newline at end of file diff --git a/MockH/MockH.csproj b/MockH/MockH.csproj index 1f5e6e0..d43f0cf 100644 --- a/MockH/MockH.csproj +++ b/MockH/MockH.csproj @@ -2,16 +2,17 @@ - net6.0;net7.0;net8.0 + net8.0;net9.0 enable - 10.0 + 13.0 enable true + true - 1.8.0.0 - 1.8.0.0 - 1.8.0 + 1.9.0.0 + 1.9.0.0 + 1.9.0 Andreas Nägeli @@ -34,9 +35,9 @@ - + - + diff --git a/MockH/MockServer.cs b/MockH/MockServer.cs index 2bc79c9..8f186e0 100644 --- a/MockH/MockServer.cs +++ b/MockH/MockServer.cs @@ -1,40 +1,36 @@ using MockH.Builder; using MockH.Environment; -namespace MockH +namespace MockH; + +/// +/// Main entry point to create a new mock server instance. +/// +public static class MockServer { /// - /// Main entry point to create a new mock server instance. + /// Creates and starts a server instance that will handle incoming + /// HTTP requests as specified by the given ruleset. /// - public static class MockServer - { - - /// - /// Creates and starts a server instance that will handle incoming - /// HTTP requests as specified by the given ruleset. - /// - /// The responses to be provided by the server - /// The newly created server instance - /// - /// Typical rules can be created using the On class. - /// - public static Server Run(params Rule[] rules) => Create(rules).Run(); - + /// The responses to be provided by the server + /// The newly created server instance + /// + /// Typical rules can be created using the On class. + /// + public static ValueTask RunAsync(params Rule[] rules) => Create(rules).RunAsync(); - /// - /// Creates a server builder for the specified ruleset. - /// - /// The responses to be provided by the server - /// The newly created server instance - /// - /// Typical rules can be created using the On class. - /// - /// This method will not actually launch a server instance, which allows to - /// perform additional modifications such as overriding the default port strategy. - /// - public static ServerBuilder Create(params Rule[] rules) => new ServerBuilder().Add(rules); - - } + /// + /// Creates a server builder for the specified ruleset. + /// + /// The responses to be provided by the server + /// The newly created server instance + /// + /// Typical rules can be created using the On class. + /// + /// This method will not actually launch a server instance, which allows to + /// perform additional modifications such as overriding the default port strategy. + /// + public static ServerBuilder Create(params Rule[] rules) => new ServerBuilder().Add(rules); -} +} \ No newline at end of file diff --git a/MockH/On.cs b/MockH/On.cs index e34615d..a245686 100644 --- a/MockH/On.cs +++ b/MockH/On.cs @@ -2,55 +2,52 @@ using GenHTTP.Api.Protocol; -namespace MockH +namespace MockH; + +/// +/// Entry point to create rules that should be adhered by a mock server instance. +/// +public static class On { /// - /// Entry point to create rules that should be adhered by a mock server instance. + /// Creates a rule that will handle an incoming HEAD request for + /// the specified path. + /// + /// The path to be handled, e.g. "/api/users" (or null, if requests to the server root should be handled) + /// The newly created rule builder + public static IncompleteRule Head(string? path = null) => new(RequestMethod.Head, path); + + /// + /// Creates a rule that will handle an incoming GET request for + /// the specified path. + /// + /// The path to be handled, e.g. "/api/users" (or null, if requests to the server root should be handled) + /// The newly created rule builder + public static IncompleteRule Get(string? path = null) => new(RequestMethod.Get, path); + + /// + /// Creates a rule that will handle an incoming POST request for + /// the specified path. /// - public static class On - { - - /// - /// Creates a rule that will handle an incoming HEAD request for - /// the specified path. - /// - /// The path to be handled, e.g. "/api/users" (or null, if requests to the server root should be handled) - /// The newly created rule builder - public static IncompleteRule Head(string? path = null) => new(RequestMethod.HEAD, path); - - /// - /// Creates a rule that will handle an incoming GET request for - /// the specified path. - /// - /// The path to be handled, e.g. "/api/users" (or null, if requests to the server root should be handled) - /// The newly created rule builder - public static IncompleteRule Get(string? path = null) => new(RequestMethod.GET, path); - - /// - /// Creates a rule that will handle an incoming POST request for - /// the specified path. - /// - /// The path to be handled, e.g. "/api/users" (or null, if requests to the server root should be handled) - /// The newly created rule builder - public static IncompleteRule Post(string? path = null) => new(RequestMethod.POST, path); - - /// - /// Creates a rule that will handle an incoming PUT request for - /// the specified path. - /// - /// The path to be handled, e.g. "/api/users" (or null, if requests to the server root should be handled) - /// The newly created rule builder - public static IncompleteRule Put(string? path = null) => new(RequestMethod.PUT, path); - - /// - /// Creates a rule that will handle an incoming DELETE request for - /// the specified path. - /// - /// The path to be handled, e.g. "/api/users" (or null, if requests to the server root should be handled) - /// The newly created rule builder - public static IncompleteRule Delete(string? path = null) => new(RequestMethod.DELETE, path); - - } - -} + /// The path to be handled, e.g. "/api/users" (or null, if requests to the server root should be handled) + /// The newly created rule builder + public static IncompleteRule Post(string? path = null) => new(RequestMethod.Post, path); + + /// + /// Creates a rule that will handle an incoming PUT request for + /// the specified path. + /// + /// The path to be handled, e.g. "/api/users" (or null, if requests to the server root should be handled) + /// The newly created rule builder + public static IncompleteRule Put(string? path = null) => new(RequestMethod.Put, path); + + /// + /// Creates a rule that will handle an incoming DELETE request for + /// the specified path. + /// + /// The path to be handled, e.g. "/api/users" (or null, if requests to the server root should be handled) + /// The newly created rule builder + public static IncompleteRule Delete(string? path = null) => new(RequestMethod.Delete, path); + +} \ No newline at end of file diff --git a/README.md b/README.md index f1eba2e..cccc820 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![CI](https://github.com/Kaliumhexacyanoferrat/MockH/actions/workflows/ci.yml/badge.svg)](https://github.com/Kaliumhexacyanoferrat/MockH/actions/workflows/ci.yml) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Kaliumhexacyanoferrat_MockH&metric=coverage)](https://sonarcloud.io/summary/new_code?id=Kaliumhexacyanoferrat_MockH) [![nuget Package](https://img.shields.io/nuget/v/MockH.svg)](https://www.nuget.org/packages/MockH/) -This library allows to mock HTTP responses for integration, component and acceptance tests of your projects written in C# / .NET 6/7/8 by hosting a webserver returning configured responses. +This library allows to mock HTTP responses for integration, component and acceptance tests of your projects written in C# / .NET 8/9 by hosting a webserver returning configured responses. - Fast and thread safe - Only a few dependencies @@ -18,7 +18,7 @@ using MockH; [TestMethod] public async Task TestSomething() { - using var server = MockServer.Run + await using var server = await MockServer.RunAsync ( On.Get("/users/1").Return(new User(...)), On.Get("/users/2").Respond(ResponseStatus.NoContent)