From bdbd20084eaf9c8bdd8c4563d791e61869c79ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=ED=83=9C=EC=98=81?= Date: Sat, 24 Aug 2024 16:20:28 +0900 Subject: [PATCH] [OpenAPI] Add endpoint for list event details #217 (#265) --- .../Endpoints/AdminEndpointUrls.cs | 25 ++-- .../Endpoints/AdminEventEndpoints.cs | 117 ++++++++++------ .../Filters/OpenApiTagFilter.cs | 43 +++--- src/AzureOpenAIProxy.ApiApp/Program.cs | 91 +++++++------ .../Endpoints/AdminGetEventsOpenApiTests.cs | 128 ++++++++++++++++++ 5 files changed, 287 insertions(+), 117 deletions(-) create mode 100644 test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminGetEventsOpenApiTests.cs diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEndpointUrls.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEndpointUrls.cs index 3fdb5d0c..82f89d60 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEndpointUrls.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEndpointUrls.cs @@ -1,9 +1,18 @@ -namespace AzureOpenAIProxy.ApiApp.Endpoints; - -public static class AdminEndpointUrls -{ - /// - /// Declares the admin event details endpoint. - /// - public const string AdminEventDetails = "/admin/events/{eventId}"; +namespace AzureOpenAIProxy.ApiApp.Endpoints; + +public static class AdminEndpointUrls +{ + /// + /// Declares the admin event details endpoint. + /// + public const string AdminEventDetails = "/admin/events/{eventId}"; + + /// + /// Declares the admin event list endpoint. + /// + /// + /// - Get method for listing all events + /// - Post method for new event creation + /// + public const string AdminEvents = "/admin/events"; } \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs index d64ac5f0..451f82ca 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs @@ -1,43 +1,74 @@ -using AzureOpenAIProxy.ApiApp.Models; - -using Microsoft.AspNetCore.Mvc; - -namespace AzureOpenAIProxy.ApiApp.Endpoints; - -/// -/// This represents the endpoint entity for get event details by admin -/// -public static class AdminEventEndpoints -{ - /// - /// Adds the get event details by admin endpoint - /// - /// instance. - /// Returns instance. - public static RouteHandlerBuilder AddAdminEvents(this WebApplication app) - { - // Todo: Issue #19 https://github.com/aliencube/azure-openai-sdk-proxy/issues/19 - // Need authorization by admin - var builder = app.MapGet(AdminEndpointUrls.AdminEventDetails, ( - [FromRoute] string eventId) => - { - // Todo: Issue #208 https://github.com/aliencube/azure-openai-sdk-proxy/issues/208 - return Results.Ok(); - // Todo: Issue #208 - }) - .Produces(statusCode: StatusCodes.Status200OK, contentType: "application/json") - .Produces(statusCode: StatusCodes.Status401Unauthorized) - .Produces(statusCode: StatusCodes.Status500InternalServerError, contentType: "text/plain") - .WithTags("admin") - .WithName("GetAdminEventDetails") - .WithOpenApi(operation => - { - operation.Summary = "Gets event details from the given event ID"; - operation.Description = "This endpoint gets the event details from the given event ID."; - - return operation; - }); - - return builder; - } -} +using AzureOpenAIProxy.ApiApp.Models; + +using Microsoft.AspNetCore.Mvc; + +namespace AzureOpenAIProxy.ApiApp.Endpoints; + +/// +/// This represents the endpoint entity for get event details by admin +/// +public static class AdminEventEndpoints +{ + /// + /// Adds the get event details by admin endpoint + /// + /// instance. + /// Returns instance. + public static RouteHandlerBuilder AddAdminEvents(this WebApplication app) + { + // Todo: Issue #19 https://github.com/aliencube/azure-openai-sdk-proxy/issues/19 + // Need authorization by admin + var builder = app.MapGet(AdminEndpointUrls.AdminEventDetails, ( + [FromRoute] string eventId) => + { + // Todo: Issue #208 https://github.com/aliencube/azure-openai-sdk-proxy/issues/208 + return Results.Ok(); + // Todo: Issue #208 + }) + .Produces(statusCode: StatusCodes.Status200OK, contentType: "application/json") + .Produces(statusCode: StatusCodes.Status401Unauthorized) + .Produces(statusCode: StatusCodes.Status500InternalServerError, contentType: "text/plain") + .WithTags("admin") + .WithName("GetAdminEventDetails") + .WithOpenApi(operation => + { + operation.Summary = "Gets event details from the given event ID"; + operation.Description = "This endpoint gets the event details from the given event ID."; + + return operation; + }); + + return builder; + } + + /// + /// Adds the get event lists by admin endpoint + /// + /// instance. + /// Returns instance. + public static RouteHandlerBuilder AddAdminEventList(this WebApplication app) + { + // Todo: Issue #19 https://github.com/aliencube/azure-openai-sdk-proxy/issues/19 + // Need authorization by admin + var builder = app.MapGet(AdminEndpointUrls.AdminEvents, () => + { + // Todo: Issue #218 https://github.com/aliencube/azure-openai-sdk-proxy/issues/218 + return Results.Ok(); + // Todo: Issue #218 + }) + .Produces>(statusCode: StatusCodes.Status200OK, contentType: "application/json") + .Produces(statusCode: StatusCodes.Status401Unauthorized) + .Produces(statusCode: StatusCodes.Status500InternalServerError, contentType: "text/plain") + .WithTags("admin") + .WithName("GetAdminEvents") + .WithOpenApi(operation => + { + operation.Summary = "Gets all events"; + operation.Description = "This endpoint gets all events"; + + return operation; + }); + + return builder; + } +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Filters/OpenApiTagFilter.cs b/src/AzureOpenAIProxy.ApiApp/Filters/OpenApiTagFilter.cs index c1d6ec6a..196c87fb 100644 --- a/src/AzureOpenAIProxy.ApiApp/Filters/OpenApiTagFilter.cs +++ b/src/AzureOpenAIProxy.ApiApp/Filters/OpenApiTagFilter.cs @@ -1,21 +1,22 @@ -using Microsoft.OpenApi.Models; - -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace AzureOpenAIProxy.ApiApp.Filters; - -/// -/// This represents the document filter entity for global tags. -/// -public class OpenApiTagFilter : IDocumentFilter -{ - /// - public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) - { - swaggerDoc.Tags = - [ - new OpenApiTag { Name = "weather", Description = "Weather forecast operations" }, - new OpenApiTag { Name = "openai", Description = "Azure OpenAI operations" }, - ]; - } -} +using Microsoft.OpenApi.Models; + +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace AzureOpenAIProxy.ApiApp.Filters; + +/// +/// This represents the document filter entity for global tags. +/// +public class OpenApiTagFilter : IDocumentFilter +{ + /// + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + swaggerDoc.Tags = + [ + new OpenApiTag { Name = "weather", Description = "Weather forecast operations" }, + new OpenApiTag { Name = "openai", Description = "Azure OpenAI operations" }, + new OpenApiTag { Name = "admin", Description = "Admin for organizing events" } + ]; + } +} diff --git a/src/AzureOpenAIProxy.ApiApp/Program.cs b/src/AzureOpenAIProxy.ApiApp/Program.cs index 2331dc75..4335c684 100644 --- a/src/AzureOpenAIProxy.ApiApp/Program.cs +++ b/src/AzureOpenAIProxy.ApiApp/Program.cs @@ -1,45 +1,46 @@ -using AzureOpenAIProxy.ApiApp.Endpoints; -using AzureOpenAIProxy.ApiApp.Extensions; - -var builder = WebApplication.CreateBuilder(args); - -builder.AddServiceDefaults(); - -// Add KeyVault service -builder.Services.AddKeyVaultService(); - -// Add Azure OpenAI service. -builder.Services.AddOpenAIService(); - -// Add OpenAPI service -builder.Services.AddOpenApiService(); - -var app = builder.Build(); - -app.MapDefaultEndpoints(); - -// https://stackoverflow.com/questions/76962735/how-do-i-set-a-prefix-in-my-asp-net-core-7-web-api-for-all-endpoints -var basePath = "/api"; -app.UsePathBase(basePath); -app.UseRouting(); - -// Configure the HTTP request pipeline. -// Use Swagger UI -app.UseSwaggerUI(basePath); - -// Enable buffering -app.Use(async (context, next) => -{ - context.Request.EnableBuffering(); - await next.Invoke(); -}); - -app.UseHttpsRedirection(); - -app.AddWeatherForecast(); -app.AddChatCompletions(); - -// Admin Endpoints -app.AddAdminEvents(); - -await app.RunAsync(); +using AzureOpenAIProxy.ApiApp.Endpoints; +using AzureOpenAIProxy.ApiApp.Extensions; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +// Add KeyVault service +builder.Services.AddKeyVaultService(); + +// Add Azure OpenAI service. +builder.Services.AddOpenAIService(); + +// Add OpenAPI service +builder.Services.AddOpenApiService(); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +// https://stackoverflow.com/questions/76962735/how-do-i-set-a-prefix-in-my-asp-net-core-7-web-api-for-all-endpoints +var basePath = "/api"; +app.UsePathBase(basePath); +app.UseRouting(); + +// Configure the HTTP request pipeline. +// Use Swagger UI +app.UseSwaggerUI(basePath); + +// Enable buffering +app.Use(async (context, next) => +{ + context.Request.EnableBuffering(); + await next.Invoke(); +}); + +app.UseHttpsRedirection(); + +app.AddWeatherForecast(); +app.AddChatCompletions(); + +// Admin Endpoints +app.AddAdminEvents(); +app.AddAdminEventList(); + +await app.RunAsync(); diff --git a/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminGetEventsOpenApiTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminGetEventsOpenApiTests.cs new file mode 100644 index 00000000..7e9d0e72 --- /dev/null +++ b/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminGetEventsOpenApiTests.cs @@ -0,0 +1,128 @@ +using System.Text.Json; + +using AzureOpenAIProxy.AppHost.Tests.Fixtures; + +using FluentAssertions; + +using IdentityModel.Client; + +namespace AzureOpenAIProxy.AppHost.Tests.ApiApp.Endpoints; + +public class AdminGetEventsOpenApiTests(AspireAppHostFixture host) : IClassFixture +{ + // TODO: [tae0y] 테스트코드 작성하기 + [Fact] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Path() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + + // Assert + var result = openapi!.RootElement.GetProperty("paths") + .TryGetProperty("/admin/events", out var property) ? property : default; + result.ValueKind.Should().Be(JsonValueKind.Object); + } + + [Fact] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Verb() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + + // Assert + var result = openapi!.RootElement.GetProperty("paths") + .GetProperty("/admin/events") + .TryGetProperty("get", out var property) ? property : default; + result.ValueKind.Should().Be(JsonValueKind.Object); + } + + [Theory] + [InlineData("admin")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Tags(string tag) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + + // Assert + var result = openapi!.RootElement.GetProperty("paths") + .GetProperty("/admin/events") + .GetProperty("get") + .TryGetProperty("tags", out var property) ? property : default; + result.ValueKind.Should().Be(JsonValueKind.Array); + result.EnumerateArray().Select(p => p.GetString()).Should().Contain(tag); + } + + [Theory] + [InlineData("summary")] + [InlineData("description")] + [InlineData("operationId")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Value(string attribute) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + + // Assert + var result = openapi!.RootElement.GetProperty("paths") + .GetProperty("/admin/events") + .GetProperty("get") + .TryGetProperty(attribute, out var property) ? property : default; + result.ValueKind.Should().Be(JsonValueKind.String); + } + + [Theory] + [InlineData("responses")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Object(string attribute) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + + // Assert + var result = openapi!.RootElement.GetProperty("paths") + .GetProperty("/admin/events") + .GetProperty("get") + .TryGetProperty(attribute, out var property) ? property : default; + result.ValueKind.Should().Be(JsonValueKind.Object); + } + + [Theory] + [InlineData("200")] + [InlineData("401")] + [InlineData("500")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Response(string attribute) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + + // Assert + var result = openapi!.RootElement.GetProperty("paths") + .GetProperty("/admin/events") + .GetProperty("get") + .GetProperty("responses") + .TryGetProperty(attribute, out var property) ? property : default; + result.ValueKind.Should().Be(JsonValueKind.Object); + } +} \ No newline at end of file