diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/PlaygroundEndpoints.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/PlaygroundEndpoints.cs index 91c01b8f..5a55fe77 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/PlaygroundEndpoints.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/PlaygroundEndpoints.cs @@ -1,3 +1,7 @@ +using Azure; + +using AzureOpenAIProxy.ApiApp.Services; + using Microsoft.AspNetCore.Mvc; namespace AzureOpenAIProxy.ApiApp.Endpoints; @@ -14,10 +18,24 @@ public static class PlaygroundEndpoints /// Returns instance. public static RouteHandlerBuilder AddListEvents(this WebApplication app) { - var builder = app.MapGet(PlaygroundEndpointUrls.Events, () => + // ASSUMPTION: User has already logged in + var builder = app.MapGet(PlaygroundEndpointUrls.Events, async ( + IPlaygroundService service, + ILoggerFactory loggerFactory) => { - // TODO: Issue #179 https://github.com/aliencube/azure-openai-sdk-proxy/issues/179 - return Results.Ok(); + var logger = loggerFactory.CreateLogger(nameof(AdminEventEndpoints)); + logger.LogInformation("Received request to fetch events list"); + + try + { + var eventDetailsList = await service.GetEvents(); + return Results.Ok(eventDetailsList); + } + catch (Exception ex) + { + logger.LogError(ex, $"Error occurred while fetching events list"); + return Results.Problem(ex.Message, statusCode: StatusCodes.Status500InternalServerError); + } }) .Produces>(statusCode: StatusCodes.Status200OK, contentType: "application/json") .Produces(statusCode: StatusCodes.Status401Unauthorized) diff --git a/src/AzureOpenAIProxy.ApiApp/Program.cs b/src/AzureOpenAIProxy.ApiApp/Program.cs index f35b83ee..969e9caa 100644 --- a/src/AzureOpenAIProxy.ApiApp/Program.cs +++ b/src/AzureOpenAIProxy.ApiApp/Program.cs @@ -28,6 +28,12 @@ // Add admin repositories builder.Services.AddAdminEventRepository(); +// Add playground services +builder.Services.AddPlaygroundService(); + +// Add playground repositories +builder.Services.AddEventRepository(); + var app = builder.Build(); app.MapDefaultEndpoints(); diff --git a/src/AzureOpenAIProxy.ApiApp/Repositories/EventRepository.cs b/src/AzureOpenAIProxy.ApiApp/Repositories/EventRepository.cs new file mode 100644 index 00000000..3cbf4e72 --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Repositories/EventRepository.cs @@ -0,0 +1,71 @@ +using Azure.Data.Tables; + +using AzureOpenAIProxy.ApiApp.Configurations; + +namespace AzureOpenAIProxy.ApiApp.Repositories; + +/// +/// This provides interfaces to the class. +/// +public interface IEventRepository +{ + /// + /// Gets the list of events. + /// + /// Returns the list of events. + Task> GetEvents(); +} + +public class EventRepository(TableServiceClient tableServiceClient, StorageAccountSettings storageAccountSettings) : IEventRepository +{ + private readonly TableServiceClient _tableServiceClient = tableServiceClient ?? throw new ArgumentNullException(nameof(tableServiceClient)); + private readonly StorageAccountSettings _storageAccountSettings = storageAccountSettings ?? throw new ArgumentNullException(nameof(storageAccountSettings)); + + /// + /// + /// The results are sorted based on the following criteria: + /// Lexical order of event titles. + /// + public async Task> GetEvents() + { + TableClient tableClient = await GetTableClientAsync(); + + List events = []; + + await foreach(EventDetails eventDetails in tableClient.QueryAsync(e => e.PartitionKey.Equals(PartitionKeys.EventDetails)).ConfigureAwait(false)) + { + events.Add(eventDetails); + } + + events.Sort((e1, e2) => e1.Title.CompareTo(e2.Title)); + + return events; + } + + private async Task GetTableClientAsync() + { + TableClient tableClient = _tableServiceClient.GetTableClient(_storageAccountSettings.TableStorage.TableName); + + await tableClient.CreateIfNotExistsAsync().ConfigureAwait(false); + + return tableClient; + } +} + +/// +/// This represents the extension class for +/// +public static class EventRepositoryExtensions +{ + /// + /// Adds the instance to the service collection. + /// + /// instance. + /// Returns instance. + public static IServiceCollection AddEventRepository(this IServiceCollection services) + { + services.AddScoped(); + + return services; + } +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Services/PlaygroundService.cs b/src/AzureOpenAIProxy.ApiApp/Services/PlaygroundService.cs new file mode 100644 index 00000000..5d9ef331 --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Services/PlaygroundService.cs @@ -0,0 +1,57 @@ +using AzureOpenAIProxy.ApiApp.Repositories; + +namespace AzureOpenAIProxy.ApiApp.Services; + +/// +/// This provides interfaces to class. +/// +public interface IPlaygroundService +{ + /// + /// Get the list of deployment model. + /// + /// Returns the list of deployment models. + Task> GetDeploymentModels(string eventId); + + /// + /// Get the list of events. + /// + /// Returns the list of events. + Task> GetEvents(); +} + +public class PlaygroundService(IEventRepository eventRepository) : IPlaygroundService +{ + private readonly IEventRepository _eventRepository = eventRepository ?? throw new ArgumentNullException(nameof(eventRepository)); + /// + public async Task> GetDeploymentModels(string eventId) + { + throw new NotImplementedException(); + } + + /// + public async Task> GetEvents() + { + var result = await _eventRepository.GetEvents().ConfigureAwait(false); + + return result; + } +} + +/// +/// This represents the extension class for +/// +public static class PlaygroundServiceExtensions +{ + /// + /// Adds the instance to the service collection. + /// + /// instance. + /// Returns instance. + public static IServiceCollection AddPlaygroundService(this IServiceCollection services) + { + services.AddScoped(); + + return services; + } +} \ No newline at end of file diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/EventRepositoryTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/EventRepositoryTests.cs new file mode 100644 index 00000000..b98a0b34 --- /dev/null +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/EventRepositoryTests.cs @@ -0,0 +1,120 @@ +using System.Linq.Expressions; + +using Azure; +using Azure.Data.Tables; + +using AzureOpenAIProxy.ApiApp.Configurations; +using AzureOpenAIProxy.ApiApp.Repositories; + +using FluentAssertions; + +using Microsoft.Extensions.DependencyInjection; + +using NSubstitute; +using NSubstitute.ExceptionExtensions; + +namespace AzureOpenAIProxy.ApiApp.Tests.Repositories; + +public class EventRepositoryTests +{ + [Fact] + public void Given_ServiceCollection_When_AddEventRepository_Invoked_Then_It_Should_Contain_EventRepository() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddEventRepository(); + + // Assert + services.SingleOrDefault(p => p.ServiceType == typeof(IEventRepository)).Should().NotBeNull(); + } + + [Fact] + public void Given_Null_TableServiceClient_When_Creating_EventRepository_Then_It_Should_Throw_Exception() + { + // Arrange + var settings = Substitute.For(); + var tableServiceClient = default(TableServiceClient); + + // Act + Action action = () => new EventRepository(tableServiceClient, settings); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void Given_Null_StorageAccountSettings_When_Creating_EventRepository_Then_It_Should_Throw_Exception() + { + // Arrange + var settings = default(StorageAccountSettings); + var tableServiceClient = Substitute.For(); + + // Act + Action action = () => new EventRepository(tableServiceClient, settings); + + // Assert + action.Should().Throw(); + } + + [Fact] + public async Task Given_Failure_In_Get_Entities_When_GetEvents_Invoked_Then_It_Should_Throw_Exception() + { + // Arrange + var settings = Substitute.For(); + var tableServiceClient = Substitute.For(); + var repository = new EventRepository(tableServiceClient, settings); + + var tableClient = Substitute.For(); + tableServiceClient.GetTableClient(Arg.Any()).Returns(tableClient); + tableClient.QueryAsync(Arg.Any>>()).Throws(new Exception("error occurred")); + + // Act + Func func = repository.GetEvents; + + // Assert + var assertion = await func.Should().ThrowAsync(); + } + + [Fact] + public async Task Given_Exist_Events_When_GetEvents_Invoked_Then_It_Should_Return_EventDetails_List() + { + // Arrange + Random rand = new(); + int listSize = rand.Next(1, 20); + Guid eventId = new(); + + var settings = Substitute.For(); + var tableServiceClient = Substitute.For(); + var repository = new EventRepository(tableServiceClient, settings); + + var eventDetails = new EventDetails + { + RowKey = eventId.ToString(), + PartitionKey = PartitionKeys.EventDetails + }; + + List events = []; + + for(int i = 0; i < listSize; ++i) + { + events.Add(eventDetails); + } + + var pages = Page.FromValues(events, default, Substitute.For()); + var asyncPages = AsyncPageable.FromPages([pages]); + + var tableClient = Substitute.For(); + tableServiceClient.GetTableClient(Arg.Any()).Returns(tableClient); + tableClient.QueryAsync(Arg.Any>>()).Returns(asyncPages); + + // Act + var result = await repository.GetEvents(); + + // Assert + result.Count.Should().Be(listSize); + result.First().Should().BeEquivalentTo(eventDetails); + } + +} \ No newline at end of file diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Services/PlaygroundServiceTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Services/PlaygroundServiceTests.cs new file mode 100644 index 00000000..38e93879 --- /dev/null +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Services/PlaygroundServiceTests.cs @@ -0,0 +1,93 @@ +using Azure; + +using AzureOpenAIProxy.ApiApp.Repositories; +using AzureOpenAIProxy.ApiApp.Services; + +using FluentAssertions; + +using Microsoft.Extensions.DependencyInjection; + +using NSubstitute; +using NSubstitute.ExceptionExtensions; + +namespace AzureOpenAIProxy.ApiApp.Tests.Services; + +public class PlaygroundServiceTests +{ + [Fact] + public void Given_ServiceCollection_When_AddPlaygroundService_Invoked_Then_It_Should_Contain_PlaygroundService() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddPlaygroundService(); + + // Assert + services.SingleOrDefault(p => p.ServiceType == typeof(IPlaygroundService)).Should().NotBeNull(); + } + + [Fact] + public void Given_Instance_When_GetDeploymentModels_Invoked_Then_It_Should_Throw_Exception() + { + // Arrange + string eventId = "test-id"; + var repository = Substitute.For(); + var service = new PlaygroundService(repository); + + // Act + Func func = async () => await service.GetDeploymentModels(eventId); + + // Assert + func.Should().ThrowAsync(); + } + + [Fact] + public async Task Given_Failure_In_Get_Entities_When_GetEvents_Invoked_Then_It_Should_Throw_Exception() + { + // Arrange + var eventId = Guid.NewGuid(); + var repository = Substitute.For(); + var service = new PlaygroundService(repository); + + repository.GetEvents().ThrowsAsync(new Exception("Error occurred")); + + // Act + Func func = service.GetEvents; + + // Assert + var assertion = await func.Should().ThrowAsync(); + } + + [Fact] + public async Task Given_Exist_Events_When_GetEvents_Invoked_Then_It_Should_Return_EventDetails_List() + { + // Arrange + Random rand = new(); + int listSize = rand.Next(1, 20); + Guid eventId = new(); + var repository = Substitute.For(); + var service = new PlaygroundService(repository); + + var eventDetails = new EventDetails + { + RowKey = eventId.ToString() + }; + + List events = []; + for(int i = 0; i < listSize; ++i) + { + events.Add(eventDetails); + } + + repository.GetEvents().Returns(events); + + // Act + var result = await service.GetEvents(); + + // Assert + result.Count.Should().Be(listSize); + result.First().Should().BeEquivalentTo(eventDetails); + } + +} \ No newline at end of file