diff --git a/Internal/Xcepto.Internal.Http/Builders/HttpStateBuilder.T.cs b/Internal/Xcepto.Internal.Http/Builders/HttpStateBuilder.T.cs index 34f9721..f46cb28 100644 --- a/Internal/Xcepto.Internal.Http/Builders/HttpStateBuilder.T.cs +++ b/Internal/Xcepto.Internal.Http/Builders/HttpStateBuilder.T.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -18,10 +19,10 @@ public abstract class HttpStateBuilderIdentity : AbstractStateBuilderI { protected Func ClientProducer = () => new(); - protected Uri BaseUrl = new("http://localhost:8080"); + protected Func BaseUrl = () => new("http://localhost:8080"); protected HttpMethodVerb MethodVerb = HttpMethodVerb.Get; - protected PathString PathString = "/"; - protected readonly List> QueryArgs = new(); + protected Func PathString = () => "/"; + protected readonly List>> QueryArgs = new(); protected readonly List ResponseAssertions = new(); protected HttpStateBuilderIdentity(IStateMachineBuilder stateMachineBuilder, IStateBuilderIdentity stateBuilderIdentity) : base(stateMachineBuilder, stateBuilderIdentity) { } @@ -47,15 +48,18 @@ protected override bool DefaultRetry } - protected Uri Url + protected Func Url { get { if (BaseUrl is null) throw new BuilderException("no Url defined"); - if (!Uri.TryCreate(BaseUrl, PathString + QueryString.Create(QueryArgs), out var uri)) - throw new ArgumentException("Url creation failed"); - return uri; + return () => + { + if (!Uri.TryCreate(BaseUrl(), PathString() + QueryString.Create(QueryArgs.Select(x=> x())), out var uri)) + throw new ArgumentException("Url creation failed"); + return uri; + }; } } @@ -72,6 +76,12 @@ public TBuilder WithCustomClient(Func clientProducer) } public TBuilder WithCustomBaseUrl(Uri uri) + { + BaseUrl = () => uri; + return (TBuilder)this; + } + + public TBuilder WithCustomBaseUrl(Func uri) { BaseUrl = uri; return (TBuilder)this; @@ -79,7 +89,13 @@ public TBuilder WithCustomBaseUrl(Uri uri) public TBuilder AddQueryArgument(string key, string value) { - QueryArgs.Add(new KeyValuePair(key, value)); + QueryArgs.Add(() => new KeyValuePair(key, value)); + return (TBuilder)this; + } + + public TBuilder AddQueryArgument(Func> argument) + { + QueryArgs.Add(argument); return (TBuilder)this; } @@ -91,7 +107,13 @@ public TBuilder WithHttpVerb(HttpMethodVerb verb) public TBuilder WithPathString(PathString pathString) { - PathString = pathString; + PathString = () => pathString; + return (TBuilder)this; + } + + public TBuilder WithPathString(Func pathStringPromise) + { + PathString = pathStringPromise; return (TBuilder)this; } diff --git a/Samples/RestAuth/Samples.Rest.API.Tests/BaseTest.cs b/Samples/RestAuth/Samples.Rest.API.Tests/BaseTest.cs index 4fce925..8525e9b 100644 --- a/Samples/RestAuth/Samples.Rest.API.Tests/BaseTest.cs +++ b/Samples/RestAuth/Samples.Rest.API.Tests/BaseTest.cs @@ -4,7 +4,7 @@ namespace Samples.Rest.API.Tests; public class BaseTest { - protected (string encoded, byte[] hashed) CreateToken() + protected static (string encoded, byte[] hashed) CreateToken() { byte[] bytes = new byte[32]; var randomNumberGenerator = RandomNumberGenerator.Create(); diff --git a/Samples/RestAuth/Samples.Rest.API.Tests/PromisedPathTests.cs b/Samples/RestAuth/Samples.Rest.API.Tests/PromisedPathTests.cs new file mode 100644 index 0000000..c4bcfc2 --- /dev/null +++ b/Samples/RestAuth/Samples.Rest.API.Tests/PromisedPathTests.cs @@ -0,0 +1,123 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using Samples.Rest.API.Requests; +using Samples.Rest.API.Responses; +using Samples.Rest.API.Tests.Scenarios; +using Xcepto; +using Xcepto.Config; +using Xcepto.NewtonsoftJson; +using Xcepto.Rest.Extensions; +using Xcepto.Scenarios; + +namespace Samples.Rest.API.Tests; + +[TestFixture] +public class PromisedPathTests: BaseTest +{ + readonly TimeoutConfig _timeoutConfig = new TimeoutConfig( + TimeSpan.FromSeconds(60), + TimeSpan.FromSeconds(10) + ); + private readonly MockedTokenScenario _scenario = new MockedTokenScenario(CreateToken().hashed); + + [Test] + public async Task PromisedPathFromResponse_Get_Works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var rest = builder.RestAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.ApiPort}")) + .WithSerializer(new NewtonsoftSerializer()) + .Build(); + + var promiseResponse = rest.Get("/api/GetPath") + .WithResponseType() + .AssertSuccess() + .PromiseResponse(); + + rest.Get(() => promiseResponse.Resolve().Path) + .AssertSuccess(); + }); + } + + [Test] + public async Task PromisedPathFromResponse_Post_Works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var rest = builder.RestAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.ApiPort}")) + .WithSerializer(new NewtonsoftSerializer()) + .Build(); + + var promiseResponse = rest.Get("/api/PostPath") + .WithResponseType() + .AssertSuccess() + .PromiseResponse(); + + rest.Post(() => promiseResponse.Resolve().Path) + .AssertSuccess(); + }); + } + + [Test] + public async Task PromisedPathFromResponse_Patch_Works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var rest = builder.RestAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.ApiPort}")) + .WithSerializer(new NewtonsoftSerializer()) + .Build(); + + var promiseResponse = rest.Get("/api/PatchPath") + .WithResponseType() + .AssertSuccess() + .PromiseResponse(); + + rest.Patch(() => promiseResponse.Resolve().Path) + .AssertSuccess(); + }); + } + + [Test] + public async Task PromisedPathFromResponse_Put_Works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var rest = builder.RestAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.ApiPort}")) + .WithSerializer(new NewtonsoftSerializer()) + .Build(); + + var promiseResponse = rest.Get("/api/PutPath") + .WithResponseType() + .AssertSuccess() + .PromiseResponse(); + + rest.Put(() => promiseResponse.Resolve().Path) + .AssertSuccess(); + }); + } + + [Test] + public async Task PromisedPathFromResponse_Delete_Works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var rest = builder.RestAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.ApiPort}")) + .WithSerializer(new NewtonsoftSerializer()) + .Build(); + + var promiseResponse = rest.Get("/api/DeletePath") + .WithResponseType() + .AssertSuccess() + .PromiseResponse(); + + rest.Delete(() => promiseResponse.Resolve().Path) + .AssertSuccess(); + }); + } +} \ No newline at end of file diff --git a/Samples/RestAuth/Samples.Rest.API.Tests/StaticPathTests.cs b/Samples/RestAuth/Samples.Rest.API.Tests/StaticPathTests.cs new file mode 100644 index 0000000..e2df186 --- /dev/null +++ b/Samples/RestAuth/Samples.Rest.API.Tests/StaticPathTests.cs @@ -0,0 +1,98 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using Samples.Rest.API.Requests; +using Samples.Rest.API.Responses; +using Samples.Rest.API.Tests.Scenarios; +using Xcepto; +using Xcepto.Config; +using Xcepto.NewtonsoftJson; +using Xcepto.Rest.Extensions; +using Xcepto.Scenarios; + +namespace Samples.Rest.API.Tests; + +[TestFixture] +public class StaticPathTests: BaseTest +{ + readonly TimeoutConfig _timeoutConfig = new TimeoutConfig( + TimeSpan.FromSeconds(60), + TimeSpan.FromSeconds(10) + ); + private readonly MockedTokenScenario _scenario = new MockedTokenScenario(CreateToken().hashed); + + [Test] + public async Task StaticPath_Get_Works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var rest = builder.RestAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.ApiPort}")) + .WithSerializer(new NewtonsoftSerializer()) + .Build(); + + rest.Get("/api/GetPath/validate") + .AssertSuccess(); + }); + } + + [Test] + public async Task StaticPath_Post_Works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var rest = builder.RestAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.ApiPort}")) + .WithSerializer(new NewtonsoftSerializer()) + .Build(); + + rest.Post("/api/PostPath/validate") + .AssertSuccess(); + }); + } + + [Test] + public async Task StaticPath_Patch_Works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var rest = builder.RestAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.ApiPort}")) + .WithSerializer(new NewtonsoftSerializer()) + .Build(); + + rest.Patch("/api/PatchPath/validate") + .AssertSuccess(); + }); + } + + [Test] + public async Task StaticPath_Put_Works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var rest = builder.RestAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.ApiPort}")) + .WithSerializer(new NewtonsoftSerializer()) + .Build(); + + rest.Put("/api/PutPath/validate") + .AssertSuccess(); + }); + } + + [Test] + public async Task StaticPath_Delete_Works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var rest = builder.RestAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.ApiPort}")) + .WithSerializer(new NewtonsoftSerializer()) + .Build(); + + rest.Delete("/api/DeletePath/validate") + .AssertSuccess(); + }); + } +} \ No newline at end of file diff --git a/Samples/RestAuth/Samples.Rest.API/Program.cs b/Samples/RestAuth/Samples.Rest.API/Program.cs index 8b11bff..3a5cf05 100644 --- a/Samples/RestAuth/Samples.Rest.API/Program.cs +++ b/Samples/RestAuth/Samples.Rest.API/Program.cs @@ -44,6 +44,22 @@ return Results.Json(new AuthenticatedTestResponse()); }); +app.MapGet("/api/GetPath", () => Results.Json(new PathResponse("/api/GetPath/validate"))); +app.MapGet("/api/GetPath/validate", () => Results.StatusCode(204)); + +app.MapGet("/api/PostPath", () => Results.Json(new PathResponse("/api/PostPath/validate"))); +app.MapPost("/api/PostPath/validate", () => Results.StatusCode(204)); + +app.MapGet("/api/PatchPath", () => Results.Json(new PathResponse("/api/PatchPath/validate"))); +app.MapPatch("/api/PatchPath/validate", () => Results.StatusCode(204)); + +app.MapGet("/api/DeletePath", () => Results.Json(new PathResponse("/api/DeletePath/validate"))); +app.MapDelete("/api/DeletePath/validate", () => Results.StatusCode(204)); + +app.MapGet("/api/PutPath", () => Results.Json(new PathResponse("/api/PutPath/validate"))); +app.MapPut("/api/PutPath/validate", () => Results.StatusCode(204)); + + static byte[] UrlDecode(string s) { s = s.Replace("-", "+").Replace("_", "/"); diff --git a/Samples/RestAuth/Samples.Rest.API/Requests/APathRequest.cs b/Samples/RestAuth/Samples.Rest.API/Requests/APathRequest.cs new file mode 100644 index 0000000..c47afcd --- /dev/null +++ b/Samples/RestAuth/Samples.Rest.API/Requests/APathRequest.cs @@ -0,0 +1,3 @@ +namespace Samples.Rest.API.Requests; + +public class APathRequest(); \ No newline at end of file diff --git a/Samples/RestAuth/Samples.Rest.API/Requests/DynamicPathRequest.cs b/Samples/RestAuth/Samples.Rest.API/Requests/DynamicPathRequest.cs new file mode 100644 index 0000000..bd66cb6 --- /dev/null +++ b/Samples/RestAuth/Samples.Rest.API/Requests/DynamicPathRequest.cs @@ -0,0 +1,3 @@ +namespace Samples.Rest.API.Requests; + +public record DynamicPathRequest(PathString Path); \ No newline at end of file diff --git a/Samples/RestAuth/Samples.Rest.API/Responses/PathResponse.cs b/Samples/RestAuth/Samples.Rest.API/Responses/PathResponse.cs new file mode 100644 index 0000000..6f9530b --- /dev/null +++ b/Samples/RestAuth/Samples.Rest.API/Responses/PathResponse.cs @@ -0,0 +1,3 @@ +namespace Samples.Rest.API.Responses; + +public record PathResponse(string Path); \ No newline at end of file diff --git a/Samples/SSR/Samples.SSR.GUI.Tests/Scenarios/SSRGuiScenario.cs b/Samples/SSR/Samples.SSR.GUI.Tests/Scenarios/SSRGuiScenario.cs index 1aa6d4e..182f594 100644 --- a/Samples/SSR/Samples.SSR.GUI.Tests/Scenarios/SSRGuiScenario.cs +++ b/Samples/SSR/Samples.SSR.GUI.Tests/Scenarios/SSRGuiScenario.cs @@ -46,7 +46,6 @@ private async Task StartEnvironment(IServiceProvider serviceProvider) .WithPassword("test") .WithPortBinding(5432, true) .WithWaitStrategy(Wait.ForUnixContainer().UntilCommandIsCompleted("pg_isready -U test -d test")) - .WithLogger(testContainerSupport.CreateLogger("postres")) .WithNetwork(network) .WithOutputConsumer(testContainerSupport.CreateOutputConsumer("postgres", false)) .WithNetworkAliases("postgres") diff --git a/Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/PromisedPathTests.cs b/Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/PromisedPathTests.cs new file mode 100644 index 0000000..6819b7c --- /dev/null +++ b/Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/PromisedPathTests.cs @@ -0,0 +1,126 @@ +using System.Net; +using System.Text.RegularExpressions; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using Microsoft.Extensions.DependencyInjection; +using Samples.SSR.GUI.Requests; +using Samples.SSR.GUI.Tests.Scenarios; +using Testcontainers.PostgreSql; +using Xcepto; +using Xcepto.Config; +using Xcepto.Interfaces; +using Xcepto.Internal.Http.Data; +using Xcepto.SSR; +using Xcepto.SSR.Extensions; + +namespace Samples.SSR.GUI.Tests.Xcepto; + +[TestFixture] +public class PromisedPathTests +{ + private static string ParsePath(string html) + { + var regex = new Regex(@"Path:\s*([^<]+)"); + var match = regex.Match(html); + + if (!match.Success) + throw new Exception($"Path not found in response: {html}"); + return match.Groups[1].Value; + } + + readonly SsrGuiScenario _scenario = new(); + readonly TimeoutConfig _timeoutConfig = new( + TimeSpan.FromSeconds(120), + TimeSpan.FromSeconds(10) + ); + + [Test] + public async Task PromisedPath_GET_works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var ssr = builder.SsrAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.GuiPort}")) + .Build(); + + var promise = ssr.Get("/path/get/get") + .AssertSuccess() + .PromiseResponse(); + + ssr.Get(() => ParsePath(promise.Resolve())) + .AssertSuccess(); + }); + } + + [Test] + public async Task PromisedPath_POST_works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var ssr = builder.SsrAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.GuiPort}")) + .Build(); + + var promise = ssr.Get("/path/get/post") + .AssertSuccess() + .PromiseResponse(); + + ssr.Post(() => ParsePath(promise.Resolve())) + .AssertSuccess(); + }); + } + + [Test] + public async Task PromisedPath_PATCH_works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var ssr = builder.SsrAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.GuiPort}")) + .Build(); + + var promise = ssr.Get("/path/get/patch") + .AssertSuccess() + .PromiseResponse(); + + ssr.Request(() => ParsePath(promise.Resolve()), HttpMethodVerb.Patch) + .AssertSuccess(); + }); + } + + [Test] + public async Task PromisedPath_PUT_works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var ssr = builder.SsrAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.GuiPort}")) + .Build(); + + var promise = ssr.Get("/path/get/put") + .AssertSuccess() + .PromiseResponse(); + + ssr.Request(() => ParsePath(promise.Resolve()), HttpMethodVerb.Put) + .AssertSuccess(); + }); + } + + [Test] + public async Task PromisedPath_DELETE_works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var ssr = builder.SsrAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.GuiPort}")) + .Build(); + + var promise = ssr.Get("/path/get/delete") + .AssertSuccess() + .PromiseResponse(); + + ssr.Request(() => ParsePath(promise.Resolve()), HttpMethodVerb.Delete) + .AssertSuccess(); + }); + } +} \ No newline at end of file diff --git a/Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/QueryArgTests.cs b/Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/QueryArgTests.cs new file mode 100644 index 0000000..4fef1a3 --- /dev/null +++ b/Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/QueryArgTests.cs @@ -0,0 +1,69 @@ +using System.Text.RegularExpressions; +using Samples.SSR.GUI.Tests.Scenarios; +using Xcepto; +using Xcepto.Config; +using Xcepto.Internal.Http.Data; +using Xcepto.SSR.Extensions; + +namespace Samples.SSR.GUI.Tests.Xcepto; + +[TestFixture] +public class QueryArgTests +{ + readonly SsrGuiScenario _scenario = new(); + readonly TimeoutConfig _timeoutConfig = new( + TimeSpan.FromSeconds(120), + TimeSpan.FromSeconds(10) + ); + + private static string ParseNextPage(string html) + { + var regex = new Regex(@"NextPage:\s*([^<]+)"); + var match = regex.Match(html); + + if (!match.Success) + throw new Exception($"Path not found in response: {html}"); + return match.Groups[1].Value; + } + + [Test] + public async Task StaticQueryArgs() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var ssr = builder.SsrAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.GuiPort}")) + .Build(); + + ssr.Get("/query") + .AddQueryArgument("page", "1") + .AssertSuccess() + .AssertThatResponseContentString(Does.Contain("Page1")) + .AssertThatResponseContentString(Does.Not.Contain("Page2")); + }); + } + + [Test] + public async Task DynamicQueryArgs() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var ssr = builder.SsrAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.GuiPort}")) + .Build(); + + var promiseResponse = ssr.Get("/query") + .AddQueryArgument("page", "1") + .AssertSuccess() + .AssertThatResponseContentString(Does.Contain("Page1")) + .AssertThatResponseContentString(Does.Not.Contain("Page2")) + .PromiseResponse(); + + ssr.Get("/query") + .AddQueryArgument(() => new("page", ParseNextPage(promiseResponse.Resolve()))) + .AssertSuccess() + .AssertThatResponseContentString(Does.Not.Contain("Page1")) + .AssertThatResponseContentString(Does.Contain("Page2")); + }); + } +} \ No newline at end of file diff --git a/Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/StaticPathTests.cs b/Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/StaticPathTests.cs new file mode 100644 index 0000000..2c3cd31 --- /dev/null +++ b/Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/StaticPathTests.cs @@ -0,0 +1,88 @@ +using System.Text.RegularExpressions; +using Samples.SSR.GUI.Tests.Scenarios; +using Xcepto; +using Xcepto.Config; +using Xcepto.Internal.Http.Data; +using Xcepto.SSR.Extensions; + +namespace Samples.SSR.GUI.Tests.Xcepto; + +[TestFixture] +public class StaticPathTests +{ + readonly SsrGuiScenario _scenario = new(); + readonly TimeoutConfig _timeoutConfig = new( + TimeSpan.FromSeconds(120), + TimeSpan.FromSeconds(10) + ); + + [Test] + public async Task PromisedPath_GET_works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var ssr = builder.SsrAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.GuiPort}")) + .Build(); + + ssr.Get("/path/validate/get") + .AssertSuccess(); + }); + } + + [Test] + public async Task PromisedPath_POST_works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var ssr = builder.SsrAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.GuiPort}")) + .Build(); + + ssr.Post("/path/validate/post") + .AssertSuccess(); + }); + } + + [Test] + public async Task PromisedPath_PATCH_works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var ssr = builder.SsrAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.GuiPort}")) + .Build(); + + ssr.Request("/path/validate/patch", HttpMethodVerb.Patch) + .AssertSuccess(); + }); + } + + [Test] + public async Task PromisedPath_PUT_works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var ssr = builder.SsrAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.GuiPort}")) + .Build(); + + ssr.Request("/path/validate/put", HttpMethodVerb.Put) + .AssertSuccess(); + }); + } + + [Test] + public async Task PromisedPath_DELETE_works() + { + await XceptoTest.Given(_scenario, _timeoutConfig, builder => + { + var ssr = builder.SsrAdapterBuilder() + .WithBaseUrl(new Uri($"http://localhost:{_scenario.GuiPort}")) + .Build(); + + ssr.Request("/path/validate/delete", HttpMethodVerb.Delete) + .AssertSuccess(); + }); + } +} \ No newline at end of file diff --git a/Samples/SSR/Samples.SSR.GUI/Controllers/PromiseValidationController.cs b/Samples/SSR/Samples.SSR.GUI/Controllers/PromiseValidationController.cs index 25adc24..827eb08 100644 --- a/Samples/SSR/Samples.SSR.GUI/Controllers/PromiseValidationController.cs +++ b/Samples/SSR/Samples.SSR.GUI/Controllers/PromiseValidationController.cs @@ -4,7 +4,7 @@ namespace Samples.SSR.GUI.Controllers; [Route("validate")] -public class PromiseValidationController +public class PromiseValidationController: Controller { [HttpPost] public IActionResult Index(ValidationRequest validationRequest) diff --git a/Samples/SSR/Samples.SSR.GUI/Controllers/PromisedPathController.cs b/Samples/SSR/Samples.SSR.GUI/Controllers/PromisedPathController.cs new file mode 100644 index 0000000..87eb8d8 --- /dev/null +++ b/Samples/SSR/Samples.SSR.GUI/Controllers/PromisedPathController.cs @@ -0,0 +1,67 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Samples.SSR.GUI.Requests; +using Samples.SSR.GUI.ViewModels; + +namespace Samples.SSR.GUI.Controllers; + +[Route("path")] +public class PromisedPathController: Controller +{ + + [HttpGet("get/get")] + public IActionResult GetGet() + { + return View("Response", new PromisedPathViewModel("/path/validate/get")); + } + [HttpGet("validate/get")] + public IActionResult ValidateGet() + { + return new StatusCodeResult(204); + } + + [HttpGet("get/post")] + public IActionResult GetPost() + { + return View("Response", new PromisedPathViewModel("/path/validate/post")); + } + [HttpPost("validate/post")] + public IActionResult ValidatePost() + { + return new StatusCodeResult(204); + } + + [HttpGet("get/patch")] + public IActionResult GetPatch() + { + return View("Response", new PromisedPathViewModel("/path/validate/patch")); + } + [HttpPatch("validate/patch")] + public IActionResult ValidatePatch() + { + return new StatusCodeResult(204); + } + + + [HttpGet("get/put")] + public IActionResult GetPut() + { + return View("Response", new PromisedPathViewModel("/path/validate/put")); + } + [HttpPut("validate/put")] + public IActionResult ValidatePut() + { + return new StatusCodeResult(204); + } + + [HttpGet("get/delete")] + public IActionResult GetDelete() + { + return View("Response", new PromisedPathViewModel("/path/validate/delete")); + } + [HttpDelete("validate/delete")] + public IActionResult ValidateDelete() + { + return new StatusCodeResult(204); + } +} \ No newline at end of file diff --git a/Samples/SSR/Samples.SSR.GUI/Controllers/QueryController.cs b/Samples/SSR/Samples.SSR.GUI/Controllers/QueryController.cs new file mode 100644 index 0000000..3b6fc84 --- /dev/null +++ b/Samples/SSR/Samples.SSR.GUI/Controllers/QueryController.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Samples.SSR.GUI.Controllers; + +[Route("query")] +public class QueryController: Controller +{ + [HttpGet] + public IActionResult Index([FromQuery] int page) + { + if (page == 1) + return View("Page1"); + if (page == 2) + return View("Page2"); + throw new ArgumentException($"page {page} not found"); + } +} \ No newline at end of file diff --git a/Samples/SSR/Samples.SSR.GUI/ViewModels/PromisedPathViewModel.cs b/Samples/SSR/Samples.SSR.GUI/ViewModels/PromisedPathViewModel.cs new file mode 100644 index 0000000..e62e69d --- /dev/null +++ b/Samples/SSR/Samples.SSR.GUI/ViewModels/PromisedPathViewModel.cs @@ -0,0 +1,3 @@ +namespace Samples.SSR.GUI.ViewModels; + +public record PromisedPathViewModel(String path); \ No newline at end of file diff --git a/Samples/SSR/Samples.SSR.GUI/Views/PromisedPath/Response.cshtml b/Samples/SSR/Samples.SSR.GUI/Views/PromisedPath/Response.cshtml new file mode 100644 index 0000000..5a312c4 --- /dev/null +++ b/Samples/SSR/Samples.SSR.GUI/Views/PromisedPath/Response.cshtml @@ -0,0 +1,3 @@ +@model Samples.SSR.GUI.ViewModels.PromisedPathViewModel + +
Path: @Model.path
\ No newline at end of file diff --git a/Samples/SSR/Samples.SSR.GUI/Views/Query/Page1.cshtml b/Samples/SSR/Samples.SSR.GUI/Views/Query/Page1.cshtml new file mode 100644 index 0000000..4bf08cd --- /dev/null +++ b/Samples/SSR/Samples.SSR.GUI/Views/Query/Page1.cshtml @@ -0,0 +1,2 @@ +
Page1
+
NextPage: 2
diff --git a/Samples/SSR/Samples.SSR.GUI/Views/Query/Page2.cshtml b/Samples/SSR/Samples.SSR.GUI/Views/Query/Page2.cshtml new file mode 100644 index 0000000..2c13032 --- /dev/null +++ b/Samples/SSR/Samples.SSR.GUI/Views/Query/Page2.cshtml @@ -0,0 +1,2 @@ +
Page2
+
NextPage: 3
\ No newline at end of file diff --git a/Xcepto.Rest/Builders/RestStateBuilder.T.cs b/Xcepto.Rest/Builders/RestStateBuilder.T.cs index 8863a42..665c035 100644 --- a/Xcepto.Rest/Builders/RestStateBuilder.T.cs +++ b/Xcepto.Rest/Builders/RestStateBuilder.T.cs @@ -81,7 +81,7 @@ public DeserializedResponseRestStateBuilderIdentity WithResponseType< builder.InjectRequestBody(RequestBody); foreach (var pair in QueryArgs) { - builder.AddQueryArgument(pair.Key, pair.Value); + builder.AddQueryArgument(pair); } foreach (var assertion in ResponseAssertions) { diff --git a/Xcepto.Rest/Internals/XceptoRestState.cs b/Xcepto.Rest/Internals/XceptoRestState.cs index 38165ae..7f872e0 100644 --- a/Xcepto.Rest/Internals/XceptoRestState.cs +++ b/Xcepto.Rest/Internals/XceptoRestState.cs @@ -16,7 +16,7 @@ internal class XceptoRestState : XceptoHttpState { public XceptoRestState(string name, RequestBody? requestBody, - Uri url, + Func urlProducer, Func clientProducer, HttpMethodVerb methodVerb, bool retry, @@ -28,12 +28,12 @@ public XceptoRestState(string name, _methodVerb = methodVerb; _clientProducer = clientProducer; - _url = url; + _urlProducer = urlProducer; _requestBody = requestBody; } private readonly RequestBody? _requestBody; - private readonly Uri _url; + private readonly Func _urlProducer; private readonly Func _clientProducer; private readonly HttpMethodVerb _methodVerb; @@ -51,31 +51,32 @@ protected override async Task ExecuteRequest(IServiceProvid requestBody = new StringContent(_requestBody.SerializationMethod(_requestBody.RequestObjectPromise()), Encoding.UTF8, "application/json"); } - - loggingProvider.LogDebug($"Send {_methodVerb} REST request to {_url}"); + + var url = _urlProducer(); + loggingProvider.LogDebug($"Send {_methodVerb} REST request to {url}"); HttpClient client = _clientProducer(); HttpResponseMessage response; switch (_methodVerb) { case HttpMethodVerb.Get: - response = await client.GetAsync(_url); + response = await client.GetAsync(url); break; case HttpMethodVerb.Post: - response = await client.PostAsync(_url, requestBody); + response = await client.PostAsync(url, requestBody); break; case HttpMethodVerb.Patch: - var request = new HttpRequestMessage(new HttpMethod("PATCH"), _url) + var request = new HttpRequestMessage(new HttpMethod("PATCH"), url) { Content = requestBody }; response = await client.SendAsync(request); break; case HttpMethodVerb.Put: - response = await client.PutAsync(_url, requestBody); + response = await client.PutAsync(url, requestBody); break; case HttpMethodVerb.Delete: - response = await client.DeleteAsync(_url); + response = await client.DeleteAsync(url); break; default: throw new ArgumentOutOfRangeException(); diff --git a/Xcepto.Rest/XceptoRestAdapter.cs b/Xcepto.Rest/XceptoRestAdapter.cs index d2f45aa..1a2424d 100644 --- a/Xcepto.Rest/XceptoRestAdapter.cs +++ b/Xcepto.Rest/XceptoRestAdapter.cs @@ -24,7 +24,7 @@ internal XceptoRestAdapter(HttpClient client, Uri? baseUrl, ISerializer? seriali _client = client; } - private RestStateBuilderIdentity Inject(RestStateBuilderIdentity builderIdentity, HttpMethodVerb verb, PathString pathString) + private RestStateBuilderIdentity Inject(RestStateBuilderIdentity builderIdentity, HttpMethodVerb verb, Func pathString) { if(_baseUrl is not null) builderIdentity.WithCustomBaseUrl(_baseUrl); @@ -37,26 +37,51 @@ private RestStateBuilderIdentity Inject(RestStateBuilderIdentity builderIdentity } public RestStateBuilderIdentity Get(PathString pathString) + { + return Inject(new RestStateBuilderIdentity(Builder), HttpMethodVerb.Get, () => pathString); + } + + public RestStateBuilderIdentity Get(Func pathString) { return Inject(new RestStateBuilderIdentity(Builder), HttpMethodVerb.Get, pathString); } public RestStateBuilderIdentity Post(PathString pathString) + { + return Inject(new RestStateBuilderIdentity(Builder), HttpMethodVerb.Post, () => pathString); + } + + public RestStateBuilderIdentity Post(Func pathString) { return Inject(new RestStateBuilderIdentity(Builder), HttpMethodVerb.Post, pathString); } public RestStateBuilderIdentity Patch(PathString pathString) + { + return Inject(new RestStateBuilderIdentity(Builder), HttpMethodVerb.Patch, () => pathString); + } + + public RestStateBuilderIdentity Patch(Func pathString) { return Inject(new RestStateBuilderIdentity(Builder), HttpMethodVerb.Patch, pathString); } public RestStateBuilderIdentity Put(PathString pathString) + { + return Inject(new RestStateBuilderIdentity(Builder), HttpMethodVerb.Put, () => pathString); + } + + public RestStateBuilderIdentity Put(Func pathString) { return Inject(new RestStateBuilderIdentity(Builder), HttpMethodVerb.Put, pathString); } public RestStateBuilderIdentity Delete(PathString pathString) + { + return Inject(new RestStateBuilderIdentity(Builder), HttpMethodVerb.Delete, () => pathString); + } + + public RestStateBuilderIdentity Delete(Func pathString) { return Inject(new RestStateBuilderIdentity(Builder), HttpMethodVerb.Delete, pathString); } diff --git a/Xcepto.SSR/Internals/XceptoSsrState.cs b/Xcepto.SSR/Internals/XceptoSsrState.cs index ecb1c4f..1b2ec06 100644 --- a/Xcepto.SSR/Internals/XceptoSsrState.cs +++ b/Xcepto.SSR/Internals/XceptoSsrState.cs @@ -19,7 +19,7 @@ namespace Xcepto.SSR internal class XceptoSsrState : XceptoHttpState { public XceptoSsrState(string name, - Uri url, + Func urlProducer, Func? requestBody, IEnumerable assertions, bool retry, @@ -30,12 +30,12 @@ public XceptoSsrState(string name, { _methodVerb = methodVerb; _requestBody = requestBody; - _url = url; + _urlProducer = urlProducer; _clientProducer = clientProducer; } private readonly Func _clientProducer; - private readonly Uri _url; + private readonly Func _urlProducer; private readonly Func? _requestBody; private HttpMethodVerb _methodVerb; @@ -47,30 +47,31 @@ protected override async Task ExecuteRequest(IServiceProvid HttpContent requestBody = _requestBody is not null ? _requestBody() : new StringContent("", Encoding.Default); - loggingProvider.LogDebug($"Send {_methodVerb} SSR request to {_url}"); + var url = _urlProducer(); + loggingProvider.LogDebug($"Send {_methodVerb} SSR request to {url}"); HttpClient client = _clientProducer(); HttpResponseMessage response; switch (_methodVerb) { case HttpMethodVerb.Get: - response = await client.GetAsync(_url); + response = await client.GetAsync(url); break; case HttpMethodVerb.Post: - response = await client.PostAsync(_url, requestBody); + response = await client.PostAsync(url, requestBody); break; case HttpMethodVerb.Patch: - var request = new HttpRequestMessage(new HttpMethod("PATCH"), _url) + var request = new HttpRequestMessage(new HttpMethod("PATCH"), url) { Content = requestBody }; response = await client.SendAsync(request); break; case HttpMethodVerb.Put: - response = await client.PutAsync(_url, requestBody); + response = await client.PutAsync(url, requestBody); break; case HttpMethodVerb.Delete: - response = await client.DeleteAsync(_url); + response = await client.DeleteAsync(url); break; default: throw new ArgumentOutOfRangeException(); diff --git a/Xcepto.SSR/XceptoSsrAdapter.cs b/Xcepto.SSR/XceptoSsrAdapter.cs index 1875a34..cb8c10c 100644 --- a/Xcepto.SSR/XceptoSsrAdapter.cs +++ b/Xcepto.SSR/XceptoSsrAdapter.cs @@ -20,7 +20,7 @@ internal XceptoSsrAdapter(HttpClient httpClient, Uri? baseUrl) _client = httpClient; } - private SsrStateBuilderIdentity Inject(SsrStateBuilderIdentity builderIdentity, PathString pathString, HttpMethodVerb httpMethodVerb) + private SsrStateBuilderIdentity Inject(SsrStateBuilderIdentity builderIdentity, Func pathString, HttpMethodVerb httpMethodVerb) { if (_baseUrl is not null) builderIdentity.WithCustomBaseUrl(_baseUrl); @@ -32,12 +32,32 @@ private SsrStateBuilderIdentity Inject(SsrStateBuilderIdentity builderIdentity, return builderIdentity; } + public SsrStateBuilderIdentity Request(PathString pathString, HttpMethodVerb verb) + { + return Inject(new SsrStateBuilderIdentity(Builder), () => pathString, verb); + } + + public SsrStateBuilderIdentity Request(Func pathString, HttpMethodVerb verb) + { + return Inject(new SsrStateBuilderIdentity(Builder), pathString, verb); + } + public SsrStateBuilderIdentity Get(PathString pathString) { - return Inject(new SsrStateBuilderIdentity(Builder), pathString, HttpMethodVerb.Get); + return Inject(new SsrStateBuilderIdentity(Builder), () => pathString, HttpMethodVerb.Get); } public SsrStateBuilderIdentity Post(PathString pathString) + { + return Inject(new SsrStateBuilderIdentity(Builder), () => pathString, HttpMethodVerb.Post); + } + + public SsrStateBuilderIdentity Get(Func pathString) + { + return Inject(new SsrStateBuilderIdentity(Builder), pathString, HttpMethodVerb.Get); + } + + public SsrStateBuilderIdentity Post(Func pathString) { return Inject(new SsrStateBuilderIdentity(Builder), pathString, HttpMethodVerb.Post); }