From bcdc02783390f2b4f47a77f4de54958c6e90250a Mon Sep 17 00:00:00 2001
From: praivesi <praivesi@gmail.com>
Date: Wed, 18 Sep 2024 17:27:31 +0900
Subject: [PATCH 1/3] =?UTF-8?q?chore:=20=EC=98=A4=ED=83=88=EC=9E=90=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../Extensions/ServiceCollectionExtensions.cs                   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/AzureOpenAIProxy.ApiApp/Extensions/ServiceCollectionExtensions.cs b/src/AzureOpenAIProxy.ApiApp/Extensions/ServiceCollectionExtensions.cs
index 00783177..965f119d 100644
--- a/src/AzureOpenAIProxy.ApiApp/Extensions/ServiceCollectionExtensions.cs
+++ b/src/AzureOpenAIProxy.ApiApp/Extensions/ServiceCollectionExtensions.cs
@@ -145,7 +145,7 @@ public static IServiceCollection AddTableStorageService(this IServiceCollection
     {
         services.AddSingleton<TableServiceClient>(sp => {
             var configuration = sp.GetService<IConfiguration>()
-                ?? throw new InvalidOperationException($"{nameof(IConfiguration)} service is not registerd.");
+                ?? throw new InvalidOperationException($"{nameof(IConfiguration)} service is not registered.");
             
             var settings = configuration.GetSection(AzureSettings.Name).GetSection(KeyVaultSettings.Name).Get<KeyVaultSettings>()
                 ?? throw new InvalidOperationException($"{nameof(KeyVaultSettings)} could not be retrieved from the configuration.");

From 42c6d00058d323b268fe939c9e87126d5e9d9dad Mon Sep 17 00:00:00 2001
From: praivesi <praivesi@gmail.com>
Date: Fri, 27 Sep 2024 08:37:45 +0900
Subject: [PATCH 2/3] feat: add AdminEventRepository CRUD tests & implement
 CRUD

---
 .../Repositories/AdminEventRepository.cs      | 30 +++++++-
 .../Repositories/AdminEventRepositoryTests.cs | 74 ++++++++++---------
 2 files changed, 65 insertions(+), 39 deletions(-)

diff --git a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs
index 1ef0dfcb..6fc42bc0 100644
--- a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs
+++ b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs
@@ -1,4 +1,7 @@
-using Azure.Data.Tables;
+using System.Collections.Specialized;
+using System.Text.Json;
+
+using Azure.Data.Tables;
 
 using AzureOpenAIProxy.ApiApp.Configurations;
 using AzureOpenAIProxy.ApiApp.Extensions;
@@ -51,13 +54,26 @@ public class AdminEventRepository(TableServiceClient tableServiceClient, Storage
     /// <inheritdoc />
     public async Task<AdminEventDetails> CreateEvent(AdminEventDetails eventDetails)
     {
-        throw new NotImplementedException();
+        var tableClient = _tableServiceClient.GetTableClient(_storageAccountSettings.TableStorage.TableName);
+        
+        await tableClient.AddEntityAsync<AdminEventDetails>(eventDetails);
+        
+        return eventDetails;
     }
 
     /// <inheritdoc />
     public async Task<List<AdminEventDetails>> GetEvents()
     {
-        throw new NotImplementedException();
+        var tableClient = await GetTableClientAsync();
+
+        var eventDetailsList = new List<AdminEventDetails>();
+
+        await foreach (var entity in tableClient.QueryAsync<AdminEventDetails>())
+        {
+            eventDetailsList.Add(entity);
+        }
+
+        return eventDetailsList;
     }
 
     /// <inheritdoc />
@@ -76,7 +92,13 @@ public async Task<AdminEventDetails> GetEvent(Guid eventId)
     /// <inheritdoc />
     public async Task<AdminEventDetails> UpdateEvent(Guid eventId, AdminEventDetails eventDetails)
     {
-        throw new NotImplementedException();
+        var tableClient = await GetTableClientAsync();
+
+        eventDetails.EventId = eventId;
+
+        await tableClient.UpdateEntityAsync<AdminEventDetails>(eventDetails, eventDetails.ETag, TableUpdateMode.Replace);
+
+        return eventDetails;
     }
 
     private async Task<TableClient> GetTableClientAsync()
diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs
index ddc0facd..f60e733c 100644
--- a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs
+++ b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs
@@ -1,4 +1,6 @@
-using Azure;
+using System.Configuration;
+
+using Azure;
 using Azure.Data.Tables;
 
 using AzureOpenAIProxy.ApiApp.Configurations;
@@ -12,10 +14,25 @@
 using NSubstitute;
 using NSubstitute.ExceptionExtensions;
 
+using Xunit.Sdk;
+
 namespace AzureOpenAIProxy.ApiApp.Tests.Repositories;
 
 public class AdminEventRepositoryTests
 {
+    private StorageAccountSettings mockSettings;
+    private TableServiceClient mockTableServiceClient;
+    private TableClient mockTableClient;
+
+    public AdminEventRepositoryTests()
+    {
+        mockSettings = Substitute.For<StorageAccountSettings>();
+        mockTableServiceClient = Substitute.For<TableServiceClient>();
+        mockTableClient = Substitute.For<TableClient>();
+
+        mockTableServiceClient.GetTableClient(Arg.Any<string>()).Returns(mockTableClient);
+    }
+
     [Fact]
     public void Given_ServiceCollection_When_AddAdminEventRepository_Invoked_Then_It_Should_Contain_AdminEventRepository()
     {
@@ -33,11 +50,10 @@ public void Given_ServiceCollection_When_AddAdminEventRepository_Invoked_Then_It
     public void Given_Null_TableServiceClient_When_Creating_AdminEventRepository_Then_It_Should_Throw_Exception()
     {
         // Arrange
-        var settings = Substitute.For<StorageAccountSettings>();
         var tableServiceClient = default(TableServiceClient);
         
         // Act
-        Action action = () => new AdminEventRepository(tableServiceClient, settings);
+        Action action = () => new AdminEventRepository(tableServiceClient, mockSettings);
 
         // Assert
         action.Should().Throw<ArgumentNullException>();
@@ -48,44 +64,39 @@ public void Given_Null_StorageAccountSettings_When_Creating_AdminEventRepository
     {
         // Arrange
         var settings = default(StorageAccountSettings);
-        var tableServiceClient = Substitute.For<TableServiceClient>();
         
         // Act
-        Action action = () => new AdminEventRepository(tableServiceClient, settings);
+        Action action = () => new AdminEventRepository(mockTableServiceClient, settings);
 
         // Assert
         action.Should().Throw<ArgumentNullException>();
     }
 
     [Fact]
-    public void Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Throw_Exception()
+    public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Return_Created_Event()
     {
         // Arrange
-        var settings = Substitute.For<StorageAccountSettings>();
-        var tableServiceClient = Substitute.For<TableServiceClient>();
         var eventDetails = new AdminEventDetails();
-        var repository = new AdminEventRepository(tableServiceClient, settings);
+        var repository = new AdminEventRepository(mockTableServiceClient, mockSettings);
 
         // Act
-        Func<Task> func = async () => await repository.CreateEvent(eventDetails);
+        await repository.CreateEvent(eventDetails);
 
         // Assert
-        func.Should().ThrowAsync<NotImplementedException>();
+        await mockTableClient.Received(1).AddEntityAsync(eventDetails, default);
     }
 
     [Fact]
-    public void Given_Instance_When_GetEvents_Invoked_Then_It_Should_Throw_Exception()
+    public async Task Given_Instance_When_GetEvents_Invoked_Then_It_Should_Invoke_QueryAsync_Method()
     {
         // Arrange
-        var settings = Substitute.For<StorageAccountSettings>();
-        var tableServiceClient = Substitute.For<TableServiceClient>();
-        var repository = new AdminEventRepository(tableServiceClient, settings);
+        var repository = new AdminEventRepository(mockTableServiceClient, mockSettings);
 
         // Act
-        Func<Task> func = async () => await repository.GetEvents();
+        await repository.GetEvents();
 
         // Assert
-        func.Should().ThrowAsync<NotImplementedException>();
+        mockTableClient.Received(1).QueryAsync<AdminEventDetails>();
     }
 
     [Theory]
@@ -94,16 +105,12 @@ public void Given_Instance_When_GetEvents_Invoked_Then_It_Should_Throw_Exception
     public async Task Given_Failure_In_Get_Entity_When_GetEvent_Invoked_Then_It_Should_Throw_Exception(int statusCode)
     {
         // Arrange
-        var settings = Substitute.For<StorageAccountSettings>();
-        var tableServiceClient = Substitute.For<TableServiceClient>();
         var eventId = Guid.NewGuid();
-        var repository = new AdminEventRepository(tableServiceClient, settings);
+        var repository = new AdminEventRepository(mockTableServiceClient, mockSettings);
 
         var exception = new RequestFailedException(statusCode, "Request Error", default, default);
 
-        var tableClient = Substitute.For<TableClient>();
-        tableServiceClient.GetTableClient(Arg.Any<string>()).Returns(tableClient);
-        tableClient.GetEntityAsync<AdminEventDetails>(Arg.Any<string>(), Arg.Any<string>())
+        mockTableClient.GetEntityAsync<AdminEventDetails>(Arg.Any<string>(), Arg.Any<string>())
             .ThrowsAsync(exception);
 
         // Act
@@ -119,9 +126,7 @@ public async Task Given_Failure_In_Get_Entity_When_GetEvent_Invoked_Then_It_Shou
     public async Task Given_Exist_EventId_When_GetEvent_Invoked_Then_It_Should_Return_AdminEventDetails(string eventId, string partitionKey)
     {
         // Arrange
-        var settings = Substitute.For<StorageAccountSettings>();
-        var tableServiceClient = Substitute.For<TableServiceClient>();
-        var repository = new AdminEventRepository(tableServiceClient, settings);
+        var repository = new AdminEventRepository(mockTableServiceClient, mockSettings);
 
         var eventDetails = new AdminEventDetails
         {
@@ -131,9 +136,7 @@ public async Task Given_Exist_EventId_When_GetEvent_Invoked_Then_It_Should_Retur
 
         var response = Response.FromValue(eventDetails, Substitute.For<Response>());
 
-        var tableClient = Substitute.For<TableClient>();
-        tableServiceClient.GetTableClient(Arg.Any<string>()).Returns(tableClient);
-        tableClient.GetEntityAsync<AdminEventDetails>(partitionKey, eventId)
+        mockTableClient.GetEntityAsync<AdminEventDetails>(partitionKey, eventId)
             .Returns(Task.FromResult(response));
 
         // Act
@@ -144,19 +147,20 @@ public async Task Given_Exist_EventId_When_GetEvent_Invoked_Then_It_Should_Retur
     }
 
     [Fact]
-    public void Given_Instance_When_UpdateEvent_Invoked_Then_It_Should_Throw_Exception()
+    public async Task Given_Instance_When_UpdateEvent_Invoked_Then_It_Should_Invoke_UpdateEntityAsync_Method()
     {
         // Arrange
-        var settings = Substitute.For<StorageAccountSettings>();
-        var tableServiceClient = Substitute.For<TableServiceClient>();
         var eventId = Guid.NewGuid();
         var eventDetails = new AdminEventDetails();
-        var repository = new AdminEventRepository(tableServiceClient, settings);
+        var repository = new AdminEventRepository(mockTableServiceClient, mockSettings);
 
         // Act
-        Func<Task> func = async () => await repository.UpdateEvent(eventId, eventDetails);
+        await repository.UpdateEvent(eventId, eventDetails);
 
         // Assert
-        func.Should().ThrowAsync<NotImplementedException>();
+        await mockTableClient.Received(1)
+                    .UpdateEntityAsync<AdminEventDetails>(Arg.Any<AdminEventDetails>(),
+                                                          Arg.Any<Azure.ETag>(),
+                                                          TableUpdateMode.Replace);
     }
 }

From 65be6a9fb3d612919124c69d29b29d108945f821 Mon Sep 17 00:00:00 2001
From: praivesi <praivesi@gmail.com>
Date: Sat, 28 Sep 2024 13:53:06 +0900
Subject: [PATCH 3/3] feat: add AdminEventService Tests

---
 .../Repositories/AdminEventRepository.cs      | 17 +++++-
 .../Services/AdminEventService.cs             | 15 ++++++
 .../Repositories/AdminEventRepositoryTests.cs | 31 ++++++++---
 .../Services/AdminEventServiceTests.cs        | 52 +++++++++++++------
 4 files changed, 91 insertions(+), 24 deletions(-)

diff --git a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs
index 6fc42bc0..f72ec2db 100644
--- a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs
+++ b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs
@@ -41,6 +41,13 @@ public interface IAdminEventRepository
     /// <param name="eventDetails">Event details instance.</param>
     /// <returns>Returns the updated record of the event details.</returns>
     Task<AdminEventDetails> UpdateEvent(Guid eventId, AdminEventDetails eventDetails);
+
+    /// <summary>
+    /// Deletes the event details.
+    /// </summary>
+    /// <param name="eventDetails">Event details instance.</param>
+    /// <returns>Removed EventID of event details instance.</returns>
+    Task<Guid> DeleteEvent(AdminEventDetails eventDetails);
 }
 
 /// <summary>
@@ -65,7 +72,6 @@ public async Task<AdminEventDetails> CreateEvent(AdminEventDetails eventDetails)
     public async Task<List<AdminEventDetails>> GetEvents()
     {
         var tableClient = await GetTableClientAsync();
-
         var eventDetailsList = new List<AdminEventDetails>();
 
         await foreach (var entity in tableClient.QueryAsync<AdminEventDetails>())
@@ -101,6 +107,15 @@ public async Task<AdminEventDetails> UpdateEvent(Guid eventId, AdminEventDetails
         return eventDetails;
     }
 
+    public async Task<Guid> DeleteEvent(AdminEventDetails eventDetails)
+    {
+        var tableClient = await GetTableClientAsync();
+
+        await tableClient.DeleteEntityAsync(eventDetails.PartitionKey, eventDetails.RowKey);
+
+        return eventDetails.EventId;
+    }
+
     private async Task<TableClient> GetTableClientAsync()
     {
         TableClient tableClient = _tableServiceClient.GetTableClient(_storageAccountSettings.TableStorage.TableName);
diff --git a/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs b/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs
index 09f21797..d4e0746c 100644
--- a/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs
+++ b/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs
@@ -35,6 +35,13 @@ public interface IAdminEventService
     /// <param name="eventDetails">Event details to update.</param>
     /// <returns>Returns the updated event details.</returns>
     Task<AdminEventDetails> UpdateEvent(Guid eventId, AdminEventDetails eventDetails);
+
+     /// <summary>
+    /// Deletes the event details.
+    /// </summary>
+    /// <param name="eventDetails">Event details to update.</param>
+    /// <returns>Removed EventID of event details instance.</returns>
+    Task<Guid> DeleteEvent(AdminEventDetails eventDetails);
 }
 
 /// <summary>
@@ -75,6 +82,14 @@ public async Task<AdminEventDetails> UpdateEvent(Guid eventId, AdminEventDetails
 
         return result;
     }
+
+    /// <inheritdoc />
+    public async Task<Guid> DeleteEvent(AdminEventDetails eventDetails)
+    {
+        var result = await this._repository.DeleteEvent(eventDetails).ConfigureAwait(false);
+
+        return result;
+    }
 }
 
 /// <summary>
diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs
index f60e733c..bba20443 100644
--- a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs
+++ b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs
@@ -1,6 +1,4 @@
-using System.Configuration;
-
-using Azure;
+using Azure;
 using Azure.Data.Tables;
 
 using AzureOpenAIProxy.ApiApp.Configurations;
@@ -14,15 +12,13 @@
 using NSubstitute;
 using NSubstitute.ExceptionExtensions;
 
-using Xunit.Sdk;
-
 namespace AzureOpenAIProxy.ApiApp.Tests.Repositories;
 
 public class AdminEventRepositoryTests
 {
-    private StorageAccountSettings mockSettings;
-    private TableServiceClient mockTableServiceClient;
-    private TableClient mockTableClient;
+    private readonly StorageAccountSettings mockSettings;
+    private readonly TableServiceClient mockTableServiceClient;
+    private readonly TableClient mockTableClient;
 
     public AdminEventRepositoryTests()
     {
@@ -163,4 +159,23 @@ await mockTableClient.Received(1)
                                                           Arg.Any<Azure.ETag>(),
                                                           TableUpdateMode.Replace);
     }
+
+    [Fact]
+    public async Task Given_Instance_When_DeleteEvent_Invoked_Then_It_Should_Invoke_DeleteEntityAsync_Method()
+    {
+        // Arrange
+        var eventDetails = new AdminEventDetails();
+        var repository = new AdminEventRepository(mockTableServiceClient, mockSettings);
+        var eventId = Guid.NewGuid();
+
+        eventDetails.EventId = eventId;
+
+        // Act
+        Guid deletedEventId = await repository.DeleteEvent(eventDetails);
+
+        // Assert
+        deletedEventId.Should().Be(eventId);
+        await mockTableClient.Received(1)
+                             .DeleteEntityAsync(Arg.Any<string>(), Arg.Any<string>());
+    }
 }
diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs
index f0442c76..a1f4bd23 100644
--- a/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs
+++ b/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs
@@ -15,6 +15,13 @@ namespace AzureOpenAIProxy.ApiApp.Tests.Services;
 
 public class AdminEventServiceTests
 {
+    private readonly IAdminEventRepository mockRepository;
+
+    public AdminEventServiceTests()
+    {
+        mockRepository = Substitute.For<IAdminEventRepository>();
+    }
+
     [Fact]
     public void Given_ServiceCollection_When_AddAdminEventService_Invoked_Then_It_Should_Contain_AdminEventService()
     {
@@ -33,8 +40,7 @@ public void Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Throw_Excepti
     {
         // Arrange
         var eventDetails = new AdminEventDetails();
-        var repository = Substitute.For<IAdminEventRepository>();
-        var service = new AdminEventService(repository);
+        var service = new AdminEventService(mockRepository);
 
         // Act
         Func<Task> func = async () => await service.CreateEvent(eventDetails);
@@ -47,8 +53,7 @@ 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 repository = Substitute.For<IAdminEventRepository>();
-        var service = new AdminEventService(repository);
+        var service = new AdminEventService(mockRepository);
 
         // Act
         Func<Task> func = async () => await service.GetEvents();
@@ -65,12 +70,11 @@ public async Task Given_Failure_In_Get_Entity_When_GetEvent_Invoked_Then_It_Shou
     {
         // Arrange
         var eventId = Guid.NewGuid();
-        var repository = Substitute.For<IAdminEventRepository>();
-        var service = new AdminEventService(repository);
+        var service = new AdminEventService(mockRepository);
 
         var exception = new RequestFailedException(statusCode, "Request Failed", default, default);
 
-        repository.GetEvent(Arg.Any<Guid>()).ThrowsAsync(exception);
+        mockRepository.GetEvent(Arg.Any<Guid>()).ThrowsAsync(exception);
 
         // Act
         Func<Task> func = () => service.GetEvent(eventId);
@@ -85,8 +89,7 @@ public async Task Given_Failure_In_Get_Entity_When_GetEvent_Invoked_Then_It_Shou
     public async Task Given_Exist_EventId_When_GetEvent_Invoked_Then_It_Should_Return_AdminEventDetails(string eventId)
     {
         // Arrange
-        var repository = Substitute.For<IAdminEventRepository>();
-        var service = new AdminEventService(repository);
+        var service = new AdminEventService(mockRepository);
 
         var eventDetails = new AdminEventDetails
         {
@@ -95,7 +98,7 @@ public async Task Given_Exist_EventId_When_GetEvent_Invoked_Then_It_Should_Retur
 
         var guid = Guid.Parse(eventId);
 
-        repository.GetEvent(guid).Returns(Task.FromResult(eventDetails));
+        mockRepository.GetEvent(guid).Returns(Task.FromResult(eventDetails));
 
         // Act
         var result = await service.GetEvent(guid);
@@ -105,18 +108,37 @@ public async Task Given_Exist_EventId_When_GetEvent_Invoked_Then_It_Should_Retur
     }
 
     [Fact]
-    public void Given_Instance_When_UpdateEvent_Invoked_Then_It_Should_Throw_Exception()
+    public async Task Given_Instance_When_UpdateEvent_Invoked_Then_It_Should_Return_Updated_Event_Details()
     {
         // Arrange
         var eventId = Guid.NewGuid();
         var eventDetails = new AdminEventDetails();
-        var repository = Substitute.For<IAdminEventRepository>();
-        var service = new AdminEventService(repository);
+        var service = new AdminEventService(mockRepository);
+
+        mockRepository.UpdateEvent(eventId, eventDetails).Returns(eventDetails);
 
         // Act
-        Func<Task> func = async () => await service.UpdateEvent(eventId, eventDetails);
+        var updatedEventDetails = await service.UpdateEvent(eventId, eventDetails);
 
         // Assert
-        func.Should().ThrowAsync<NotImplementedException>();
+        updatedEventDetails.Should().BeEquivalentTo(eventDetails);
+    }
+
+    [Fact]
+    public async Task Given_Instance_When_DeleteEvent_Invoked_Then_It_Should_Return_Deleted_Event_Id()
+    {
+        // Arrange
+        var eventId = Guid.NewGuid();
+        var eventDetails = new AdminEventDetails();
+        var service = new AdminEventService(mockRepository);
+
+        eventDetails.EventId = eventId;
+        mockRepository.DeleteEvent(eventDetails).Returns(eventDetails.EventId);
+
+        // Act
+        var deletedEventId = await service.DeleteEvent(eventDetails);
+
+        // Assert
+        deletedEventId.Should().Be(eventId);
     }
 }