diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/EndpointUrls.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/EndpointUrls.cs index a5c6d5d2..b69068eb 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/EndpointUrls.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/EndpointUrls.cs @@ -1,4 +1,4 @@ -namespace AzureOpenAIProxy.ApiApp.Endpoints; +namespace AzureOpenAIProxy.ApiApp.Endpoints; /// /// This represents the collection of the endpoint URLs. @@ -14,4 +14,9 @@ public static class EndpointUrls /// Declares the chat completions endpoint. /// public const string ChatCompletions = "/openai/deployments/{deploymentName}/chat/completions"; -} \ No newline at end of file + + /// + /// Declares the event endpoint. + /// + public const string Events = "/events"; +} diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/EventEndpoint.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/EventEndpoint.cs new file mode 100644 index 00000000..a82efb31 --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/EventEndpoint.cs @@ -0,0 +1,39 @@ +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 +{ + /// + /// Adds the event endpoint. + /// + /// instance. + /// Returns instance. + public static RouteHandlerBuilder AddEventList(this WebApplication app) + { + var builder = app.MapGet(EndpointUrls.Events, () => + { + // TODO: Issue #179 https://github.com/aliencube/azure-openai-sdk-proxy/issues/179 + return Results.Ok(); + }) + .Produces>(statusCode: StatusCodes.Status200OK, contentType: "application/json") + .Produces(statusCode: StatusCodes.Status401Unauthorized) + .Produces(statusCode: StatusCodes.Status500InternalServerError, contentType: "text/plain") + .WithTags("events") + .WithName("GetEvents") + .WithOpenApi(operation => + { + operation.Description = "Gets all events' details that the user joined."; + operation.Summary = "This endpoint gets all events' details that the user joined."; + + return operation; + }); + + return builder; + } +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Extensions/ServiceCollectionExtensions.cs b/src/AzureOpenAIProxy.ApiApp/Extensions/ServiceCollectionExtensions.cs index d47f5097..c5ad44fe 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; @@ -132,4 +132,4 @@ public static IServiceCollection AddOpenApiService(this IServiceCollection servi return services; } -} +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Filters/OpenApiTagFilter.cs b/src/AzureOpenAIProxy.ApiApp/Filters/OpenApiTagFilter.cs index 196c87fb..9ed10fa3 100644 --- a/src/AzureOpenAIProxy.ApiApp/Filters/OpenApiTagFilter.cs +++ b/src/AzureOpenAIProxy.ApiApp/Filters/OpenApiTagFilter.cs @@ -16,7 +16,8 @@ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) [ new OpenApiTag { Name = "weather", Description = "Weather forecast operations" }, new OpenApiTag { Name = "openai", Description = "Azure OpenAI operations" }, - new OpenApiTag { Name = "admin", Description = "Admin for organizing events" } + new OpenApiTag { Name = "admin", Description = "Admin for organizing events" }, + new OpenApiTag { Name = "events", Description = "User events" } ]; } } diff --git a/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs b/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs index 19a20eed..4f587217 100644 --- a/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs +++ b/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs @@ -3,30 +3,15 @@ /// /// This represent the event detail data for response by admin event endpoint. /// -public class AdminEventDetails +public class AdminEventDetails : EventDetails { /// - /// Gets or sets the event id. - /// - public required string? EventId { get; set; } - - /// - /// Gets or sets the event title name. - /// - public required string? Title { get; set; } - - /// - /// Gets or sets the event summary. - /// - public required string? Summary { get; set; } - - /// - /// Gets or sets the event description. + /// Gets or sets the event description. /// public string? Description { get; set; } /// - /// Gets or sets the event start date. + /// Gets or sets the event start date. /// public required DateTimeOffset? DateStart { get; set; } @@ -46,7 +31,7 @@ public class AdminEventDetails public required bool? IsActive { get; set; } /// - /// Gets or sets the event organizer name. + /// Gets or sets the event organizer name. /// public required string? OrganizerName { get; set; } @@ -64,14 +49,4 @@ public class AdminEventDetails /// Gets or sets the event coorganizer email. /// public string? CoorganizerEmail { get; set; } - - /// - /// Gets or sets the Azure OpenAI Service request max token capacity. - /// - public required int? MaxTokenCap { get; set; } - - /// - /// Gets or sets the Azure OpenAI Service daily request capacity. - /// - public required int? DailyRequestCap { get; set; } } \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Models/EventDetails.cs b/src/AzureOpenAIProxy.ApiApp/Models/EventDetails.cs new file mode 100644 index 00000000..5c33fafb --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Models/EventDetails.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +using AzureOpenAIProxy.ApiApp.Models; + +/// +/// This represents the event's detailed data for response by EventEndpoint. +/// +public class EventDetails +{ + /// + /// Gets or sets the event id. + /// + public required string? EventId { get; set; } + + /// + /// Gets or sets the event title name. + /// + public required string? Title { get; set; } + + /// + /// Gets or sets the event summary. + /// + public required string? Summary { get; set; } + + /// + /// Gets or sets the Azure OpenAI Service request max token capacity. + /// + public required int? MaxTokenCap { get; set; } + + /// + /// Gets or sets the Azure OpenAI Service daily request capacity. + /// + public required 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 07457d08..832c6dd6 100644 --- a/src/AzureOpenAIProxy.ApiApp/Program.cs +++ b/src/AzureOpenAIProxy.ApiApp/Program.cs @@ -39,6 +39,9 @@ app.AddWeatherForecast(); app.AddChatCompletions(); +// Event Endpoints +app.AddEventList(); + // Admin Endpoints app.AddAdminEvents(); app.AddAdminEventList(); diff --git a/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/GetEventsOpenApiTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/GetEventsOpenApiTests.cs new file mode 100644 index 00000000..8aad2350 --- /dev/null +++ b/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/GetEventsOpenApiTests.cs @@ -0,0 +1,126 @@ +using System.Text.Json; + +using AzureOpenAIProxy.AppHost.Tests.Fixtures; + +using FluentAssertions; + +using IdentityModel.Client; + +namespace AzureOpenAIProxy.AppHost.Tests.ApiApp.Endpoints; + +public class GetEventsOpenApiTests(AspireAppHostFixture host) : IClassFixture +{ + [Fact] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Path() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var apiDocument = JsonSerializer.Deserialize(json); + + // Assert + var result = apiDocument!.RootElement.GetProperty("paths") + .TryGetProperty("/events", out var property) ? property : default; + result.ValueKind.Should().Be(JsonValueKind.Object); + } + + [Fact] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Verb() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var apiDocument = JsonSerializer.Deserialize(json); + + // Assert + var result = apiDocument!.RootElement.GetProperty("paths") + .GetProperty("/events") + .TryGetProperty("get", out var property) ? property : default; + result.ValueKind.Should().Be(JsonValueKind.Object); + } + + [Theory] + [InlineData("events")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Tags(string tag) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var apiDocument = JsonSerializer.Deserialize(json); + + // Assert + var result = apiDocument!.RootElement.GetProperty("paths") + .GetProperty("/events") + .GetProperty("get") + .TryGetProperty("tags", out var property) ? property : default; + result.ValueKind.Should().Be(JsonValueKind.Array); + result.EnumerateArray().Select(p => p.GetString()).Should().Contain(tag); + } + + [Theory] + [InlineData("summary")] + [InlineData("operationId")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Value(string attribute) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var apiDocument = JsonSerializer.Deserialize(json); + + // Assert + var result = apiDocument!.RootElement.GetProperty("paths") + .GetProperty("/events") + .GetProperty("get") + .TryGetProperty(attribute, out var property) ? property : default; + result.ValueKind.Should().Be(JsonValueKind.String); + } + + [Theory] + [InlineData("responses")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Object(string attribute) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var apiDocument = JsonSerializer.Deserialize(json); + + // Assert + var result = apiDocument!.RootElement.GetProperty("paths") + .GetProperty("/events") + .GetProperty("get") + .TryGetProperty(attribute, out var property) ? property : default; + result.ValueKind.Should().Be(JsonValueKind.Object); + } + + [Theory] + [InlineData("200")] + [InlineData("401")] + [InlineData("500")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Response(string attribute) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("apiapp"); + + // Act + var json = await httpClient.GetStringAsync("/swagger/v1.0.0/swagger.json"); + var apiDocument = JsonSerializer.Deserialize(json); + + // Assert + var result = apiDocument!.RootElement.GetProperty("paths") + .GetProperty("/events") + .GetProperty("get") + .GetProperty("responses") + .TryGetProperty(attribute, out var property) ? property : default; + result.ValueKind.Should().Be(JsonValueKind.Object); + } +} \ No newline at end of file