diff --git a/Modules/ApiBrowsing/ApiBrowser.cs b/Modules/ApiBrowsing/ApiBrowser.cs index ddbd288c..caf4df21 100644 --- a/Modules/ApiBrowsing/ApiBrowser.cs +++ b/Modules/ApiBrowsing/ApiBrowser.cs @@ -8,27 +8,32 @@ public static class ApiBrowser #region Factories - public static BrowserHandlerBuilder SwaggerUI() => new("Swagger"); + public static BrowserHandlerBuilder SwaggerUI() => new("Swagger", "Swagger UI"); - public static BrowserHandlerBuilder Redoc() => new("Redoc"); + public static BrowserHandlerBuilder Redoc() => new("Redoc", "Redoc"); #endregion #region Layout extensions - public static LayoutBuilder AddSwaggerUI(this LayoutBuilder layout, string segment = "swagger", string? url = null) - => AddBrowser(layout, SwaggerUI(), segment, url); + public static LayoutBuilder AddSwaggerUI(this LayoutBuilder layout, string segment = "swagger", string? url = null, string? title = null) + => AddBrowser(layout, SwaggerUI(), segment, url, title); - public static LayoutBuilder AddRedoc(this LayoutBuilder layout, string segment = "redoc", string? url = null) - => AddBrowser(layout, Redoc(), segment, url); + public static LayoutBuilder AddRedoc(this LayoutBuilder layout, string segment = "redoc", string? url = null, string? title = null) + => AddBrowser(layout, Redoc(), segment, url, title); - private static LayoutBuilder AddBrowser(this LayoutBuilder layout, BrowserHandlerBuilder builder, string segment, string? url) + private static LayoutBuilder AddBrowser(this LayoutBuilder layout, BrowserHandlerBuilder builder, string segment, string? url, string? title) { if (url != null) { builder.Url(url); } + if (title != null) + { + builder.Title(title); + } + return layout.Add(segment, builder); } diff --git a/Modules/ApiBrowsing/Common/BrowserHandler.cs b/Modules/ApiBrowsing/Common/BrowserHandler.cs index d82357bd..ec3a37d6 100644 --- a/Modules/ApiBrowsing/Common/BrowserHandler.cs +++ b/Modules/ApiBrowsing/Common/BrowserHandler.cs @@ -17,20 +17,20 @@ public sealed class BrowserHandler: IHandler public TemplateRenderer Template { get; } - public string Url { get; } + public BrowserMetaData MetaData { get; } #endregion #region Initialization - public BrowserHandler(string resourceRoot, string? url) + public BrowserHandler(string resourceRoot, BrowserMetaData metaData) { StaticResources = Resources.From(ResourceTree.FromAssembly($"{resourceRoot}.Static")) .Build(); Template = Renderer.From(Resource.FromAssembly($"{resourceRoot}.Index.html").Build()); - Url = url ?? "../openapi.json"; + MetaData = metaData; } #endregion @@ -50,7 +50,8 @@ public BrowserHandler(string resourceRoot, string? url) { var config = new Dictionary { - ["url"] = Url + ["title"] = MetaData.Title, + ["url"] = (MetaData.Url ?? "../openapi.json") }; var content = await Template.RenderAsync(config); diff --git a/Modules/ApiBrowsing/Common/BrowserHandlerBuilder.cs b/Modules/ApiBrowsing/Common/BrowserHandlerBuilder.cs index f71aec88..92f584d2 100644 --- a/Modules/ApiBrowsing/Common/BrowserHandlerBuilder.cs +++ b/Modules/ApiBrowsing/Common/BrowserHandlerBuilder.cs @@ -2,24 +2,37 @@ namespace GenHTTP.Modules.ApiBrowsing.Common; -public class BrowserHandlerBuilder(string resourceRoot) : IHandlerBuilder +public class BrowserHandlerBuilder(string resourceRoot, string title) : IHandlerBuilder { private readonly List _Concerns = []; private string? _Url; + private string _Title = title; + public BrowserHandlerBuilder Url(string url) { _Url = url; return this; } + public BrowserHandlerBuilder Title(string title) + { + _Title = title; + return this; + } + public BrowserHandlerBuilder Add(IConcernBuilder concern) { _Concerns.Add(concern); return this; } - public IHandler Build() => Concerns.Chain(_Concerns, new BrowserHandler(resourceRoot, _Url)); + public IHandler Build() + { + var meta = new BrowserMetaData(_Url, _Title); + + return Concerns.Chain(_Concerns, new BrowserHandler(resourceRoot, meta)); + } } diff --git a/Modules/ApiBrowsing/Common/BrowserMetaData.cs b/Modules/ApiBrowsing/Common/BrowserMetaData.cs new file mode 100644 index 00000000..c527451f --- /dev/null +++ b/Modules/ApiBrowsing/Common/BrowserMetaData.cs @@ -0,0 +1,3 @@ +namespace GenHTTP.Modules.ApiBrowsing.Common; + +public record BrowserMetaData(string? Url, string Title); diff --git a/Modules/ApiBrowsing/Redoc/Index.html b/Modules/ApiBrowsing/Redoc/Index.html index d17d10a4..ad80df19 100644 --- a/Modules/ApiBrowsing/Redoc/Index.html +++ b/Modules/ApiBrowsing/Redoc/Index.html @@ -1,7 +1,7 @@  - Redoc + {title} diff --git a/Modules/ApiBrowsing/Swagger/Index.html b/Modules/ApiBrowsing/Swagger/Index.html index 00b77259..02a2363f 100644 --- a/Modules/ApiBrowsing/Swagger/Index.html +++ b/Modules/ApiBrowsing/Swagger/Index.html @@ -3,8 +3,7 @@ - - SwaggerUI + {title} diff --git a/Playground/Program.cs b/Playground/Program.cs index ca6a44f2..a3046b66 100644 --- a/Playground/Program.cs +++ b/Playground/Program.cs @@ -1,21 +1,8 @@ using GenHTTP.Engine.Kestrel; -using GenHTTP.Modules.Functional; using GenHTTP.Modules.IO; -using GenHTTP.Modules.OpenApi; using GenHTTP.Modules.Practices; -using GenHTTP.Modules.ApiBrowsing; -using GenHTTP.Modules.Layouting; -var api = Inline.Create() - .Get(() => 42); - -var app = Layout.Create() - .Add(api) - .AddOpenApi() - .AddSwaggerUI() - .AddRedoc(); - -// var app = Content.From(Resource.FromString("Hello World")); +var app = Content.From(Resource.FromString("Hello World")); await Host.Create() .Handler(app) diff --git a/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj b/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj index c74df7d8..2c11cea2 100644 --- a/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj +++ b/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj @@ -66,11 +66,11 @@ - - - + + + diff --git a/Testing/Acceptance/Modules/ApiBrowsing/ApplicationTests.cs b/Testing/Acceptance/Modules/ApiBrowsing/ApplicationTests.cs new file mode 100644 index 00000000..344abbf6 --- /dev/null +++ b/Testing/Acceptance/Modules/ApiBrowsing/ApplicationTests.cs @@ -0,0 +1,50 @@ +using System.Net; +using GenHTTP.Modules.ApiBrowsing; +using GenHTTP.Modules.Functional; +using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.OpenApi; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.ApiBrowsing; + +[TestClass] +public class ApplicationTests +{ + + [TestMethod] + [MultiEngineTest] + public async Task TestSwagger(TestEngine engine) + { + var app = Layout.Create() + .Add(Inline.Create().Get(() => 42)) + .AddOpenApi() + .AddSwaggerUI(); + + await using var host = await TestHost.RunAsync(app, engine: engine); + + using var response = await host.GetResponseAsync("/swagger/"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + + AssertX.Contains("Swagger", await response.GetContentAsync()); + } + + [TestMethod] + [MultiEngineTest] + public async Task TestRedoc(TestEngine engine) + { + var app = Layout.Create() + .Add(Inline.Create().Get(() => 42)) + .AddOpenApi() + .AddRedoc(); + + await using var host = await TestHost.RunAsync(app, engine: engine); + + using var response = await host.GetResponseAsync("/redoc/"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + + AssertX.Contains("Redoc", await response.GetContentAsync()); + } + +} diff --git a/Testing/Acceptance/Modules/ApiBrowsing/HandlerTests.cs b/Testing/Acceptance/Modules/ApiBrowsing/HandlerTests.cs new file mode 100644 index 00000000..da30a2f9 --- /dev/null +++ b/Testing/Acceptance/Modules/ApiBrowsing/HandlerTests.cs @@ -0,0 +1,81 @@ +using System.Net; +using GenHTTP.Modules.ApiBrowsing; +using GenHTTP.Modules.Functional; +using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Layouting.Provider; +using GenHTTP.Modules.OpenApi; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.ApiBrowsing; + +[TestClass] +public class HandlerTests +{ + + [TestMethod] + [MultiEngineTest] + public async Task TestGetOnly(TestEngine engine) + { + await using var host = await TestHost.RunAsync(GetApi().AddSwaggerUI(), engine: engine); + + var request = host.GetRequest("/swagger"); + + request.Method = HttpMethod.Post; + request.Content = new StringContent("Content"); + + using var response = await host.GetResponseAsync(request); + + await response.AssertStatusAsync(HttpStatusCode.MethodNotAllowed); + + Assert.AreEqual("GET", response.GetContentHeader("Allow")); + } + + [TestMethod] + [MultiEngineTest] + public async Task TestResourceAccess(TestEngine engine) + { + await using var host = await TestHost.RunAsync(GetApi().AddSwaggerUI(), engine: engine); + + using var response = await host.GetResponseAsync("/swagger/static/swagger-ui.css"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + } + + [TestMethod] + [MultiEngineTest] + public async Task TestNotFound(TestEngine engine) + { + await using var host = await TestHost.RunAsync(GetApi().AddSwaggerUI(), engine: engine); + + using var response = await host.GetResponseAsync("/swagger/notfound"); + + await response.AssertStatusAsync(HttpStatusCode.NotFound); + } + + [TestMethod] + [MultiEngineTest] + public async Task TestCustomMeta(TestEngine engine) + { + var api = GetApi().AddSwaggerUI("docs", "https://localhost:5001/swagger.json", "My API"); + + await using var host = await TestHost.RunAsync(api, engine: engine); + + using var response = await host.GetResponseAsync("/docs/"); + + await response.AssertStatusAsync(HttpStatusCode.OK); + + var content = await response.GetContentAsync(); + + AssertX.Contains("https://localhost:5001/swagger.json", content); + AssertX.Contains("My API", content); + } + + private static LayoutBuilder GetApi() + { + return Layout.Create() + .Add(Inline.Create().Get(() => 42)) + .AddOpenApi(); + } + +}