From 8e42529cc29477790106e3a5fa3639dfcf9b3c15 Mon Sep 17 00:00:00 2001 From: themassiveone Date: Sat, 14 Feb 2026 09:08:36 +0100 Subject: [PATCH 1/6] feat: deferred pathString definitions to support promised values --- .../Builders/HttpStateBuilder.T.cs | 29 +++++++++---- .../PromisedPathTests.cs | 42 +++++++++++++++++++ Samples/RestAuth/Samples.Rest.API/Program.cs | 5 +++ .../Samples.Rest.API/Requests/APathRequest.cs | 3 ++ .../Samples.Rest.API/Requests/BPathRequest.cs | 3 ++ .../Responses/APathResponse.cs | 3 ++ Xcepto.Rest/Internals/XceptoRestState.cs | 21 +++++----- Xcepto.Rest/XceptoRestAdapter.cs | 27 +++++++++++- Xcepto.SSR/Internals/XceptoSsrState.cs | 19 +++++---- 9 files changed, 125 insertions(+), 27 deletions(-) create mode 100644 Samples/RestAuth/Samples.Rest.API.Tests/PromisedPathTests.cs create mode 100644 Samples/RestAuth/Samples.Rest.API/Requests/APathRequest.cs create mode 100644 Samples/RestAuth/Samples.Rest.API/Requests/BPathRequest.cs create mode 100644 Samples/RestAuth/Samples.Rest.API/Responses/APathResponse.cs diff --git a/Internal/Xcepto.Internal.Http/Builders/HttpStateBuilder.T.cs b/Internal/Xcepto.Internal.Http/Builders/HttpStateBuilder.T.cs index 34f9721..e7b19e5 100644 --- a/Internal/Xcepto.Internal.Http/Builders/HttpStateBuilder.T.cs +++ b/Internal/Xcepto.Internal.Http/Builders/HttpStateBuilder.T.cs @@ -18,9 +18,9 @@ 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 Func PathString = () => "/"; protected readonly List> QueryArgs = new(); protected readonly List ResponseAssertions = new(); @@ -47,15 +47,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), out var uri)) + throw new ArgumentException("Url creation failed"); + return uri; + }; } } @@ -72,6 +75,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; @@ -91,7 +100,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/PromisedPathTests.cs b/Samples/RestAuth/Samples.Rest.API.Tests/PromisedPathTests.cs new file mode 100644 index 0000000..c1d1cd0 --- /dev/null +++ b/Samples/RestAuth/Samples.Rest.API.Tests/PromisedPathTests.cs @@ -0,0 +1,42 @@ +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; + +namespace Samples.Rest.API.Tests; + +[TestFixture] +public class PromisedPathTests: BaseTest +{ + [Test] + public async Task PromisedPathFromResponse_Works() + { + TimeoutConfig timeoutConfig = new TimeoutConfig( + TimeSpan.FromSeconds(60), + TimeSpan.FromSeconds(10) + ); + var scenario = new MockedTokenScenario(CreateToken().hashed); + 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/APath") + .WithResponseType() + .AssertSuccess() + .PromiseResponse(); + + rest.Post(() => promiseResponse.Resolve().Path) + .WithRequestBody(() => new BPathRequest()) + .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..f268698 100644 --- a/Samples/RestAuth/Samples.Rest.API/Program.cs +++ b/Samples/RestAuth/Samples.Rest.API/Program.cs @@ -44,6 +44,11 @@ return Results.Json(new AuthenticatedTestResponse()); }); + +app.MapGet("/api/APath", () => Results.Json(new APathResponse("/api/BPath/validate"))); + +app.MapPost("/api/BPath/validate", (BRequest _) => Results.StatusCode(200)); + 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/BPathRequest.cs b/Samples/RestAuth/Samples.Rest.API/Requests/BPathRequest.cs new file mode 100644 index 0000000..0b5675f --- /dev/null +++ b/Samples/RestAuth/Samples.Rest.API/Requests/BPathRequest.cs @@ -0,0 +1,3 @@ +namespace Samples.Rest.API.Requests; + +public record BPathRequest(); \ No newline at end of file diff --git a/Samples/RestAuth/Samples.Rest.API/Responses/APathResponse.cs b/Samples/RestAuth/Samples.Rest.API/Responses/APathResponse.cs new file mode 100644 index 0000000..cea028d --- /dev/null +++ b/Samples/RestAuth/Samples.Rest.API/Responses/APathResponse.cs @@ -0,0 +1,3 @@ +namespace Samples.Rest.API.Responses; + +public record APathResponse(string Path); \ No newline at end of file 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(); From 87ddeac99da28c9a65d3c27024c18b343d08b9e6 Mon Sep 17 00:00:00 2001 From: themassiveone Date: Sat, 14 Feb 2026 10:49:27 +0100 Subject: [PATCH 2/6] test: dynamic paths --- .../Samples.Rest.API.Tests/BaseTest.cs | 2 +- .../PromisedPathTests.cs | 103 ++++++++++++++++-- Samples/RestAuth/Samples.Rest.API/Program.cs | 15 ++- .../Samples.Rest.API/Requests/BPathRequest.cs | 3 - .../Requests/DynamicPathRequest.cs | 3 + .../Responses/APathResponse.cs | 3 - .../Responses/PathResponse.cs | 3 + 7 files changed, 112 insertions(+), 20 deletions(-) delete mode 100644 Samples/RestAuth/Samples.Rest.API/Requests/BPathRequest.cs create mode 100644 Samples/RestAuth/Samples.Rest.API/Requests/DynamicPathRequest.cs delete mode 100644 Samples/RestAuth/Samples.Rest.API/Responses/APathResponse.cs create mode 100644 Samples/RestAuth/Samples.Rest.API/Responses/PathResponse.cs 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 index c1d1cd0..c4bcfc2 100644 --- a/Samples/RestAuth/Samples.Rest.API.Tests/PromisedPathTests.cs +++ b/Samples/RestAuth/Samples.Rest.API.Tests/PromisedPathTests.cs @@ -8,34 +8,115 @@ 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_Works() + public async Task PromisedPathFromResponse_Post_Works() { - TimeoutConfig timeoutConfig = new TimeoutConfig( - TimeSpan.FromSeconds(60), - TimeSpan.FromSeconds(10) - ); - var scenario = new MockedTokenScenario(CreateToken().hashed); - await XceptoTest.Given(scenario, timeoutConfig, builder => + await XceptoTest.Given(_scenario, _timeoutConfig, builder => { var rest = builder.RestAdapterBuilder() - .WithBaseUrl(new Uri($"http://localhost:{scenario.ApiPort}")) + .WithBaseUrl(new Uri($"http://localhost:{_scenario.ApiPort}")) .WithSerializer(new NewtonsoftSerializer()) .Build(); - var promiseResponse = rest.Get("/api/APath") - .WithResponseType() + var promiseResponse = rest.Get("/api/PostPath") + .WithResponseType() .AssertSuccess() .PromiseResponse(); rest.Post(() => promiseResponse.Resolve().Path) - .WithRequestBody(() => new BPathRequest()) + .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(); }); } diff --git a/Samples/RestAuth/Samples.Rest.API/Program.cs b/Samples/RestAuth/Samples.Rest.API/Program.cs index f268698..3a5cf05 100644 --- a/Samples/RestAuth/Samples.Rest.API/Program.cs +++ b/Samples/RestAuth/Samples.Rest.API/Program.cs @@ -44,10 +44,21 @@ 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/APath", () => Results.Json(new APathResponse("/api/BPath/validate"))); +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)); -app.MapPost("/api/BPath/validate", (BRequest _) => Results.StatusCode(200)); static byte[] UrlDecode(string s) { diff --git a/Samples/RestAuth/Samples.Rest.API/Requests/BPathRequest.cs b/Samples/RestAuth/Samples.Rest.API/Requests/BPathRequest.cs deleted file mode 100644 index 0b5675f..0000000 --- a/Samples/RestAuth/Samples.Rest.API/Requests/BPathRequest.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Samples.Rest.API.Requests; - -public record BPathRequest(); \ 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/APathResponse.cs b/Samples/RestAuth/Samples.Rest.API/Responses/APathResponse.cs deleted file mode 100644 index cea028d..0000000 --- a/Samples/RestAuth/Samples.Rest.API/Responses/APathResponse.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Samples.Rest.API.Responses; - -public record APathResponse(string 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 From c830a53ffac9cd922ab570536d8f94a34c0760d2 Mon Sep 17 00:00:00 2001 From: themassiveone Date: Sat, 14 Feb 2026 11:04:51 +0100 Subject: [PATCH 3/6] test: static paths --- .../Samples.Rest.API.Tests/StaticPathTests.cs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 Samples/RestAuth/Samples.Rest.API.Tests/StaticPathTests.cs 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 From 368a1753ca31971954ee5e1df24f86e89cd094a0 Mon Sep 17 00:00:00 2001 From: themassiveone Date: Sat, 14 Feb 2026 12:18:26 +0100 Subject: [PATCH 4/6] test: ssr static & dynamic paths --- .../Scenarios/SSRGuiScenario.cs | 1 - .../Xcepto/PromisedPathTests.cs | 126 ++++++++++++++++++ .../Xcepto/StaticPathTests.cs | 106 +++++++++++++++ .../PromiseValidationController.cs | 2 +- .../Controllers/PromisedPathController.cs | 67 ++++++++++ .../ViewModels/PromisedPathViewModel.cs | 3 + .../Views/PromisedPath/Response.cshtml | 3 + Xcepto.SSR/XceptoSsrAdapter.cs | 24 +++- 8 files changed, 328 insertions(+), 4 deletions(-) create mode 100644 Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/PromisedPathTests.cs create mode 100644 Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/StaticPathTests.cs create mode 100644 Samples/SSR/Samples.SSR.GUI/Controllers/PromisedPathController.cs create mode 100644 Samples/SSR/Samples.SSR.GUI/ViewModels/PromisedPathViewModel.cs create mode 100644 Samples/SSR/Samples.SSR.GUI/Views/PromisedPath/Response.cshtml 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/StaticPathTests.cs b/Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/StaticPathTests.cs new file mode 100644 index 0000000..89798ef --- /dev/null +++ b/Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/StaticPathTests.cs @@ -0,0 +1,106 @@ +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 StaticPathTests +{ + 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(); + + 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/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/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); } From e406aec77f23e6ff30ca103b7651f42a06f77b7d Mon Sep 17 00:00:00 2001 From: themassiveone Date: Sat, 14 Feb 2026 12:29:54 +0100 Subject: [PATCH 5/6] test: static paths without promised paths --- .../Xcepto/StaticPathTests.cs | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/StaticPathTests.cs b/Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/StaticPathTests.cs index 89798ef..027f91c 100644 --- a/Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/StaticPathTests.cs +++ b/Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/StaticPathTests.cs @@ -1,16 +1,8 @@ -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 System.Text.RegularExpressions; 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; @@ -43,7 +35,7 @@ await XceptoTest.Given(_scenario, _timeoutConfig, builder => .WithBaseUrl(new Uri($"http://localhost:{_scenario.GuiPort}")) .Build(); - ssr.Get(() => "/path/validate/get") + ssr.Get("/path/validate/get") .AssertSuccess(); }); } @@ -57,7 +49,7 @@ await XceptoTest.Given(_scenario, _timeoutConfig, builder => .WithBaseUrl(new Uri($"http://localhost:{_scenario.GuiPort}")) .Build(); - ssr.Post(() => "/path/validate/post") + ssr.Post("/path/validate/post") .AssertSuccess(); }); } @@ -71,7 +63,7 @@ await XceptoTest.Given(_scenario, _timeoutConfig, builder => .WithBaseUrl(new Uri($"http://localhost:{_scenario.GuiPort}")) .Build(); - ssr.Request(() => "/path/validate/patch", HttpMethodVerb.Patch) + ssr.Request("/path/validate/patch", HttpMethodVerb.Patch) .AssertSuccess(); }); } @@ -85,7 +77,7 @@ await XceptoTest.Given(_scenario, _timeoutConfig, builder => .WithBaseUrl(new Uri($"http://localhost:{_scenario.GuiPort}")) .Build(); - ssr.Request(() => "/path/validate/put", HttpMethodVerb.Put) + ssr.Request("/path/validate/put", HttpMethodVerb.Put) .AssertSuccess(); }); } @@ -99,7 +91,7 @@ await XceptoTest.Given(_scenario, _timeoutConfig, builder => .WithBaseUrl(new Uri($"http://localhost:{_scenario.GuiPort}")) .Build(); - ssr.Request(() => "/path/validate/delete", HttpMethodVerb.Delete) + ssr.Request("/path/validate/delete", HttpMethodVerb.Delete) .AssertSuccess(); }); } From 1732adb70544498b9e078885e6f7f6eb5b616ecd Mon Sep 17 00:00:00 2001 From: themassiveone Date: Sat, 14 Feb 2026 12:51:12 +0100 Subject: [PATCH 6/6] feat: dynamic query args --- .../Builders/HttpStateBuilder.T.cs | 13 +++- .../Xcepto/QueryArgTests.cs | 69 +++++++++++++++++++ .../Xcepto/StaticPathTests.cs | 10 --- .../Controllers/QueryController.cs | 17 +++++ .../Samples.SSR.GUI/Views/Query/Page1.cshtml | 2 + .../Samples.SSR.GUI/Views/Query/Page2.cshtml | 2 + Xcepto.Rest/Builders/RestStateBuilder.T.cs | 2 +- 7 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/QueryArgTests.cs create mode 100644 Samples/SSR/Samples.SSR.GUI/Controllers/QueryController.cs create mode 100644 Samples/SSR/Samples.SSR.GUI/Views/Query/Page1.cshtml create mode 100644 Samples/SSR/Samples.SSR.GUI/Views/Query/Page2.cshtml diff --git a/Internal/Xcepto.Internal.Http/Builders/HttpStateBuilder.T.cs b/Internal/Xcepto.Internal.Http/Builders/HttpStateBuilder.T.cs index e7b19e5..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; @@ -21,7 +22,7 @@ public abstract class HttpStateBuilderIdentity : AbstractStateBuilderI protected Func BaseUrl = () => new("http://localhost:8080"); protected HttpMethodVerb MethodVerb = HttpMethodVerb.Get; protected Func PathString = () => "/"; - protected readonly List> QueryArgs = new(); + protected readonly List>> QueryArgs = new(); protected readonly List ResponseAssertions = new(); protected HttpStateBuilderIdentity(IStateMachineBuilder stateMachineBuilder, IStateBuilderIdentity stateBuilderIdentity) : base(stateMachineBuilder, stateBuilderIdentity) { } @@ -55,7 +56,7 @@ protected Func Url throw new BuilderException("no Url defined"); return () => { - if (!Uri.TryCreate(BaseUrl(), PathString() + QueryString.Create(QueryArgs), out var uri)) + if (!Uri.TryCreate(BaseUrl(), PathString() + QueryString.Create(QueryArgs.Select(x=> x())), out var uri)) throw new ArgumentException("Url creation failed"); return uri; }; @@ -88,7 +89,13 @@ public TBuilder WithCustomBaseUrl(Func 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; } 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 index 027f91c..2c3cd31 100644 --- a/Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/StaticPathTests.cs +++ b/Samples/SSR/Samples.SSR.GUI.Tests/Xcepto/StaticPathTests.cs @@ -10,16 +10,6 @@ namespace Samples.SSR.GUI.Tests.Xcepto; [TestFixture] public class StaticPathTests { - 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), 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/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) {