From d7aab48fcc3134941858d65da3f0f71651533e36 Mon Sep 17 00:00:00 2001 From: Lucy Oh <56earls@gmail.com> Date: Sat, 31 Aug 2024 16:34:12 +0900 Subject: [PATCH 01/13] [Playground] Component - Tab for system message and parameters (UI only) #225 (#244) --- .../Components/Pages/Home.razor | 2 +- .../Components/Pages/Playground.razor | 4 +- .../Components/UI/ConfigTabComponent.razor | 20 ++++++ .../Components/_Imports.razor | 1 + src/AzureOpenAIProxy.PlaygroundApp/Program.cs | 2 +- .../UI/ConfigTabComponentTest.cs | 71 +++++++++++++++++++ 6 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ConfigTabComponent.razor create mode 100644 test/AzureOpenAIProxy.PlaygroundApp.Tests/UI/ConfigTabComponentTest.cs diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Home.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Home.razor index d05c2a66..7c8ab364 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Home.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Home.razor @@ -7,4 +7,4 @@ Welcome to your new app. - \ No newline at end of file + diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Playground.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Playground.razor index 5e52f38a..08f49d57 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Playground.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Playground.razor @@ -2,4 +2,6 @@ Playground Page -

playground page!

\ No newline at end of file +

playground page!

+ + \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ConfigTabComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ConfigTabComponent.razor new file mode 100644 index 00000000..33d9589a --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ConfigTabComponent.razor @@ -0,0 +1,20 @@ + + + This is "System message" tab. + + + This is "Parameters" tab. + + + +

[TEST] Active tab changed to: @SelectedTab?.Id

+ +@code { + FluentTab? SelectedTab; + + private async Task ChangeTab(FluentTab tab) + { + SelectedTab = tab; + await Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/_Imports.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/_Imports.razor index e8aaa070..beab74d2 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/_Imports.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/_Imports.razor @@ -14,3 +14,4 @@ @using AzureOpenAIProxy.PlaygroundApp @using AzureOpenAIProxy.PlaygroundApp.Components +@using AzureOpenAIProxy.PlaygroundApp.Components.UI \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Program.cs b/src/AzureOpenAIProxy.PlaygroundApp/Program.cs index a61639f7..7db92b99 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Program.cs +++ b/src/AzureOpenAIProxy.PlaygroundApp/Program.cs @@ -36,4 +36,4 @@ app.MapRazorComponents() .AddInteractiveServerRenderMode(); -await app.RunAsync(); +await app.RunAsync(); \ No newline at end of file diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/UI/ConfigTabComponentTest.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/UI/ConfigTabComponentTest.cs new file mode 100644 index 00000000..fbb98d36 --- /dev/null +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/UI/ConfigTabComponentTest.cs @@ -0,0 +1,71 @@ +using Microsoft.Playwright; +using Microsoft.Playwright.NUnit; + +namespace AzureOpenAIProxy.PlaygroundApp.Tests.UI; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +[Property("Category", "Integration")] +public class ConfigTabComponentTest : PageTest +{ + public override BrowserNewContextOptions ContextOptions() => new() { IgnoreHTTPSErrors = true, }; + + [SetUp] + public async Task SetUp() + { + await Page.GotoAsync("https://localhost:5001/playground/"); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + } + + [Test] + public async Task Given_ConfigTab_When_Endpoint_Invoked_Then_ConfigTab_Should_Be_Displayed() + { + // Act + var configTab = Page.Locator("fluent-tabs#config-tab"); + + // Assert + await Expect(configTab).ToBeVisibleAsync(); + } + + [Test] + public async Task Given_ConfigTab_When_Endpoint_Invoked_Then_Id_Should_Be_System_Message_Tab() + { + // Act + var sysMsgPanel = Page.Locator("fluent-tab-panel#system-message-tab-panel"); + var parameterPanel = Page.Locator("fluent-tab-panel#parameters-tab-panel"); + + // Assert + await Expect(sysMsgPanel).ToBeVisibleAsync(); + await Expect(parameterPanel).ToBeHiddenAsync(); + } + + [Test] + [TestCase( + "fluent-tab#parameters-tab", + "fluent-tab-panel#parameters-tab-panel", + "fluent-tab-panel#system-message-tab-panel" + )] + [TestCase( + "fluent-tab#system-message-tab", + "fluent-tab-panel#system-message-tab-panel", + "fluent-tab-panel#parameters-tab-panel" + )] + public async Task Given_ConfigTab_When_Changed_Then_Tab_Should_Be_Updated( + string selectedTabSelector, + string selectedPanelSelector, + string hiddenPanelSelector + ) + { + // Arrange + var selectedTab = Page.Locator(selectedTabSelector); + var selectedPanel = Page.Locator(selectedPanelSelector); + var hiddenPanel = Page.Locator(hiddenPanelSelector); + + // Act + await selectedTab.ClickAsync(); + + // Assert + await Expect(selectedPanel).ToBeVisibleAsync(); + await Expect(hiddenPanel).ToBeHiddenAsync(); + } +} \ No newline at end of file From 947d240da4062cebf6ab08d8fad4ef63f333389c Mon Sep 17 00:00:00 2001 From: sikutisa <32262904+sikutisa@users.noreply.github.com> Date: Sat, 31 Aug 2024 17:32:43 +0900 Subject: [PATCH 02/13] [Backend API]Refactor OpenAPI doc version to appsettings.json (#274) --- .github/workflows/azure-dev-build-only.yml | 3 +- .github/workflows/azure-dev.yml | 3 +- .../Configurations/OpenApiSettings.cs | 17 +++ src/AzureOpenAIProxy.ApiApp/Constants.cs | 5 - .../ApplicationBuilderExtensions.cs | 4 +- .../Extensions/OpenApiSettingsExtensions.cs | 37 ++++++ .../Extensions/ServiceCollectionExtensions.cs | 10 +- .../OpenApiSettingsExtensionsTests.cs | 121 ++++++++++++++++++ 8 files changed, 186 insertions(+), 14 deletions(-) create mode 100644 src/AzureOpenAIProxy.ApiApp/Configurations/OpenApiSettings.cs create mode 100644 src/AzureOpenAIProxy.ApiApp/Extensions/OpenApiSettingsExtensions.cs create mode 100644 test/AzureOpenAIProxy.ApiApp.Tests/Extensions/OpenApiSettingsExtensionsTests.cs diff --git a/.github/workflows/azure-dev-build-only.yml b/.github/workflows/azure-dev-build-only.yml index c42de024..ad48cba8 100644 --- a/.github/workflows/azure-dev-build-only.yml +++ b/.github/workflows/azure-dev-build-only.yml @@ -83,8 +83,7 @@ jobs: - name: Create openapi.json shell: pwsh run: | - $fileContent = Get-Content './src/AzureOpenAIProxy.ApiApp/Constants.cs' - $API_VERSION = [regex]::Match($fileContent, 'public const string Version = "([^"]+)"').Groups[1].Value + $API_VERSION = $(Get-Content ./src/AzureOpenAIProxy.ApiApp/appsettings.json | ConvertFrom-Json).OpenApi.DocVersion Invoke-WebRequest -Uri "https://localhost:7001/swagger/$API_VERSION/swagger.json" -OutFile "openapi.json" diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 7500eda5..7f7b121b 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -97,8 +97,7 @@ jobs: - name: Create openapi.json shell: pwsh run: | - $fileContent = Get-Content './src/AzureOpenAIProxy.ApiApp/Constants.cs' - $API_VERSION = [regex]::Match($fileContent, 'public const string Version = "([^"]+)"').Groups[1].Value + $API_VERSION = $(Get-Content ./src/AzureOpenAIProxy.ApiApp/appsettings.json | ConvertFrom-Json).OpenApi.DocVersion Invoke-WebRequest -Uri "https://localhost:7001/swagger/$API_VERSION/swagger.json" -OutFile "openapi.json" diff --git a/src/AzureOpenAIProxy.ApiApp/Configurations/OpenApiSettings.cs b/src/AzureOpenAIProxy.ApiApp/Configurations/OpenApiSettings.cs new file mode 100644 index 00000000..36ae9ecd --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Configurations/OpenApiSettings.cs @@ -0,0 +1,17 @@ +namespace AzureOpenAIProxy.ApiApp.Configurations; + +/// +/// This represents the settings entity for Open API. +/// +public class OpenApiSettings +{ + /// + /// Gets the name of the configuration settings. + /// + public const string Name = "OpenApi"; + + /// + /// Gets or sets the Open API Doc version. + /// + public string? DocVersion { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Constants.cs b/src/AzureOpenAIProxy.ApiApp/Constants.cs index a133b166..ab6f5722 100644 --- a/src/AzureOpenAIProxy.ApiApp/Constants.cs +++ b/src/AzureOpenAIProxy.ApiApp/Constants.cs @@ -5,11 +5,6 @@ /// public static class Constants { - /// - /// Declares the current version of the API. - /// - public const string Version = "v1.0.0"; - /// /// Declares the title of the OpenAPI doc. /// diff --git a/src/AzureOpenAIProxy.ApiApp/Extensions/ApplicationBuilderExtensions.cs b/src/AzureOpenAIProxy.ApiApp/Extensions/ApplicationBuilderExtensions.cs index 390767c1..b24e4943 100644 --- a/src/AzureOpenAIProxy.ApiApp/Extensions/ApplicationBuilderExtensions.cs +++ b/src/AzureOpenAIProxy.ApiApp/Extensions/ApplicationBuilderExtensions.cs @@ -18,6 +18,8 @@ public static IApplicationBuilder UseSwaggerUI(this WebApplication app, string b return app; } + var settings = app.Services.GetOpenApiSettings(); + app.UseSwagger(options => { //options.RouteTemplate = $"swagger/{Constants.Version}/swagger.json"; @@ -32,7 +34,7 @@ public static IApplicationBuilder UseSwaggerUI(this WebApplication app, string b app.UseSwaggerUI(options => { - options.SwaggerEndpoint($"{Constants.Version}/swagger.json", Constants.Title); + options.SwaggerEndpoint($"{settings.DocVersion}/swagger.json", Constants.Title); }); return app; diff --git a/src/AzureOpenAIProxy.ApiApp/Extensions/OpenApiSettingsExtensions.cs b/src/AzureOpenAIProxy.ApiApp/Extensions/OpenApiSettingsExtensions.cs new file mode 100644 index 00000000..ef8a7877 --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Extensions/OpenApiSettingsExtensions.cs @@ -0,0 +1,37 @@ +using AzureOpenAIProxy.ApiApp.Configurations; + +namespace AzureOpenAIProxy.ApiApp.Extensions; + +/// +/// This represents the extension entity for the class. +/// +public static class OpenApiSettingsExtensions +{ + /// + /// Gets the OpenApi configuration settings by reading appsettings.json. + /// + /// instance. + /// Returns instance. + public static OpenApiSettings GetOpenApiSettings(this IServiceProvider serviceProvider) + { + var configuration = serviceProvider.GetService() + ?? throw new InvalidOperationException($"{nameof(IConfiguration)} service is not registered."); + + var settings = configuration.GetSection(OpenApiSettings.Name).Get() + ?? throw new InvalidOperationException($"{nameof(OpenApiSettings)} could not be retrieved from the configuration."); + + return settings; + } + + /// + /// Gets the OpenApi configuration settings by reading appsettings.json. + /// + /// instance. + /// Returns instance. + public static OpenApiSettings GetOpenApiSettings(this IServiceCollection services) + { + var serviceProvider = services.BuildServiceProvider(); + + return serviceProvider.GetOpenApiSettings(); + } +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Extensions/ServiceCollectionExtensions.cs b/src/AzureOpenAIProxy.ApiApp/Extensions/ServiceCollectionExtensions.cs index c5ad44fe..ae2971bb 100644 --- a/src/AzureOpenAIProxy.ApiApp/Extensions/ServiceCollectionExtensions.cs +++ b/src/AzureOpenAIProxy.ApiApp/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -using Azure.Identity; +using Azure.Identity; using Azure.Security.KeyVault.Secrets; using AzureOpenAIProxy.ApiApp.Builders; @@ -88,13 +88,15 @@ public static IServiceCollection AddOpenAIService(this IServiceCollection servic /// Returns instance. public static IServiceCollection AddOpenApiService(this IServiceCollection services) { + var settings = services.GetOpenApiSettings(); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle services.AddEndpointsApiExplorer(); services.AddSwaggerGen(options => { var info = new OpenApiInfo() { - Version = Constants.Version, + Version = settings.DocVersion, Title = Constants.Title, Description = "Providing a proxy service to Azure OpenAI API", Contact = new OpenApiContact() @@ -104,7 +106,7 @@ public static IServiceCollection AddOpenApiService(this IServiceCollection servi Url = new Uri("https://aka.ms/aoai-proxy.net") }, }; - options.SwaggerDoc(Constants.Version, info); + options.SwaggerDoc(settings.DocVersion, info); options.AddSecurityDefinition( "apiKey", @@ -132,4 +134,4 @@ public static IServiceCollection AddOpenApiService(this IServiceCollection servi return services; } -} \ No newline at end of file +} diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Extensions/OpenApiSettingsExtensionsTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Extensions/OpenApiSettingsExtensionsTests.cs new file mode 100644 index 00000000..4cf01827 --- /dev/null +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Extensions/OpenApiSettingsExtensionsTests.cs @@ -0,0 +1,121 @@ +using AzureOpenAIProxy.ApiApp.Extensions; + +using FluentAssertions; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +using NSubstitute; + +namespace AzureOpenAIProxy.ApiApp.Tests.Extensions; + +public class OpenApiSettingsExtensionsTests +{ + [Fact] + public void Given_Null_OpenApiSettings_When_Added_ToServiceProvider_Then_It_Should_Throw_Exception() + { + // Arrange + var config = default(IConfiguration); + + var sp = Substitute.For(); + ServiceProviderServiceExtensions.GetService(sp).Returns(config); + + // Act + Action action = () => sp.GetOpenApiSettings(); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void Given_Empty_OpenApiSettings_When_Added_ToServiceProvider_Then_It_Should_Throw_Exception() + { + // Arrange + var dict = new Dictionary() + { + { "OpenApi", "" } + }; +#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build(); +#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + + var sp = Substitute.For(); + ServiceProviderServiceExtensions.GetService(sp).Returns(config); + + // Act + Action action = () => sp.GetOpenApiSettings(); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void Given_Empty_OpenApiSettings_When_Added_ToServiceCollection_Then_It_Should_Throw_Exception() + { + // Arrange + var dict = new Dictionary() + { + { "OpenApi", "" } + }; +#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build(); +#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + + var sc = new ServiceCollection(); + sc.AddSingleton(config); + + // Act + Action action = () => sc.GetOpenApiSettings(); + + // Assert + action.Should().Throw(); + } + + [Theory] + [InlineData("")] + [InlineData("v1.0.0")] + public void Given_OpenApiSettings_When_Added_ToServiceProvider_Then_It_Should_Return_DocVersion(string docVersion) + { + // Arrange + var dict = new Dictionary() + { + { "OpenApi:DocVersion", docVersion } + }; +#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build(); +#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + + var sp = Substitute.For(); + ServiceProviderServiceExtensions.GetService(sp).Returns(config); + + // Act + var result = sp.GetOpenApiSettings(); + + // Assert + result.DocVersion.Should().Be(docVersion); + } + + [Theory] + [InlineData("")] + [InlineData("v1.0.0")] + public void Given_OpenApiSettings_When_Added_ToServiceCollection_Then_It_Should_Return_DocVersion(string docVersion) + { + // Arrange + var dict = new Dictionary() + { + { "OpenApi:DocVersion", docVersion } + }; +#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build(); +#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + + var sc = new ServiceCollection(); + sc.AddSingleton(config); + + // Act + var result = sc.GetOpenApiSettings(); + + // Assert + result.DocVersion.Should().Be(docVersion); + } +} \ No newline at end of file From 0d15c4a2c4a29d2dc1e7607376fe464b60c9eec9 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sat, 31 Aug 2024 18:28:34 +0900 Subject: [PATCH 03/13] Update directory structure and file naming (#282) --- .../Endpoints/AdminEndpointUrls.cs | 11 +- .../Endpoints/AdminEventEndpoints.cs | 76 +++++----- .../Endpoints/PlaygroundEndpointUrls.cs | 15 ++ ...ventEndpoint.cs => PlaygroundEndpoints.cs} | 10 +- ...int.cs => ProxyChatCompletionsEndpoint.cs} | 6 +- .../{EndpointUrls.cs => ProxyEndpointUrls.cs} | 9 +- .../Endpoints/WeatherForecastEndpoint.cs | 2 +- .../Extensions/HttpRequestExtensions.cs | 2 +- src/AzureOpenAIProxy.ApiApp/Program.cs | 16 +- .../PlaygroundPageTests.cs} | 38 ++--- .../PlaygroundPageTests.cs} | 140 +++++++++--------- 11 files changed, 170 insertions(+), 155 deletions(-) create mode 100644 src/AzureOpenAIProxy.ApiApp/Endpoints/PlaygroundEndpointUrls.cs rename src/AzureOpenAIProxy.ApiApp/Endpoints/{EventEndpoint.cs => PlaygroundEndpoints.cs} (83%) rename src/AzureOpenAIProxy.ApiApp/Endpoints/{ChatCompletionsEndpoint.cs => ProxyChatCompletionsEndpoint.cs} (92%) rename src/AzureOpenAIProxy.ApiApp/Endpoints/{EndpointUrls.cs => ProxyEndpointUrls.cs} (66%) rename test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/{Components/Pages/PlaygroundPageTest.cs => Pages/PlaygroundPageTests.cs} (75%) rename test/AzureOpenAIProxy.PlaygroundApp.Tests/{UI/ConfigTabComponentTest.cs => Pages/PlaygroundPageTests.cs} (86%) diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEndpointUrls.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEndpointUrls.cs index 82f89d60..0fdba526 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEndpointUrls.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEndpointUrls.cs @@ -1,18 +1,25 @@ namespace AzureOpenAIProxy.ApiApp.Endpoints; +/// +/// This represents the collection of the admin endpoint URLs. +/// public static class AdminEndpointUrls { /// /// Declares the admin event details endpoint. /// + /// + /// - GET method for an event details + /// - PUT method for update an event details + /// 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 + /// - 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 c319e491..0cde354b 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs @@ -5,35 +5,36 @@ namespace AzureOpenAIProxy.ApiApp.Endpoints; /// -/// This represents the endpoint entity for get event details by admin +/// This represents the endpoint entity for event details by admin /// public static class AdminEventEndpoints { /// - /// Adds the get event details by admin endpoint + /// Adds the admin event endpoint /// /// instance. /// Returns instance. - public static RouteHandlerBuilder AddAdminEvents(this WebApplication app) + public static RouteHandlerBuilder AddNewAdminEvent(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) => + var builder = app.MapPost(AdminEndpointUrls.AdminEvents, async ( + [FromBody] AdminEventDetails payload, + HttpRequest request) => { - // Todo: Issue #208 https://github.com/aliencube/azure-openai-sdk-proxy/issues/208 - return Results.Ok(); - // Todo: Issue #208 + return await Task.FromResult(Results.Ok()); }) + // TODO: Check both request/response payloads + .Accepts(contentType: "application/json") .Produces(statusCode: StatusCodes.Status200OK, contentType: "application/json") + // TODO: Check both request/response payloads + .Produces(statusCode: StatusCodes.Status400BadRequest) .Produces(statusCode: StatusCodes.Status401Unauthorized) .Produces(statusCode: StatusCodes.Status500InternalServerError, contentType: "text/plain") .WithTags("admin") - .WithName("GetAdminEventDetails") + .WithName("CreateAdminEvent") .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."; + operation.Summary = "Create admin event"; + operation.Description = "Create admin event"; return operation; }); @@ -46,7 +47,7 @@ public static RouteHandlerBuilder AddAdminEvents(this WebApplication app) /// /// instance. /// Returns instance. - public static RouteHandlerBuilder AddAdminEventList(this WebApplication app) + public static RouteHandlerBuilder AddListAdminEvents(this WebApplication app) { // Todo: Issue #19 https://github.com/aliencube/azure-openai-sdk-proxy/issues/19 // Need authorization by admin @@ -73,65 +74,64 @@ public static RouteHandlerBuilder AddAdminEventList(this WebApplication app) } /// - /// Adds the update event details by admin endpoint + /// Adds the get event details by admin endpoint /// /// instance. /// Returns instance. - public static RouteHandlerBuilder AddUpdateAdminEvents(this WebApplication app) + public static RouteHandlerBuilder AddGetAdminEvent(this WebApplication app) { // Todo: Issue #19 https://github.com/aliencube/azure-openai-sdk-proxy/issues/19 // Need authorization by admin - var builder = app.MapPut(AdminEndpointUrls.AdminEventDetails, ( - [FromRoute] string eventId, - [FromBody] AdminEventDetails payload) => + var builder = app.MapGet(AdminEndpointUrls.AdminEventDetails, ( + [FromRoute] string eventId) => { - // Todo: Issue #203 https://github.com/aliencube/azure-openai-sdk-proxy/issues/203 + // Todo: Issue #208 https://github.com/aliencube/azure-openai-sdk-proxy/issues/208 return Results.Ok(); + // Todo: Issue #208 }) - .Accepts(contentType: "application/json") .Produces(statusCode: StatusCodes.Status200OK, contentType: "application/json") .Produces(statusCode: StatusCodes.Status401Unauthorized) - .Produces(statusCode: StatusCodes.Status404NotFound) .Produces(statusCode: StatusCodes.Status500InternalServerError, contentType: "text/plain") .WithTags("admin") - .WithName("UpdateAdminEventDetails") + .WithName("GetAdminEvent") .WithOpenApi(operation => { - operation.Summary = "Updates event details from the given event ID"; - operation.Description = "This endpoint updates the event details from the given event ID."; + 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 admin event endpoint + /// Adds the update event details by admin endpoint /// /// instance. /// Returns instance. - public static RouteHandlerBuilder CreateAdminEvent(this WebApplication app) + public static RouteHandlerBuilder AddUpdateAdminEvent(this WebApplication app) { - var builder = app.MapPost(AdminEndpointUrls.AdminEvents, async ( - [FromBody] AdminEventDetails payload, - HttpRequest request) => + // Todo: Issue #19 https://github.com/aliencube/azure-openai-sdk-proxy/issues/19 + // Need authorization by admin + var builder = app.MapPut(AdminEndpointUrls.AdminEventDetails, ( + [FromRoute] string eventId, + [FromBody] AdminEventDetails payload) => { - return await Task.FromResult(Results.Ok()); + // Todo: Issue #203 https://github.com/aliencube/azure-openai-sdk-proxy/issues/203 + return Results.Ok(); }) - // TODO: Check both request/response payloads .Accepts(contentType: "application/json") .Produces(statusCode: StatusCodes.Status200OK, contentType: "application/json") - // TODO: Check both request/response payloads - .Produces(statusCode: StatusCodes.Status400BadRequest) .Produces(statusCode: StatusCodes.Status401Unauthorized) + .Produces(statusCode: StatusCodes.Status404NotFound) .Produces(statusCode: StatusCodes.Status500InternalServerError, contentType: "text/plain") .WithTags("admin") - .WithName("CreateAdminEvent") + .WithName("UpdateAdminEvent") .WithOpenApi(operation => { - operation.Summary = "Create admin event"; - operation.Description = "Create admin event"; + operation.Summary = "Updates event details from the given event ID"; + operation.Description = "This endpoint updates the event details from the given event ID."; return operation; }); diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/PlaygroundEndpointUrls.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/PlaygroundEndpointUrls.cs new file mode 100644 index 00000000..b8ccbd2b --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/PlaygroundEndpointUrls.cs @@ -0,0 +1,15 @@ +namespace AzureOpenAIProxy.ApiApp.Endpoints; + +/// +/// This represents the collection of the playground endpoint URLs. +/// +public static class PlaygroundEndpointUrls +{ + /// + /// Declares the event endpoint. + /// + /// + /// - GET method for listing all events + /// + public const string Events = "/events"; +} diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/EventEndpoint.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/PlaygroundEndpoints.cs similarity index 83% rename from src/AzureOpenAIProxy.ApiApp/Endpoints/EventEndpoint.cs rename to src/AzureOpenAIProxy.ApiApp/Endpoints/PlaygroundEndpoints.cs index a82efb31..c21d6029 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/EventEndpoint.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/PlaygroundEndpoints.cs @@ -1,22 +1,18 @@ -using System.Text.Json; - -using AzureOpenAIProxy.ApiApp.Models; - namespace AzureOpenAIProxy.ApiApp.Endpoints; /// /// This represents the endpoint entity for events that the logged user joined. /// -public static class EventEndpoint +public static class PlaygroundEndpoints { /// /// Adds the event endpoint. /// /// instance. /// Returns instance. - public static RouteHandlerBuilder AddEventList(this WebApplication app) + public static RouteHandlerBuilder AddListEvents(this WebApplication app) { - var builder = app.MapGet(EndpointUrls.Events, () => + var builder = app.MapGet(PlaygroundEndpointUrls.Events, () => { // TODO: Issue #179 https://github.com/aliencube/azure-openai-sdk-proxy/issues/179 return Results.Ok(); diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/ChatCompletionsEndpoint.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/ProxyChatCompletionsEndpoint.cs similarity index 92% rename from src/AzureOpenAIProxy.ApiApp/Endpoints/ChatCompletionsEndpoint.cs rename to src/AzureOpenAIProxy.ApiApp/Endpoints/ProxyChatCompletionsEndpoint.cs index 72d7572e..a758150f 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/ChatCompletionsEndpoint.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/ProxyChatCompletionsEndpoint.cs @@ -12,7 +12,7 @@ namespace AzureOpenAIProxy.ApiApp.Endpoints; /// /// This represents the endpoint entity for chat completions. /// -public static class ChatCompletionsEndpoint +public static class ProxyChatCompletionsEndpoint { /// /// Adds the chat completion endpoint. @@ -21,7 +21,7 @@ public static class ChatCompletionsEndpoint /// Returns instance. public static RouteHandlerBuilder AddChatCompletions(this WebApplication app) { - var builder = app.MapPost(EndpointUrls.ChatCompletions, async ( + var builder = app.MapPost(ProxyEndpointUrls.ChatCompletions, async ( [OpenApiParameterIgnore][FromHeader(Name = "api-key")] string apiKey, [FromRoute] string deploymentName, [FromQuery(Name = "api-version")] string apiVersion, @@ -30,7 +30,7 @@ public static RouteHandlerBuilder AddChatCompletions(this WebApplication app) IOpenAIService openai, ILoggerFactory loggerFactory) => { - var logger = loggerFactory.CreateLogger(nameof(ChatCompletionsEndpoint)); + var logger = loggerFactory.CreateLogger(nameof(ProxyChatCompletionsEndpoint)); logger.LogInformation("Received a chat completion request"); request.Body.Position = 0; diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/EndpointUrls.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/ProxyEndpointUrls.cs similarity index 66% rename from src/AzureOpenAIProxy.ApiApp/Endpoints/EndpointUrls.cs rename to src/AzureOpenAIProxy.ApiApp/Endpoints/ProxyEndpointUrls.cs index b69068eb..0808fc42 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/EndpointUrls.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/ProxyEndpointUrls.cs @@ -1,9 +1,9 @@ namespace AzureOpenAIProxy.ApiApp.Endpoints; /// -/// This represents the collection of the endpoint URLs. +/// This represents the collection of the proxy endpoint URLs. /// -public static class EndpointUrls +public static class ProxyEndpointUrls { /// /// Declares the weather forecast endpoint. @@ -14,9 +14,4 @@ public static class EndpointUrls /// Declares the chat completions endpoint. /// public const string ChatCompletions = "/openai/deployments/{deploymentName}/chat/completions"; - - /// - /// Declares the event endpoint. - /// - public const string Events = "/events"; } diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/WeatherForecastEndpoint.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/WeatherForecastEndpoint.cs index 8656c921..7e87fed0 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/WeatherForecastEndpoint.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/WeatherForecastEndpoint.cs @@ -19,7 +19,7 @@ public static class WeatherForecastEndpoint /// Returns instance. public static RouteHandlerBuilder AddWeatherForecast(this WebApplication app) { - var builder = app.MapGet(EndpointUrls.WeatherForecast, () => + var builder = app.MapGet(ProxyEndpointUrls.WeatherForecast, () => { var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast diff --git a/src/AzureOpenAIProxy.ApiApp/Extensions/HttpRequestExtensions.cs b/src/AzureOpenAIProxy.ApiApp/Extensions/HttpRequestExtensions.cs index d3cfa52b..951f6841 100644 --- a/src/AzureOpenAIProxy.ApiApp/Extensions/HttpRequestExtensions.cs +++ b/src/AzureOpenAIProxy.ApiApp/Extensions/HttpRequestExtensions.cs @@ -10,7 +10,7 @@ public static class HttpRequestExtensions /// Gets the base URL. /// /// instance. - /// + /// Returns the base URL from . public static string? BaseUrl(this HttpRequest req) { if (req == null) return null; diff --git a/src/AzureOpenAIProxy.ApiApp/Program.cs b/src/AzureOpenAIProxy.ApiApp/Program.cs index e64fbddf..5ce6fbe0 100644 --- a/src/AzureOpenAIProxy.ApiApp/Program.cs +++ b/src/AzureOpenAIProxy.ApiApp/Program.cs @@ -37,15 +37,17 @@ app.UseHttpsRedirection(); app.AddWeatherForecast(); + +// Proxy endpoints app.AddChatCompletions(); -// Event Endpoints -app.AddEventList(); +// Playground endpoints +app.AddListEvents(); -// Admin Endpoints -app.AddAdminEvents(); -app.AddAdminEventList(); -app.AddUpdateAdminEvents(); -app.CreateAdminEvent(); +// Admin endpoints +app.AddNewAdminEvent(); +app.AddListAdminEvents(); +app.AddGetAdminEvent(); +app.AddUpdateAdminEvent(); await app.RunAsync(); diff --git a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Components/Pages/PlaygroundPageTest.cs b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/PlaygroundPageTests.cs similarity index 75% rename from test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Components/Pages/PlaygroundPageTest.cs rename to test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/PlaygroundPageTests.cs index 16b905bf..05b1b953 100644 --- a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Components/Pages/PlaygroundPageTest.cs +++ b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/PlaygroundPageTests.cs @@ -1,20 +1,20 @@ -using AzureOpenAIProxy.AppHost.Tests.Fixtures; - -namespace AzureOpenAIProxy.AppHost.Tests.PlaygroundApp.Components.Pages; - -public class PlaygroundPageTest(AspireAppHostFixture host) : IClassFixture -{ - [Fact] - public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_OK() - { - // Arrange - var httpClient = host.App!.CreateHttpClient("playgroundapp"); - await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); - - // Act - var response = await httpClient.GetAsync("/playground"); - - // Assert - response.EnsureSuccessStatusCode(); // Status Code 200-299 - } +using AzureOpenAIProxy.AppHost.Tests.Fixtures; + +namespace AzureOpenAIProxy.AppHost.Tests.PlaygroundApp.Pages; + +public class PlaygroundPageTests(AspireAppHostFixture host) : IClassFixture +{ + [Fact] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_OK() + { + // Arrange + var httpClient = host.App!.CreateHttpClient("playgroundapp"); + await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var response = await httpClient.GetAsync("/playground"); + + // Assert + response.EnsureSuccessStatusCode(); // Status Code 200-299 + } } \ No newline at end of file diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/UI/ConfigTabComponentTest.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaygroundPageTests.cs similarity index 86% rename from test/AzureOpenAIProxy.PlaygroundApp.Tests/UI/ConfigTabComponentTest.cs rename to test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaygroundPageTests.cs index fbb98d36..7b012f98 100644 --- a/test/AzureOpenAIProxy.PlaygroundApp.Tests/UI/ConfigTabComponentTest.cs +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaygroundPageTests.cs @@ -1,71 +1,71 @@ -using Microsoft.Playwright; -using Microsoft.Playwright.NUnit; - -namespace AzureOpenAIProxy.PlaygroundApp.Tests.UI; - -[Parallelizable(ParallelScope.Self)] -[TestFixture] -[Property("Category", "Integration")] -public class ConfigTabComponentTest : PageTest -{ - public override BrowserNewContextOptions ContextOptions() => new() { IgnoreHTTPSErrors = true, }; - - [SetUp] - public async Task SetUp() - { - await Page.GotoAsync("https://localhost:5001/playground/"); - await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); - } - - [Test] - public async Task Given_ConfigTab_When_Endpoint_Invoked_Then_ConfigTab_Should_Be_Displayed() - { - // Act - var configTab = Page.Locator("fluent-tabs#config-tab"); - - // Assert - await Expect(configTab).ToBeVisibleAsync(); - } - - [Test] - public async Task Given_ConfigTab_When_Endpoint_Invoked_Then_Id_Should_Be_System_Message_Tab() - { - // Act - var sysMsgPanel = Page.Locator("fluent-tab-panel#system-message-tab-panel"); - var parameterPanel = Page.Locator("fluent-tab-panel#parameters-tab-panel"); - - // Assert - await Expect(sysMsgPanel).ToBeVisibleAsync(); - await Expect(parameterPanel).ToBeHiddenAsync(); - } - - [Test] - [TestCase( - "fluent-tab#parameters-tab", - "fluent-tab-panel#parameters-tab-panel", - "fluent-tab-panel#system-message-tab-panel" - )] - [TestCase( - "fluent-tab#system-message-tab", - "fluent-tab-panel#system-message-tab-panel", - "fluent-tab-panel#parameters-tab-panel" - )] - public async Task Given_ConfigTab_When_Changed_Then_Tab_Should_Be_Updated( - string selectedTabSelector, - string selectedPanelSelector, - string hiddenPanelSelector - ) - { - // Arrange - var selectedTab = Page.Locator(selectedTabSelector); - var selectedPanel = Page.Locator(selectedPanelSelector); - var hiddenPanel = Page.Locator(hiddenPanelSelector); - - // Act - await selectedTab.ClickAsync(); - - // Assert - await Expect(selectedPanel).ToBeVisibleAsync(); - await Expect(hiddenPanel).ToBeHiddenAsync(); - } +using Microsoft.Playwright; +using Microsoft.Playwright.NUnit; + +namespace AzureOpenAIProxy.PlaygroundApp.Tests.Pages; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +[Property("Category", "Integration")] +public class PlaygroundPageTests : PageTest +{ + public override BrowserNewContextOptions ContextOptions() => new() { IgnoreHTTPSErrors = true, }; + + [SetUp] + public async Task SetUp() + { + await Page.GotoAsync("https://localhost:5001/playground/"); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + } + + [Test] + public async Task Given_ConfigTab_When_Endpoint_Invoked_Then_ConfigTab_Should_Be_Displayed() + { + // Act + var configTab = Page.Locator("fluent-tabs#config-tab"); + + // Assert + await Expect(configTab).ToBeVisibleAsync(); + } + + [Test] + public async Task Given_ConfigTab_When_Endpoint_Invoked_Then_Id_Should_Be_System_Message_Tab() + { + // Act + var systemMessagePanel = Page.Locator("fluent-tab-panel#system-message-tab-panel"); + var parameterPanel = Page.Locator("fluent-tab-panel#parameters-tab-panel"); + + // Assert + await Expect(systemMessagePanel).ToBeVisibleAsync(); + await Expect(parameterPanel).ToBeHiddenAsync(); + } + + [Test] + [TestCase( + "fluent-tab#parameters-tab", + "fluent-tab-panel#parameters-tab-panel", + "fluent-tab-panel#system-message-tab-panel" + )] + [TestCase( + "fluent-tab#system-message-tab", + "fluent-tab-panel#system-message-tab-panel", + "fluent-tab-panel#parameters-tab-panel" + )] + public async Task Given_ConfigTab_When_Changed_Then_Tab_Should_Be_Updated( + string selectedTabSelector, + string selectedPanelSelector, + string hiddenPanelSelector + ) + { + // Arrange + var selectedTab = Page.Locator(selectedTabSelector); + var selectedPanel = Page.Locator(selectedPanelSelector); + var hiddenPanel = Page.Locator(hiddenPanelSelector); + + // Act + await selectedTab.ClickAsync(); + + // Assert + await Expect(selectedPanel).ToBeVisibleAsync(); + await Expect(hiddenPanel).ToBeHiddenAsync(); + } } \ No newline at end of file From 25e963097b3f8dababd208259b8e66d96c66b09f Mon Sep 17 00:00:00 2001 From: o-ii <148066227+o-ii@users.noreply.github.com> Date: Sat, 31 Aug 2024 22:21:51 +0900 Subject: [PATCH 04/13] [Playground] Component - button to display a given input #171 (#236) --- .../Components/Layout/MainLayout.razor | 4 +- .../Components/Pages/Tests.razor | 14 ++++++ .../Components/UI/DebugButtonComponent.razor | 22 +++++++++ .../Components/UI/DebugTargetComponent.razor | 15 ++++++ .../Pages/TestsPageTests.cs | 47 +++++++++++++++++++ 5 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Tests.razor create mode 100644 src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DebugButtonComponent.razor create mode 100644 src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DebugTargetComponent.razor create mode 100644 test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/TestsPageTests.cs diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/Layout/MainLayout.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/Layout/MainLayout.razor index 1bad0f45..56bf7fcf 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/Layout/MainLayout.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/Layout/MainLayout.razor @@ -21,7 +21,7 @@ 🗙 - + - + \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Tests.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Tests.razor new file mode 100644 index 00000000..0929588c --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Tests.razor @@ -0,0 +1,14 @@ +@page "/tests" +@rendermode InteractiveServer + + + + +@code { + private object? currentValue; + + private void SetInput(int newValue) + { + currentValue = newValue; + } +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DebugButtonComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DebugButtonComponent.razor new file mode 100644 index 00000000..93abbb75 --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DebugButtonComponent.razor @@ -0,0 +1,22 @@ +@inject IToastService ToastService + + +Debug + +@code { + [Parameter] + public object? Input { get; set; } + + private async Task ShowToast() + { + if (Input is null) + { + ToastService.ShowToast(ToastIntent.Warning, "Input is null."); + await Task.CompletedTask; + return; + } + + ToastService.ShowToast(ToastIntent.Success, $"{Input} (Type: {Input.GetType()})"); + await Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DebugTargetComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DebugTargetComponent.razor new file mode 100644 index 00000000..4f2c30bb --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DebugTargetComponent.razor @@ -0,0 +1,15 @@ + + 123 + 456 + 789 + + +@code { + [Parameter] + public EventCallback OnValueChanged { get; set; } + + private async Task SetValue(int value) + { + await OnValueChanged.InvokeAsync(value); + } +} \ No newline at end of file diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/TestsPageTests.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/TestsPageTests.cs new file mode 100644 index 00000000..f0b6f8c6 --- /dev/null +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/TestsPageTests.cs @@ -0,0 +1,47 @@ +using Microsoft.Playwright; +using Microsoft.Playwright.NUnit; + +namespace AzureOpenAIProxy.PlaygroundApp.Tests.Pages; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +[Property("Category", "Integration")] +public class TestsPageTests : PageTest +{ + public override BrowserNewContextOptions ContextOptions() => new() + { + IgnoreHTTPSErrors = true, + }; + + [SetUp] + public async Task Setup() + { + // Arrange + await Page.GotoAsync("https://localhost:5001/tests"); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + } + + [Test] + public async Task Given_No_Input_When_DebugButton_Clicked_Then_Toast_Should_Show_NullMessage() + { + // Act + await Page.GetByRole(AriaRole.Button, new() { Name = "Debug" }).ClickAsync(); + + // Assert + await Expect(Page.Locator(".fluent-toast-title")).ToHaveTextAsync("Input is null."); + } + + [Test] + [TestCase(123, typeof(int))] + [TestCase(456, typeof(int))] + [TestCase(789, typeof(int))] + public async Task Given_Input_When_DebugButton_Clicked_Then_Toast_Should_Show_Input(int inputValue, Type inputType) + { + // Act + await Page.GetByRole(AriaRole.Radio, new() { Name = $"{inputValue}" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Debug" }).ClickAsync(); + + // Assert + await Expect(Page.Locator(".fluent-toast-title")).ToHaveTextAsync($"{inputValue} (Type: {inputType})"); + } +} \ No newline at end of file From bbef6f10e01831177bb0bfa448109f21700be851 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sun, 1 Sep 2024 12:53:06 +0900 Subject: [PATCH 05/13] [Admin] Add admin event service layer (#286) --- .../Endpoints/AdminEventEndpoints.cs | 31 ++++++- .../Models/AdminEventDetails.cs | 22 +++-- .../Models/EventDetails.cs | 18 ++-- src/AzureOpenAIProxy.ApiApp/Program.cs | 4 + .../Services/AdminEventService.cs | 85 +++++++++++++++++++ .../Services/AdminEventServiceTests.cs | 80 +++++++++++++++++ .../AppHostProgramTests.cs | 2 +- 7 files changed, 223 insertions(+), 19 deletions(-) create mode 100644 src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs create mode 100644 test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs index 0cde354b..beb5371f 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs @@ -1,4 +1,5 @@ using AzureOpenAIProxy.ApiApp.Models; +using AzureOpenAIProxy.ApiApp.Services; using Microsoft.AspNetCore.Mvc; @@ -18,14 +19,38 @@ public static RouteHandlerBuilder AddNewAdminEvent(this WebApplication app) { var builder = app.MapPost(AdminEndpointUrls.AdminEvents, async ( [FromBody] AdminEventDetails payload, - HttpRequest request) => + IAdminEventService service, + ILoggerFactory loggerFactory) => { + var logger = loggerFactory.CreateLogger(nameof(AdminEventEndpoints)); + logger.LogInformation("Received a new event request"); + + if (payload is null) + { + logger.LogError("No payload found"); + + return Results.BadRequest("Payload is null"); + } + + //try + //{ + // var result = await service.CreateEvent(payload); + + // logger.LogInformation("Created a new event"); + + // return Results.Ok(result); + //} + //catch (Exception ex) + //{ + // logger.LogError(ex, "Failed to create a new event"); + + // return Results.Problem(ex.Message, statusCode: StatusCodes.Status500InternalServerError); + //} + return await Task.FromResult(Results.Ok()); }) - // TODO: Check both request/response payloads .Accepts(contentType: "application/json") .Produces(statusCode: StatusCodes.Status200OK, contentType: "application/json") - // TODO: Check both request/response payloads .Produces(statusCode: StatusCodes.Status400BadRequest) .Produces(statusCode: StatusCodes.Status401Unauthorized) .Produces(statusCode: StatusCodes.Status500InternalServerError, contentType: "text/plain") diff --git a/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs b/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs index 4f587217..a3ccdf25 100644 --- a/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs +++ b/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs @@ -1,4 +1,6 @@ -namespace AzureOpenAIProxy.ApiApp.Models; +using System.Text.Json.Serialization; + +namespace AzureOpenAIProxy.ApiApp.Models; /// /// This represent the event detail data for response by admin event endpoint. @@ -13,32 +15,38 @@ public class AdminEventDetails : EventDetails /// /// Gets or sets the event start date. /// - public required DateTimeOffset? DateStart { get; set; } + [JsonRequired] + public DateTimeOffset DateStart { get; set; } /// /// Gets or sets the event end date. /// - public required DateTimeOffset? DateEnd { get; set; } + [JsonRequired] + public DateTimeOffset DateEnd { get; set; } /// /// Gets or sets the event start to end date timezone. /// - public required string? TimeZone { get; set; } + [JsonRequired] + public string TimeZone { get; set; } = string.Empty; /// /// Gets or sets the event active status. /// - public required bool? IsActive { get; set; } + [JsonRequired] + public bool IsActive { get; set; } /// /// Gets or sets the event organizer name. /// - public required string? OrganizerName { get; set; } + [JsonRequired] + public string OrganizerName { get; set; } = string.Empty; /// /// Gets or sets the event organizer email. /// - public required string? OrganizerEmail { get; set; } + [JsonRequired] + public string OrganizerEmail { get; set; } = string.Empty; /// /// Gets or sets the event coorganizer name. diff --git a/src/AzureOpenAIProxy.ApiApp/Models/EventDetails.cs b/src/AzureOpenAIProxy.ApiApp/Models/EventDetails.cs index 5c33fafb..ae3594f2 100644 --- a/src/AzureOpenAIProxy.ApiApp/Models/EventDetails.cs +++ b/src/AzureOpenAIProxy.ApiApp/Models/EventDetails.cs @@ -1,8 +1,5 @@ -using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -using AzureOpenAIProxy.ApiApp.Models; - /// /// This represents the event's detailed data for response by EventEndpoint. /// @@ -11,25 +8,30 @@ public class EventDetails /// /// Gets or sets the event id. /// - public required string? EventId { get; set; } + [JsonRequired] + public Guid EventId { get; set; } /// /// Gets or sets the event title name. /// - public required string? Title { get; set; } + [JsonRequired] + public string Title { get; set; } = string.Empty; /// /// Gets or sets the event summary. /// - public required string? Summary { get; set; } + [JsonRequired] + public string Summary { get; set; } = string.Empty; /// /// Gets or sets the Azure OpenAI Service request max token capacity. /// - public required int? MaxTokenCap { get; set; } + [JsonRequired] + public int MaxTokenCap { get; set; } /// /// Gets or sets the Azure OpenAI Service daily request capacity. /// - public required int? DailyRequestCap { get; set; } + [JsonRequired] + public int DailyRequestCap { get; set; } } \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Program.cs b/src/AzureOpenAIProxy.ApiApp/Program.cs index 5ce6fbe0..1ef3a28c 100644 --- a/src/AzureOpenAIProxy.ApiApp/Program.cs +++ b/src/AzureOpenAIProxy.ApiApp/Program.cs @@ -1,5 +1,6 @@ using AzureOpenAIProxy.ApiApp.Endpoints; using AzureOpenAIProxy.ApiApp.Extensions; +using AzureOpenAIProxy.ApiApp.Services; var builder = WebApplication.CreateBuilder(args); @@ -14,6 +15,9 @@ // Add OpenAPI service builder.Services.AddOpenApiService(); +// Add admin services +builder.Services.AddAdminEventService(); + var app = builder.Build(); app.MapDefaultEndpoints(); diff --git a/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs b/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs new file mode 100644 index 00000000..d3e0533f --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs @@ -0,0 +1,85 @@ +using AzureOpenAIProxy.ApiApp.Models; + +namespace AzureOpenAIProxy.ApiApp.Services; + +/// +/// This provides interfaces to class. +/// +public interface IAdminEventService +{ + /// + /// Creates a new event. + /// + /// Event payload. + /// Returns the event payload created. + Task CreateEvent(AdminEventDetails eventDetails); + + /// + /// Gets the list of events. + /// + /// Returns the list of events. + Task> GetEvents(); + + /// + /// Gets the event details. + /// + /// Event ID. + /// Returns the event details. + Task GetEvent(Guid eventId); + + /// + /// Updates the event details. + /// + /// Event ID. + /// Event details to update. + /// Returns the updated event details. + Task UpdateEvent(Guid eventId, AdminEventDetails eventDetails); +} + +/// +/// This represents the service entity for admin event. +/// +public class AdminEventService : IAdminEventService +{ + /// + public async Task CreateEvent(AdminEventDetails eventDetails) + { + throw new NotImplementedException(); + } + + /// + public async Task> GetEvents() + { + throw new NotImplementedException(); + } + + /// + public async Task GetEvent(Guid eventId) + { + throw new NotImplementedException(); + } + + /// + public async Task UpdateEvent(Guid eventId, AdminEventDetails eventDetails) + { + throw new NotImplementedException(); + } +} + +/// +/// This represents the extension class for +/// +public static class AdminEventServiceExtensions +{ + /// + /// Adds the instance to the service collection. + /// + /// instance. + /// Returns instance. + public static IServiceCollection AddAdminEventService(this IServiceCollection services) + { + services.AddScoped(); + + return services; + } +} diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs new file mode 100644 index 00000000..e459e4d0 --- /dev/null +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs @@ -0,0 +1,80 @@ +using AzureOpenAIProxy.ApiApp.Models; +using AzureOpenAIProxy.ApiApp.Services; + +using FluentAssertions; + +using Microsoft.Extensions.DependencyInjection; + +namespace AzureOpenAIProxy.ApiApp.Tests.Services; + +public class AdminEventServiceTests +{ + [Fact] + public void Given_ServiceCollection_When_AddAdminEventService_Invoked_Then_It_Should_Contain_AdminEventService() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddAdminEventService(); + + // Assert + services.SingleOrDefault(p => p.ServiceType == typeof(IAdminEventService)).Should().NotBeNull(); + } + + [Fact] + public void Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Throw_Exception() + { + // Arrange + var eventDetails = new AdminEventDetails(); + var service = new AdminEventService(); + + // Act + Func func = async () => await service.CreateEvent(eventDetails); + + // Assert + func.Should().ThrowAsync(); + } + + [Fact] + public void Given_Instance_When_GetEvents_Invoked_Then_It_Should_Throw_Exception() + { + // Arrange + var service = new AdminEventService(); + + // Act + Func func = async () => await service.GetEvents(); + + // Assert + func.Should().ThrowAsync(); + } + + [Fact] + public void Given_Instance_When_GetEvent_Invoked_Then_It_Should_Throw_Exception() + { + // Arrange + var eventId = Guid.NewGuid(); + var service = new AdminEventService(); + + // Act + Func func = async () => await service.GetEvent(eventId); + + // Assert + func.Should().ThrowAsync(); + } + + [Fact] + public void Given_Instance_When_UpdateEvent_Invoked_Then_It_Should_Throw_Exception() + { + // Arrange + var eventId = Guid.NewGuid(); + var eventDetails = new AdminEventDetails(); + var service = new AdminEventService(); + + // Act + Func func = async () => await service.UpdateEvent(eventId, eventDetails); + + // Assert + func.Should().ThrowAsync(); + } +} diff --git a/test/AzureOpenAIProxy.AppHost.Tests/AppHostProgramTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/AppHostProgramTests.cs index 22adb326..4af8e386 100644 --- a/test/AzureOpenAIProxy.AppHost.Tests/AppHostProgramTests.cs +++ b/test/AzureOpenAIProxy.AppHost.Tests/AppHostProgramTests.cs @@ -4,7 +4,7 @@ using FluentAssertions; -namespace AzureOpenAIProxy.Tests; +namespace AzureOpenAIProxy.AppHost.Tests; public class AppHostProgramTests(AspireAppHostFixture host) : IClassFixture { From 6aef2d5768ad13ae55545a78d59b677abfd6ad00 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sun, 1 Sep 2024 13:53:24 +0900 Subject: [PATCH 06/13] [Admin] Add repository layer for admin events (#287) --- src/AzureOpenAIProxy.ApiApp/Program.cs | 4 + .../Repositories/AdminEventRepository.cs | 85 +++++++++++++++++++ .../Services/AdminEventService.cs | 21 +++-- .../Repositories/AdminEventRepositoryTests.cs | 80 +++++++++++++++++ .../Services/AdminEventServiceTests.cs | 15 +++- 5 files changed, 196 insertions(+), 9 deletions(-) create mode 100644 src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs create mode 100644 test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs diff --git a/src/AzureOpenAIProxy.ApiApp/Program.cs b/src/AzureOpenAIProxy.ApiApp/Program.cs index 1ef3a28c..4c5a0e55 100644 --- a/src/AzureOpenAIProxy.ApiApp/Program.cs +++ b/src/AzureOpenAIProxy.ApiApp/Program.cs @@ -1,5 +1,6 @@ using AzureOpenAIProxy.ApiApp.Endpoints; using AzureOpenAIProxy.ApiApp.Extensions; +using AzureOpenAIProxy.ApiApp.Repositories; using AzureOpenAIProxy.ApiApp.Services; var builder = WebApplication.CreateBuilder(args); @@ -18,6 +19,9 @@ // Add admin services builder.Services.AddAdminEventService(); +// Add admin repositories +builder.Services.AddAdminEventRepository(); + var app = builder.Build(); app.MapDefaultEndpoints(); diff --git a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs new file mode 100644 index 00000000..552b6416 --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs @@ -0,0 +1,85 @@ +using AzureOpenAIProxy.ApiApp.Models; + +namespace AzureOpenAIProxy.ApiApp.Repositories; + +/// +/// This provides interfaces to the class. +/// +public interface IAdminEventRepository +{ + /// + /// Creates a new record of event details. + /// + /// Event details instance. + /// Returns the event details instance created. + Task CreateEvent(AdminEventDetails eventDetails); + + /// + /// Gets the list of events. + /// + /// Returns the list of events. + Task> GetEvents(); + + /// + /// Gets the event details. + /// + /// Event ID. + /// Returns the event details record. + Task GetEvent(Guid eventId); + + /// + /// Updates the event details. + /// + /// Event ID. + /// Event details instance. + /// Returns the updated record of the event details. + Task UpdateEvent(Guid eventId, AdminEventDetails eventDetails); +} + +/// +/// This represents the repository entity for the admin event. +/// +public class AdminEventRepository : IAdminEventRepository +{ + /// + public async Task CreateEvent(AdminEventDetails eventDetails) + { + throw new NotImplementedException(); + } + + /// + public async Task> GetEvents() + { + throw new NotImplementedException(); + } + + /// + public async Task GetEvent(Guid eventId) + { + throw new NotImplementedException(); + } + + /// + public async Task UpdateEvent(Guid eventId, AdminEventDetails eventDetails) + { + throw new NotImplementedException(); + } +} + +/// +/// This represents the extension class for +/// +public static class AdminEventRepositoryExtensions +{ + /// + /// Adds the instance to the service collection. + /// + /// instance. + /// Returns instance. + public static IServiceCollection AddAdminEventRepository(this IServiceCollection services) + { + services.AddScoped(); + + return services; + } +} diff --git a/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs b/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs index d3e0533f..09f21797 100644 --- a/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs +++ b/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs @@ -1,4 +1,5 @@ using AzureOpenAIProxy.ApiApp.Models; +using AzureOpenAIProxy.ApiApp.Repositories; namespace AzureOpenAIProxy.ApiApp.Services; @@ -39,30 +40,40 @@ public interface IAdminEventService /// /// This represents the service entity for admin event. /// -public class AdminEventService : IAdminEventService +public class AdminEventService(IAdminEventRepository repository) : IAdminEventService { + private readonly IAdminEventRepository _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + /// public async Task CreateEvent(AdminEventDetails eventDetails) { - throw new NotImplementedException(); + var result = await this._repository.CreateEvent(eventDetails).ConfigureAwait(false); + + return result; } /// public async Task> GetEvents() { - throw new NotImplementedException(); + var result = await this._repository.GetEvents().ConfigureAwait(false); + + return result; } /// public async Task GetEvent(Guid eventId) { - throw new NotImplementedException(); + var result = await this._repository.GetEvent(eventId).ConfigureAwait(false); + + return result; } /// public async Task UpdateEvent(Guid eventId, AdminEventDetails eventDetails) { - throw new NotImplementedException(); + var result = await this._repository.UpdateEvent(eventId, eventDetails).ConfigureAwait(false); + + return result; } } diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs new file mode 100644 index 00000000..0e00c5d4 --- /dev/null +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs @@ -0,0 +1,80 @@ +using AzureOpenAIProxy.ApiApp.Models; +using AzureOpenAIProxy.ApiApp.Repositories; + +using FluentAssertions; + +using Microsoft.Extensions.DependencyInjection; + +namespace AzureOpenAIProxy.ApiApp.Tests.Repositories; + +public class AdminEventRepositoryTests +{ + [Fact] + public void Given_ServiceCollection_When_AddAdminEventRepository_Invoked_Then_It_Should_Contain_AdminEventRepository() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddAdminEventRepository(); + + // Assert + services.SingleOrDefault(p => p.ServiceType == typeof(IAdminEventRepository)).Should().NotBeNull(); + } + + [Fact] + public void Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Throw_Exception() + { + // Arrange + var eventDetails = new AdminEventDetails(); + var repository = new AdminEventRepository(); + + // Act + Func func = async () => await repository.CreateEvent(eventDetails); + + // Assert + func.Should().ThrowAsync(); + } + + [Fact] + public void Given_Instance_When_GetEvents_Invoked_Then_It_Should_Throw_Exception() + { + // Arrange + var repository = new AdminEventRepository(); + + // Act + Func func = async () => await repository.GetEvents(); + + // Assert + func.Should().ThrowAsync(); + } + + [Fact] + public void Given_Instance_When_GetEvent_Invoked_Then_It_Should_Throw_Exception() + { + // Arrange + var eventId = Guid.NewGuid(); + var repository = new AdminEventRepository(); + + // Act + Func func = async () => await repository.GetEvent(eventId); + + // Assert + func.Should().ThrowAsync(); + } + + [Fact] + public void Given_Instance_When_UpdateEvent_Invoked_Then_It_Should_Throw_Exception() + { + // Arrange + var eventId = Guid.NewGuid(); + var eventDetails = new AdminEventDetails(); + var repository = new AdminEventRepository(); + + // Act + Func func = async () => await repository.UpdateEvent(eventId, eventDetails); + + // Assert + func.Should().ThrowAsync(); + } +} diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs index e459e4d0..cf56f8e2 100644 --- a/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs @@ -1,10 +1,13 @@ using AzureOpenAIProxy.ApiApp.Models; +using AzureOpenAIProxy.ApiApp.Repositories; using AzureOpenAIProxy.ApiApp.Services; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; +using NSubstitute; + namespace AzureOpenAIProxy.ApiApp.Tests.Services; public class AdminEventServiceTests @@ -27,7 +30,8 @@ public void Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Throw_Excepti { // Arrange var eventDetails = new AdminEventDetails(); - var service = new AdminEventService(); + var repository = Substitute.For(); + var service = new AdminEventService(repository); // Act Func func = async () => await service.CreateEvent(eventDetails); @@ -40,7 +44,8 @@ public void Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Throw_Excepti public void Given_Instance_When_GetEvents_Invoked_Then_It_Should_Throw_Exception() { // Arrange - var service = new AdminEventService(); + var repository = Substitute.For(); + var service = new AdminEventService(repository); // Act Func func = async () => await service.GetEvents(); @@ -54,7 +59,8 @@ public void Given_Instance_When_GetEvent_Invoked_Then_It_Should_Throw_Exception( { // Arrange var eventId = Guid.NewGuid(); - var service = new AdminEventService(); + var repository = Substitute.For(); + var service = new AdminEventService(repository); // Act Func func = async () => await service.GetEvent(eventId); @@ -69,7 +75,8 @@ public void Given_Instance_When_UpdateEvent_Invoked_Then_It_Should_Throw_Excepti // Arrange var eventId = Guid.NewGuid(); var eventDetails = new AdminEventDetails(); - var service = new AdminEventService(); + var repository = Substitute.For(); + var service = new AdminEventService(repository); // Act Func func = async () => await service.UpdateEvent(eventId, eventDetails); From 43ed06156995472310f945d7d0ce48386a9ac5fb Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sun, 1 Sep 2024 15:42:12 +0900 Subject: [PATCH 07/13] [Playground] playground page layout (#291) --- .../Components/App.razor | 4 +-- .../Components/Layout/MainLayout.razor.css | 30 +++++++++---------- .../Components/Layout/NavMenu.razor.css | 14 ++++----- .../Components/Pages/Playground.razor | 15 ++++++++-- .../wwwroot/app.css | 2 +- .../Pages/PlaygroundPageTests.cs | 30 +++++++++++++++++++ 6 files changed, 68 insertions(+), 27 deletions(-) diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/App.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/App.razor index 6b73ab6b..4d95987f 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/App.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/App.razor @@ -5,10 +5,10 @@ - + @* *@ + - diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/Layout/MainLayout.razor.css b/src/AzureOpenAIProxy.PlaygroundApp/Components/Layout/MainLayout.razor.css index 038baf17..a6731151 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/Layout/MainLayout.razor.css +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/Layout/MainLayout.razor.css @@ -27,23 +27,23 @@ main { text-decoration: none; } - .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { - text-decoration: underline; - } + .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; + } - .top-row ::deep a:first-child { - overflow: hidden; - text-overflow: ellipsis; - } + .top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } @media (max-width: 640.98px) { .top-row { justify-content: space-between; } - .top-row ::deep a, .top-row ::deep .btn-link { - margin-left: 0; - } + .top-row ::deep a, .top-row ::deep .btn-link { + margin-left: 0; + } } @media (min-width: 641px) { @@ -64,11 +64,11 @@ main { z-index: 1; } - .top-row.auth ::deep a:first-child { - flex: 1; - text-align: right; - width: 0; - } + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } .top-row, article { padding-left: 2rem !important; diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/Layout/NavMenu.razor.css b/src/AzureOpenAIProxy.PlaygroundApp/Components/Layout/NavMenu.razor.css index 4e15395e..e8902497 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/Layout/NavMenu.razor.css +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/Layout/NavMenu.razor.css @@ -1,4 +1,4 @@ -.navbar-toggler { +/*.navbar-toggler { appearance: none; cursor: pointer; width: 3.5rem; @@ -94,12 +94,12 @@ display: none; } - .nav-scrollable { - /* Never collapse the sidebar for wide screens */ - display: block; + .nav-scrollable {*/ +/* Never collapse the sidebar for wide screens */ +/*display: block;*/ - /* Allow sidebar to scroll for tall menus */ - height: calc(100vh - 3.5rem); +/* Allow sidebar to scroll for tall menus */ +/*height: calc(100vh - 3.5rem); overflow-y: auto; } -} +}*/ diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Playground.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Playground.razor index 08f49d57..06fe0b5d 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Playground.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Playground.razor @@ -1,7 +1,18 @@ @page "/playground" +@rendermode InteractiveServer Playground Page -

playground page!

+ +

Azure OpenAI Proxy Playground

- \ No newline at end of file + + + + + + + Chat Window + + +
diff --git a/src/AzureOpenAIProxy.PlaygroundApp/wwwroot/app.css b/src/AzureOpenAIProxy.PlaygroundApp/wwwroot/app.css index 2bd9b789..5588cc12 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/wwwroot/app.css +++ b/src/AzureOpenAIProxy.PlaygroundApp/wwwroot/app.css @@ -13,7 +13,7 @@ a, .btn-link { } .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { - box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; } .content { diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaygroundPageTests.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaygroundPageTests.cs index 7b012f98..338975ce 100644 --- a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaygroundPageTests.cs +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaygroundPageTests.cs @@ -1,3 +1,5 @@ +using FluentAssertions; + using Microsoft.Playwright; using Microsoft.Playwright.NUnit; @@ -17,6 +19,34 @@ public async Task SetUp() await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); } + [Test] + public async Task Given_Page_When_Endpoint_Invoked_Then_It_Should_Show_Header() + { + // Arrange + var header = Page.Locator("div.layout") + .Locator("header.header") + .Locator("div.header-gutters") + .Locator("h1"); + + // Act + var headerText = await header.TextContentAsync(); + + // Assert + headerText.Should().Be("Azure OpenAI Proxy Playground"); + } + + [Test] + [TestCase("config-grid")] + [TestCase("chat-grid")] + public async Task Given_Page_When_Endpoint_Invoked_Then_It_Should_Show_Panels(string id) + { + // Act + var panel = Page.Locator($"div.{id}"); + + // Assert + await Expect(panel).ToBeVisibleAsync(); + } + [Test] public async Task Given_ConfigTab_When_Endpoint_Invoked_Then_ConfigTab_Should_Be_Displayed() { From 421e0739e76e9e2c3993b2a418b30549fa00da37 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sun, 1 Sep 2024 23:35:46 +0900 Subject: [PATCH 08/13] Add new event page (#296) --- .../Components/Pages/AdminNewEvent.razor | 5 ++ .../Pages/AdminNewEventPageTests.cs | 69 +++++++++++++++++++ .../PlaygroundApp/Pages/AdminPageTests.cs | 18 +++++ .../PlaygroundApp/Pages/HomePageTests.cs | 3 + .../Pages/PlaygroundPageTests.cs | 49 ++++++++++++- 5 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminNewEvent.razor create mode 100644 test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminNewEventPageTests.cs diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminNewEvent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminNewEvent.razor new file mode 100644 index 00000000..6341ab35 --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminNewEvent.razor @@ -0,0 +1,5 @@ +@page "/admin/events/new" + +New event + +

New event

diff --git a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminNewEventPageTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminNewEventPageTests.cs new file mode 100644 index 00000000..ced6d1df --- /dev/null +++ b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminNewEventPageTests.cs @@ -0,0 +1,69 @@ +using System.Net; + +using AzureOpenAIProxy.AppHost.Tests.Fixtures; + +using FluentAssertions; + +namespace AzureOpenAIProxy.AppHost.Tests.PlaygroundApp.Pages; + +public class AdminNewEventPageTests(AspireAppHostFixture host) : IClassFixture +{ + [Fact] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_OK() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("playgroundapp"); + await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var response = await httpClient.GetAsync("/admin/events/new"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + [Theory] + [InlineData("_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_CSS_Elements(string expected) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("playgroundapp"); + await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var html = await httpClient.GetStringAsync("/admin/events/new"); + + // Assert + html.Should().Contain(expected); + } + + [Theory] + [InlineData("_content/Microsoft.FluentUI.AspNetCore.Components/Microsoft.FluentUI.AspNetCore.Components.lib.module.js")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_JavaScript_Elements(string expected) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("playgroundapp"); + await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var html = await httpClient.GetStringAsync("/admin/events/new"); + + // Assert + html.Should().Contain(expected); + } + + [Theory] + [InlineData("
")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_HTML_Elements(string expected) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("playgroundapp"); + await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var html = await httpClient.GetStringAsync("/admin/events/new"); + + // Assert + html.Should().Contain(expected); + } +} \ No newline at end of file diff --git a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminPageTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminPageTests.cs index 07f8a419..e0e15ec2 100644 --- a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminPageTests.cs +++ b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminPageTests.cs @@ -5,6 +5,7 @@ using FluentAssertions; namespace AzureOpenAIProxy.AppHost.Tests.PlaygroundApp.Pages; + public class AdminPageTests(AspireAppHostFixture host) : IClassFixture { [Fact] @@ -21,12 +22,28 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_OK( response.StatusCode.Should().Be(HttpStatusCode.OK); } + [Theory] + [InlineData("_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_CSS_Elements(string expected) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("playgroundapp"); + await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var html = await httpClient.GetStringAsync("/admin"); + + // Assert + html.Should().Contain(expected); + } + [Theory] [InlineData("_content/Microsoft.FluentUI.AspNetCore.Components/Microsoft.FluentUI.AspNetCore.Components.lib.module.js")] public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_JavaScript_Elements(string expected) { // Arrange using var httpClient = host.App!.CreateHttpClient("playgroundapp"); + await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); // Act var html = await httpClient.GetStringAsync("/admin"); @@ -41,6 +58,7 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_HTM { // Arrange using var httpClient = host.App!.CreateHttpClient("playgroundapp"); + await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); // Act var html = await httpClient.GetStringAsync("/admin"); diff --git a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/HomePageTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/HomePageTests.cs index 8d3b9bc1..2ed277df 100644 --- a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/HomePageTests.cs +++ b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/HomePageTests.cs @@ -28,6 +28,7 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_CSS { // Arrange using var httpClient = host.App!.CreateHttpClient("playgroundapp"); + await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); // Act var html = await httpClient.GetStringAsync("/"); @@ -42,6 +43,7 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Jav { // Arrange using var httpClient = host.App!.CreateHttpClient("playgroundapp"); + await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); // Act var html = await httpClient.GetStringAsync("/"); @@ -56,6 +58,7 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_HTM { // Arrange using var httpClient = host.App!.CreateHttpClient("playgroundapp"); + await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); // Act var html = await httpClient.GetStringAsync("/"); diff --git a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/PlaygroundPageTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/PlaygroundPageTests.cs index 05b1b953..db9a3d6a 100644 --- a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/PlaygroundPageTests.cs +++ b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/PlaygroundPageTests.cs @@ -1,5 +1,7 @@ using AzureOpenAIProxy.AppHost.Tests.Fixtures; +using FluentAssertions; + namespace AzureOpenAIProxy.AppHost.Tests.PlaygroundApp.Pages; public class PlaygroundPageTests(AspireAppHostFixture host) : IClassFixture @@ -15,6 +17,51 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_OK( var response = await httpClient.GetAsync("/playground"); // Assert - response.EnsureSuccessStatusCode(); // Status Code 200-299 + response.EnsureSuccessStatusCode(); + } + + [Theory] + [InlineData("_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_CSS_Elements(string expected) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("playgroundapp"); + await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var html = await httpClient.GetStringAsync("/playground"); + + // Assert + html.Should().Contain(expected); + } + + [Theory] + [InlineData("_content/Microsoft.FluentUI.AspNetCore.Components/Microsoft.FluentUI.AspNetCore.Components.lib.module.js")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_JavaScript_Elements(string expected) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("playgroundapp"); + await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var html = await httpClient.GetStringAsync("/playground"); + + // Assert + html.Should().Contain(expected); + } + + [Theory] + [InlineData("
")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_HTML_Elements(string expected) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("playgroundapp"); + await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var html = await httpClient.GetStringAsync("/playground"); + + // Assert + html.Should().Contain(expected); } } \ No newline at end of file From c0aa2643ac2e1e7344d51b18a7aff983d6db4ccd Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Mon, 2 Sep 2024 00:44:59 +0900 Subject: [PATCH 09/13] Update debug button component (#297) --- .../Components/Pages/Tests.razor | 8 +++++--- .../Components/UI/DebugButtonComponent.razor | 5 ++++- .../Components/UI/DebugTargetComponent.razor | 5 ++++- .../Pages/HomePageTests.cs | 14 +++++++++----- .../Pages/TestsPageTests.cs | 18 +++++++++++++----- 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Tests.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Tests.razor index 0929588c..b9979cbb 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Tests.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Tests.razor @@ -1,14 +1,16 @@ @page "/tests" @rendermode InteractiveServer - - +

Debug Button

+ + @code { private object? currentValue; - private void SetInput(int newValue) + private async Task SetInput(int newValue) { currentValue = newValue; + await Task.CompletedTask; } } \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DebugButtonComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DebugButtonComponent.razor index 93abbb75..8335d49e 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DebugButtonComponent.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DebugButtonComponent.razor @@ -1,9 +1,12 @@ @inject IToastService ToastService -Debug +Debug @code { + [Parameter] + public string? Id { get; set; } + [Parameter] public object? Input { get; set; } diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DebugTargetComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DebugTargetComponent.razor index 4f2c30bb..bfb54641 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DebugTargetComponent.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DebugTargetComponent.razor @@ -1,10 +1,13 @@ - + 123 456 789 @code { + [Parameter] + public string? Id { get; set; } + [Parameter] public EventCallback OnValueChanged { get; set; } diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/HomePageTests.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/HomePageTests.cs index aad307da..e81b6ee1 100644 --- a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/HomePageTests.cs +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/HomePageTests.cs @@ -13,14 +13,18 @@ public class HomePageTests : PageTest IgnoreHTTPSErrors = true, }; - [Test] - public async Task Given_Root_Page_When_Navigated_Then_It_Should_No_Sidebar() + [SetUp] + public async Task Setup() { - // Arrange - await this.Page.GotoAsync("https://localhost:5001"); + await Page.GotoAsync("https://localhost:5001"); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + } + [Test] + public void Given_Root_Page_When_Navigated_Then_It_Should_No_Sidebar() + { // Act - var sidebar = this.Page.Locator("div.sidebar"); + var sidebar = Page.Locator("div.sidebar"); // Assert Expect(sidebar).Equals(null); diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/TestsPageTests.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/TestsPageTests.cs index f0b6f8c6..d9957c23 100644 --- a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/TestsPageTests.cs +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/TestsPageTests.cs @@ -22,10 +22,13 @@ public async Task Setup() } [Test] - public async Task Given_No_Input_When_DebugButton_Clicked_Then_Toast_Should_Show_NullMessage() + public async Task Given_No_Input_On_DebugTarget_When_DebugButton_Clicked_Then_Toast_Should_Show_NullMessage() { + // Arrange + var button = Page.Locator("fluent-button#debug-button"); + // Act - await Page.GetByRole(AriaRole.Button, new() { Name = "Debug" }).ClickAsync(); + await button.ClickAsync(); // Assert await Expect(Page.Locator(".fluent-toast-title")).ToHaveTextAsync("Input is null."); @@ -35,11 +38,16 @@ public async Task Given_No_Input_When_DebugButton_Clicked_Then_Toast_Should_Show [TestCase(123, typeof(int))] [TestCase(456, typeof(int))] [TestCase(789, typeof(int))] - public async Task Given_Input_When_DebugButton_Clicked_Then_Toast_Should_Show_Input(int inputValue, Type inputType) + public async Task Given_Input_On_DebugTarget_When_DebugButton_Clicked_Then_Toast_Should_Show_Input(int inputValue, Type inputType) { + // Arrange + var radio = Page.Locator("fluent-radio-group#debug-target") + .Locator($"fluent-radio[current-value='{inputValue}']"); + var button = Page.Locator("fluent-button#debug-button"); + // Act - await Page.GetByRole(AriaRole.Radio, new() { Name = $"{inputValue}" }).ClickAsync(); - await Page.GetByRole(AriaRole.Button, new() { Name = "Debug" }).ClickAsync(); + await radio.ClickAsync(); + await button.ClickAsync(); // Assert await Expect(Page.Locator(".fluent-toast-title")).ToHaveTextAsync($"{inputValue} (Type: {inputType})"); From 7a2c3b889b07ba589ee65a909bf988ecbda3d66e Mon Sep 17 00:00:00 2001 From: praivesi Date: Fri, 6 Sep 2024 13:20:35 +0900 Subject: [PATCH 10/13] [Admin] Component: List event details - UI component #219 (#266) --- .../Components/Pages/AdminEvents.razor | 9 ++ .../Admin/AdminEventIsActiveComponent.razor | 18 ++++ .../AdminEventIsActiveComponent.razor.css | 23 +++++ .../UI/Admin/AdminEventsComponent.razor | 89 +++++++++++++++++++ .../UI/Admin/AdminEventsComponent.razor.css | 17 ++++ .../Components/_Imports.razor | 3 +- .../Models/AdminEventDetails.cs | 60 +++++++++++++ .../Models/EventDetails.cs | 39 ++++++++ .../Pages/AdminEventsPageTests.cs | 47 ++++++++++ 9 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminEvents.razor create mode 100644 src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventIsActiveComponent.razor create mode 100644 src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventIsActiveComponent.razor.css create mode 100644 src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventsComponent.razor create mode 100644 src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventsComponent.razor.css create mode 100644 src/AzureOpenAIProxy.PlaygroundApp/Models/AdminEventDetails.cs create mode 100644 src/AzureOpenAIProxy.PlaygroundApp/Models/EventDetails.cs create mode 100644 test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/AdminEventsPageTests.cs diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminEvents.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminEvents.razor new file mode 100644 index 00000000..0882151e --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminEvents.razor @@ -0,0 +1,9 @@ +@page "/admin/events" + +AdminEvents + +

AdminEvents

+ +

This component demonstrates showing admin events.

+ + diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventIsActiveComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventIsActiveComponent.razor new file mode 100644 index 00000000..2940a276 --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventIsActiveComponent.razor @@ -0,0 +1,18 @@ +
+
+
+ +@code { + [Parameter] + public required bool IsActive { get; set; } + + private string GetActiveClass(bool? isActive) + { + if (!isActive.HasValue) + { + return "deactivated"; + } + + return isActive.Value ? "activated" : "deactivated"; + } +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventIsActiveComponent.razor.css b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventIsActiveComponent.razor.css new file mode 100644 index 00000000..a68c5fec --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventIsActiveComponent.razor.css @@ -0,0 +1,23 @@ +.admin-event-active-state { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; +} + +.activated { + width: 10px; + height: 10px; + background-color: green; + border-radius: 50%; + display: inline-block; +} + +.deactivated { + width: 10px; + height: 10px; + background-color: red; + border-radius: 50%; + display: inline-block; +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventsComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventsComponent.razor new file mode 100644 index 00000000..97db777b --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventsComponent.razor @@ -0,0 +1,89 @@ +@using AzureOpenAIProxy.PlaygroundApp.Models + +
+ @if (eventDetails == null) + { +

Loading...

+ } + else + { +
+ + + + + + + + + + + + + + + + + +
+ +
+ @if (pagination.TotalItemCount.HasValue) + { + for (var pageIndex = 0; pageIndex <= pagination.LastPageIndex; pageIndex++) + { + var capturedIndex = pageIndex; + + @(capturedIndex + 1) + + } + } +
+ } +
+ +@code { + private IQueryable? eventDetails; + private PaginationState pagination = new PaginationState { ItemsPerPage = 10 }; + + protected override async Task OnInitializedAsync() + { + // Simulate asynchronous loading to demonstrate streaming rendering + await Task.Delay(100); + + var startDate = DateOnly.FromDateTime(DateTime.Now); + + // make dummy data + eventDetails = Enumerable.Range(1, 150).Select(index => new AdminEventDetails + { + EventId = Guid.NewGuid(), + Title = $"event title #{index}", + Summary = "dummy summary", + Description = "dummy description", + DateStart = DateTimeOffset.Now, + DateEnd = DateTimeOffset.Now.AddDays(7 + index), + TimeZone = "KST", + IsActive = index % 3 == 0, + OrganizerName = $"Charlie_{index}", + OrganizerEmail = $"user_{index}@gmail.com", + CoorganizerName = $"Bravo_{index}", + CoorganizerEmail = $"support_{index}@gmail.com", + MaxTokenCap = (100 + index) * 100, + DailyRequestCap = index * 10 + }).AsQueryable(); + + pagination.TotalItemCountChanged += (sender, eventArgs) => StateHasChanged(); + } + + private async Task GoToPageAsync(int pageIndex) + { + await pagination.SetCurrentPageIndexAsync(pageIndex); + } + + private Appearance PageButtonAppearance(int pageIndex) + => pagination.CurrentPageIndex == pageIndex ? Appearance.Accent : Appearance.Neutral; + + private string? AriaCurrentValue(int pageIndex) + => pagination.CurrentPageIndex == pageIndex ? "page" : null; +} diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventsComponent.razor.css b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventsComponent.razor.css new file mode 100644 index 00000000..7eb01214 --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventsComponent.razor.css @@ -0,0 +1,17 @@ +.fluent-datagrid-cell { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} + +.page-button-box { + display: flex; + justify-content: center; + align-items: center; + margin-top: 20px; +} + +.page-button { + margin-left: 10px; +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/_Imports.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/_Imports.razor index beab74d2..1e0a44ec 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/_Imports.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/_Imports.razor @@ -14,4 +14,5 @@ @using AzureOpenAIProxy.PlaygroundApp @using AzureOpenAIProxy.PlaygroundApp.Components -@using AzureOpenAIProxy.PlaygroundApp.Components.UI \ No newline at end of file +@using AzureOpenAIProxy.PlaygroundApp.Components.UI +@using AzureOpenAIProxy.PlaygroundApp.Components.UI.Admin diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Models/AdminEventDetails.cs b/src/AzureOpenAIProxy.PlaygroundApp/Models/AdminEventDetails.cs new file mode 100644 index 00000000..0221a475 --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Models/AdminEventDetails.cs @@ -0,0 +1,60 @@ +using System.Text.Json.Serialization; + +namespace AzureOpenAIProxy.PlaygroundApp.Models; + +/// +/// This represent the event detail data for response by admin event endpoint. +/// +public class AdminEventDetails : EventDetails +{ + /// + /// Gets or sets the event description. + /// + public string? Description { get; set; } + + /// + /// Gets or sets the event start date. + /// + [JsonRequired] + public DateTimeOffset DateStart { get; set; } + + /// + /// Gets or sets the event end date. + /// + [JsonRequired] + public DateTimeOffset DateEnd { get; set; } + + /// + /// Gets or sets the event start to end date timezone. + /// + [JsonRequired] + public string TimeZone { get; set; } = string.Empty; + + /// + /// Gets or sets the event active status. + /// + [JsonRequired] + public bool IsActive { get; set; } + + /// + /// Gets or sets the event organizer name. + /// + [JsonRequired] + public string OrganizerName { get; set; } = string.Empty; + + /// + /// Gets or sets the event organizer email. + /// + [JsonRequired] + public string OrganizerEmail { get; set; } = string.Empty; + + /// + /// Gets or sets the event coorganizer name. + /// + public string? CoorganizerName { get; set; } + + /// + /// Gets or sets the event coorganizer email. + /// + public string? CoorganizerEmail { get; set; } +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Models/EventDetails.cs b/src/AzureOpenAIProxy.PlaygroundApp/Models/EventDetails.cs new file mode 100644 index 00000000..56cc3a7c --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Models/EventDetails.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace AzureOpenAIProxy.PlaygroundApp.Models; + +/// +/// This represents the event's detailed data for response by EventEndpoint. +/// +public class EventDetails +{ + /// + /// Gets or sets the event id. + /// + [JsonRequired] + public Guid EventId { get; set; } + + /// + /// Gets or sets the event title name. + /// + [JsonRequired] + public string Title { get; set; } = string.Empty; + + /// + /// Gets or sets the event summary. + /// + [JsonRequired] + public string Summary { get; set; } = string.Empty; + + /// + /// Gets or sets the Azure OpenAI Service request max token capacity. + /// + [JsonRequired] + public int MaxTokenCap { get; set; } + + /// + /// Gets or sets the Azure OpenAI Service daily request capacity. + /// + [JsonRequired] + public int DailyRequestCap { get; set; } +} \ No newline at end of file diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/AdminEventsPageTests.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/AdminEventsPageTests.cs new file mode 100644 index 00000000..9fe77857 --- /dev/null +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/AdminEventsPageTests.cs @@ -0,0 +1,47 @@ +using FluentAssertions; + +using Microsoft.Playwright; +using Microsoft.Playwright.NUnit; + +namespace AzureOpenAIProxy.PlaygroundApp.Tests.Pages; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +[Property("Category", "Integration")] +public class AdminEventsPageTests : PageTest +{ + public override BrowserNewContextOptions ContextOptions() => new() + { + IgnoreHTTPSErrors = true, + }; + + [SetUp] + public async Task Setup() + { + await Page.GotoAsync("https://localhost:5001/admin/events"); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + } + + [Test] + public async Task Given_Events_Page_When_Navigated_Then_It_Should_Have_ListEventDetailsComponent() + { + // Act + var adminEventsComponent = await Page.QuerySelectorAsync("#admin-events-component"); + + // Assert + adminEventsComponent.Should().NotBeNull(); + } + + [Test] + public async Task Given_Events_Page_When_Navigated_Then_It_Should_Have_EventDetailsTable() + { + // wait for construct table + await Task.Delay(2000); + + // Act + var adminEventsTable = await Page.QuerySelectorAsync("#admin-events-table"); + + // Assert + adminEventsTable.Should().NotBeNull(); + } +} From a074158dc2ee30590b87483cdac6648d177dac39 Mon Sep 17 00:00:00 2001 From: YooJung Chun Date: Fri, 6 Sep 2024 14:20:27 +0900 Subject: [PATCH 11/13] [Playground] Component - list of deployment models (UI only) #172 (#260) --- .../Components/Pages/Tests.razor | 12 +- .../UI/DeploymentModelListComponent.razor | 73 ++++++++++++ ...zureOpenAIProxy.PlaygroundApp.Tests.csproj | 4 +- .../Pages/TestsPageTests.cs | 17 +++ .../UI/DeploymentModelListComponentTests.cs | 106 ++++++++++++++++++ 5 files changed, 209 insertions(+), 3 deletions(-) create mode 100644 src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DeploymentModelListComponent.razor create mode 100644 test/AzureOpenAIProxy.PlaygroundApp.Tests/UI/DeploymentModelListComponentTests.cs diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Tests.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Tests.razor index b9979cbb..dcc16730 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Tests.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Tests.razor @@ -5,12 +5,22 @@ + + + @code { private object? currentValue; + private string? selectedModelValue = ""; private async Task SetInput(int newValue) { currentValue = newValue; await Task.CompletedTask; } -} \ No newline at end of file + + private async Task HandleSelectedModelValue(string val) + { + selectedModelValue = val; + await Task.CompletedTask; + } +} diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DeploymentModelListComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DeploymentModelListComponent.razor new file mode 100644 index 00000000..13783dd1 --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DeploymentModelListComponent.razor @@ -0,0 +1,73 @@ + +
+ + * +
+ + + +
+ +@code { + [Parameter] + public string? Id { get; set; } + private Option? selectedOption { get; set; } = new(); + + [Parameter] + public EventCallback OnUserOptionSelected { get; set; } + + private async Task OnValueChanged() + { + string? selectedValue = selectedOption?.Value?.ToString(); + await OnUserOptionSelected.InvokeAsync(selectedValue); + } + + static List> deploymentModelOptions = new() + { + new Option { Value = "AL", Text = "Alabama" }, + new Option { Value = "AK", Text = "Alaska" }, + new Option { Value = "AZ", Text = "Arizona" }, + new Option { Value = "AR", Text = "Arkansas" }, + new Option { Value = "CA", Text = "California" }, + new Option { Value = "CO", Text = "Colorado" }, + new Option { Value = "CT", Text = "Connecticut" }, + new Option { Value = "DE", Text = "Delaware" }, + new Option { Value = "FL", Text = "Florida" }, + new Option { Value = "GA", Text = "Georgia" }, + new Option { Value = "HI", Text = "Hawaii" }, + new Option { Value = "ID", Text = "Idaho" }, + new Option { Value = "IL", Text = "Illinois" }, + new Option { Value = "IN", Text = "Indiana" }, + new Option { Value = "IA", Text = "Iowa" }, + new Option { Value = "KS", Text = "Kansas" }, + new Option { Value = "KY", Text = "Kentucky" }, + new Option { Value = "LA", Text = "Louisiana" }, + new Option { Value = "ME", Text = "Maine" }, + new Option { Value = "MD", Text = "Maryland" }, + new Option { Value = "MA", Text = "Massachussets" }, + new Option { Value = "MI", Text = "Michigain" }, + new Option { Value = "MN", Text = "Minnesota" }, + new Option { Value = "MS", Text = "Mississippi" }, + new Option { Value = "MO", Text = "Missouri" }, + new Option { Value = "MT", Text = "Montana" }, + new Option { Value = "NE", Text = "Nebraska" }, + new Option { Value = "NV", Text = "Nevada" }, + new Option { Value = "NH", Text = "New Hampshire" }, + new Option { Value = "NJ", Text = "New Jersey" }, + new Option { Value = "NM", Text = "New Mexico" }, + new Option { Value = "NY", Text = "New York" }, + new Option { Value = "NC", Text = "North Carolina" }, + new Option { Value = "ND", Text = "North Dakota" }, + new Option { Value = "OH", Text = "Ohio" }, + new Option { Value = "OK", Text = "Oklahoma" }, + new Option { Value = "OR", Text = "Oregon" }, + new Option { Value = "PA", Text = "Pennsylvania" } + }; +} \ No newline at end of file diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/AzureOpenAIProxy.PlaygroundApp.Tests.csproj b/test/AzureOpenAIProxy.PlaygroundApp.Tests/AzureOpenAIProxy.PlaygroundApp.Tests.csproj index 0b9dccf2..0ed50954 100644 --- a/test/AzureOpenAIProxy.PlaygroundApp.Tests/AzureOpenAIProxy.PlaygroundApp.Tests.csproj +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/AzureOpenAIProxy.PlaygroundApp.Tests.csproj @@ -23,9 +23,9 @@ - + --> diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/TestsPageTests.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/TestsPageTests.cs index d9957c23..4acd2d1d 100644 --- a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/TestsPageTests.cs +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/TestsPageTests.cs @@ -21,6 +21,23 @@ public async Task Setup() await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); } + [Test] + [TestCase("debug-target")] + [TestCase("debug-button")] + [TestCase("deployment-model-list")] + [TestCase("debug-button-selected-model")] + public async Task Given_ComponentID_When_Page_Loaded_Then_Component_Should_Be_Visible(string id) + { + // Arrange + var expectedId = id; + + // Act + var component = Page.Locator($"#{expectedId}"); + + // Assert + await Expect(component).ToBeVisibleAsync(); + } + [Test] public async Task Given_No_Input_On_DebugTarget_When_DebugButton_Clicked_Then_Toast_Should_Show_NullMessage() { diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/UI/DeploymentModelListComponentTests.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/UI/DeploymentModelListComponentTests.cs new file mode 100644 index 00000000..d418011d --- /dev/null +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/UI/DeploymentModelListComponentTests.cs @@ -0,0 +1,106 @@ +using Microsoft.Playwright.NUnit; +using Microsoft.Playwright; +using FluentAssertions; + +namespace AzureOpenAIProxy.PlaygroundApp.Tests.UI +{ + [Parallelizable(ParallelScope.Self)] + [TestFixture] + [Property("Category", "Integration")] + public class ModelDropdownListComponentTests : PageTest + { + public override BrowserNewContextOptions ContextOptions() => new() + { + IgnoreHTTPSErrors = true, + }; + + [SetUp] + public async Task Init() + { + await Page.GotoAsync("http://localhost:5000/tests"); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + } + + [Test] + public async Task Given_DropdownComponentID_When_Page_Loaded_Then_DropdownComponent_Should_Be_Visible() + { + // Arrange + var expectedId = "deployment-model-list"; + + // Act + var component = Page.Locator($"#{expectedId}"); + + // Assert + await Expect(component).ToBeVisibleAsync(); + } + + [Test] + // 페이지에서 컴포넌트 레이블이 올바르게 표시되는지 확인 + public async Task Given_Label_When_Page_Loaded_Then_Label_Should_Be_Visible() + { + // Act + var label = Page.GetByText("Deployment"); + + // Assert + await Expect(label).ToBeVisibleAsync(); + } + + [Test] + // 페이지에서 드롭다운 컴포넌트가 올바르게 표시되는지 확인 + public async Task Given_DropdownList_When_Page_Loaded_Then_DropdownList_Should_Be_Visible() + { + // Act + var fluentSelect = Page.Locator("fluent-select#deployment-model-list-options"); + + // Assert + await Expect(fluentSelect).ToBeVisibleAsync(); + } + + [Test] + // 드롭다운의 옵션 값이 존재하는지 확인 + public async Task Given_DropdownList_When_DropdownList_Clicked_And_DropdownOptions_Appeared_Then_All_DropdownOptions_Should_Be_Visible() + { + // Arrange + var fluentSelect = Page.Locator("fluent-select#deployment-model-list-options"); + + // Act + await fluentSelect.ClickAsync(); + var fluentOptions = fluentSelect.Locator("fluent-option"); + + // Assert + for (int i = 0; i < await fluentOptions.CountAsync(); i++) + { + await Expect(fluentOptions.Nth(i)).ToBeVisibleAsync(); + } + } + + [Test] + [TestCase("AZ", 2)] + [TestCase("CA", 4)] + [TestCase("CT", 6)] + [TestCase("FL", 8)] + // 드롭다운의 옵션 값을 선택하면 부모 컴포넌트(페이지 컴포넌트)에 올바르게 업데이트 되는지 확인 + public async Task Given_DropdownOptions_And_ExpectedValue_When_Third_DropdownOption_Selected_And_DropdownValue_Updated_Then_DropdownValue_Should_Match_ExpectedValue(string exp, int n) + { + // Arrange + var fluentSelect = Page.Locator("fluent-select#deployment-model-list-options"); + await fluentSelect.ClickAsync(); + var fluentOptions = fluentSelect.Locator("fluent-option"); + var expectedValue = exp; // 실제 컴포넌트 옵션 값 + + // Act + await fluentOptions.Nth(n).ScrollIntoViewIfNeededAsync(); // 선택할 컴포넌트 옵션 보이도록 스크롤 + await fluentOptions.Nth(n).ClickAsync(); // 옵션 클릭 + var actualValue = await Page.EvaluateAsync("() => document.querySelector('fluent-select#deployment-model-list-options').value"); // 페이지 내 컴포넌트 값 가져오기 + + // Assert + actualValue.Should().Be(expectedValue); + } + + [TearDown] + public async Task CleanUp() + { + await Page.CloseAsync(); + } + } +} \ No newline at end of file From 54ef75a7272e819b36e9939e7c1ec93104258161 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Fri, 6 Sep 2024 15:35:28 +0900 Subject: [PATCH 12/13] Update UI test methods (#301) --- .../Components/Pages/AdminEvents.razor | 2 +- .../Components/Pages/Tests.razor | 55 ++--- .../UI/Admin/AdminEventsComponent.razor | 7 +- .../UI/DeploymentModelListComponent.razor | 3 +- .../Pages/AdminEventsPageTests.cs | 10 +- .../Pages/HomePageTests.cs | 70 +++--- .../Pages/PlaygroundPageTests.cs | 206 +++++++++--------- .../Pages/TestsPageTests.cs | 198 +++++++++++------ .../UI/DeploymentModelListComponentTests.cs | 106 --------- 9 files changed, 316 insertions(+), 341 deletions(-) delete mode 100644 test/AzureOpenAIProxy.PlaygroundApp.Tests/UI/DeploymentModelListComponentTests.cs diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminEvents.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminEvents.razor index 0882151e..a70d183e 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminEvents.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminEvents.razor @@ -6,4 +6,4 @@

This component demonstrates showing admin events.

- + diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Tests.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Tests.razor index dcc16730..40ebae28 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Tests.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Tests.razor @@ -1,26 +1,29 @@ -@page "/tests" -@rendermode InteractiveServer - -

Debug Button

- - - - - - -@code { - private object? currentValue; - private string? selectedModelValue = ""; - - private async Task SetInput(int newValue) - { - currentValue = newValue; - await Task.CompletedTask; - } - - private async Task HandleSelectedModelValue(string val) - { - selectedModelValue = val; - await Task.CompletedTask; - } -} +@page "/tests" +@rendermode InteractiveServer + +

Component Tests

+ +

Debug Button

+ + + +

Deployment Models

+ + + +@code { + private object? targetValue; + private string? selectedModel; + + private async Task SetInput(int value) + { + targetValue = value; + await Task.CompletedTask; + } + + private async Task SetDeploymentModel(string value) + { + selectedModel = value; + await Task.CompletedTask; + } +} diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventsComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventsComponent.razor index 97db777b..09c43446 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventsComponent.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventsComponent.razor @@ -1,6 +1,6 @@ @using AzureOpenAIProxy.PlaygroundApp.Models -
+ @if (eventDetails == null) {

Loading...

@@ -41,12 +41,15 @@ }
} - + @code { private IQueryable? eventDetails; private PaginationState pagination = new PaginationState { ItemsPerPage = 10 }; + [Parameter] + public string? Id { get; set; } + protected override async Task OnInitializedAsync() { // Simulate asynchronous loading to demonstrate streaming rendering diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DeploymentModelListComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DeploymentModelListComponent.razor index 13783dd1..ac39ba4c 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DeploymentModelListComponent.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/DeploymentModelListComponent.razor @@ -16,9 +16,10 @@ @code { + private Option? selectedOption { get; set; } = new(); + [Parameter] public string? Id { get; set; } - private Option? selectedOption { get; set; } = new(); [Parameter] public EventCallback OnUserOptionSelected { get; set; } diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/AdminEventsPageTests.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/AdminEventsPageTests.cs index 9fe77857..6708b13b 100644 --- a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/AdminEventsPageTests.cs +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/AdminEventsPageTests.cs @@ -16,7 +16,7 @@ public class AdminEventsPageTests : PageTest }; [SetUp] - public async Task Setup() + public async Task Init() { await Page.GotoAsync("https://localhost:5001/admin/events"); await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); @@ -26,7 +26,7 @@ public async Task Setup() public async Task Given_Events_Page_When_Navigated_Then_It_Should_Have_ListEventDetailsComponent() { // Act - var adminEventsComponent = await Page.QuerySelectorAsync("#admin-events-component"); + var adminEventsComponent = await Page.QuerySelectorAsync("#admin-events"); // Assert adminEventsComponent.Should().NotBeNull(); @@ -44,4 +44,10 @@ public async Task Given_Events_Page_When_Navigated_Then_It_Should_Have_EventDeta // Assert adminEventsTable.Should().NotBeNull(); } + + [TearDown] + public async Task CleanUp() + { + await Page.CloseAsync(); + } } diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/HomePageTests.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/HomePageTests.cs index e81b6ee1..61d2e50d 100644 --- a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/HomePageTests.cs +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/HomePageTests.cs @@ -1,32 +1,38 @@ -using Microsoft.Playwright; -using Microsoft.Playwright.NUnit; - -namespace AzureOpenAIProxy.PlaygroundApp.Tests.Pages; - -[Parallelizable(ParallelScope.Self)] -[TestFixture] -[Property("Category", "Integration")] -public class HomePageTests : PageTest -{ - public override BrowserNewContextOptions ContextOptions() => new() - { - IgnoreHTTPSErrors = true, - }; - - [SetUp] - public async Task Setup() - { - await Page.GotoAsync("https://localhost:5001"); - await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); - } - - [Test] - public void Given_Root_Page_When_Navigated_Then_It_Should_No_Sidebar() - { - // Act - var sidebar = Page.Locator("div.sidebar"); - - // Assert - Expect(sidebar).Equals(null); - } -} +using Microsoft.Playwright; +using Microsoft.Playwright.NUnit; + +namespace AzureOpenAIProxy.PlaygroundApp.Tests.Pages; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +[Property("Category", "Integration")] +public class HomePageTests : PageTest +{ + public override BrowserNewContextOptions ContextOptions() => new() + { + IgnoreHTTPSErrors = true, + }; + + [SetUp] + public async Task Init() + { + await Page.GotoAsync("https://localhost:5001"); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + } + + [Test] + public void Given_Root_Page_When_Navigated_Then_It_Should_No_Sidebar() + { + // Act + var sidebar = Page.Locator("div.sidebar"); + + // Assert + Expect(sidebar).Equals(null); + } + + [TearDown] + public async Task CleanUp() + { + await Page.CloseAsync(); + } +} diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaygroundPageTests.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaygroundPageTests.cs index 338975ce..c4b4da2a 100644 --- a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaygroundPageTests.cs +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaygroundPageTests.cs @@ -1,101 +1,107 @@ -using FluentAssertions; - -using Microsoft.Playwright; -using Microsoft.Playwright.NUnit; - -namespace AzureOpenAIProxy.PlaygroundApp.Tests.Pages; - -[Parallelizable(ParallelScope.Self)] -[TestFixture] -[Property("Category", "Integration")] -public class PlaygroundPageTests : PageTest -{ - public override BrowserNewContextOptions ContextOptions() => new() { IgnoreHTTPSErrors = true, }; - - [SetUp] - public async Task SetUp() - { - await Page.GotoAsync("https://localhost:5001/playground/"); - await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); - } - - [Test] - public async Task Given_Page_When_Endpoint_Invoked_Then_It_Should_Show_Header() - { - // Arrange - var header = Page.Locator("div.layout") - .Locator("header.header") - .Locator("div.header-gutters") - .Locator("h1"); - - // Act - var headerText = await header.TextContentAsync(); - - // Assert - headerText.Should().Be("Azure OpenAI Proxy Playground"); - } - - [Test] - [TestCase("config-grid")] - [TestCase("chat-grid")] - public async Task Given_Page_When_Endpoint_Invoked_Then_It_Should_Show_Panels(string id) - { - // Act - var panel = Page.Locator($"div.{id}"); - - // Assert - await Expect(panel).ToBeVisibleAsync(); - } - - [Test] - public async Task Given_ConfigTab_When_Endpoint_Invoked_Then_ConfigTab_Should_Be_Displayed() - { - // Act - var configTab = Page.Locator("fluent-tabs#config-tab"); - - // Assert - await Expect(configTab).ToBeVisibleAsync(); - } - - [Test] - public async Task Given_ConfigTab_When_Endpoint_Invoked_Then_Id_Should_Be_System_Message_Tab() - { - // Act - var systemMessagePanel = Page.Locator("fluent-tab-panel#system-message-tab-panel"); - var parameterPanel = Page.Locator("fluent-tab-panel#parameters-tab-panel"); - - // Assert - await Expect(systemMessagePanel).ToBeVisibleAsync(); - await Expect(parameterPanel).ToBeHiddenAsync(); - } - - [Test] - [TestCase( - "fluent-tab#parameters-tab", - "fluent-tab-panel#parameters-tab-panel", - "fluent-tab-panel#system-message-tab-panel" - )] - [TestCase( - "fluent-tab#system-message-tab", - "fluent-tab-panel#system-message-tab-panel", - "fluent-tab-panel#parameters-tab-panel" - )] - public async Task Given_ConfigTab_When_Changed_Then_Tab_Should_Be_Updated( - string selectedTabSelector, - string selectedPanelSelector, - string hiddenPanelSelector - ) - { - // Arrange - var selectedTab = Page.Locator(selectedTabSelector); - var selectedPanel = Page.Locator(selectedPanelSelector); - var hiddenPanel = Page.Locator(hiddenPanelSelector); - - // Act - await selectedTab.ClickAsync(); - - // Assert - await Expect(selectedPanel).ToBeVisibleAsync(); - await Expect(hiddenPanel).ToBeHiddenAsync(); - } +using FluentAssertions; + +using Microsoft.Playwright; +using Microsoft.Playwright.NUnit; + +namespace AzureOpenAIProxy.PlaygroundApp.Tests.Pages; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +[Property("Category", "Integration")] +public class PlaygroundPageTests : PageTest +{ + public override BrowserNewContextOptions ContextOptions() => new() { IgnoreHTTPSErrors = true, }; + + [SetUp] + public async Task Init() + { + await Page.GotoAsync("https://localhost:5001/playground/"); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + } + + [Test] + public async Task Given_Page_When_Endpoint_Invoked_Then_It_Should_Show_Header() + { + // Arrange + var header = Page.Locator("div.layout") + .Locator("header.header") + .Locator("div.header-gutters") + .Locator("h1"); + + // Act + var headerText = await header.TextContentAsync(); + + // Assert + headerText.Should().Be("Azure OpenAI Proxy Playground"); + } + + [Test] + [TestCase("config-grid")] + [TestCase("chat-grid")] + public async Task Given_Page_When_Endpoint_Invoked_Then_It_Should_Show_Panels(string id) + { + // Act + var panel = Page.Locator($"div.{id}"); + + // Assert + await Expect(panel).ToBeVisibleAsync(); + } + + [Test] + public async Task Given_ConfigTab_When_Endpoint_Invoked_Then_ConfigTab_Should_Be_Displayed() + { + // Act + var configTab = Page.Locator("fluent-tabs#config-tab"); + + // Assert + await Expect(configTab).ToBeVisibleAsync(); + } + + [Test] + public async Task Given_ConfigTab_When_Endpoint_Invoked_Then_Id_Should_Be_System_Message_Tab() + { + // Act + var systemMessagePanel = Page.Locator("fluent-tab-panel#system-message-tab-panel"); + var parameterPanel = Page.Locator("fluent-tab-panel#parameters-tab-panel"); + + // Assert + await Expect(systemMessagePanel).ToBeVisibleAsync(); + await Expect(parameterPanel).ToBeHiddenAsync(); + } + + [Test] + [TestCase( + "fluent-tab#parameters-tab", + "fluent-tab-panel#parameters-tab-panel", + "fluent-tab-panel#system-message-tab-panel" + )] + [TestCase( + "fluent-tab#system-message-tab", + "fluent-tab-panel#system-message-tab-panel", + "fluent-tab-panel#parameters-tab-panel" + )] + public async Task Given_ConfigTab_When_Changed_Then_Tab_Should_Be_Updated( + string selectedTabSelector, + string selectedPanelSelector, + string hiddenPanelSelector + ) + { + // Arrange + var selectedTab = Page.Locator(selectedTabSelector); + var selectedPanel = Page.Locator(selectedPanelSelector); + var hiddenPanel = Page.Locator(hiddenPanelSelector); + + // Act + await selectedTab.ClickAsync(); + + // Assert + await Expect(selectedPanel).ToBeVisibleAsync(); + await Expect(hiddenPanel).ToBeHiddenAsync(); + } + + [TearDown] + public async Task CleanUp() + { + await Page.CloseAsync(); + } } \ No newline at end of file diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/TestsPageTests.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/TestsPageTests.cs index 4acd2d1d..0d4ca2f1 100644 --- a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/TestsPageTests.cs +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/TestsPageTests.cs @@ -1,72 +1,128 @@ -using Microsoft.Playwright; -using Microsoft.Playwright.NUnit; - -namespace AzureOpenAIProxy.PlaygroundApp.Tests.Pages; - -[Parallelizable(ParallelScope.Self)] -[TestFixture] -[Property("Category", "Integration")] -public class TestsPageTests : PageTest -{ - public override BrowserNewContextOptions ContextOptions() => new() - { - IgnoreHTTPSErrors = true, - }; - - [SetUp] - public async Task Setup() - { - // Arrange - await Page.GotoAsync("https://localhost:5001/tests"); - await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); - } - - [Test] - [TestCase("debug-target")] - [TestCase("debug-button")] - [TestCase("deployment-model-list")] - [TestCase("debug-button-selected-model")] - public async Task Given_ComponentID_When_Page_Loaded_Then_Component_Should_Be_Visible(string id) - { - // Arrange - var expectedId = id; - - // Act - var component = Page.Locator($"#{expectedId}"); - - // Assert - await Expect(component).ToBeVisibleAsync(); - } - - [Test] - public async Task Given_No_Input_On_DebugTarget_When_DebugButton_Clicked_Then_Toast_Should_Show_NullMessage() - { - // Arrange - var button = Page.Locator("fluent-button#debug-button"); - - // Act - await button.ClickAsync(); - - // Assert - await Expect(Page.Locator(".fluent-toast-title")).ToHaveTextAsync("Input is null."); - } - - [Test] - [TestCase(123, typeof(int))] - [TestCase(456, typeof(int))] - [TestCase(789, typeof(int))] - public async Task Given_Input_On_DebugTarget_When_DebugButton_Clicked_Then_Toast_Should_Show_Input(int inputValue, Type inputType) - { - // Arrange - var radio = Page.Locator("fluent-radio-group#debug-target") - .Locator($"fluent-radio[current-value='{inputValue}']"); - var button = Page.Locator("fluent-button#debug-button"); - - // Act - await radio.ClickAsync(); - await button.ClickAsync(); - - // Assert - await Expect(Page.Locator(".fluent-toast-title")).ToHaveTextAsync($"{inputValue} (Type: {inputType})"); - } +using FluentAssertions; + +using Microsoft.Playwright; +using Microsoft.Playwright.NUnit; + +namespace AzureOpenAIProxy.PlaygroundApp.Tests.Pages; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +[Property("Category", "Integration")] +public class TestsPageTests : PageTest +{ + public override BrowserNewContextOptions ContextOptions() => new() + { + IgnoreHTTPSErrors = true, + }; + + [SetUp] + public async Task Setup() + { + // Arrange + await Page.GotoAsync("https://localhost:5001/tests"); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + } + + [Test] + public async Task Given_No_Input_On_DebugTarget_When_DebugButton_Clicked_Then_Toast_Should_Show_NullMessage() + { + // Arrange + var button = Page.Locator("fluent-button#debug-button"); + + // Act + await button.ClickAsync(); + + // Assert + await Expect(Page.Locator(".fluent-toast-title")).ToHaveTextAsync("Input is null."); + } + + [Test] + [TestCase(123, typeof(int))] + [TestCase(456, typeof(int))] + [TestCase(789, typeof(int))] + public async Task Given_Input_On_DebugTarget_When_DebugButton_Clicked_Then_Toast_Should_Show_Input(int inputValue, Type expectedType) + { + // Arrange + var radio = Page.Locator("fluent-radio-group#debug-target") + .Locator($"fluent-radio[current-value='{inputValue}']"); + var button = Page.Locator("fluent-button#debug-button"); + + // Act + await radio.ClickAsync(); + await button.ClickAsync(); + + // Assert + await Expect(Page.Locator(".fluent-toast-title")).ToHaveTextAsync($"{inputValue} (Type: {expectedType})"); + } + + [Test] + [TestCase("deployment-model-label", "Deployment")] + public async Task Given_Label_When_Page_Loaded_Then_Label_Should_Be_Visible(string id, string expected) + { + // Arrange + var label = Page.Locator($"label#{id}"); + + // Act + var result = await label.TextContentAsync(); + + // Assert + result.Should().Be(expected); + } + + [Test] + [TestCase("deployment-model-list-options")] + public async Task Given_DropdownList_When_Page_Loaded_Then_DropdownList_Should_Be_Visible(string id) + { + // Act + var select = Page.Locator($"fluent-select#{id}"); + + // Assert + await Expect(select).ToBeVisibleAsync(); + } + + [Test] + [TestCase("deployment-model-list-options")] + public async Task Given_DropdownList_When_DropdownList_Clicked_And_DropdownOptions_Appeared_Then_All_DropdownOptions_Should_Be_Visible(string id) + { + // Arrange + var fluentSelect = Page.Locator($"fluent-select#{id}"); + + // Act + await fluentSelect.ClickAsync(); + var fluentOptions = fluentSelect.Locator("fluent-option"); + + // Assert + for (int i = 0; i < await fluentOptions.CountAsync(); i++) + { + await Expect(fluentOptions.Nth(i)).ToBeVisibleAsync(); + } + } + + [Test] + [TestCase(2, "AZ", typeof(string))] + [TestCase(4, "CA", typeof(string))] + [TestCase(6, "CT", typeof(string))] + [TestCase(8, "FL", typeof(string))] + public async Task Given_DropdownOptions_And_ExpectedValue_When_Third_DropdownOption_Selected_And_DropdownValue_Updated_Then_DropdownValue_Should_Match_ExpectedValue(int index, string expectedValue, Type expectedType) + { + // Arrange + var fluentSelect = Page.Locator("fluent-select#deployment-model-list-options"); + await fluentSelect.ClickAsync(); + var fluentOptions = fluentSelect.Locator("fluent-option"); + var button = Page.Locator("fluent-button#debug-button-deployment-model-list"); + + // Act + await fluentOptions.Nth(index).ScrollIntoViewIfNeededAsync(); + await fluentOptions.Nth(index).ClickAsync(); + await button.ClickAsync(); + + // Assert + await Expect(Page.Locator(".fluent-toast-title")).ToHaveTextAsync($"{expectedValue} (Type: {expectedType})"); + } + + [TearDown] + public async Task CleanUp() + { + await Page.CloseAsync(); + } } \ No newline at end of file diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/UI/DeploymentModelListComponentTests.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/UI/DeploymentModelListComponentTests.cs deleted file mode 100644 index d418011d..00000000 --- a/test/AzureOpenAIProxy.PlaygroundApp.Tests/UI/DeploymentModelListComponentTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -using Microsoft.Playwright.NUnit; -using Microsoft.Playwright; -using FluentAssertions; - -namespace AzureOpenAIProxy.PlaygroundApp.Tests.UI -{ - [Parallelizable(ParallelScope.Self)] - [TestFixture] - [Property("Category", "Integration")] - public class ModelDropdownListComponentTests : PageTest - { - public override BrowserNewContextOptions ContextOptions() => new() - { - IgnoreHTTPSErrors = true, - }; - - [SetUp] - public async Task Init() - { - await Page.GotoAsync("http://localhost:5000/tests"); - await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); - } - - [Test] - public async Task Given_DropdownComponentID_When_Page_Loaded_Then_DropdownComponent_Should_Be_Visible() - { - // Arrange - var expectedId = "deployment-model-list"; - - // Act - var component = Page.Locator($"#{expectedId}"); - - // Assert - await Expect(component).ToBeVisibleAsync(); - } - - [Test] - // 페이지에서 컴포넌트 레이블이 올바르게 표시되는지 확인 - public async Task Given_Label_When_Page_Loaded_Then_Label_Should_Be_Visible() - { - // Act - var label = Page.GetByText("Deployment"); - - // Assert - await Expect(label).ToBeVisibleAsync(); - } - - [Test] - // 페이지에서 드롭다운 컴포넌트가 올바르게 표시되는지 확인 - public async Task Given_DropdownList_When_Page_Loaded_Then_DropdownList_Should_Be_Visible() - { - // Act - var fluentSelect = Page.Locator("fluent-select#deployment-model-list-options"); - - // Assert - await Expect(fluentSelect).ToBeVisibleAsync(); - } - - [Test] - // 드롭다운의 옵션 값이 존재하는지 확인 - public async Task Given_DropdownList_When_DropdownList_Clicked_And_DropdownOptions_Appeared_Then_All_DropdownOptions_Should_Be_Visible() - { - // Arrange - var fluentSelect = Page.Locator("fluent-select#deployment-model-list-options"); - - // Act - await fluentSelect.ClickAsync(); - var fluentOptions = fluentSelect.Locator("fluent-option"); - - // Assert - for (int i = 0; i < await fluentOptions.CountAsync(); i++) - { - await Expect(fluentOptions.Nth(i)).ToBeVisibleAsync(); - } - } - - [Test] - [TestCase("AZ", 2)] - [TestCase("CA", 4)] - [TestCase("CT", 6)] - [TestCase("FL", 8)] - // 드롭다운의 옵션 값을 선택하면 부모 컴포넌트(페이지 컴포넌트)에 올바르게 업데이트 되는지 확인 - public async Task Given_DropdownOptions_And_ExpectedValue_When_Third_DropdownOption_Selected_And_DropdownValue_Updated_Then_DropdownValue_Should_Match_ExpectedValue(string exp, int n) - { - // Arrange - var fluentSelect = Page.Locator("fluent-select#deployment-model-list-options"); - await fluentSelect.ClickAsync(); - var fluentOptions = fluentSelect.Locator("fluent-option"); - var expectedValue = exp; // 실제 컴포넌트 옵션 값 - - // Act - await fluentOptions.Nth(n).ScrollIntoViewIfNeededAsync(); // 선택할 컴포넌트 옵션 보이도록 스크롤 - await fluentOptions.Nth(n).ClickAsync(); // 옵션 클릭 - var actualValue = await Page.EvaluateAsync("() => document.querySelector('fluent-select#deployment-model-list-options').value"); // 페이지 내 컴포넌트 값 가져오기 - - // Assert - actualValue.Should().Be(expectedValue); - } - - [TearDown] - public async Task CleanUp() - { - await Page.CloseAsync(); - } - } -} \ No newline at end of file From c8c1e83d6c6d9ed94a8eed7c607ae281db62d08c Mon Sep 17 00:00:00 2001 From: Jihong Min <158474415+jhmin99@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:12:02 +0900 Subject: [PATCH 13/13] [Chat Completions] Response payload definition #22 (backup) (#298) --- .../Converters/EnumMemberConverter.cs | 72 + .../Endpoints/ProxyChatCompletionsEndpoint.cs | 3 +- .../Models/CreateChatCompletionResponse.cs | 647 +++++++++ .../Converters/EnumMemberConverterTests.cs | 198 +++ .../CreateChatCompletionResponseTests.cs | 278 ++++ .../ChatCompletionSchemasOpenApiTests.cs | 1167 +++++++++++++++++ 6 files changed, 2364 insertions(+), 1 deletion(-) create mode 100644 src/AzureOpenAIProxy.ApiApp/Converters/EnumMemberConverter.cs create mode 100644 src/AzureOpenAIProxy.ApiApp/Models/CreateChatCompletionResponse.cs create mode 100644 test/AzureOpenAIProxy.ApiApp.Tests/Converters/EnumMemberConverterTests.cs create mode 100644 test/AzureOpenAIProxy.ApiApp.Tests/Models/CreateChatCompletionResponseTests.cs create mode 100644 test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Schemas/ChatCompletionSchemasOpenApiTests.cs diff --git a/src/AzureOpenAIProxy.ApiApp/Converters/EnumMemberConverter.cs b/src/AzureOpenAIProxy.ApiApp/Converters/EnumMemberConverter.cs new file mode 100644 index 00000000..beded902 --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Converters/EnumMemberConverter.cs @@ -0,0 +1,72 @@ +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AzureOpenAIProxy.ApiApp.Converters; + +/// +/// This represents the converter entity for . +/// +/// The type of the enum to be converted. +public class EnumMemberConverter : JsonConverter where T : Enum +{ + /// + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var enumText = reader.GetString(); + + if (enumText == null) + { + throw new JsonException($"Unable to convert null to Enum \"{typeToConvert}\"."); + } + + foreach (var field in typeToConvert.GetFields()) + { + var attribute = Attribute.GetCustomAttribute(field, typeof(EnumMemberAttribute)) as EnumMemberAttribute; + + if (attribute != null && attribute.Value == enumText) + { + var value = field.GetValue(null); + if (value != null) + { + return (T)value; + } + } + else if (field.Name == enumText) + { + var value = field.GetValue(null); + if (value != null) + { + return (T)value; + } + } + } + + throw new JsonException($"Unable to convert \"{enumText}\" to Enum \"{typeToConvert}\"."); + } + + /// + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + var field = value.GetType().GetField(value.ToString()); + + if (field != null) + { + var attribute = Attribute.GetCustomAttribute(field, typeof(EnumMemberAttribute)) as EnumMemberAttribute; + + if (attribute != null) + { + writer.WriteStringValue(attribute.Value); + } + else + { + writer.WriteStringValue(value.ToString()); + } + } + else + { + writer.WriteStringValue(value.ToString()); + } + } + +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/ProxyChatCompletionsEndpoint.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/ProxyChatCompletionsEndpoint.cs index a758150f..84d3e9e2 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/ProxyChatCompletionsEndpoint.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/ProxyChatCompletionsEndpoint.cs @@ -1,6 +1,7 @@ using System.Text.Json; using AzureOpenAIProxy.ApiApp.Attributes; +using AzureOpenAIProxy.ApiApp.Models; using AzureOpenAIProxy.ApiApp.Services; using Microsoft.AspNetCore.Mvc; @@ -54,7 +55,7 @@ public static RouteHandlerBuilder AddChatCompletions(this WebApplication app) }) // TODO: Check both request/response payloads .Accepts(contentType: "application/json") - .Produces(statusCode: StatusCodes.Status200OK, contentType: "application/json") + .Produces(statusCode: StatusCodes.Status200OK, contentType: "application/json") // TODO: Check both request/response payloads .Produces(statusCode: StatusCodes.Status401Unauthorized) .Produces(statusCode: StatusCodes.Status500InternalServerError, contentType: "text/plain") diff --git a/src/AzureOpenAIProxy.ApiApp/Models/CreateChatCompletionResponse.cs b/src/AzureOpenAIProxy.ApiApp/Models/CreateChatCompletionResponse.cs new file mode 100644 index 00000000..75a51d7e --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Models/CreateChatCompletionResponse.cs @@ -0,0 +1,647 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +using AzureOpenAIProxy.ApiApp.Converters; + +namespace AzureOpenAIProxy.ApiApp.Models; + +/// +/// The response from creating a chat completion. +/// +/// +/// For more information, see azure-rest-api-specs(2024-06-01) +/// +public class CreateChatCompletionResponse +{ + /// + /// Gets or sets a unique identifier for the chat completion. + /// + [JsonPropertyName("id"), Required] + public string? Id { get; set; } + + /// + /// Gets or sets the object type. + /// + [JsonPropertyName("object"), Required] + public ChatCompletionResponseObject? Object { get; set; } + + /// + /// Gets or sets the Unix timestamp (in seconds) of when the chat completion was created. + /// + [JsonPropertyName("created"), Required] + public long? Created { get; set; } + + /// + /// Gets or sets the model used for the chat completion. + /// + [JsonPropertyName("model"), Required] + public string? Model { get; set; } + + /// + /// Gets or sets usage statistics for the completion request. + /// + [JsonPropertyName("usage")] + public CompletionUsage? Usage { get; set; } + + /// + /// Gets or sets the system fingerprint. + /// Can be used in conjunction with the `seed` request parameter to understand when backend changes have been made that might impact determinism. + /// + [JsonPropertyName("system_fingerprint")] + public string? SystemFingerprint { get; set; } + + /// + /// Gets or sets content filtering results for zero or more prompts in the request. + /// In a streaming request, results for different prompts may arrive at different times or in different orders. + /// + [JsonPropertyName("prompt_filter_results")] + public List? PromptFilterResults { get; set; } + + /// + /// Gets or sets a list of choices. + /// + [JsonPropertyName("choices"), Required] + public List? Choices { get; set; } +} + +/// +/// Represents a choice in the chat completion response. +/// +public class ChatCompletionChoice +{ + /// + /// Gets or sets an index. + /// + [JsonPropertyName("index")] + public int? Index { get; set; } + + /// + /// Gets or sets the finish reason. + /// + [JsonPropertyName("finish_reason")] + public string? FinishReason { get; set; } + + /// + /// Gets or sets a chat completion message generated by the model. + /// + [JsonPropertyName("message")] + public ChatCompletionResponseMessage? Message { get; set; } + + /// + /// Information about the content filtering category (hate, sexual, violence, self_harm), if it has been detected, as well as the severity level (very_low, low, medium, high-scale that determines the intensity and risk level of harmful content) and if it has been filtered or not. Information about third party text and profanity, if it has been detected, and if it has been filtered or not. And information about customer block list, if it has been filtered and its id. + /// + [JsonPropertyName("content_filter_results")] + public ContentFilterChoiceResults? ContentFilterResults { get; set; } + + /// + /// Gets or sets log probability information for the choice. + /// + [JsonPropertyName("logprobs")] + public ChatCompletionChoiceLogProbs? LogProbs { get; set; } +} + +/// +/// A chat completion message generated by the model. +/// +public class ChatCompletionResponseMessage +{ + /// + /// Gets or sets the role of the author of the response message. + /// + [JsonPropertyName("role")] + public ChatCompletionResponseMessageRole? Role { get; set; } + + /// + /// Gets or sets the contents of the message. + /// + [JsonPropertyName("content")] + public string? Content { get; set; } + + /// + /// Gets or sets the tool calls generated by the model, such as function calls. + /// + [JsonPropertyName("tool_calls")] + public List? ToolCalls { get; set; } + + /// + /// Gets or sets the function call + /// Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model. + /// + [JsonPropertyName("function_call")] + public ChatCompletionFunctionCall? FunctionCall { get; set; } + + /// + /// Gets or sets a representation of the additional context information available when Azure OpenAI chat extensions are involved in the generation of a corresponding chat completions response. + /// This context information is only populated when using an Azure OpenAI request configured to use a matching extension. + /// + [JsonPropertyName("context")] + public AzureChatExtensionsMessageContext? Context { get; set; } +} + +/// +/// Information about the content filtering category (hate, sexual, violence, self_harm), if it has been detected, +/// as well as the severity level (very_low, low, medium, high-scale that determines the intensity and risk level of harmful content) +/// and if it has been filtered or not. Information about third-party text and profanity, if it has been detected, and if it has been filtered or not. +/// Also includes information about the customer block list, if it has been filtered and its ID. +/// +public class ContentFilterChoiceResults +{ + /// + /// Gets or sets the severity result for sexual content. + /// + [JsonPropertyName("sexual")] + public ContentFilterSeverityResult? Sexual { get; set; } + + /// + /// Gets or sets the severity result for violent content. + /// + [JsonPropertyName("violence")] + public ContentFilterSeverityResult? Violence { get; set; } + + /// + /// Gets or sets the severity result for hateful content. + /// + [JsonPropertyName("hate")] + public ContentFilterSeverityResult? Hate { get; set; } + + /// + /// Gets or sets the severity result for self-harm content. + /// + [JsonPropertyName("self_harm")] + public ContentFilterSeverityResult? SelfHarm { get; set; } + + /// + /// Gets or sets the detected result for profane content. + /// + [JsonPropertyName("profanity")] + public ContentFilterDetectedResult? Profanity { get; set; } + + /// + /// Gets or sets error details for content filtering. + /// + [JsonPropertyName("error")] + public ErrorBase? Error { get; set; } + + /// + /// Gets or sets the detected result for protected material in text. + /// + [JsonPropertyName("protected_material_text")] + public ContentFilterDetectedResult? ProtectedMaterialText { get; set; } + + /// + /// Gets or sets the detected result for protected material in code, including citation information. + /// + [JsonPropertyName("protected_material_code")] + public ContentFilterDetectedWithCitationResult? ProtectedMaterialCode { get; set; } +} + +/// +/// Log probability information for the choice. +/// +public class ChatCompletionChoiceLogProbs +{ + /// + /// Gets or sets a list of message content tokens with log probability information. + /// + [JsonPropertyName("content"), Required] + public List? Content { get; set; } +} + +/// +/// Usage statistics for the completion request. +/// +public class CompletionUsage +{ + /// + /// Gets or sets number of tokens in the prompt. + /// + [JsonPropertyName("prompt_tokens"), Required] + public int? PromptTokens { get; set; } + + /// + /// Gets or sets number of tokens in the generated completion. + /// + [JsonPropertyName("completion_tokens"), Required] + public int? CompletionTokens { get; set; } + + /// + /// Gets of sets total number of tokens used in the request (prompt + completion). + /// + [JsonPropertyName("total_tokens"), Required] + public int? TotalTokens { get; set; } +} + +/// +/// Content filtering results for a single prompt in the request. +/// +public class PromptFilterResult +{ + /// + /// Gets or sets prompt index. + /// + [JsonPropertyName("prompt_index")] + public int? PromptIndex { get; set; } + + /// + /// Gets or sets information about the content filtering category (hate, sexual, violence, self_harm), + /// if it has been detected, as well as the severity level (very_low, low, medium, high-scale that determines the intensity and risk level of harmful content) and + /// if it has been filtered or not. Information about jailbreak content and profanity, + /// if it has been detected, and if it has been filtered or not. And information about customer block list, if it has been filtered and its id. + /// + [JsonPropertyName("content_filter_results")] + public ContentFilterPromptResults? ContentFilterResults { get; set; } +} + +/// +/// Represents a tool call generated by the model. +/// +public class ChatCompletionMessageToolCall +{ + /// + /// Gets or sets the ID of the tool call. + /// + [JsonPropertyName("id"), Required] + public string? Id { get; set; } + + /// + /// Gets or sets the type of the tool call, in this case `function`. + /// + [JsonPropertyName("type"), Required] + public ToolCallType? Type { get; set; } + + /// + /// Gets or sets the function that the model called. + /// + [JsonPropertyName("function"), Required] + public FunctionObject? Function { get; set; } +} + +/// +/// The function that the model called. +/// +public class FunctionObject +{ + /// + /// Gets or sets the name of the function to call. + /// + [JsonPropertyName("name"), Required] + public string? Name { get; set; } + + /// + /// Gets or sets the arguments to call the function with, as generated by the model in JSON format. + /// Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function. + /// + [JsonPropertyName("arguments"), Required] + public string? Arguments { get; set; } +} + +/// +/// Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model. +/// +public class ChatCompletionFunctionCall +{ + /// + /// Gets or sets the name of the function to call. + /// + [JsonPropertyName("name"), Required] + public string? Name { get; set; } + + /// + /// Gets or sets the arguments to call the function with, as generated by the model in JSON format. + /// Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function. + /// + [JsonPropertyName("arguments"), Required] + public string? Arguments { get; set; } +} + +/// +/// A representation of the additional context information available when Azure OpenAI chat extensions are involved +/// in the generation of a corresponding chat completions response. This context information is only populated when +/// using an Azure OpenAI request configured to use a matching extension. +/// +public class AzureChatExtensionsMessageContext +{ + /// + /// Gets or sets the data source retrieval result, used to generate the assistant message in the response. + /// + [JsonPropertyName("citations")] + public List? Citations { get; set; } + + /// + /// Gets or sets the detected intent from the chat history, used to pass to the next turn to carry over the context. + /// + [JsonPropertyName("intent")] + public string? Intent { get; set; } +} + +/// +/// Content filtering results with citation information. +/// +public class ContentFilterDetectedWithCitationResult +{ + /// + /// Gets or sets a value indicating whether the content was filtered. + /// + [JsonPropertyName("filtered"), Required] + public bool? Filtered { get; set; } + + /// + /// Gets or sets a value indicating whether the content was detected. + /// + [JsonPropertyName("detected"), Required] + public bool? Detected { get; set; } + + /// + /// Gets or sets the citation details related to the content filtering result. + /// + [JsonPropertyName("citation")] + public CitationObject? Citation { get; set; } +} + +/// +/// Represents the citation details, including the URL and license information. +/// +public class CitationObject +{ + /// + /// Gets or sets the URL of the citation. + /// + [JsonPropertyName("URL")] + public string? URL { get; set; } + + /// + /// Gets or sets the license information associated with the citation. + /// + [JsonPropertyName("license")] + public string? License { get; set; } +} + +/// +/// Token log probability information. +/// +public class ChatCompletionTokenLogProb +{ + /// + /// Gets or sets the token. + /// + [JsonPropertyName("token"), Required] + public string? Token { get; set; } + + /// + /// Gets or sets the log probability of this token. + /// + [JsonPropertyName("logprob"), Required] + public double? LogProb { get; set; } + + /// + /// Gets or sets a list of integers representing the UTF-8 bytes representation of the token. + /// + [JsonPropertyName("bytes"), Required] + public List? Bytes { get; set; } + + /// + /// Gets or sets list of the most likely tokens and their log probability, at this token position. + /// In rare cases, there may be fewer than the number of requested `top_logprobs` returned. + /// + [JsonPropertyName("top_logprobs"), Required] + public List? TopLogProbs { get; set; } +} + +/// +/// List of the most likely tokens and their log probability, at this token position. +/// In rare cases, there may be fewer than the number of requested `top_logprobs` returned. +/// +public class TopLogProbs +{ + /// + /// Gets or sets the token. + /// + [JsonPropertyName("token"), Required] + public string? Token { get; set; } + + /// + /// Gets or sets the log probability of this token. + /// + [JsonPropertyName("logprob"), Required] + public double? LogProb { get; set; } + + /// + /// Gets or sets a list of integers representing the UTF-8 bytes representation of the token. + /// Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. + /// Can be `null` if there is no bytes representation for the token. + /// + [JsonPropertyName("bytes"), Required] + public List? Bytes { get; set; } +} + +/// +/// Information about the content filtering category (hate, sexual, violence, self_harm), if it has been detected, +/// as well as the severity level (very_low, low, medium, high-scale that determines the intensity and risk level of harmful content) +/// and if it has been filtered or not. Information about jailbreak content and profanity, if it has been detected, +/// and if it has been filtered or not. And information about customer block list, if it has been filtered and its id. +/// +public class ContentFilterPromptResults +{ + /// + /// Gets or sets the severity result for sexual content. + /// + [JsonPropertyName("sexual")] + public ContentFilterSeverityResult? Sexual { get; set; } + + /// + /// Gets or sets the severity result for violent content. + /// + [JsonPropertyName("violence")] + public ContentFilterSeverityResult? Violence { get; set; } + + /// + /// Gets or sets the severity result for hateful content. + /// + [JsonPropertyName("hate")] + public ContentFilterSeverityResult? Hate { get; set; } + + /// + /// Gets or sets the severity result for self-harm content. + /// + [JsonPropertyName("self_harm")] + public ContentFilterSeverityResult? SelfHarm { get; set; } + + /// + /// Gets or sets the detected result for profane content. + /// + [JsonPropertyName("profanity")] + public ContentFilterDetectedResult? Profanity { get; set; } + + /// + /// Gets or sets error details for content filtering. + /// + [JsonPropertyName("error")] + public ErrorBase? Error { get; set; } + + /// + /// Gets or sets the detected result for jailbreak content. + /// + [JsonPropertyName("jailbreak")] + public ContentFilterDetectedResult? Jailbreak { get; set; } +} + +/// +/// Citation information for a chat completions response message. +/// +public class Citation +{ + /// + /// Gets or sets the content of the citation. + /// + [JsonPropertyName("content"), Required] + public string? Content { get; set; } + + /// + /// Gets or sets the title of the citation. + /// + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// + /// Gets or sets the URL of the citation. + /// + [JsonPropertyName("url")] + public string? Url { get; set; } + + /// + /// Gets or sets the file path of the citation. + /// + [JsonPropertyName("filepath")] + public string? Filepath { get; set; } + + /// + /// Gets or sets the chunk ID of the citation. + /// + [JsonPropertyName("chunk_id")] + public string? ChunkId { get; set; } +} + +/// +/// Represents the result of content detection, indicating whether specific content was detected and whether it was filtered. +/// +public class ContentFilterDetectedResult +{ + /// + /// Gets or sets a value indicating whether the content has been filtered. + /// + [JsonPropertyName("filtered"), Required] + public bool? Filtered { get; set; } + + /// + /// Gets or sets a value indicating whether the content has been detected. + /// + [JsonPropertyName("detected"), Required] + public bool? Detected { get; set; } +} + +/// +/// Severity information for content filtering. +/// +public class ContentFilterSeverityResult +{ + /// + /// Gets or sets a value indicating whether the content has been filtered. + /// + [JsonPropertyName("filtered"), Required] + public bool? Filtered { get; set; } + + /// + /// Gets or sets the severity level of the content. + /// + [JsonPropertyName("severity"), Required] + public ContentFilterSeverity? Severity { get; set; } +} + +/// +/// Error details for content filtering. +/// +public class ErrorBase +{ + /// + /// Gets or sets the error code. + /// + [JsonPropertyName("code")] + public string? Code { get; set; } + + /// + /// Gets or sets the error message. + /// + [JsonPropertyName("message")] + public string? Message { get; set; } +} + +/// +/// The type of the tool call, in this case `function`. +/// +[JsonConverter(typeof(EnumMemberConverter))] +public enum ToolCallType +{ + /// + /// The tool call type is function. + /// + [EnumMember(Value = "function")] + Function +} + +/// +/// The role of the author of the response message. +/// +[JsonConverter(typeof(EnumMemberConverter))] +public enum ChatCompletionResponseMessageRole +{ + /// + /// The role of the assistant generating the response. + /// + [EnumMember(Value = "assistant")] + Assistant +} + +/// +/// The object type. +/// +[JsonConverter(typeof(EnumMemberConverter))] +public enum ChatCompletionResponseObject +{ + /// + /// The object type is chat completion. + /// + [EnumMember(Value = "chat.completion")] + ChatCompletion +} + +/// +/// Severity levels for content filtering. +/// +[JsonConverter(typeof(EnumMemberConverter))] +public enum ContentFilterSeverity +{ + /// + /// General content or related content in generic or non-harmful contexts. + /// + [EnumMember(Value = "safe")] + Safe, + + /// + /// Harmful content at a low intensity and risk level. + /// + [EnumMember(Value = "low")] + Low, + + /// + /// Harmful content at a medium intensity and risk level. + /// + [EnumMember(Value = "medium")] + Medium, + + /// + /// Harmful content at a high intensity and risk level. + /// + [EnumMember(Value = "high")] + High +} \ No newline at end of file diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Converters/EnumMemberConverterTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Converters/EnumMemberConverterTests.cs new file mode 100644 index 00000000..aa722d97 --- /dev/null +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Converters/EnumMemberConverterTests.cs @@ -0,0 +1,198 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Runtime.Serialization; +using FluentAssertions; +using AzureOpenAIProxy.ApiApp.Converters; + +namespace AzureOpenAIProxy.ApiApp.Tests.Converters; + +public class EnumMemberConverterTests +{ + private enum TestEnum + { + [EnumMember(Value = "first_value")] + FirstValue, + + [EnumMember(Value = "second.value")] + SecondValue, + + [EnumMember(Value = "thirdvalue")] + ThirdValue, + + UnmappedValue + } + + private readonly JsonConverter _converter = new EnumMemberConverter(); + + [Fact] + public void Given_EnumMemberAttribute_When_Deserializing_Then_ShouldReturnCorrectEnumValue() + { + // Arrange + var json = "\"first_value\""; + + // Act + var options = new JsonSerializerOptions + { + Converters = { _converter } + }; + var result = JsonSerializer.Deserialize(json, options); + + // Assert + result.Should().Be(TestEnum.FirstValue); + } + + [Fact] + public void Given_DotInEnumMemberAttribute_When_Deserializing_Then_ShouldReturnCorrectEnumValue() + { + // Arrange + var json = "\"second.value\""; + + // Act + var options = new JsonSerializerOptions + { + Converters = { _converter } + }; + var result = JsonSerializer.Deserialize(json, options); + + // Assert + result.Should().Be(TestEnum.SecondValue); + } + + [Fact] + public void Given_ThirdEnumMemberAttribute_When_Deserializing_Then_ShouldReturnCorrectEnumValue() + { + // Arrange + var json = "\"thirdvalue\""; + + // Act + var options = new JsonSerializerOptions + { + Converters = { _converter } + }; + var result = JsonSerializer.Deserialize(json, options); + + // Assert + result.Should().Be(TestEnum.ThirdValue); + } + + [Fact] + public void Given_NoEnumMemberAttribute_When_Deserializing_Then_ShouldReturnCorrectEnumValue() + { + // Arrange + var json = "\"UnmappedValue\""; + + // Act + var options = new JsonSerializerOptions + { + Converters = { _converter } + }; + var result = JsonSerializer.Deserialize(json, options); + + // Assert + result.Should().Be(TestEnum.UnmappedValue); + } + + [Fact] + public void Given_InvalidEnumValue_When_Deserializing_Then_ShouldThrowJsonException() + { + // Arrange + var json = "\"invalid_value\""; + + // Act + var options = new JsonSerializerOptions + { + Converters = { _converter } + }; + + // Assert + Action action = () => JsonSerializer.Deserialize(json, options); + action.Should().Throw() + .WithMessage("Unable to convert \"invalid_value\" to Enum*"); + } + + [Fact] + public void Given_NullValue_When_Deserializing_Then_ShouldThrowJsonException() + { + // Arrange + var json = "null"; + + // Act + var options = new JsonSerializerOptions + { + Converters = { _converter } + }; + + // Assert + Action action = () => JsonSerializer.Deserialize(json, options); + action.Should().Throw() + .WithMessage("Unable to convert null to Enum*"); + } + + [Fact] + public void Given_EnumMemberAttribute_When_Serializing_Then_ShouldReturnCorrectJsonString() + { + // Arrange + var value = TestEnum.FirstValue; + + // Act + var options = new JsonSerializerOptions + { + Converters = { _converter } + }; + var result = JsonSerializer.Serialize(value, options); + + // Assert + result.Should().Be("\"first_value\""); + } + + [Fact] + public void Given_DotInEnumMemberAttribute_When_Serializing_Then_ShouldReturnCorrectJsonString() + { + // Arrange + var value = TestEnum.SecondValue; + + // Act + var options = new JsonSerializerOptions + { + Converters = { _converter } + }; + var result = JsonSerializer.Serialize(value, options); + + // Assert + result.Should().Be("\"second.value\""); + } + + [Fact] + public void Given_ThirdEnumMemberAttribute_When_Serializing_Then_ShouldReturnCorrectJsonString() + { + // Arrange + var value = TestEnum.ThirdValue; + + // Act + var options = new JsonSerializerOptions + { + Converters = { _converter } + }; + var result = JsonSerializer.Serialize(value, options); + + // Assert + result.Should().Be("\"thirdvalue\""); + } + + [Fact] + public void Given_NoEnumMemberAttribute_When_Serializing_Then_ShouldReturnCorrectJsonString() + { + // Arrange + var value = TestEnum.UnmappedValue; + + // Act + var options = new JsonSerializerOptions + { + Converters = { _converter } + }; + var result = JsonSerializer.Serialize(value, options); + + // Assert + result.Should().Be("\"UnmappedValue\""); + } +} \ No newline at end of file diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Models/CreateChatCompletionResponseTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Models/CreateChatCompletionResponseTests.cs new file mode 100644 index 00000000..6cdcdf5d --- /dev/null +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Models/CreateChatCompletionResponseTests.cs @@ -0,0 +1,278 @@ +using System.Text.Json; +using AzureOpenAIProxy.ApiApp.Models; +using FluentAssertions; + +namespace AzureOpenAIProxy.ApiApp.Tests.Models; + +public class CreateChatCompletionResponseTests +{ + private readonly string _exampleJson = @" + { + ""id"": ""string"", + ""object"": ""chat.completion"", + ""created"": 1620241923, + ""model"": ""string"", + ""usage"": { + ""prompt_tokens"": 0, + ""completion_tokens"": 0, + ""total_tokens"": 0 + }, + ""system_fingerprint"": ""string"", + ""prompt_filter_results"": [ + { + ""prompt_index"": 0, + ""content_filter_results"": { + ""sexual"": { + ""filtered"": true, + ""severity"": ""safe"" + }, + ""violence"": { + ""filtered"": true, + ""severity"": ""safe"" + }, + ""hate"": { + ""filtered"": true, + ""severity"": ""safe"" + }, + ""self_harm"": { + ""filtered"": true, + ""severity"": ""safe"" + }, + ""profanity"": { + ""filtered"": true, + ""detected"": true + }, + ""error"": { + ""code"": ""string"", + ""message"": ""string"" + }, + ""jailbreak"": { + ""filtered"": true, + ""detected"": true + } + } + } + ], + ""choices"": [ + { + ""index"": 0, + ""finish_reason"": ""string"", + ""message"": { + ""role"": ""assistant"", + ""content"": ""string"", + ""tool_calls"": [ + { + ""id"": ""string"", + ""type"": ""function"", + ""function"": { + ""name"": ""string"", + ""arguments"": ""string"" + } + } + ], + ""function_call"": { + ""name"": ""string"", + ""arguments"": ""string"" + }, + ""context"": { + ""citations"": [ + { + ""content"": ""string"", + ""title"": ""string"", + ""url"": ""string"", + ""filepath"": ""string"", + ""chunk_id"": ""string"" + } + ], + ""intent"": ""string"" + } + }, + ""content_filter_results"": { + ""sexual"": { + ""filtered"": true, + ""severity"": ""safe"" + }, + ""violence"": { + ""filtered"": true, + ""severity"": ""safe"" + }, + ""hate"": { + ""filtered"": true, + ""severity"": ""safe"" + }, + ""self_harm"": { + ""filtered"": true, + ""severity"": ""safe"" + }, + ""profanity"": { + ""filtered"": true, + ""detected"": true + }, + ""error"": { + ""code"": ""string"", + ""message"": ""string"" + }, + ""protected_material_text"": { + ""filtered"": true, + ""detected"": true + }, + ""protected_material_code"": { + ""filtered"": true, + ""detected"": true, + ""citation"": { + ""URL"": ""string"", + ""license"": ""string"" + } + } + }, + ""logprobs"": { + ""content"": [ + { + ""token"": ""string"", + ""logprob"": 0, + ""bytes"": [ + 0 + ], + ""top_logprobs"": [ + { + ""token"": ""string"", + ""logprob"": 0, + ""bytes"": [ + 0 + ] + } + ] + } + ] + } + } + ] + }"; + + private readonly CreateChatCompletionResponse exampleResponse = new CreateChatCompletionResponse + { + Id = "string", + Object = ChatCompletionResponseObject.ChatCompletion, + Created = 1620241923, + Model = "string", + Usage = new CompletionUsage + { + PromptTokens = 0, + CompletionTokens = 0, + TotalTokens = 0 + }, + SystemFingerprint = "string", + PromptFilterResults = new List + { + new PromptFilterResult + { + PromptIndex = 0, + ContentFilterResults = new ContentFilterPromptResults + { + Sexual = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, + Violence = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, + Hate = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, + SelfHarm = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, + Profanity = new ContentFilterDetectedResult { Filtered = true, Detected = true }, + Error = new ErrorBase { Code = "string", Message = "string" }, + Jailbreak = new ContentFilterDetectedResult { Filtered = true, Detected = true } + } + } + }, + Choices = new List + { + new ChatCompletionChoice + { + Index = 0, + FinishReason = "string", + Message = new ChatCompletionResponseMessage + { + Role = ChatCompletionResponseMessageRole.Assistant, + Content = "string", + ToolCalls = new List + { + new ChatCompletionMessageToolCall + { + Id = "string", + Type = ToolCallType.Function, + Function = new FunctionObject + { + Name = "string", + Arguments = "string" + } + } + }, + FunctionCall = new ChatCompletionFunctionCall { Name = "string", Arguments = "string" }, + Context = new AzureChatExtensionsMessageContext + { + Citations = new List + { + new Citation + { + Content = "string", + Title = "string", + Url = "string", + Filepath = "string", + ChunkId = "string" + } + }, + Intent = "string" + } + }, + ContentFilterResults = new ContentFilterChoiceResults + { + Sexual = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, + Violence = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, + Hate = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, + SelfHarm = new ContentFilterSeverityResult { Filtered = true, Severity = ContentFilterSeverity.Safe }, + Profanity = new ContentFilterDetectedResult { Filtered = true, Detected = true }, + Error = new ErrorBase { Code = "string", Message = "string" }, + ProtectedMaterialText = new ContentFilterDetectedResult { Filtered = true, Detected = true }, + ProtectedMaterialCode = new ContentFilterDetectedWithCitationResult + { + Filtered = true, + Detected = true, + Citation = new CitationObject { URL = "string", License = "string" } + } + }, + LogProbs = new ChatCompletionChoiceLogProbs + { + Content = new List + { + new ChatCompletionTokenLogProb + { + Token = "string", + LogProb = 0, + Bytes = new List { 0 }, + TopLogProbs = new List + { + new TopLogProbs { Token = "string", LogProb = 0, Bytes = new List { 0 } } + } + } + } + } + } + } + }; + + [Fact] + public void Given_ExampleResponse_When_Serialized_Then_ShouldMatchExpectedJson() + { + // Act + var serializedJson = JsonSerializer.Serialize(exampleResponse, new JsonSerializerOptions { WriteIndented = false }); + + // Assert + serializedJson.Should().Be(_exampleJson.Replace("\r", "").Replace("\n", "").Replace(" ", "")); + } + + [Fact] + public void Given_ExampleJson_When_Deserialized_Then_ShouldReturnValidObject() + { + // Arrange & Act + var deserializedResponse = JsonSerializer.Deserialize(_exampleJson); + + // Assert + deserializedResponse.Should().NotBeNull(); + deserializedResponse.Should().BeEquivalentTo(exampleResponse); + } +} \ No newline at end of file diff --git a/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Schemas/ChatCompletionSchemasOpenApiTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Schemas/ChatCompletionSchemasOpenApiTests.cs new file mode 100644 index 00000000..4905faa5 --- /dev/null +++ b/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Schemas/ChatCompletionSchemasOpenApiTests.cs @@ -0,0 +1,1167 @@ +using System.Text.Json; + +using AzureOpenAIProxy.AppHost.Tests.Fixtures; + +using FluentAssertions; +namespace AzureOpenAIProxy.AppHost.Tests.ApiApp.Schemas; + +/// +/// It includes tests for required fields, reference validation, enum values, and properties +/// +public class ChatCompletionSchemasOpenApiTests(AspireAppHostFixture host) : IClassFixture +{ + // Required Fields Validation + + [Fact] + public async Task Given_ChatCompletionResponseSchema_When_ValidatingRequiredFields_Then_RequiredFieldsShouldBePresent() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var createChatCompletionResponseSchema = schemas.GetProperty("CreateChatCompletionResponse"); + var requiredFields = createChatCompletionResponseSchema.GetProperty("required").EnumerateArray().Select(x => x.GetString()).ToList(); + + // Assert + requiredFields.Should().Contain(new[] { "id", "object", "created", "model", "choices" }); + } + + [Fact] + public async Task Given_ChatCompletionChoiceLogProbsSchema_When_ValidatingRequiredFields_Then_RequiredFieldsShouldBePresent() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionChoiceLogProbsSchema = schemas.GetProperty("ChatCompletionChoiceLogProbs"); + var requiredFields = chatCompletionChoiceLogProbsSchema.GetProperty("required").EnumerateArray().Select(x => x.GetString()).ToList(); + + // Assert + requiredFields.Should().Contain(new[] { "content" }); + } + + [Fact] + public async Task Given_CompletionUsageSchema_When_ValidatingRequiredFields_Then_RequiredFieldsShouldBePresent() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var completionUsageSchema = schemas.GetProperty("CompletionUsage"); + var requiredFields = completionUsageSchema.GetProperty("required").EnumerateArray().Select(x => x.GetString()).ToList(); + + // Assert + requiredFields.Should().Contain(new[] { "prompt_tokens", "completion_tokens", "total_tokens" }); + } + + [Fact] + public async Task Given_ChatCompletionMessageToolCallSchema_When_ValidatingRequiredFields_Then_RequiredFieldsShouldBePresent() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionMessageToolCallSchema = schemas.GetProperty("ChatCompletionMessageToolCall"); + var requiredFields = chatCompletionMessageToolCallSchema.GetProperty("required").EnumerateArray().Select(x => x.GetString()).ToList(); + + // Assert + requiredFields.Should().Contain(new[] { "id", "type", "function" }); + } + + [Fact] + public async Task Given_FunctionObjectSchema_When_ValidatingRequiredFields_Then_RequiredFieldsShouldBePresent() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var functionObjectSchema = schemas.GetProperty("FunctionObject"); + var requiredFields = functionObjectSchema.GetProperty("required").EnumerateArray().Select(x => x.GetString()).ToList(); + + // Assert + requiredFields.Should().Contain(new[] { "name", "arguments" }); + } + + [Fact] + public async Task Given_ChatCompletionFunctionCallSchema_When_ValidatingRequiredFields_Then_RequiredFieldsShouldBePresent() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionFunctionCallSchema = schemas.GetProperty("ChatCompletionFunctionCall"); + var requiredFields = chatCompletionFunctionCallSchema.GetProperty("required").EnumerateArray().Select(x => x.GetString()).ToList(); + + // Assert + requiredFields.Should().Contain(new[] { "name", "arguments" }); + } + + [Fact] + public async Task Given_ContentFilterDetectedResultSchema_When_ValidatingRequiredFields_Then_RequiredFieldsShouldBePresent() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var contentFilterDetectedResultSchema = schemas.GetProperty("ContentFilterDetectedResult"); + var requiredFields = contentFilterDetectedResultSchema.GetProperty("required").EnumerateArray().Select(x => x.GetString()).ToList(); + + // Assert + requiredFields.Should().Contain(new[] { "detected", "filtered" }); + } + + [Fact] + public async Task Given_ContentFilterDetectedWithCitationResultSchema_When_ValidatingRequiredFields_Then_RequiredFieldsShouldBePresent() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var contentFilterDetectedWithCitationResultSchema = schemas.GetProperty("ContentFilterDetectedWithCitationResult"); + var requiredFields = contentFilterDetectedWithCitationResultSchema.GetProperty("required").EnumerateArray().Select(x => x.GetString()).ToList(); + + // Assert + requiredFields.Should().Contain(new[] { "detected", "filtered" }); + } + + [Fact] + public async Task Given_ChatCompletionTokenLogProbSchema_When_ValidatingRequiredFields_Then_RequiredFieldsShouldBePresent() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionTokenLogProbSchema = schemas.GetProperty("ChatCompletionTokenLogProb"); + var requiredFields = chatCompletionTokenLogProbSchema.GetProperty("required").EnumerateArray().Select(x => x.GetString()).ToList(); + + // Assert + requiredFields.Should().Contain(new[] { "token", "logprob", "bytes", "top_logprobs" }); + } + + [Fact] + public async Task Given_TopLogProbsSchema_When_ValidatingRequiredFields_Then_RequiredFieldsShouldBePresent() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var topLogProbsSchema = schemas.GetProperty("TopLogProbs"); + var requiredFields = topLogProbsSchema.GetProperty("required").EnumerateArray().Select(x => x.GetString()).ToList(); + + // Assert + requiredFields.Should().Contain(new[] { "token", "logprob", "bytes" }); + } + + [Fact] + public async Task Given_CitationSchema_When_ValidatingRequiredFields_Then_RequiredFieldsShouldBePresent() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var citationSchema = schemas.GetProperty("Citation"); + var requiredFields = citationSchema.GetProperty("required").EnumerateArray().Select(x => x.GetString()).ToList(); + + // Assert + requiredFields.Should().Contain(new[] { "content" }); + } + + [Fact] + public async Task Given_ContentFilterSeverityResultSchema_When_ValidatingRequiredFields_Then_RequiredFieldsShouldBePresent() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var contentFilterSeverityResultSchema = schemas.GetProperty("ContentFilterSeverityResult"); + var requiredFields = contentFilterSeverityResultSchema.GetProperty("required").EnumerateArray().Select(x => x.GetString()).ToList(); + + // Assert + requiredFields.Should().Contain(new[] { "severity", "filtered" }); + } + + // $ref Validation + + [Fact] + public async Task Given_CreateChatCompletionResponseSchema_When_ValidatingPromptFilterResultsRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var createChatCompletionResponseSchema = schemas.GetProperty("CreateChatCompletionResponse"); + var prompt_filter_resultsFields = createChatCompletionResponseSchema.GetProperty("properties") + .GetProperty("prompt_filter_results"); + + // Assert + prompt_filter_resultsFields.GetProperty("items").GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/PromptFilterResult"); + } + + [Fact] + public async Task Given_CreateChatCompletionResponseSchema_When_ValidatingChoicesRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var createChatCompletionResponseSchema = schemas.GetProperty("CreateChatCompletionResponse"); + var choicesFields = createChatCompletionResponseSchema.GetProperty("properties") + .GetProperty("choices"); + + // Assert + choicesFields.GetProperty("items") + .GetProperty("$ref").GetString() + .Should().Be("#/components/schemas/ChatCompletionChoice"); + } + + [Fact] + public async Task Given_ChatCompletionChoiceSchema_When_ValidatingMessageRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionChoiceSchema = schemas.GetProperty("ChatCompletionChoice"); + var messageFields = chatCompletionChoiceSchema.GetProperty("properties") + .GetProperty("message"); + + // Assert + messageFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ChatCompletionResponseMessage"); + } + + [Fact] + public async Task Given_ChatCompletionChoiceSchema_When_ValidatingContentFilterResultsRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionChoiceSchema = schemas.GetProperty("ChatCompletionChoice"); + var content_filter_resultsFields = chatCompletionChoiceSchema.GetProperty("properties") + .GetProperty("content_filter_results"); + + // Assert + content_filter_resultsFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ContentFilterChoiceResults"); + } + + [Fact] + public async Task Given_ChatCompletionChoiceSchema_When_ValidatingLogProbsRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionChoiceSchema = schemas.GetProperty("ChatCompletionChoice"); + var logprobsFields = chatCompletionChoiceSchema.GetProperty("properties") + .GetProperty("logprobs"); + + // Assert + logprobsFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ChatCompletionChoiceLogProbs"); + } + + [Fact] + public async Task Given_CreateChatCompletionResponseSchema_When_ValidatingObjectRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var createChatCompletionResponseSchema = schemas.GetProperty("CreateChatCompletionResponse"); + var objectFields = createChatCompletionResponseSchema.GetProperty("properties") + .GetProperty("object"); + + // Assert + objectFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ChatCompletionResponseObject"); + } + + [Fact] + public async Task Given_CreateChatCompletionResponseSchema_When_ValidatingUsageRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var createChatCompletionResponseSchema = schemas.GetProperty("CreateChatCompletionResponse"); + var usageFields = createChatCompletionResponseSchema.GetProperty("properties") + .GetProperty("usage"); + + // Assert + usageFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/CompletionUsage"); + } + + [Fact] + public async Task Given_ChatCompletionResponseMessageSchema_When_ValidatingRoleRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionResponseMessageSchema = schemas.GetProperty("ChatCompletionResponseMessage"); + var roleFields = chatCompletionResponseMessageSchema.GetProperty("properties") + .GetProperty("role"); + + // Assert + roleFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ChatCompletionResponseMessageRole"); + } + + [Fact] + public async Task Given_ChatCompletionResponseMessageSchema_When_ValidatingToolCallsRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionResponseMessageSchema = schemas.GetProperty("ChatCompletionResponseMessage"); + var toolCallsFields = chatCompletionResponseMessageSchema.GetProperty("properties") + .GetProperty("tool_calls"); + + // Assert + toolCallsFields.GetProperty("items").GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ChatCompletionMessageToolCall"); + } + + [Fact] + public async Task Given_ChatCompletionResponseMessageSchema_When_ValidatingFunctionCallRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionResponseMessageSchema = schemas.GetProperty("ChatCompletionResponseMessage"); + var function_callFields = chatCompletionResponseMessageSchema.GetProperty("properties") + .GetProperty("function_call"); + + // Assert + function_callFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ChatCompletionFunctionCall"); + } + + [Fact] + public async Task Given_ChatCompletionResponseMessageSchema_When_ValidatingContextRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionResponseMessageSchema = schemas.GetProperty("ChatCompletionResponseMessage"); + var contextFields = chatCompletionResponseMessageSchema.GetProperty("properties") + .GetProperty("context"); + + // Assert + contextFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/AzureChatExtensionsMessageContext"); + } + + [Fact] + public async Task Given_ContentFilterChoiceResultsSchema_When_ValidatingProtectedMaterialTextRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var contentFilterChoiceResultsSchema = schemas.GetProperty("ContentFilterChoiceResults"); + var protected_material_textFields = contentFilterChoiceResultsSchema.GetProperty("properties") + .GetProperty("protected_material_text"); + + // Assert + protected_material_textFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ContentFilterDetectedResult"); + } + + [Fact] + public async Task Given_ContentFilterChoiceResultsSchema_When_ValidatingProtectedMaterialCodeRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var contentFilterChoiceResultsSchema = schemas.GetProperty("ContentFilterChoiceResults"); + var protected_material_codeFields = contentFilterChoiceResultsSchema.GetProperty("properties") + .GetProperty("protected_material_code"); + + // Assert + protected_material_codeFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ContentFilterDetectedWithCitationResult"); + } + + [Fact] + public async Task Given_ChatCompletionChoiceLogProbsSchema_When_ValidatingContentRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionChoiceLogProbsSchema = schemas.GetProperty("ChatCompletionChoiceLogProbs"); + var contentFields = chatCompletionChoiceLogProbsSchema.GetProperty("properties") + .GetProperty("content"); + + // Assert + contentFields.GetProperty("items").GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ChatCompletionTokenLogProb"); + } + + [Fact] + public async Task Given_PromptFilterResultSchema_When_ValidatingContentFilterResultsRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var promptFilterResultSchema = schemas.GetProperty("PromptFilterResult"); + var content_filter_resultsFields = promptFilterResultSchema.GetProperty("properties") + .GetProperty("content_filter_results"); + + // Assert + content_filter_resultsFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ContentFilterPromptResults"); + } + + [Fact] + public async Task Given_ChatCompletionMessageToolCallSchema_When_ValidatingTypeRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionMessageToolCallSchema = schemas.GetProperty("ChatCompletionMessageToolCall"); + var typeFields = chatCompletionMessageToolCallSchema.GetProperty("properties") + .GetProperty("type"); + + // Assert + typeFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ToolCallType"); + } + + [Fact] + public async Task Given_ChatCompletionMessageToolCallSchema_When_ValidatingFunctionRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionMessageToolCallSchema = schemas.GetProperty("ChatCompletionMessageToolCall"); + var functionFields = chatCompletionMessageToolCallSchema.GetProperty("properties") + .GetProperty("function"); + + // Assert + functionFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/FunctionObject"); + } + + [Fact] + public async Task Given_AzureChatExtensionsMessageContextSchema_When_ValidatingCitationsRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var azureChatExtensionsMessageContextSchema = schemas.GetProperty("AzureChatExtensionsMessageContext"); + var citationsFields = azureChatExtensionsMessageContextSchema.GetProperty("properties") + .GetProperty("citations"); + + // Assert + citationsFields.GetProperty("items").GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/Citation"); + } + + [Fact] + public async Task Given_ContentFilterPromptResultsSchema_When_ValidatingJailbreakRef_Then_RefShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var contentFilterPromptResultsSchema = schemas.GetProperty("ContentFilterPromptResults"); + var jailbreakFields = contentFilterPromptResultsSchema.GetProperty("properties") + .GetProperty("jailbreak"); + + // Assert + jailbreakFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ContentFilterDetectedResult"); + } + + [Fact] + public async Task Given_ContentFilterPromptResultsSchema_When_ValidatingSeverityRefs_Then_RefsShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var contentFilterPromptResultsSchema = schemas.GetProperty("ContentFilterPromptResults"); + var sexualFields = contentFilterPromptResultsSchema.GetProperty("properties") + .GetProperty("sexual"); + var violenceFields = contentFilterPromptResultsSchema.GetProperty("properties") + .GetProperty("violence"); + var hateFields = contentFilterPromptResultsSchema.GetProperty("properties") + .GetProperty("hate"); + var self_harmFields = contentFilterPromptResultsSchema.GetProperty("properties") + .GetProperty("self_harm"); + var profanityFields = contentFilterPromptResultsSchema.GetProperty("properties") + .GetProperty("profanity"); + var errorFields = contentFilterPromptResultsSchema.GetProperty("properties") + .GetProperty("error"); + + // Assert + sexualFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ContentFilterSeverityResult"); + violenceFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ContentFilterSeverityResult"); + hateFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ContentFilterSeverityResult"); + self_harmFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ContentFilterSeverityResult"); + profanityFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ContentFilterDetectedResult"); + errorFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ErrorBase"); + } + + [Fact] + public async Task Given_ContentFilterChoiceResultsSchema_When_ValidatingSeverityRefs_Then_RefsShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var contentFilterChoiceResultsSchema = schemas.GetProperty("ContentFilterChoiceResults"); + var sexualFields = contentFilterChoiceResultsSchema.GetProperty("properties") + .GetProperty("sexual"); + var violenceFields = contentFilterChoiceResultsSchema.GetProperty("properties") + .GetProperty("violence"); + var hateFields = contentFilterChoiceResultsSchema.GetProperty("properties") + .GetProperty("hate"); + var self_harmFields = contentFilterChoiceResultsSchema.GetProperty("properties") + .GetProperty("self_harm"); + var profanityFields = contentFilterChoiceResultsSchema.GetProperty("properties") + .GetProperty("profanity"); + var errorFields = contentFilterChoiceResultsSchema.GetProperty("properties") + .GetProperty("error"); + + // Assert + sexualFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ContentFilterSeverityResult"); + violenceFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ContentFilterSeverityResult"); + hateFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ContentFilterSeverityResult"); + self_harmFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ContentFilterSeverityResult"); + profanityFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ContentFilterDetectedResult"); + errorFields.GetProperty("$ref") + .GetString().Should().Be("#/components/schemas/ErrorBase"); + } + + // Enum validation + + [Fact] + public async Task Given_ChatCompletionResponseObjectSchema_When_ValidatingEnum_Then_EnumValuesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionResponseObjectSchema = schemas.GetProperty("ChatCompletionResponseObject"); + var enumValues = chatCompletionResponseObjectSchema.GetProperty("enum").EnumerateArray().Select(x => x.GetString()).ToList(); + + // Assert + enumValues.Should().Contain(new[] { "chat.completion" }); + } + + [Fact] + public async Task Given_ChatCompletionResponseMessageRoleSchema_When_ValidatingEnum_Then_EnumValuesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionResponseMessageRoleSchema = schemas.GetProperty("ChatCompletionResponseMessageRole"); + var enumValues = chatCompletionResponseMessageRoleSchema.GetProperty("enum").EnumerateArray().Select(x => x.GetString()).ToList(); + + // Assert + enumValues.Should().Contain(new[] { "assistant" }); + } + + [Fact] + public async Task Given_ContentFilterSeveritySchema_When_ValidatingEnum_Then_EnumValuesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var contentFilterSeveritySchema = schemas.GetProperty("ContentFilterSeverity"); + var enumValues = contentFilterSeveritySchema.GetProperty("enum").EnumerateArray().Select(x => x.GetString()).ToList(); + + // Assert + enumValues.Should().Contain(new[] { "safe", "low", "medium", "high" }); + } + + // Properties validation + + [Fact] + public async Task Given_CreateChatCompletionResponseSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var createChatCompletionResponseSchema = schemas.GetProperty("CreateChatCompletionResponse"); + var properties = createChatCompletionResponseSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "id", "object", "created", "model", "usage", "system_fingerprint", "prompt_filter_results", "choices" }); + } + + [Fact] + public async Task Given_ChatCompletionChoiceSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionChoiceSchema = schemas.GetProperty("ChatCompletionChoice"); + var properties = chatCompletionChoiceSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "index", "finish_reason", "message", "content_filter_results", "logprobs" }); + } + + [Fact] + public async Task Given_ChatCompletionResponseMessageSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionResponseMessageSchema = schemas.GetProperty("ChatCompletionResponseMessage"); + var properties = chatCompletionResponseMessageSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "role", "content", "tool_calls", "function_call", "context" }); + } + + [Fact] + public async Task Given_ChatCompletionFunctionCallSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionFunctionCallSchema = schemas.GetProperty("ChatCompletionFunctionCall"); + var properties = chatCompletionFunctionCallSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "name", "arguments" }); + } + + [Fact] + public async Task Given_AzureChatExtensionsMessageContextSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var azureChatExtensionsMessageContextSchema = schemas.GetProperty("AzureChatExtensionsMessageContext"); + var properties = azureChatExtensionsMessageContextSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "citations", "intent" }); + } + + [Fact] + public async Task Given_ContentFilterSeverityResultSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var contentFilterSeverityResultSchema = schemas.GetProperty("ContentFilterSeverityResult"); + var properties = contentFilterSeverityResultSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "severity", "filtered" }); + } + + [Fact] + public async Task Given_CompletionUsageSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var completionUsageSchema = schemas.GetProperty("CompletionUsage"); + var properties = completionUsageSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "prompt_tokens", "completion_tokens", "total_tokens" }); + } + + [Fact] + public async Task Given_ContentFilterDetectedResultSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var contentFilterDetectedResultSchema = schemas.GetProperty("ContentFilterDetectedResult"); + var properties = contentFilterDetectedResultSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "filtered", "detected" }); + } + + [Fact] + public async Task Given_ContentFilterDetectedWithCitationResultSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var contentFilterDetectedWithCitationResultSchema = schemas.GetProperty("ContentFilterDetectedWithCitationResult"); + var properties = contentFilterDetectedWithCitationResultSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "filtered", "detected", "citation" }); + } + + [Fact] + public async Task Given_FunctionObjectSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var functionObjectSchema = schemas.GetProperty("FunctionObject"); + var properties = functionObjectSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "name", "arguments" }); + } + + [Fact] + public async Task Given_ChatCompletionChoiceLogProbsSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionChoiceLogProbsSchema = schemas.GetProperty("ChatCompletionChoiceLogProbs"); + var properties = chatCompletionChoiceLogProbsSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "content" }); + } + + [Fact] + public async Task Given_ChatCompletionTokenLogProbSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionTokenLogProbSchema = schemas.GetProperty("ChatCompletionTokenLogProb"); + var properties = chatCompletionTokenLogProbSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "token", "logprob", "bytes", "top_logprobs" }); + } + + [Fact] + public async Task Given_TopLogProbsSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var topLogProbsSchema = schemas.GetProperty("TopLogProbs"); + var properties = topLogProbsSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "token", "logprob", "bytes" }); + } + + [Fact] + public async Task Given_CitationSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var citationSchema = schemas.GetProperty("Citation"); + var properties = citationSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "content", "title", "url", "filepath", "chunk_id" }); + } + + [Fact] + public async Task Given_PromptFilterResultSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var promptFilterResultSchema = schemas.GetProperty("PromptFilterResult"); + var properties = promptFilterResultSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "prompt_index", "content_filter_results" }); + } + + [Fact] + public async Task Given_ContentFilterPromptResultsSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var contentFilterPromptResultsSchema = schemas.GetProperty("ContentFilterPromptResults"); + var properties = contentFilterPromptResultsSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "sexual", "violence", "hate", "self_harm", "profanity", "error", "jailbreak" }); + } + + [Fact] + public async Task Given_ContentFilterChoiceResultsSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var contentFilterChoiceResultsSchema = schemas.GetProperty("ContentFilterChoiceResults"); + var properties = contentFilterChoiceResultsSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "sexual", "violence", "hate", "self_harm", "profanity", "error", "protected_material_text", "protected_material_code" }); + } + + [Fact] + public async Task Given_ChatCompletionMessageToolCallSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var chatCompletionMessageToolCallSchema = schemas.GetProperty("ChatCompletionMessageToolCall"); + var properties = chatCompletionMessageToolCallSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "id", "type", "function" }); + } + + [Fact] + public async Task Given_ErrorBaseSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var errorBaseSchema = schemas.GetProperty("ErrorBase"); + var properties = errorBaseSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "code", "message" }); + } + + [Fact] + public async Task Given_CitationObjectSchema_When_ValidatingProperties_Then_PropertiesShouldBeCorrect() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + await host.ResourceNotificationService.WaitForResourceAsync("apiapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var openapi = JsonSerializer.Deserialize(json); + var schemas = openapi!.RootElement.GetProperty("components").GetProperty("schemas"); + + // Act + var citationObjectSchema = schemas.GetProperty("CitationObject"); + var properties = citationObjectSchema.GetProperty("properties").EnumerateObject().Select(x => x.Name).ToList(); + + // Assert + properties.Should().Contain(new[] { "URL", "license" }); + } +} \ No newline at end of file