From 59d099ec4bea6ab2c0aadf83ac2aeaaca843a42e Mon Sep 17 00:00:00 2001 From: macbook Date: Tue, 10 Sep 2024 21:10:18 +0900 Subject: [PATCH 1/3] [OpenAPI] Add endpoint for create new admin resource details #307 --- .../Endpoints/AdminEndpointUrls.cs | 8 + .../Endpoints/AdminResourceEndpoints.cs | 54 ++++++ src/AzureOpenAIProxy.ApiApp/Program.cs | 2 + .../AdminCreateResourcesOpenApiTests.cs | 168 ++++++++++++++++++ 4 files changed, 232 insertions(+) create mode 100644 src/AzureOpenAIProxy.ApiApp/Endpoints/AdminResourceEndpoints.cs create mode 100644 test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminCreateResourcesOpenApiTests.cs diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEndpointUrls.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEndpointUrls.cs index 0fdba526..1ad428ab 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEndpointUrls.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEndpointUrls.cs @@ -22,4 +22,12 @@ public static class AdminEndpointUrls /// - POST method for new event creation /// public const string AdminEvents = "/admin/events"; + + /// + /// Declares the admin resource details endpoint. + /// + /// + /// - POST method for new resource creation + /// + public const string AdminResources = "/admin/resources"; } \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminResourceEndpoints.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminResourceEndpoints.cs new file mode 100644 index 00000000..4dddf5fd --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminResourceEndpoints.cs @@ -0,0 +1,54 @@ +using AzureOpenAIProxy.ApiApp.Models; +using AzureOpenAIProxy.ApiApp.Services; + +using Microsoft.AspNetCore.Mvc; + +namespace AzureOpenAIProxy.ApiApp.Endpoints; + +/// +/// This represents the endpoint entity for resource details by admin +/// +public static class AdminResourceEndpoints +{ + /// + /// Adds the admin resource endpoint + /// + /// instance. + /// Returns instance. + public static RouteHandlerBuilder AddNewAdminResource(this WebApplication app) + { + var builder = app.MapPost(AdminEndpointUrls.AdminResources, async ( + [FromBody] AdminResourceDetails payload, + IAdminEventService service, + ILoggerFactory loggerFactory) => + { + var logger = loggerFactory.CreateLogger(nameof(AdminResourceEndpoints)); + logger.LogInformation("Received a new resource request"); + + if (payload is null) + { + logger.LogError("No payload found"); + + return Results.BadRequest("Payload is null"); + } + + return await Task.FromResult(Results.Ok()); + }) + .Accepts(contentType: "application/json") + .Produces(statusCode: StatusCodes.Status200OK, contentType: "application/json") + .Produces(statusCode: StatusCodes.Status400BadRequest) + .Produces(statusCode: StatusCodes.Status401Unauthorized) + .Produces(statusCode: StatusCodes.Status500InternalServerError, contentType: "text/plain") + .WithTags("admin") + .WithName("CreateAdminResource") + .WithOpenApi(operation => + { + operation.Summary = "Create admin resource"; + operation.Description = "Create admin resource"; + + return operation; + }); + + return builder; + } +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Program.cs b/src/AzureOpenAIProxy.ApiApp/Program.cs index 4c5a0e55..ef0a1b4f 100644 --- a/src/AzureOpenAIProxy.ApiApp/Program.cs +++ b/src/AzureOpenAIProxy.ApiApp/Program.cs @@ -58,4 +58,6 @@ app.AddGetAdminEvent(); app.AddUpdateAdminEvent(); +app.AddNewAdminResource(); + await app.RunAsync(); diff --git a/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminCreateResourcesOpenApiTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminCreateResourcesOpenApiTests.cs new file mode 100644 index 00000000..c1217313 --- /dev/null +++ b/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminCreateResourcesOpenApiTests.cs @@ -0,0 +1,168 @@ +using System.Text.Json; + +using AzureOpenAIProxy.AppHost.Tests.Fixtures; + +using FluentAssertions; + +namespace AzureOpenAIProxy.AppHost.Tests.ApiApp.Endpoints; + +public class AdminCreateResourcesOpenApiTests(AspireAppHostFixture host) : IClassFixture +{ + [Fact] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Path() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // 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/resources"); + 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"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // 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/resources") + .TryGetProperty("post", 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"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // 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/resources") + .GetProperty("post") + .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"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // 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/resources") + .GetProperty("post") + .TryGetProperty(attribute, out var property) ? property : default; + result.ValueKind.Should().Be(JsonValueKind.String); + } + + [Theory] + [InlineData("requestBody")] + [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"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // 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/resources") + .GetProperty("post") + .TryGetProperty(attribute, out var property) ? property : default; + result.ValueKind.Should().Be(JsonValueKind.Object); + } + + [Theory] + [InlineData("200")] + [InlineData("400")] + [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"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // 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/resources") + .GetProperty("post") + .GetProperty("responses") + .TryGetProperty(attribute, out var property) ? property : default; + result.ValueKind.Should().Be(JsonValueKind.Object); + } + + [Fact] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Schemas() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + + // Assert + var result = openapi!.RootElement.GetProperty("components") + .TryGetProperty("schemas", out var property) ? property : default; + result.ValueKind.Should().Be(JsonValueKind.Object); + } + + [Fact] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Model() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + + // Assert + var result = openapi!.RootElement.GetProperty("components") + .GetProperty("schemas") + .TryGetProperty("AdminResourceDetails", out var property) ? property : default; + result.ValueKind.Should().Be(JsonValueKind.Object); + } +} \ No newline at end of file From 5fd1c8ce5439985359fcd751fac48f0c95799a29 Mon Sep 17 00:00:00 2001 From: macbook Date: Sat, 14 Sep 2024 11:12:27 +0900 Subject: [PATCH 2/3] Added tests for AdminResourceDetails and updated admin tag description --- .../Filters/OpenApiTagFilter.cs | 2 +- .../AdminCreateResourcesOpenApiTests.cs | 139 +++++++++++++++++- 2 files changed, 133 insertions(+), 8 deletions(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Filters/OpenApiTagFilter.cs b/src/AzureOpenAIProxy.ApiApp/Filters/OpenApiTagFilter.cs index 9ed10fa3..be00f63d 100644 --- a/src/AzureOpenAIProxy.ApiApp/Filters/OpenApiTagFilter.cs +++ b/src/AzureOpenAIProxy.ApiApp/Filters/OpenApiTagFilter.cs @@ -16,7 +16,7 @@ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) [ 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" }, + new OpenApiTag { Name = "admin", Description = "Admin operations for managing events and resources" }, new OpenApiTag { Name = "events", Description = "User events" } ]; } diff --git a/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminCreateResourcesOpenApiTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminCreateResourcesOpenApiTests.cs index c1217313..ad77ca59 100644 --- a/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminCreateResourcesOpenApiTests.cs +++ b/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminCreateResourcesOpenApiTests.cs @@ -4,6 +4,8 @@ using FluentAssertions; +using IdentityModel.Client; + namespace AzureOpenAIProxy.AppHost.Tests.ApiApp.Endpoints; public class AdminCreateResourcesOpenApiTests(AspireAppHostFixture host) : IClassFixture @@ -39,7 +41,7 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Ver // Assert var result = openapi!.RootElement.GetProperty("paths") .GetProperty("/admin/resources") - .TryGetProperty("post", out var property) ? property : default; + .GetProperty("post"); result.ValueKind.Should().Be(JsonValueKind.Object); } @@ -59,7 +61,7 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Tag var result = openapi!.RootElement.GetProperty("paths") .GetProperty("/admin/resources") .GetProperty("post") - .TryGetProperty("tags", out var property) ? property : default; + .GetProperty("tags"); result.ValueKind.Should().Be(JsonValueKind.Array); result.EnumerateArray().Select(p => p.GetString()).Should().Contain(tag); } @@ -82,7 +84,7 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Val var result = openapi!.RootElement.GetProperty("paths") .GetProperty("/admin/resources") .GetProperty("post") - .TryGetProperty(attribute, out var property) ? property : default; + .GetProperty(attribute); result.ValueKind.Should().Be(JsonValueKind.String); } @@ -103,7 +105,7 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Obj var result = openapi!.RootElement.GetProperty("paths") .GetProperty("/admin/resources") .GetProperty("post") - .TryGetProperty(attribute, out var property) ? property : default; + .GetProperty(attribute); result.ValueKind.Should().Be(JsonValueKind.Object); } @@ -127,7 +129,7 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Res .GetProperty("/admin/resources") .GetProperty("post") .GetProperty("responses") - .TryGetProperty(attribute, out var property) ? property : default; + .GetProperty(attribute); result.ValueKind.Should().Be(JsonValueKind.Object); } @@ -144,7 +146,7 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Sch // Assert var result = openapi!.RootElement.GetProperty("components") - .TryGetProperty("schemas", out var property) ? property : default; + .GetProperty("schemas"); result.ValueKind.Should().Be(JsonValueKind.Object); } @@ -162,7 +164,130 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Mod // Assert var result = openapi!.RootElement.GetProperty("components") .GetProperty("schemas") - .TryGetProperty("AdminResourceDetails", out var property) ? property : default; + .GetProperty("AdminResourceDetails"); result.ValueKind.Should().Be(JsonValueKind.Object); } + + public static IEnumerable AttributeData => + [ + ["resourceId", true, "string"], + ["friendlyName", true, "string"], + ["deploymentName", true, "string"], + ["resourceType", true, "string"], + ["endpoint", true, "string"], + ["apiKey", true, "string"], + ["region", true, "string"], + ["isActive", true, "boolean"] + ]; + + [Theory] + [MemberData(nameof(AttributeData))] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Required(string attribute, bool isRequired, string type) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + + // Assert + var result = openapi!.RootElement.GetProperty("components") + .GetProperty("schemas") + .GetProperty("AdminResourceDetails") + .TryGetStringArray("required") + .ToList(); + result.Contains(attribute).Should().Be(isRequired); + } + + [Theory] + [MemberData(nameof(AttributeData))] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Property(string attribute, bool isRequired, string type) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + + // Assert + var result = openapi!.RootElement.GetProperty("components") + .GetProperty("schemas") + .GetProperty("AdminResourceDetails") + .GetProperty("properties") + .GetProperty(attribute); + result.ValueKind.Should().Be(JsonValueKind.Object); + } + + [Theory] + [MemberData(nameof(AttributeData))] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Type(string attribute, bool isRequired, string type) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + + // Assert + var result = openapi!.RootElement.GetProperty("components") + .GetProperty("schemas") + .GetProperty("AdminResourceDetails") + .GetProperty("properties") + .GetProperty(attribute); + + if (!result.TryGetProperty("type", out var typeProperty)) + { + var refPath = result.TryGetString("$ref").TrimStart('#', '/').Split('/'); + var refSchema = openapi.RootElement; + + foreach (var part in refPath) + { + refSchema = refSchema.GetProperty(part); + } + + typeProperty = refSchema.GetProperty("type"); + } + + typeProperty.GetString().Should().Be(type); + } + + [Fact] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Validate_ResourceType_As_Enum() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + + // Assert + var result = openapi!.RootElement.GetProperty("components") + .GetProperty("schemas") + .GetProperty("AdminResourceDetails") + .GetProperty("properties") + .GetProperty("resourceType"); + + var refPath = result.TryGetString("$ref").TrimStart('#', '/').Split('/'); + var refSchema = openapi.RootElement; + + foreach (var part in refPath) + { + refSchema = refSchema.GetProperty(part); + } + + var enumValues = refSchema.GetProperty("enum") + .EnumerateArray() + .Select(p => p.GetString()) + .ToList(); + + enumValues.Should().BeEquivalentTo(["none", "chat", "image"]); + } } \ No newline at end of file From d95cc42e727e6205e88d905add6a95e4cf269456 Mon Sep 17 00:00:00 2001 From: macbook Date: Sat, 14 Sep 2024 16:38:46 +0900 Subject: [PATCH 3/3] Refactor AdminCreateResourcesOpenApiTests to use InlineData for test cases --- .../AdminCreateResourcesOpenApiTests.cs | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminCreateResourcesOpenApiTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminCreateResourcesOpenApiTests.cs index ad77ca59..d5054f51 100644 --- a/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminCreateResourcesOpenApiTests.cs +++ b/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminCreateResourcesOpenApiTests.cs @@ -168,21 +168,16 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Mod result.ValueKind.Should().Be(JsonValueKind.Object); } - public static IEnumerable AttributeData => - [ - ["resourceId", true, "string"], - ["friendlyName", true, "string"], - ["deploymentName", true, "string"], - ["resourceType", true, "string"], - ["endpoint", true, "string"], - ["apiKey", true, "string"], - ["region", true, "string"], - ["isActive", true, "boolean"] - ]; - [Theory] - [MemberData(nameof(AttributeData))] - public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Required(string attribute, bool isRequired, string type) + [InlineData("resourceId", true)] + [InlineData("friendlyName", true)] + [InlineData("deploymentName", true)] + [InlineData("resourceType", true)] + [InlineData("endpoint", true)] + [InlineData("apiKey", true)] + [InlineData("region", true)] + [InlineData("isActive", true)] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Required(string attribute, bool isRequired) { // Arrange using var httpClient = host.App!.CreateHttpClient("apiapp"); @@ -202,8 +197,15 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Req } [Theory] - [MemberData(nameof(AttributeData))] - public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Property(string attribute, bool isRequired, string type) + [InlineData("resourceId")] + [InlineData("friendlyName")] + [InlineData("deploymentName")] + [InlineData("resourceType")] + [InlineData("endpoint")] + [InlineData("apiKey")] + [InlineData("region")] + [InlineData("isActive")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Property(string attribute) { // Arrange using var httpClient = host.App!.CreateHttpClient("apiapp"); @@ -223,8 +225,15 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Pro } [Theory] - [MemberData(nameof(AttributeData))] - public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Type(string attribute, bool isRequired, string type) + [InlineData("resourceId", "string")] + [InlineData("friendlyName", "string")] + [InlineData("deploymentName", "string")] + [InlineData("resourceType", "string")] + [InlineData("endpoint", "string")] + [InlineData("apiKey", "string")] + [InlineData("region", "string")] + [InlineData("isActive", "boolean")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Type(string attribute, string type) { // Arrange using var httpClient = host.App!.CreateHttpClient("apiapp");