From e93e518d98c710bc5bf949188b7623c6ab967d7b Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 31 Aug 2024 15:07:35 +0900 Subject: [PATCH 01/35] =?UTF-8?q?add=20:=20Azure.Data.Tables=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AzureOpenAIProxy.ApiApp/AzureOpenAIProxy.ApiApp.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/AzureOpenAIProxy.ApiApp/AzureOpenAIProxy.ApiApp.csproj b/src/AzureOpenAIProxy.ApiApp/AzureOpenAIProxy.ApiApp.csproj index 92c1aae3..554f15c3 100644 --- a/src/AzureOpenAIProxy.ApiApp/AzureOpenAIProxy.ApiApp.csproj +++ b/src/AzureOpenAIProxy.ApiApp/AzureOpenAIProxy.ApiApp.csproj @@ -7,6 +7,7 @@ + From b507d995ba72a52bdc7b89c11f5861f349806530 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 31 Aug 2024 15:43:58 +0900 Subject: [PATCH 02/35] =?UTF-8?q?add=20:=20aspire.bicep=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=8A=A4=ED=86=A0=EB=A6=AC=EC=A7=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/aspire.bicep | 167 ++++++++++++++++++++++++++------------------- 1 file changed, 98 insertions(+), 69 deletions(-) diff --git a/infra/aspire.bicep b/infra/aspire.bicep index add37d05..b98f5f56 100644 --- a/infra/aspire.bicep +++ b/infra/aspire.bicep @@ -1,69 +1,98 @@ -// The main bicep module to provision Azure resources. -// For a more complete walkthrough to understand how this file works with azd, -// see https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-create - -@minLength(1) -@maxLength(64) -@description('Name of the the environment which is used to generate a short unique hash used in all resources.') -param environmentName string - -@minLength(1) -@description('Primary location for all resources') -param location string - -// Parameters for Key Vault -param keyVaultName string = '' -param enabledForDeployment bool = true -param enabledForTemplateDeployment bool = true -param enableRbacAuthorization bool = true - -var abbrs = loadJsonContent('./abbreviations.json') - -// Tags that should be applied to all resources. -var tags = { - // Tag all resources with the environment name. - 'azd-env-name': environmentName -} - -// Generate a unique token to be used in naming resources. -// Remove linter suppression after using. -#disable-next-line no-unused-vars -// var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) -var resourceToken = uniqueString(resourceGroup().id) - -// Name of the service defined in azure.yaml -// A tag named azd-service-name with this value should be applied to the service host resource, such as: -// Microsoft.Web/sites for appservice, function -// Example usage: -// tags: union(tags, { 'azd-service-name': apiServiceName }) -#disable-next-line no-unused-vars -// var apiServiceName = 'python-api' - -// Add resources to be provisioned below. - -// Provision Key Vault -module keyVault './core/security/keyvault.bicep' = { - name: 'keyVault' - params: { - name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' - location: location - tags: tags - enabledForDeployment: enabledForDeployment - enabledForTemplateDeployment: enabledForTemplateDeployment - enableRbacAuthorization: enableRbacAuthorization - } -} - -// Add outputs from the deployment here, if needed. -// -// This allows the outputs to be referenced by other bicep deployments in the deployment pipeline, -// or by the local machine as a way to reference created resources in Azure for local development. -// Secrets should not be added here. -// -// Outputs are automatically saved in the local azd environment .env file. -// To see these outputs, run `azd env get-values`, or `azd env get-values --output json` for json output. -output AZURE_LOCATION string = location -output AZURE_TENANT_ID string = tenant().tenantId - -output AZURE_KEYVAULT_NAME string = keyVault.outputs.name -output AZURE_KEYVAULT_ENDPOINT string = keyVault.outputs.endpoint +// The main bicep module to provision Azure resources. +// For a more complete walkthrough to understand how this file works with azd, +// see https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-create + +@minLength(1) +@maxLength(64) +@description('Name of the the environment which is used to generate a short unique hash used in all resources.') +param environmentName string + +@minLength(1) +@description('Primary location for all resources') +param location string + +// Parameters for Key Vault +param keyVaultName string = '' +param enabledForDeployment bool = true +param enabledForTemplateDeployment bool = true +param enableRbacAuthorization bool = true + +var abbrs = loadJsonContent('./abbreviations.json') + +// Tags that should be applied to all resources. +var tags = { + // Tag all resources with the environment name. + 'azd-env-name': environmentName +} + +// Generate a unique token to be used in naming resources. +// Remove linter suppression after using. +#disable-next-line no-unused-vars +// var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var resourceToken = uniqueString(resourceGroup().id) + +// Name of the service defined in azure.yaml +// A tag named azd-service-name with this value should be applied to the service host resource, such as: +// Microsoft.Web/sites for appservice, function +// Example usage: +// tags: union(tags, { 'azd-service-name': apiServiceName }) +#disable-next-line no-unused-vars +// var apiServiceName = 'python-api' + +// Add resources to be provisioned below. + +// Provision Key Vault +module keyVault './core/security/keyvault.bicep' = { + name: 'keyVault' + params: { + name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' + location: location + tags: tags + enabledForDeployment: enabledForDeployment + enabledForTemplateDeployment: enabledForTemplateDeployment + enableRbacAuthorization: enableRbacAuthorization + } +} + +// Provision Storage Account +resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = { + name: 'storage${resourceToken}' + location: location + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: { + supportsHttpsTrafficOnly: true + } +} + +// Provision Table Service +resource tableService 'Microsoft.Storage/storageAccounts/tableServices@2021-04-01' = { + parent: storageAccount + name: 'default' +} + +// Provision Table Storage +resource table 'Microsoft.Storage/storageAccounts/tableServices/tables@2021-04-01' = { + parent: tableService + name: 'mytable' +} + +// Add outputs from the deployment here, if needed. +// +// This allows the outputs to be referenced by other bicep deployments in the deployment pipeline, +// or by the local machine as a way to reference created resources in Azure for local development. +// Secrets should not be added here. +// +// Outputs are automatically saved in the local azd environment .env file. +// To see these outputs, run `azd env get-values`, or `azd env get-values --output json` for json output. +output AZURE_LOCATION string = location +output AZURE_TENANT_ID string = tenant().tenantId + +output AZURE_KEYVAULT_NAME string = keyVault.outputs.name +output AZURE_KEYVAULT_ENDPOINT string = keyVault.outputs.endpoint + +output AZURE_STORAGE_ACCOUNT_NAME string = storageAccount.name +output AZURE_STORAGE_ACCOUNT_TABLE_NAME string = table.name +//TODO: connection string을 깃헙 액션으로 안전하게 전달하기 From 75d7042e386fae0eb278a3dd8ce375be29a10ba0 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 31 Aug 2024 17:58:41 +0900 Subject: [PATCH 03/35] =?UTF-8?q?add=20:=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EB=A6=AC=EC=A7=80=20=EC=97=B0=EA=B2=B0,=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=83=9D=EC=84=B1=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A4=80=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Endpoints/AdminEventEndpoints.cs | 314 ++++++++++-------- src/AzureOpenAIProxy.ApiApp/Program.cs | 116 ++++--- .../appsettings.Development.sample.json | 58 ++-- src/AzureOpenAIProxy.ApiApp/appsettings.json | 4 + 4 files changed, 274 insertions(+), 218 deletions(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs index c319e491..5dbf0049 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs @@ -1,141 +1,175 @@ -using AzureOpenAIProxy.ApiApp.Models; - -using Microsoft.AspNetCore.Mvc; - -namespace AzureOpenAIProxy.ApiApp.Endpoints; - -/// -/// This represents the endpoint entity for get event details by admin -/// -public static class AdminEventEndpoints -{ - /// - /// Adds the get event details by admin endpoint - /// - /// instance. - /// Returns instance. - public static RouteHandlerBuilder AddAdminEvents(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) => - { - // Todo: Issue #208 https://github.com/aliencube/azure-openai-sdk-proxy/issues/208 - return Results.Ok(); - // Todo: Issue #208 - }) - .Produces(statusCode: StatusCodes.Status200OK, contentType: "application/json") - .Produces(statusCode: StatusCodes.Status401Unauthorized) - .Produces(statusCode: StatusCodes.Status500InternalServerError, contentType: "text/plain") - .WithTags("admin") - .WithName("GetAdminEventDetails") - .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."; - - return operation; - }); - - return builder; - } - - /// - /// Adds the get event lists by admin endpoint - /// - /// instance. - /// Returns instance. - public static RouteHandlerBuilder AddAdminEventList(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.AdminEvents, () => - { - // Todo: Issue #218 https://github.com/aliencube/azure-openai-sdk-proxy/issues/218 - return Results.Ok(); - // Todo: Issue #218 - }) - .Produces>(statusCode: StatusCodes.Status200OK, contentType: "application/json") - .Produces(statusCode: StatusCodes.Status401Unauthorized) - .Produces(statusCode: StatusCodes.Status500InternalServerError, contentType: "text/plain") - .WithTags("admin") - .WithName("GetAdminEvents") - .WithOpenApi(operation => - { - operation.Summary = "Gets all events"; - operation.Description = "This endpoint gets all events"; - - return operation; - }); - - return builder; - } - - /// - /// Adds the update event details by admin endpoint - /// - /// instance. - /// Returns instance. - public static RouteHandlerBuilder AddUpdateAdminEvents(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) => - { - // Todo: Issue #203 https://github.com/aliencube/azure-openai-sdk-proxy/issues/203 - return Results.Ok(); - }) - .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") - .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."; - - return operation; - }); - - return builder; - } - - /// - /// Adds the admin event endpoint - /// - /// instance. - /// Returns instance. - public static RouteHandlerBuilder CreateAdminEvent(this WebApplication app) - { - var builder = app.MapPost(AdminEndpointUrls.AdminEvents, async ( - [FromBody] AdminEventDetails payload, - HttpRequest request) => - { - 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("CreateAdminEvent") - .WithOpenApi(operation => - { - operation.Summary = "Create admin event"; - operation.Description = "Create admin event"; - - return operation; - }); - - return builder; - } +using AzureOpenAIProxy.ApiApp.Models; + +using Microsoft.AspNetCore.Mvc; + +using Azure.Data.Tables; +using Azure; + +namespace AzureOpenAIProxy.ApiApp.Endpoints; + +/// +/// This represents the endpoint entity for get event details by admin +/// +public static class AdminEventEndpoints +{ + /// + /// Adds the get event details by admin endpoint + /// + /// instance. + /// Returns instance. + public static RouteHandlerBuilder AddAdminEvents(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) => + { + // Todo: Issue #208 https://github.com/aliencube/azure-openai-sdk-proxy/issues/208 + return Results.Ok(); + // Todo: Issue #208 + }) + .Produces(statusCode: StatusCodes.Status200OK, contentType: "application/json") + .Produces(statusCode: StatusCodes.Status401Unauthorized) + .Produces(statusCode: StatusCodes.Status500InternalServerError, contentType: "text/plain") + .WithTags("admin") + .WithName("GetAdminEventDetails") + .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."; + + return operation; + }); + + return builder; + } + + /// + /// Adds the get event lists by admin endpoint + /// + /// instance. + /// Returns instance. + public static RouteHandlerBuilder AddAdminEventList(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.AdminEvents, () => + { + // Todo: Issue #218 https://github.com/aliencube/azure-openai-sdk-proxy/issues/218 + return Results.Ok(); + // Todo: Issue #218 + }) + .Produces>(statusCode: StatusCodes.Status200OK, contentType: "application/json") + .Produces(statusCode: StatusCodes.Status401Unauthorized) + .Produces(statusCode: StatusCodes.Status500InternalServerError, contentType: "text/plain") + .WithTags("admin") + .WithName("GetAdminEvents") + .WithOpenApi(operation => + { + operation.Summary = "Gets all events"; + operation.Description = "This endpoint gets all events"; + + return operation; + }); + + return builder; + } + + /// + /// Adds the update event details by admin endpoint + /// + /// instance. + /// Returns instance. + public static RouteHandlerBuilder AddUpdateAdminEvents(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) => + { + // Todo: Issue #203 https://github.com/aliencube/azure-openai-sdk-proxy/issues/203 + return Results.Ok(); + }) + .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") + .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."; + + return operation; + }); + + return builder; + } + + /// + /// Adds the admin event endpoint + /// + /// instance. + /// Returns instance. + public static RouteHandlerBuilder CreateAdminEvent(this WebApplication app) + { + //TODO: 테이블 이름을 어딘가에서 상수처럼 관리하기 + const string TableName = "AdminEvents"; + + var builder = app.MapPost(AdminEndpointUrls.AdminEvents, async ( + [FromServices] TableServiceClient tableStorageService, + [FromBody] AdminEventDetails payload, + HttpRequest request) => + { + try{ + //TODO: 테이블 서비스/클라이언트는 의존성 주입 받아서 사용하도록 리팩토링 + await tableStorageService.CreateTableIfNotExistsAsync(TableName); + var tableClient = tableStorageService.GetTableClient(TableName); + + //TODO: PartitonKey: OrganizerName+OrganizerEmail 조합, RowKey: EventId 으로 제안드립니다! + //TODO: ITableEntity 상속 정리, EventDetails 제네릭 사용하기 + var entity = new TableEntity($"{payload.OrganizerName}_{payload.OrganizerEmail}", payload.EventId) + { + ["EventName"] = payload.Title, + ["EventDescription"] = payload.Description, + ["EventStartDate"] = payload.DateStart, + ["EventEndDate"] = payload.DateEnd, + ["TimeZone"] = payload.TimeZone, + ["IsActive"] = payload.IsActive, + ["OrganizerEmail"] = payload.OrganizerEmail, + ["CoorganizerName"] = payload.CoorganizerName, + ["CoorganizerEmail"] = payload.CoorganizerEmail, + ["MaxTokenCap"] = payload.MaxTokenCap, + ["DailyRequestCap"] = payload.DailyRequestCap + }; + + await tableClient.AddEntityAsync(entity); + return Results.Ok(); + } catch (RequestFailedException ex) + { + return Results.BadRequest(ex.Message); + } + }) + // 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("CreateAdminEvent") + .WithOpenApi(operation => + { + operation.Summary = "Create admin event"; + operation.Description = "Create admin event"; + + return operation; + }); + + return builder; + } } \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Program.cs b/src/AzureOpenAIProxy.ApiApp/Program.cs index e64fbddf..6c44c06a 100644 --- a/src/AzureOpenAIProxy.ApiApp/Program.cs +++ b/src/AzureOpenAIProxy.ApiApp/Program.cs @@ -1,51 +1,65 @@ -using AzureOpenAIProxy.ApiApp.Endpoints; -using AzureOpenAIProxy.ApiApp.Extensions; - -var builder = WebApplication.CreateBuilder(args); - -builder.AddServiceDefaults(); - -// Add KeyVault service -builder.Services.AddKeyVaultService(); - -// Add Azure OpenAI service. -builder.Services.AddOpenAIService(); - -// Add OpenAPI service -builder.Services.AddOpenApiService(); - -var app = builder.Build(); - -app.MapDefaultEndpoints(); - -// https://stackoverflow.com/questions/76962735/how-do-i-set-a-prefix-in-my-asp-net-core-7-web-api-for-all-endpoints -var basePath = "/api"; -app.UsePathBase(basePath); -app.UseRouting(); - -// Configure the HTTP request pipeline. -// Use Swagger UI -app.UseSwaggerUI(basePath); - -// Enable buffering -app.Use(async (context, next) => -{ - context.Request.EnableBuffering(); - await next.Invoke(); -}); - -app.UseHttpsRedirection(); - -app.AddWeatherForecast(); -app.AddChatCompletions(); - -// Event Endpoints -app.AddEventList(); - -// Admin Endpoints -app.AddAdminEvents(); -app.AddAdminEventList(); -app.AddUpdateAdminEvents(); -app.CreateAdminEvent(); - -await app.RunAsync(); +using Azure.Data.Tables; + +using AzureOpenAIProxy.ApiApp.Endpoints; +using AzureOpenAIProxy.ApiApp.Extensions; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +// Add KeyVault service +builder.Services.AddKeyVaultService(); + +// Add Azure OpenAI service. +builder.Services.AddOpenAIService(); + +// Add OpenAPI service +builder.Services.AddOpenApiService(); + +builder.Services.AddSingleton(sp => +{ + //TODO: StorageAccount를 bicep으로 배포하기 (지금은 az 명령어로 수동생성함) + //TODO: connection string을 apphost에서 넘겨받기 + //TODO: connection string을 StorageAccount 배포중에 넘겨받아서 안전하게 전달/관리하기 + // 그 전까지는 appsettings.json에서 connection string을 가져오자 + var configuration = sp.GetRequiredService(); + var connectionString = configuration["Azure:StorageAccount:ConnectionString"]; //TODO: 이게맞나? 리팩토링 고민 + + return new TableServiceClient(connectionString); +}); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +// https://stackoverflow.com/questions/76962735/how-do-i-set-a-prefix-in-my-asp-net-core-7-web-api-for-all-endpoints +var basePath = "/api"; +app.UsePathBase(basePath); +app.UseRouting(); + +// Configure the HTTP request pipeline. +// Use Swagger UI +app.UseSwaggerUI(basePath); + +// Enable buffering +app.Use(async (context, next) => +{ + context.Request.EnableBuffering(); + await next.Invoke(); +}); + +app.UseHttpsRedirection(); + +app.AddWeatherForecast(); +app.AddChatCompletions(); + +// Event Endpoints +app.AddEventList(); + +// Admin Endpoints +app.AddAdminEvents(); +app.AddAdminEventList(); +app.AddUpdateAdminEvents(); +app.CreateAdminEvent(); + +await app.RunAsync(); diff --git a/src/AzureOpenAIProxy.ApiApp/appsettings.Development.sample.json b/src/AzureOpenAIProxy.ApiApp/appsettings.Development.sample.json index 534af650..05804d9f 100644 --- a/src/AzureOpenAIProxy.ApiApp/appsettings.Development.sample.json +++ b/src/AzureOpenAIProxy.ApiApp/appsettings.Development.sample.json @@ -1,27 +1,31 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - - "Azure": { - "OpenAI": { - "Instances": [ - { - "Endpoint": "https://{{location}}.api.cognitive.microsoft.com/", - "ApiKey": "{{api-key}}", - "DeploymentNames": [ - "deployment-name-1", - "deployment-name-2" - ] - } - ] - }, - "KeyVault": { - "VaultUri": "https://{{key-vault-name}}.vault.azure.net/", - "SecretName": "azure-openai-instances" - } - } -} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + + "Azure": { + "OpenAI": { + "Instances": [ + { + "Endpoint": "https://{{location}}.api.cognitive.microsoft.com/", + "ApiKey": "{{api-key}}", + "DeploymentNames": [ + "deployment-name-1", + "deployment-name-2" + ] + } + ] + }, + "KeyVault": { + "VaultUri": "https://{{key-vault-name}}.vault.azure.net/", + "SecretName": "azure-openai-instances" + }, + "StorageAccount": { + "ConnectionString": "DefaultEndpointsProtocol=https;EndpointSuffix=core.windows.net;AccountName={AccountName};AccountKey={AccountKey};BlobEndpoint={BlobEndpoint};FileEndpoint={FileEndpoint};QueueEndpoint={QueueEndpoint};TableEndpoint={TableEndpoint}", + "ContainerName": "" + } + } +} diff --git a/src/AzureOpenAIProxy.ApiApp/appsettings.json b/src/AzureOpenAIProxy.ApiApp/appsettings.json index 6c89d496..0eb017c8 100644 --- a/src/AzureOpenAIProxy.ApiApp/appsettings.json +++ b/src/AzureOpenAIProxy.ApiApp/appsettings.json @@ -26,6 +26,10 @@ "KeyVault": { "VaultUri": "https://{{key-vault-name}}.vault.azure.net/", "SecretName": "azure-openai-instances" + }, + "StorageAccount": { + "ConnectionString": "", + "ContainerName": "" } }, From b94bd2cf450319d86db8ec95edc6bb313997bb7f Mon Sep 17 00:00:00 2001 From: tae0y Date: Sun, 1 Sep 2024 11:29:22 +0900 Subject: [PATCH 04/35] =?UTF-8?q?update=20:=20=ED=85=8C=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=20=EC=8A=A4=ED=86=A0=EB=A6=AC=EC=A7=80=20bicep=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=B6=84=EB=A6=AC,=20=EB=B3=84=EB=8F=84=20?= =?UTF-8?q?=EB=B8=8C=EB=9E=9C=EC=B9=98=EC=97=90=EC=84=9C=20=EC=9E=91?= =?UTF-8?q?=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/aspire.bicep | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/infra/aspire.bicep b/infra/aspire.bicep index b98f5f56..06abee9a 100644 --- a/infra/aspire.bicep +++ b/infra/aspire.bicep @@ -54,31 +54,6 @@ module keyVault './core/security/keyvault.bicep' = { } } -// Provision Storage Account -resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = { - name: 'storage${resourceToken}' - location: location - sku: { - name: 'Standard_LRS' - } - kind: 'StorageV2' - properties: { - supportsHttpsTrafficOnly: true - } -} - -// Provision Table Service -resource tableService 'Microsoft.Storage/storageAccounts/tableServices@2021-04-01' = { - parent: storageAccount - name: 'default' -} - -// Provision Table Storage -resource table 'Microsoft.Storage/storageAccounts/tableServices/tables@2021-04-01' = { - parent: tableService - name: 'mytable' -} - // Add outputs from the deployment here, if needed. // // This allows the outputs to be referenced by other bicep deployments in the deployment pipeline, @@ -92,7 +67,3 @@ output AZURE_TENANT_ID string = tenant().tenantId output AZURE_KEYVAULT_NAME string = keyVault.outputs.name output AZURE_KEYVAULT_ENDPOINT string = keyVault.outputs.endpoint - -output AZURE_STORAGE_ACCOUNT_NAME string = storageAccount.name -output AZURE_STORAGE_ACCOUNT_TABLE_NAME string = table.name -//TODO: connection string을 깃헙 액션으로 안전하게 전달하기 From 45b8e0c469abc6d36154105100269ae30bca9302 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sun, 1 Sep 2024 11:30:37 +0900 Subject: [PATCH 05/35] =?UTF-8?q?update=20:=20events=EB=A1=9C=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs index c9e0b958..bb956a68 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs @@ -19,7 +19,7 @@ public static class AdminEventEndpoints public static RouteHandlerBuilder AddNewAdminEvent(this WebApplication app) { //TODO: 테이블 이름을 어딘가에서 상수처럼 관리하기 - const string TableName = "AdminEvents"; + const string TableName = "events"; var builder = app.MapPost(AdminEndpointUrls.AdminEvents, async ( [FromServices] TableServiceClient tableStorageService, From 290d97128ed21de3f7e89e43d06296df271da0a6 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sun, 1 Sep 2024 14:46:37 +0900 Subject: [PATCH 06/35] =?UTF-8?q?update=20:=20=EB=B9=8C=EB=93=9C=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=9E=84=EC=8B=9C=EC=A1=B0=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs index 3b438731..5fb374e4 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs @@ -60,7 +60,8 @@ public static RouteHandlerBuilder AddNewAdminEvent(this WebApplication app) //TODO: PartitonKey: OrganizerName+OrganizerEmail 조합, RowKey: EventId 으로 제안드립니다! //TODO: ITableEntity 상속 정리, EventDetails 제네릭 사용하기 - TableEntity? entity = new TableEntity($"{payload.OrganizerName}_{payload.OrganizerEmail}", payload.EventId) + //TODO: payload.EventId.ToString()는 빌드 오류를 막기 위해 임시로 추가한 코드입니다. + TableEntity? entity = new TableEntity($"{payload.OrganizerName}_{payload.OrganizerEmail}", payload.EventId.ToString()) { ["EventName"] = payload.Title, ["EventDescription"] = payload.Description, From c15830874ab3621629a8e43511d6be3a28a18781 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 14 Sep 2024 00:33:08 +0900 Subject: [PATCH 07/35] =?UTF-8?q?update=20:=20=EC=9E=91=EC=97=85=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=20placeholder=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Endpoints/AdminEventEndpoints.cs | 24 +++++++++---------- .../Repositories/AdminEventRepository.cs | 6 +++++ .../Endpoints/AdminGetEventsOpenApiTests.cs | 1 - 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs index 7770a117..a565761e 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs @@ -32,22 +32,20 @@ public static RouteHandlerBuilder AddNewAdminEvent(this WebApplication app) return Results.BadRequest("Payload is null"); } - //try - //{ - // var result = await service.CreateEvent(payload); - - // logger.LogInformation("Created a new event"); + try + { + var result = await service.CreateEvent(payload); - // return Results.Ok(result); - //} - //catch (Exception ex) - //{ - // logger.LogError(ex, "Failed to create a new event"); + logger.LogInformation("Created a new event"); - // return Results.Problem(ex.Message, statusCode: StatusCodes.Status500InternalServerError); - //} + return Results.Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to create a new event"); - return await Task.FromResult(Results.Ok()); + return Results.Problem(ex.Message, statusCode: StatusCodes.Status500InternalServerError); + } }) .Accepts(contentType: "application/json") .Produces(statusCode: StatusCodes.Status200OK, contentType: "application/json") diff --git a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs index 552b6416..99111727 100644 --- a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs +++ b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs @@ -44,6 +44,12 @@ public class AdminEventRepository : IAdminEventRepository /// public async Task CreateEvent(AdminEventDetails eventDetails) { + //TODO: [tae0y], implement this method + //TODO: [tae0y] partition key : / rowkey : + //TODO: [tae0y] ITableEntity 상속/구현 + //TODO: [tae0y] table storage client 생성, 의존성 주입 + //var tableClient = tableStorageService.GetTableClient(TableName); + throw new NotImplementedException(); } diff --git a/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminGetEventsOpenApiTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminGetEventsOpenApiTests.cs index c0180f1b..1aa8482a 100644 --- a/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminGetEventsOpenApiTests.cs +++ b/test/AzureOpenAIProxy.AppHost.Tests/ApiApp/Endpoints/AdminGetEventsOpenApiTests.cs @@ -8,7 +8,6 @@ namespace AzureOpenAIProxy.AppHost.Tests.ApiApp.Endpoints; public class AdminGetEventsOpenApiTests(AspireAppHostFixture host) : IClassFixture { - // TODO: [tae0y] 테스트코드 작성하기 [Fact] public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Path() { From 9b82301cbdd76f23812d770cdac06e38532c8616 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 14 Sep 2024 11:04:31 +0900 Subject: [PATCH 08/35] =?UTF-8?q?update=20:=20ITableEntity=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84,=20TableClient=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Models/AdminEventDetails.cs | 26 ++++++++++++++- .../Repositories/AdminEventRepository.cs | 32 +++++++++++++++---- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs b/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs index aa5dc597..0df1f8dd 100644 --- a/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs +++ b/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs @@ -1,11 +1,14 @@ using System.Text.Json.Serialization; +using Azure; +using Azure.Data.Tables; + namespace AzureOpenAIProxy.ApiApp.Models; /// /// This represent the entity for the event details for admin. /// -public class AdminEventDetails : EventDetails +public class AdminEventDetails : EventDetails, ITableEntity { /// /// Gets or sets the event description. @@ -57,4 +60,25 @@ public class AdminEventDetails : EventDetails /// Gets or sets the event coorganizer email. /// public string? CoorganizerEmail { get; set; } + + // ITableEntity implementation + /// + /// Gets or sets the event Partition Key. + /// + public string? PartitionKey { get; set; } + + /// + /// Gets or sets the event Row Key. + /// + public string? RowKey { get; set; } + + /// + /// Gets or sets the event Timestamp. + /// + public DateTimeOffset? Timestamp { get; set; } + + /// + /// Gets or sets the event ETag. + /// + public ETag ETag { get; set; } } \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs index 99111727..4c9a75fd 100644 --- a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs +++ b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs @@ -1,4 +1,6 @@ using AzureOpenAIProxy.ApiApp.Models; +using Azure.Data.Tables; +using Microsoft.Extensions.DependencyInjection; namespace AzureOpenAIProxy.ApiApp.Repositories; @@ -41,16 +43,34 @@ public interface IAdminEventRepository /// public class AdminEventRepository : IAdminEventRepository { + private readonly string TableName = "events"; + private readonly TableServiceClient _tableServiceClient; + + public AdminEventRepository(TableServiceClient tableServiceClient) + { + _tableServiceClient = tableServiceClient; + } + + /// public async Task CreateEvent(AdminEventDetails eventDetails) { - //TODO: [tae0y], implement this method - //TODO: [tae0y] partition key : / rowkey : - //TODO: [tae0y] ITableEntity 상속/구현 - //TODO: [tae0y] table storage client 생성, 의존성 주입 - //var tableClient = tableStorageService.GetTableClient(TableName); + //DONE: [tae0y] partition key : TimeZone / rowkey : Guid.NewGuid().ToString() + //DONE: [tae0y] ITableEntity 상속/구현 + //TODO: [tae0y] table storage client 생성, 의존성 주입받는 방식이 이게 맞는가 + + var tableServiceClient = _tableServiceClient.GetTableClient(TableName); - throw new NotImplementedException(); + eventDetails.PartitionKey = eventDetails.TimeZone; + eventDetails.RowKey = eventDetails.EventId.ToString(); + var response = await tableServiceClient.AddEntityAsync(eventDetails); + + if (response.Status != 200) + { + throw new Exception("Failed to create event"); + } + + return eventDetails; } /// From 954c43efab537cc258fe01bb577371621497d542 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 14 Sep 2024 11:28:50 +0900 Subject: [PATCH 09/35] =?UTF-8?q?update=20:=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EC=98=81=EC=86=8D=EC=84=B1=20=EC=A0=80=EC=9E=A5=ED=9B=84=20?= =?UTF-8?q?=EC=9E=AC=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/AdminEventRepository.cs | 9 +++++++-- "\355\206\240\354\235\230.md" | 10 ++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 "\355\206\240\354\235\230.md" diff --git a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs index 4c9a75fd..ee582537 100644 --- a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs +++ b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs @@ -51,6 +51,11 @@ public AdminEventRepository(TableServiceClient tableServiceClient) _tableServiceClient = tableServiceClient; } + public AdminEventRepository() + { + // TODO: [tae0y] TEST를 위한 임시 코드 + } + /// public async Task CreateEvent(AdminEventDetails eventDetails) @@ -64,13 +69,13 @@ public async Task CreateEvent(AdminEventDetails eventDetails) eventDetails.PartitionKey = eventDetails.TimeZone; eventDetails.RowKey = eventDetails.EventId.ToString(); var response = await tableServiceClient.AddEntityAsync(eventDetails); - if (response.Status != 200) { throw new Exception("Failed to create event"); } - return eventDetails; + var addedEntity = await tableServiceClient.GetEntityAsync(eventDetails.PartitionKey, eventDetails.RowKey); + return addedEntity; } /// diff --git "a/\355\206\240\354\235\230.md" "b/\355\206\240\354\235\230.md" new file mode 100644 index 00000000..1e44c97b --- /dev/null +++ "b/\355\206\240\354\235\230.md" @@ -0,0 +1,10 @@ +- ITableEntity 구현 + - AdminEventDetails에서 ITableEntity를 구현 + - 상위 클래스인 EventDetails에서 하는게 맞을지? + +- partition key : TimeZone / rowkey : eventDetails.EventId.ToString() + - 파티션키는 지역성을 고려해서 만들어야하니 타임존이 어떨지? + - 대체로 같은 타임존 안에서 이벤트를 사용할 것 같음 + +- + From 419a97387282c2732ca4bf0e6892f30566811a66 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 14 Sep 2024 14:15:36 +0900 Subject: [PATCH 10/35] =?UTF-8?q?update=20:=20AdminEventDetails=20?= =?UTF-8?q?=EC=97=AD=EC=A7=81=EB=A0=AC=ED=99=94=EC=8B=9C=20Etag=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs b/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs index 0df1f8dd..5fb67652 100644 --- a/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs +++ b/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs @@ -80,5 +80,7 @@ public class AdminEventDetails : EventDetails, ITableEntity /// /// Gets or sets the event ETag. /// + //TODO: [tae0y] request payload, table entity를 분리해야하는가? 혹은 Json 역직렬화시 제외만 해도 되는가? + [JsonIgnore] public ETag ETag { get; set; } } \ No newline at end of file From 719d9737997a2416b4f629974a260b4722e09708 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 14 Sep 2024 16:49:24 +0900 Subject: [PATCH 11/35] =?UTF-8?q?update=20:=20null,=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/AdminEventRepository.cs | 16 ++++++++-------- .../appsettings.Development.sample.json | 9 ++++----- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs index ee582537..5d73dbda 100644 --- a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs +++ b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs @@ -44,7 +44,7 @@ public interface IAdminEventRepository public class AdminEventRepository : IAdminEventRepository { private readonly string TableName = "events"; - private readonly TableServiceClient _tableServiceClient; + private readonly TableServiceClient? _tableServiceClient; public AdminEventRepository(TableServiceClient tableServiceClient) { @@ -60,16 +60,16 @@ public AdminEventRepository() /// public async Task CreateEvent(AdminEventDetails eventDetails) { - //DONE: [tae0y] partition key : TimeZone / rowkey : Guid.NewGuid().ToString() - //DONE: [tae0y] ITableEntity 상속/구현 - //TODO: [tae0y] table storage client 생성, 의존성 주입받는 방식이 이게 맞는가 - + if (_tableServiceClient == null) + { + throw new InvalidOperationException("TableServiceClient is not initialized."); + } var tableServiceClient = _tableServiceClient.GetTableClient(TableName); - eventDetails.PartitionKey = eventDetails.TimeZone; - eventDetails.RowKey = eventDetails.EventId.ToString(); + eventDetails.PartitionKey = string.IsNullOrEmpty(eventDetails.TimeZone) ? "KST" : eventDetails.TimeZone; + eventDetails.RowKey = string.IsNullOrEmpty(eventDetails.EventId.ToString()) ? Guid.NewGuid().ToString() : eventDetails.EventId.ToString(); var response = await tableServiceClient.AddEntityAsync(eventDetails); - if (response.Status != 200) + if (response.Status != 204) { throw new Exception("Failed to create event"); } diff --git a/src/AzureOpenAIProxy.ApiApp/appsettings.Development.sample.json b/src/AzureOpenAIProxy.ApiApp/appsettings.Development.sample.json index 05804d9f..1b4b4928 100644 --- a/src/AzureOpenAIProxy.ApiApp/appsettings.Development.sample.json +++ b/src/AzureOpenAIProxy.ApiApp/appsettings.Development.sample.json @@ -21,11 +21,10 @@ }, "KeyVault": { "VaultUri": "https://{{key-vault-name}}.vault.azure.net/", - "SecretName": "azure-openai-instances" - }, - "StorageAccount": { - "ConnectionString": "DefaultEndpointsProtocol=https;EndpointSuffix=core.windows.net;AccountName={AccountName};AccountKey={AccountKey};BlobEndpoint={BlobEndpoint};FileEndpoint={FileEndpoint};QueueEndpoint={QueueEndpoint};TableEndpoint={TableEndpoint}", - "ContainerName": "" + "SecretNames": { + "OpenAI": "azure-openai-instances", + "Storage": "storage-connection-string" + } } } } From a52167466c9eea0b59b15a3bfdbf8343371cd85a Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 14 Sep 2024 22:36:39 +0900 Subject: [PATCH 12/35] =?UTF-8?q?add=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/AdminEventRepository.cs | 25 +- .../Repositories/AdminEventRepositoryTests.cs | 254 +++++++++++++++++- "\355\206\240\354\235\230.md" | 10 - 3 files changed, 268 insertions(+), 21 deletions(-) delete mode 100644 "\355\206\240\354\235\230.md" diff --git a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs index 5d73dbda..b08759b0 100644 --- a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs +++ b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs @@ -44,36 +44,41 @@ public interface IAdminEventRepository public class AdminEventRepository : IAdminEventRepository { private readonly string TableName = "events"; - private readonly TableServiceClient? _tableServiceClient; - public AdminEventRepository(TableServiceClient tableServiceClient) - { - _tableServiceClient = tableServiceClient; - } + private readonly TableServiceClient _tableServiceClient; + /// + /// Initializes a new instance of the class. + /// public AdminEventRepository() { - // TODO: [tae0y] TEST를 위한 임시 코드 + // TODO: [tae0y] 빌드 실패 방지용 임시코드 } + /// + /// Initializes a new instance of the class. + /// + public AdminEventRepository(TableServiceClient tableServiceClient) + { + _tableServiceClient = tableServiceClient ?? throw new ArgumentNullException(nameof(tableServiceClient)); + } /// public async Task CreateEvent(AdminEventDetails eventDetails) { - if (_tableServiceClient == null) - { - throw new InvalidOperationException("TableServiceClient is not initialized."); - } var tableServiceClient = _tableServiceClient.GetTableClient(TableName); + // TODO: [tae0y] PartitionKey, RowKey 정책 확인 eventDetails.PartitionKey = string.IsNullOrEmpty(eventDetails.TimeZone) ? "KST" : eventDetails.TimeZone; eventDetails.RowKey = string.IsNullOrEmpty(eventDetails.EventId.ToString()) ? Guid.NewGuid().ToString() : eventDetails.EventId.ToString(); var response = await tableServiceClient.AddEntityAsync(eventDetails); if (response.Status != 204) { + // TODO: [tae0y] Exception 종류 확인 throw new Exception("Failed to create event"); } + // TODO: [tae0y] Azure.Tables REST API는 저장한 Entity를 반환하는 옵션이 있으나, tableServiceClient는 없으므로 추가 작업 필요 var addedEntity = await tableServiceClient.GetEntityAsync(eventDetails.PartitionKey, eventDetails.RowKey); return addedEntity; } diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs index 0e00c5d4..d6a20e74 100644 --- a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs @@ -1,14 +1,20 @@ -using AzureOpenAIProxy.ApiApp.Models; +using Azure.Data.Tables; + +using AzureOpenAIProxy.ApiApp.Models; using AzureOpenAIProxy.ApiApp.Repositories; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; +using NSubstitute; + namespace AzureOpenAIProxy.ApiApp.Tests.Repositories; public class AdminEventRepositoryTests { + + [Fact] public void Given_ServiceCollection_When_AddAdminEventRepository_Invoked_Then_It_Should_Contain_AdminEventRepository() { @@ -77,4 +83,250 @@ public void Given_Instance_When_UpdateEvent_Invoked_Then_It_Should_Throw_Excepti // Assert func.Should().ThrowAsync(); } + + // TODO: [tae0y] Add tests for other methods + /*-------------------------------------------------------------------------------- + TEST 설계 + + TableServiceClient가 초기화되지 않았을 때 _tableServiceClient == null일 때 InvalidOperationException이 발생하는지 검증. + + eventDetails.TimeZone이 비어 있을 때 PartitionKey가 "KST"로 설정되는지 검증. + eventDetails.TimeZone이 있을 때 해당 값이 PartitionKey로 설정되는지 검증. + + eventDetails.EventId가 null일 때 RowKey가 새로운 GUID로 설정되는지 검증. + eventDetails.EventId가 있을 때 해당 값이 RowKey로 설정되는지 검증. + + AddEntityAsync가 성공적으로 호출되었을 때(즉, 204 상태코드) 정상적으로 동작하는지 검증. + AddEntityAsync가 204가 아닌 상태코드를 반환했을 때 Exception이 발생하는지 검증. + GetEntityAsync가 호출되고 반환된 값이 올바른지 검증. + --------------------------------------------------------------------------------*/ + + + [Fact] + public async Task Given_TableServiceClientIsNull_When_CreateEvent_Invoked_Then_ItShould_Throw_InvalidOperationException() + { + // Arrange + var eventDetails = new AdminEventDetails(); + var repository = new AdminEventRepository(); + + // Act + Func func = async () => await repository.CreateEvent(eventDetails); + + // Assert + await func.Should().ThrowAsync(); + } + + [Fact] + public async Task Given_TimeZoneIsEmpty_When_CreateEvent_Invoked_Then_PartitionKeyShould_Be_KST() + { + // Arrange + var eventDetails = createRandomEventDetails( + new AdminEventDetails(), + new HashSet { nameof(AdminEventDetails.TimeZone) } + ); + var tableServiceClient = Substitute.For(); + var repository = new AdminEventRepository(tableServiceClient); + + // Act + var result = await repository.CreateEvent(eventDetails); + + // Assert + result.PartitionKey.Should().Be("KST"); + } + + [Fact] + public async Task Given_TimeZoneIsNotEmpty_When_CreateEvent_Invoked_Then_PartitionKeyShould_Be_TimeZone() + { + // Arrange + var eventDetails = createRandomEventDetails( + new AdminEventDetails(), + new HashSet { } + ); + var tableServiceClient = Substitute.For(); + var repository = new AdminEventRepository(tableServiceClient); + + // Act + var result = await repository.CreateEvent(eventDetails); + + // Assert + result.PartitionKey.Should().Be(eventDetails.TimeZone); + } + + [Fact] + public async Task Given_EventIdIsNull_When_CreateEvent_Invoked_Then_RowKeyShould_Be_NewGuid() + { + // Arrange + var eventDetails = createRandomEventDetails( + new AdminEventDetails(), + new HashSet { nameof(AdminEventDetails.EventId) } + ); + var tableServiceClient = Substitute.For(); + var repository = new AdminEventRepository(tableServiceClient); + + // Act + var result = await repository.CreateEvent(eventDetails); + + // Assert + result.RowKey.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task Given_EventIdIsNotEmpty_When_CreateEvent_Invoked_Then_RowKeyShould_Be_EventId() + { + // Arrange + var eventDetails = createRandomEventDetails( + new AdminEventDetails(), + new HashSet { } + ); + var tableServiceClient = Substitute.For(); + var repository = new AdminEventRepository(tableServiceClient); + + // Act + var result = await repository.CreateEvent(eventDetails); + + // Assert + result.RowKey.Should().Be(eventDetails.EventId.ToString()); + } + + [Fact] + public async Task Given_AddEntityAsyncIsSuccessful_When_CreateEvent_Invoked_Then_ItShould_WorkProperly() + { + // Arrange + var eventDetails = createRandomEventDetails( + new AdminEventDetails(), + new HashSet { } + ); + var tableServiceClient = Substitute.For(); + var repository = new AdminEventRepository(tableServiceClient); + + // Act + var result = await repository.CreateEvent(eventDetails); + + // Assert + result.Should().NotBeNull(); + } + + [Fact] + public async Task Given_AddEntityAsyncIsNotSuccessful_When_CreateEvent_Invoked_Then_ItShould_ThrowException() + { + // Arrange + // duplicate eventDetails + var eventDetails = createRandomEventDetails( + new AdminEventDetails(), + new HashSet { } + ); + var tableServiceClient = Substitute.For(); + var repository = new AdminEventRepository(tableServiceClient); + var result = await repository.CreateEvent(eventDetails); + + // Act + Func func = async () => await repository.CreateEvent(eventDetails); + + // Assert + await func.Should().ThrowAsync(); + } + + [Fact] + public async Task Given_AddEntityAsyncIsSuccessful_When_CreateEvent_Invoked_Then_GetEntityAsyncShould_ReturnCorrectValue() + { + // Arrange + var eventDetails = createRandomEventDetails( + new AdminEventDetails(), + new HashSet { } + ); + var tableServiceClient = Substitute.For(); + var repository = new AdminEventRepository(tableServiceClient); + + // Act + var result = await repository.CreateEvent(eventDetails); + + // Assert + // result Should BeEquivalentTo eventDetails except for PartitionKey, RowKey, Timestamp, ETag + result.Should().BeEquivalentTo(eventDetails, options => options + .Excluding(e => e.PartitionKey) + .Excluding(e => e.RowKey) + .Excluding(e => e.Timestamp) + .Excluding(e => e.ETag) + ); + } + + // Helper method to create random event details + private static AdminEventDetails createRandomEventDetails(AdminEventDetails details, HashSet keepNullFields) + { + Random random = new Random(); + + // Check if field should be skipped by checking if it exists in the hashset + bool ShouldSkip(string fieldName) + { + return keepNullFields.Contains(fieldName); + } + + // Fill in null or empty string properties with random values unless they should be skipped + if (!ShouldSkip(nameof(details.Title)) && string.IsNullOrEmpty(details.Title)) + { + details.Title = "Event Title " + random.Next(1000, 9999); + } + + if (!ShouldSkip(nameof(details.Summary)) && string.IsNullOrEmpty(details.Summary)) + { + details.Summary = "This is a summary for event " + random.Next(1000, 9999); + } + + if (!ShouldSkip(nameof(details.Description)) && string.IsNullOrEmpty(details.Description)) + { + details.Description = "Description for event " + random.Next(1000, 9999); + } + + if (!ShouldSkip(nameof(details.EventId)) && details.EventId == Guid.Empty) + { + details.EventId = Guid.NewGuid(); + } + + if (!ShouldSkip(nameof(details.MaxTokenCap)) && details.MaxTokenCap == 0) + { + details.MaxTokenCap = random.Next(100, 1000); // Assign random token cap + } + + if (!ShouldSkip(nameof(details.DailyRequestCap)) && details.DailyRequestCap == 0) + { + details.DailyRequestCap = random.Next(10, 100); // Assign random daily request cap + } + + if (!ShouldSkip(nameof(details.DateStart)) && details.DateStart == DateTimeOffset.MinValue) + { + details.DateStart = DateTimeOffset.Now.AddDays(random.Next(-10, 10)); // Random start date + } + + if (!ShouldSkip(nameof(details.DateEnd)) && (details.DateEnd == DateTimeOffset.MinValue || details.DateEnd <= details.DateStart)) + { + details.DateEnd = details.DateStart.AddHours(random.Next(1, 72)); // End date should be after start date + } + + if (!ShouldSkip(nameof(details.TimeZone)) && string.IsNullOrEmpty(details.TimeZone)) + { + details.TimeZone = "KST"; // Default timezone + } + + if (!ShouldSkip(nameof(details.OrganizerName)) && string.IsNullOrEmpty(details.OrganizerName)) + { + details.OrganizerName = "Organizer " + random.Next(1000, 9999); + } + + if (!ShouldSkip(nameof(details.OrganizerEmail)) && string.IsNullOrEmpty(details.OrganizerEmail)) + { + details.OrganizerEmail = $"organizer{random.Next(1000, 9999)}@example.com"; + } + + if (!ShouldSkip(nameof(details.CoorganizerName)) && string.IsNullOrEmpty(details.CoorganizerName)) + { + details.CoorganizerName = "Co-organizer " + random.Next(1000, 9999); + } + + if (!ShouldSkip(nameof(details.CoorganizerEmail)) && string.IsNullOrEmpty(details.CoorganizerEmail)) + { + details.CoorganizerEmail = $"coorganizer{random.Next(1000, 9999)}@example.com"; + } + + return details; + } } diff --git "a/\355\206\240\354\235\230.md" "b/\355\206\240\354\235\230.md" deleted file mode 100644 index 1e44c97b..00000000 --- "a/\355\206\240\354\235\230.md" +++ /dev/null @@ -1,10 +0,0 @@ -- ITableEntity 구현 - - AdminEventDetails에서 ITableEntity를 구현 - - 상위 클래스인 EventDetails에서 하는게 맞을지? - -- partition key : TimeZone / rowkey : eventDetails.EventId.ToString() - - 파티션키는 지역성을 고려해서 만들어야하니 타임존이 어떨지? - - 대체로 같은 타임존 안에서 이벤트를 사용할 것 같음 - -- - From ec396dd1dff67c1c40ea1cd8b4db2b37f4315a63 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sun, 15 Sep 2024 14:49:22 +0900 Subject: [PATCH 13/35] =?UTF-8?q?update=20:=20repository/service=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=96=B4=EB=B3=84=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC,=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/AdminEventRepository.cs | 21 ++- .../Services/AdminEventService.cs | 16 +- .../Repositories/AdminEventRepositoryTests.cs | 109 ++---------- .../Services/AdminEventServiceTests.cs | 158 ++++++++++++++++++ 4 files changed, 198 insertions(+), 106 deletions(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs index b08759b0..af2f7e39 100644 --- a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs +++ b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs @@ -68,19 +68,18 @@ public async Task CreateEvent(AdminEventDetails eventDetails) { var tableServiceClient = _tableServiceClient.GetTableClient(TableName); - // TODO: [tae0y] PartitionKey, RowKey 정책 확인 - eventDetails.PartitionKey = string.IsNullOrEmpty(eventDetails.TimeZone) ? "KST" : eventDetails.TimeZone; - eventDetails.RowKey = string.IsNullOrEmpty(eventDetails.EventId.ToString()) ? Guid.NewGuid().ToString() : eventDetails.EventId.ToString(); - var response = await tableServiceClient.AddEntityAsync(eventDetails); - if (response.Status != 204) - { - // TODO: [tae0y] Exception 종류 확인 - throw new Exception("Failed to create event"); - } + // 데이터 저장 + var createResponse = await tableServiceClient.AddEntityAsync(eventDetails).ConfigureAwait(false); + // 저장한 데이터 재조회 // TODO: [tae0y] Azure.Tables REST API는 저장한 Entity를 반환하는 옵션이 있으나, tableServiceClient는 없으므로 추가 작업 필요 - var addedEntity = await tableServiceClient.GetEntityAsync(eventDetails.PartitionKey, eventDetails.RowKey); - return addedEntity; + var getResponse = await tableServiceClient.GetEntityAsync( + eventDetails.PartitionKey, + eventDetails.RowKey + ).ConfigureAwait(false); + + // 조회한 엔티티 반환 + return getResponse.Value; } /// diff --git a/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs b/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs index 09f21797..02549fc9 100644 --- a/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs +++ b/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs @@ -47,9 +47,19 @@ public class AdminEventService(IAdminEventRepository repository) : IAdminEventSe /// public async Task CreateEvent(AdminEventDetails eventDetails) { - var result = await this._repository.CreateEvent(eventDetails).ConfigureAwait(false); - - return result; + // Validate + // TODO: [tae0y] PartitionKey, RowKey 정책 확인 + if (string.IsNullOrEmpty(eventDetails.TimeZone) || string.IsNullOrEmpty(eventDetails.EventId.ToString())) + { + var invalidFields = string.Join(", ", new List { "TimeZone", "EventId" }.Where(p => string.IsNullOrEmpty(p))); + throw new ArgumentNullException($"Invalid event details : {invalidFields} cannot be null or empty"); + } + eventDetails.PartitionKey = eventDetails.TimeZone; + eventDetails.RowKey = eventDetails.EventId.ToString(); + + // Save + var response = await this._repository.CreateEvent(eventDetails).ConfigureAwait(false); + return response; } /// diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs index d6a20e74..fa587c5b 100644 --- a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs @@ -85,25 +85,8 @@ public void Given_Instance_When_UpdateEvent_Invoked_Then_It_Should_Throw_Excepti } // TODO: [tae0y] Add tests for other methods - /*-------------------------------------------------------------------------------- - TEST 설계 - - TableServiceClient가 초기화되지 않았을 때 _tableServiceClient == null일 때 InvalidOperationException이 발생하는지 검증. - - eventDetails.TimeZone이 비어 있을 때 PartitionKey가 "KST"로 설정되는지 검증. - eventDetails.TimeZone이 있을 때 해당 값이 PartitionKey로 설정되는지 검증. - - eventDetails.EventId가 null일 때 RowKey가 새로운 GUID로 설정되는지 검증. - eventDetails.EventId가 있을 때 해당 값이 RowKey로 설정되는지 검증. - - AddEntityAsync가 성공적으로 호출되었을 때(즉, 204 상태코드) 정상적으로 동작하는지 검증. - AddEntityAsync가 204가 아닌 상태코드를 반환했을 때 Exception이 발생하는지 검증. - GetEntityAsync가 호출되고 반환된 값이 올바른지 검증. - --------------------------------------------------------------------------------*/ - - [Fact] - public async Task Given_TableServiceClientIsNull_When_CreateEvent_Invoked_Then_ItShould_Throw_InvalidOperationException() + public async Task Given_TableServiceClientIsNull_When_CreateEvent_Invoked_Then_ItShould_Throw_NullReferenceException() { // Arrange var eventDetails = new AdminEventDetails(); @@ -116,78 +99,6 @@ public async Task Given_TableServiceClientIsNull_When_CreateEvent_Invoked_Then_I await func.Should().ThrowAsync(); } - [Fact] - public async Task Given_TimeZoneIsEmpty_When_CreateEvent_Invoked_Then_PartitionKeyShould_Be_KST() - { - // Arrange - var eventDetails = createRandomEventDetails( - new AdminEventDetails(), - new HashSet { nameof(AdminEventDetails.TimeZone) } - ); - var tableServiceClient = Substitute.For(); - var repository = new AdminEventRepository(tableServiceClient); - - // Act - var result = await repository.CreateEvent(eventDetails); - - // Assert - result.PartitionKey.Should().Be("KST"); - } - - [Fact] - public async Task Given_TimeZoneIsNotEmpty_When_CreateEvent_Invoked_Then_PartitionKeyShould_Be_TimeZone() - { - // Arrange - var eventDetails = createRandomEventDetails( - new AdminEventDetails(), - new HashSet { } - ); - var tableServiceClient = Substitute.For(); - var repository = new AdminEventRepository(tableServiceClient); - - // Act - var result = await repository.CreateEvent(eventDetails); - - // Assert - result.PartitionKey.Should().Be(eventDetails.TimeZone); - } - - [Fact] - public async Task Given_EventIdIsNull_When_CreateEvent_Invoked_Then_RowKeyShould_Be_NewGuid() - { - // Arrange - var eventDetails = createRandomEventDetails( - new AdminEventDetails(), - new HashSet { nameof(AdminEventDetails.EventId) } - ); - var tableServiceClient = Substitute.For(); - var repository = new AdminEventRepository(tableServiceClient); - - // Act - var result = await repository.CreateEvent(eventDetails); - - // Assert - result.RowKey.Should().NotBeNullOrEmpty(); - } - - [Fact] - public async Task Given_EventIdIsNotEmpty_When_CreateEvent_Invoked_Then_RowKeyShould_Be_EventId() - { - // Arrange - var eventDetails = createRandomEventDetails( - new AdminEventDetails(), - new HashSet { } - ); - var tableServiceClient = Substitute.For(); - var repository = new AdminEventRepository(tableServiceClient); - - // Act - var result = await repository.CreateEvent(eventDetails); - - // Assert - result.RowKey.Should().Be(eventDetails.EventId.ToString()); - } - [Fact] public async Task Given_AddEntityAsyncIsSuccessful_When_CreateEvent_Invoked_Then_ItShould_WorkProperly() { @@ -196,6 +107,7 @@ public async Task Given_AddEntityAsyncIsSuccessful_When_CreateEvent_Invoked_Then new AdminEventDetails(), new HashSet { } ); + // TODO: [tae0y] TableServiceClient를 Mocking하지 않고 실제 의존성을 주입 var tableServiceClient = Substitute.For(); var repository = new AdminEventRepository(tableServiceClient); @@ -243,8 +155,6 @@ public async Task Given_AddEntityAsyncIsSuccessful_When_CreateEvent_Invoked_Then // Assert // result Should BeEquivalentTo eventDetails except for PartitionKey, RowKey, Timestamp, ETag result.Should().BeEquivalentTo(eventDetails, options => options - .Excluding(e => e.PartitionKey) - .Excluding(e => e.RowKey) .Excluding(e => e.Timestamp) .Excluding(e => e.ETag) ); @@ -307,6 +217,11 @@ bool ShouldSkip(string fieldName) details.TimeZone = "KST"; // Default timezone } + if (!ShouldSkip(nameof(details.IsActive))) + { + details.IsActive = true; // Default is active + } + if (!ShouldSkip(nameof(details.OrganizerName)) && string.IsNullOrEmpty(details.OrganizerName)) { details.OrganizerName = "Organizer " + random.Next(1000, 9999); @@ -327,6 +242,16 @@ bool ShouldSkip(string fieldName) details.CoorganizerEmail = $"coorganizer{random.Next(1000, 9999)}@example.com"; } + if (!ShouldSkip(nameof(details.PartitionKey)) && string.IsNullOrEmpty(details.PartitionKey)) + { + details.PartitionKey = details.TimeZone; + } + + if (!ShouldSkip(nameof(details.RowKey)) && string.IsNullOrEmpty(details.RowKey)) + { + details.RowKey = details.EventId.ToString(); + } + return details; } } diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs index cf56f8e2..e3953803 100644 --- a/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs @@ -84,4 +84,162 @@ public void Given_Instance_When_UpdateEvent_Invoked_Then_It_Should_Throw_Excepti // Assert func.Should().ThrowAsync(); } + + // TODO: [tae0y] Add tests for other methods + [Fact] + public async Task Given_TimeZoneIsEmpty_When_CreateEvent_Invoked_Then_Throw_ArgumentNullException() + { + // Arrange + var repository = Substitute.For(); + var service = new AdminEventService(repository); + var eventDetails = createRandomEventDetails( + new AdminEventDetails(), + new HashSet { nameof(AdminEventDetails.TimeZone) } + ); + + // Act + Func func = async () => await service.CreateEvent(eventDetails); + + // Assert + await func.Should().ThrowAsync(); + } + + [Fact] + public async Task Given_TimeZoneIsNotEmpty_When_CreateEvent_Invoked_Then_PartitionKeyShould_Be_TimeZone() + { + // Arrange + var repository = Substitute.For(); + var service = new AdminEventService(repository); + var eventDetails = createRandomEventDetails( + new AdminEventDetails(), + new HashSet() + ); + + // Act + var result = await service.CreateEvent(eventDetails); + + // Assert + result.PartitionKey.Should().Be(eventDetails.TimeZone); + } + + [Fact] + public async Task Given_EventIdIsNull_When_CreateEvent_Invoked_Then_Throw_ArgumentNullException() + { + // Arrange + var repository = Substitute.For(); + var service = new AdminEventService(repository); + var eventDetails = createRandomEventDetails( + new AdminEventDetails(), + new HashSet { nameof(AdminEventDetails.EventId) } + ); + + // Act + Func func = async () => await service.CreateEvent(eventDetails); + + // Assert + await func.Should().ThrowAsync(); + } + + [Fact] + public async Task Given_EventIdIsNotEmpty_When_CreateEvent_Invoked_Then_RowKeyShould_Be_EventId() + { + // Arrange + var repository = Substitute.For(); + var service = new AdminEventService(repository); + var eventDetails = createRandomEventDetails( + new AdminEventDetails(), + new HashSet() + ); + + // Act + var result = await service.CreateEvent(eventDetails); + + // Assert + result.RowKey.Should().Be(eventDetails.RowKey); + } + + // Helper method to create random event details + private static AdminEventDetails createRandomEventDetails(AdminEventDetails details, HashSet keepNullFields) + { + Random random = new Random(); + + // Check if field should be skipped by checking if it exists in the hashset + bool ShouldSkip(string fieldName) + { + return keepNullFields.Contains(fieldName); + } + + // Fill in null or empty string properties with random values unless they should be skipped + if (!ShouldSkip(nameof(details.Title)) && string.IsNullOrEmpty(details.Title)) + { + details.Title = "Event Title " + random.Next(1000, 9999); + } + + if (!ShouldSkip(nameof(details.Summary)) && string.IsNullOrEmpty(details.Summary)) + { + details.Summary = "This is a summary for event " + random.Next(1000, 9999); + } + + if (!ShouldSkip(nameof(details.Description)) && string.IsNullOrEmpty(details.Description)) + { + details.Description = "Description for event " + random.Next(1000, 9999); + } + + if (!ShouldSkip(nameof(details.EventId)) && details.EventId == Guid.Empty) + { + details.EventId = Guid.NewGuid(); + } + + if (!ShouldSkip(nameof(details.MaxTokenCap)) && details.MaxTokenCap == 0) + { + details.MaxTokenCap = random.Next(100, 1000); // Assign random token cap + } + + if (!ShouldSkip(nameof(details.DailyRequestCap)) && details.DailyRequestCap == 0) + { + details.DailyRequestCap = random.Next(10, 100); // Assign random daily request cap + } + + if (!ShouldSkip(nameof(details.DateStart)) && details.DateStart == DateTimeOffset.MinValue) + { + details.DateStart = DateTimeOffset.Now.AddDays(random.Next(-10, 10)); // Random start date + } + + if (!ShouldSkip(nameof(details.DateEnd)) && (details.DateEnd == DateTimeOffset.MinValue || details.DateEnd <= details.DateStart)) + { + details.DateEnd = details.DateStart.AddHours(random.Next(1, 72)); // End date should be after start date + } + + if (!ShouldSkip(nameof(details.TimeZone)) && string.IsNullOrEmpty(details.TimeZone)) + { + details.TimeZone = "KST"; // Default timezone + } + + if (!ShouldSkip(nameof(details.IsActive))) + { + details.IsActive = true; // Default is active + } + + if (!ShouldSkip(nameof(details.OrganizerName)) && string.IsNullOrEmpty(details.OrganizerName)) + { + details.OrganizerName = "Organizer " + random.Next(1000, 9999); + } + + if (!ShouldSkip(nameof(details.OrganizerEmail)) && string.IsNullOrEmpty(details.OrganizerEmail)) + { + details.OrganizerEmail = $"organizer{random.Next(1000, 9999)}@example.com"; + } + + if (!ShouldSkip(nameof(details.CoorganizerName)) && string.IsNullOrEmpty(details.CoorganizerName)) + { + details.CoorganizerName = "Co-organizer " + random.Next(1000, 9999); + } + + if (!ShouldSkip(nameof(details.CoorganizerEmail)) && string.IsNullOrEmpty(details.CoorganizerEmail)) + { + details.CoorganizerEmail = $"coorganizer{random.Next(1000, 9999)}@example.com"; + } + + return details; + } } From 9385d2fd24c4c7474c76878276cc357bd987abae Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 21 Sep 2024 16:22:04 +0900 Subject: [PATCH 14/35] =?UTF-8?q?update=20:=20AdminEventDetails=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=20ITableEntity=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 5 ++++ .../Models/AdminEventDetails.cs | 25 +------------------ 2 files changed, 6 insertions(+), 24 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..f522ffab --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.eol": "\n", + "code-eol.highlightNonDefault": true, + "code-eol.highlightExtraWhitespace": true +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs b/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs index 5fb67652..57e3b8e0 100644 --- a/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs +++ b/src/AzureOpenAIProxy.ApiApp/Models/AdminEventDetails.cs @@ -8,7 +8,7 @@ namespace AzureOpenAIProxy.ApiApp.Models; /// /// This represent the entity for the event details for admin. /// -public class AdminEventDetails : EventDetails, ITableEntity +public class AdminEventDetails : EventDetails { /// /// Gets or sets the event description. @@ -60,27 +60,4 @@ public class AdminEventDetails : EventDetails, ITableEntity /// Gets or sets the event coorganizer email. /// public string? CoorganizerEmail { get; set; } - - // ITableEntity implementation - /// - /// Gets or sets the event Partition Key. - /// - public string? PartitionKey { get; set; } - - /// - /// Gets or sets the event Row Key. - /// - public string? RowKey { get; set; } - - /// - /// Gets or sets the event Timestamp. - /// - public DateTimeOffset? Timestamp { get; set; } - - /// - /// Gets or sets the event ETag. - /// - //TODO: [tae0y] request payload, table entity를 분리해야하는가? 혹은 Json 역직렬화시 제외만 해도 되는가? - [JsonIgnore] - public ETag ETag { get; set; } } \ No newline at end of file From 69f35d3e867eac08c971bb0063036d524824b0bf Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 21 Sep 2024 16:26:08 +0900 Subject: [PATCH 15/35] =?UTF-8?q?update=20:=20AdminEventService=20?= =?UTF-8?q?=ED=95=84=EC=88=98=EA=B0=92=EC=B2=B4=ED=81=AC=20=EB=B0=8F=20Par?= =?UTF-8?q?titionKey=20=EB=B6=88=ED=95=84=EC=9A=94=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs b/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs index 02549fc9..ce768c7c 100644 --- a/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs +++ b/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs @@ -48,13 +48,7 @@ public class AdminEventService(IAdminEventRepository repository) : IAdminEventSe public async Task CreateEvent(AdminEventDetails eventDetails) { // Validate - // TODO: [tae0y] PartitionKey, RowKey 정책 확인 - if (string.IsNullOrEmpty(eventDetails.TimeZone) || string.IsNullOrEmpty(eventDetails.EventId.ToString())) - { - var invalidFields = string.Join(", ", new List { "TimeZone", "EventId" }.Where(p => string.IsNullOrEmpty(p))); - throw new ArgumentNullException($"Invalid event details : {invalidFields} cannot be null or empty"); - } - eventDetails.PartitionKey = eventDetails.TimeZone; + eventDetails.PartitionKey = "event-details"; eventDetails.RowKey = eventDetails.EventId.ToString(); // Save From 17c9799c8642052716a8c0772863ab3c3e66e0a1 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 21 Sep 2024 16:26:38 +0900 Subject: [PATCH 16/35] =?UTF-8?q?delete=20:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=20=EB=A1=9C=EC=BB=AC=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index f522ffab..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "files.eol": "\n", - "code-eol.highlightNonDefault": true, - "code-eol.highlightExtraWhitespace": true -} \ No newline at end of file From 9548f5f4bab939e0ac1dddbfb4f7dc0275721818 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 21 Sep 2024 16:29:56 +0900 Subject: [PATCH 17/35] =?UTF-8?q?update=20:=20=ED=85=8C=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=ED=95=98=EB=93=9C=EC=BD=94=EB=94=A9?= =?UTF-8?q?=E2=86=92=EB=8F=99=EC=A0=81=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/AdminEventRepository.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs index c2a67215..82c052c3 100644 --- a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs +++ b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs @@ -51,7 +51,9 @@ public class AdminEventRepository(TableServiceClient tableServiceClient, Storage /// public async Task CreateEvent(AdminEventDetails eventDetails) { - var tableServiceClient = _tableServiceClient.GetTableClient("events"); + // TODO: 설정파일, TableStorageSettings에 여러 개의 테이블 이름이 저장되어 있을 경우 대응 + var tableName = _storageAccountSettings.TableStorage.TableName; + var tableServiceClient = _tableServiceClient.GetTableClient(tableName); // 데이터 저장 var createResponse = await tableServiceClient.AddEntityAsync(eventDetails).ConfigureAwait(false); From 9b50e7586a07ca85194f7a0dad35b8de484c17b5 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 21 Sep 2024 16:31:27 +0900 Subject: [PATCH 18/35] =?UTF-8?q?update=20:=20tableClient=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/AdminEventRepository.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs index 82c052c3..e89adbea 100644 --- a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs +++ b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs @@ -53,14 +53,14 @@ public async Task CreateEvent(AdminEventDetails eventDetails) { // TODO: 설정파일, TableStorageSettings에 여러 개의 테이블 이름이 저장되어 있을 경우 대응 var tableName = _storageAccountSettings.TableStorage.TableName; - var tableServiceClient = _tableServiceClient.GetTableClient(tableName); + var tableClient = _tableServiceClient.GetTableClient(tableName); // 데이터 저장 - var createResponse = await tableServiceClient.AddEntityAsync(eventDetails).ConfigureAwait(false); + var createResponse = await tableClient.AddEntityAsync(eventDetails).ConfigureAwait(false); // 저장한 데이터 재조회 // TODO: [tae0y] Azure.Tables REST API는 저장한 Entity를 반환하는 옵션이 있으나, tableServiceClient는 없으므로 추가 작업 필요 - var getResponse = await tableServiceClient.GetEntityAsync( + var getResponse = await tableClient.GetEntityAsync( eventDetails.PartitionKey, eventDetails.RowKey ).ConfigureAwait(false); From ca183e00181dd98fb189f22e61b74b6ac1168892 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 21 Sep 2024 16:33:26 +0900 Subject: [PATCH 19/35] =?UTF-8?q?update=20:=20=EC=A0=80=EC=9E=A5=ED=9B=84?= =?UTF-8?q?=20=EC=9E=AC=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/AdminEventRepository.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs index e89adbea..5cc5d078 100644 --- a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs +++ b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs @@ -58,15 +58,8 @@ public async Task CreateEvent(AdminEventDetails eventDetails) // 데이터 저장 var createResponse = await tableClient.AddEntityAsync(eventDetails).ConfigureAwait(false); - // 저장한 데이터 재조회 - // TODO: [tae0y] Azure.Tables REST API는 저장한 Entity를 반환하는 옵션이 있으나, tableServiceClient는 없으므로 추가 작업 필요 - var getResponse = await tableClient.GetEntityAsync( - eventDetails.PartitionKey, - eventDetails.RowKey - ).ConfigureAwait(false); - // 조회한 엔티티 반환 - return getResponse.Value; + return eventDetails; } /// From 1c6c2d6de3aebe22e904f6a4e50710855787fb2a Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 21 Sep 2024 16:38:21 +0900 Subject: [PATCH 20/35] =?UTF-8?q?update=20:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=EC=82=AC=ED=95=AD=20=EB=A1=A4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/AdminEventRepositoryTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs index 24b4a742..8096d6dd 100644 --- a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs @@ -16,8 +16,6 @@ namespace AzureOpenAIProxy.ApiApp.Tests.Repositories; public class AdminEventRepositoryTests { - - [Fact] public void Given_ServiceCollection_When_AddAdminEventRepository_Invoked_Then_It_Should_Contain_AdminEventRepository() { From 9bef6abd32c901657724b4dff93d7c7e143c1fb7 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 21 Sep 2024 16:56:28 +0900 Subject: [PATCH 21/35] =?UTF-8?q?update=20:=20tableclient=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20wrapper=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/AdminEventRepository.cs | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs index 5cc5d078..9df3672e 100644 --- a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs +++ b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs @@ -51,9 +51,8 @@ public class AdminEventRepository(TableServiceClient tableServiceClient, Storage /// public async Task CreateEvent(AdminEventDetails eventDetails) { - // TODO: 설정파일, TableStorageSettings에 여러 개의 테이블 이름이 저장되어 있을 경우 대응 - var tableName = _storageAccountSettings.TableStorage.TableName; - var tableClient = _tableServiceClient.GetTableClient(tableName); + // 테이블 클라이언트 + var tableClient = await GetEventTableClientAsync().ConfigureAwait(false); // 데이터 저장 var createResponse = await tableClient.AddEntityAsync(eventDetails).ConfigureAwait(false); @@ -79,6 +78,25 @@ public async Task UpdateEvent(Guid eventId, AdminEventDetails { throw new NotImplementedException(); } + + /// + /// Gets the instance. + /// + /// TableClient + private async Task GetEventTableClientAsync() + { + // TODO: 설정파일, TableStorageSettings에 여러 개의 테이블 이름이 저장되어 있을 경우 대응 + var tableName = _storageAccountSettings.TableStorage.TableName; + if (string.IsNullOrWhiteSpace(tableName)) + { + throw new InvalidOperationException("Table name not found"); + } + + await _tableServiceClient.CreateTableIfNotExistsAsync(tableName).ConfigureAwait(false); + var tableClient = _tableServiceClient.GetTableClient(tableName); + + return tableClient; + } } /// From a836420a432c61801b49cb36668686f93e4eea9f Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 21 Sep 2024 17:33:54 +0900 Subject: [PATCH 22/35] =?UTF-8?q?update=20:=20aspire.bicep=20=EB=B3=91?= =?UTF-8?q?=ED=95=A9/=EB=B3=80=EA=B2=BD=EC=B6=A9=EB=8F=8C=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/aspire.bicep | 220 ++++++++++++++++++++++----------------------- 1 file changed, 110 insertions(+), 110 deletions(-) diff --git a/infra/aspire.bicep b/infra/aspire.bicep index 4756f609..e0f2712d 100644 --- a/infra/aspire.bicep +++ b/infra/aspire.bicep @@ -1,110 +1,110 @@ -// The main bicep module to provision Azure resources. -// For a more complete walkthrough to understand how this file works with azd, -// see https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-create - -@minLength(1) -@maxLength(64) -@description('Name of the the environment which is used to generate a short unique hash used in all resources.') -param environmentName string - -@minLength(1) -@description('Primary location for all resources') -param location string - -// Parameters for Key Vault -param keyVaultName string = '' -param enabledForDeployment bool = true -param enabledForTemplateDeployment bool = true -param enableRbacAuthorization bool = true - -//TODO: 배포 시점에 사용자 princpalId, apiapp principalId를 받는 방법 조사 -//param creatorAdminPrincipalId string = '' -//param apiAppUserPrincipalId string = '' - -// parameters for storage account -param storageAccountName string = '' -// tableNames passed as a comma separated string from command line -param tableNames string = 'events' - -var abbrs = loadJsonContent('./abbreviations.json') - -// Tags that should be applied to all resources. -var tags = { - // Tag all resources with the environment name. - 'azd-env-name': environmentName -} - -// Generate a unique token to be used in naming resources. -// Remove linter suppression after using. -#disable-next-line no-unused-vars -// var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) -var resourceToken = uniqueString(resourceGroup().id) - -// Name of the service defined in azure.yaml -// A tag named azd-service-name with this value should be applied to the service host resource, such as: -// Microsoft.Web/sites for appservice, function -// Example usage: -// tags: union(tags, { 'azd-service-name': apiServiceName }) -#disable-next-line no-unused-vars -// var apiServiceName = 'python-api' - -// tables for storage account seperated by comma -var tables = split(tableNames, ',') - -// Add resources to be provisioned below. - -// Provision Key Vault -module keyVault './core/security/keyvault.bicep' = { - name: 'keyVault' - params: { - name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' - location: location - tags: tags - enabledForDeployment: enabledForDeployment - enabledForTemplateDeployment: enabledForTemplateDeployment - enableRbacAuthorization: enableRbacAuthorization - } -} - -// Provision Storage Account -module storageAccount './core/storage/storage-account.bicep' = { - name: 'storageAccount' - params: { - name: !empty(storageAccountName) ? storageAccountName : '${abbrs.storageStorageAccounts}${resourceToken}' - location: location - tags: tags - tables: tables - keyVaultName: keyVault.outputs.name - } -} - -// TODO: Key vault Secret 권한부여, 생성한 사람에게 관리자 권한을, 그 외에는 secret user 권한을 부여 -//resource keyVaultSecretRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { -// name: guid(resourceGroup().id, resolvedKeyVaultName, 'secret-role-assignment') -// properties: { -// principalId: creatorAdminPrincipalId -// roleDefinitionId: '00482A5A-887F-4FB3-B363-3B7FE8E74483' // administrator role -// } -//} -// -//resource keyVaultSecretApiAppRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { -// name: guid(resourceGroup().id, resolvedKeyVaultName, 'secret-apiapp-role-assignment') -// properties: { -// principalId: apiAppUserPrincipalId -// roleDefinitionId: '4633458B-17DE-408A-B874-0445C86B69E6' // secret user role -// } -//} - -// Add outputs from the deployment here, if needed. -// -// This allows the outputs to be referenced by other bicep deployments in the deployment pipeline, -// or by the local machine as a way to reference created resources in Azure for local development. -// Secrets should not be added here. -// -// Outputs are automatically saved in the local azd environment .env file. -// To see these outputs, run `azd env get-values`, or `azd env get-values --output json` for json output. -output AZURE_LOCATION string = location -output AZURE_TENANT_ID string = tenant().tenantId - -output AZURE_KEYVAULT_NAME string = keyVault.outputs.name -output AZURE_KEYVAULT_ENDPOINT string = keyVault.outputs.endpoint +// The main bicep module to provision Azure resources. +// For a more complete walkthrough to understand how this file works with azd, +// see https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-create + +@minLength(1) +@maxLength(64) +@description('Name of the the environment which is used to generate a short unique hash used in all resources.') +param environmentName string + +@minLength(1) +@description('Primary location for all resources') +param location string + +// Parameters for Key Vault +param keyVaultName string = '' +param enabledForDeployment bool = true +param enabledForTemplateDeployment bool = true +param enableRbacAuthorization bool = true + +//TODO: 배포 시점에 사용자 princpalId, apiapp principalId를 받는 방법 조사 +//param creatorAdminPrincipalId string = '' +//param apiAppUserPrincipalId string = '' + +// parameters for storage account +param storageAccountName string = '' +// tableNames passed as a comma separated string from command line +param tableNames string = 'events' + +var abbrs = loadJsonContent('./abbreviations.json') + +// Tags that should be applied to all resources. +var tags = { + // Tag all resources with the environment name. + 'azd-env-name': environmentName +} + +// Generate a unique token to be used in naming resources. +// Remove linter suppression after using. +#disable-next-line no-unused-vars +// var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var resourceToken = uniqueString(resourceGroup().id) + +// Name of the service defined in azure.yaml +// A tag named azd-service-name with this value should be applied to the service host resource, such as: +// Microsoft.Web/sites for appservice, function +// Example usage: +// tags: union(tags, { 'azd-service-name': apiServiceName }) +#disable-next-line no-unused-vars +// var apiServiceName = 'python-api' + +// tables for storage account seperated by comma +var tables = split(tableNames, ',') + +// Add resources to be provisioned below. + +// Provision Key Vault +module keyVault './core/security/keyvault.bicep' = { + name: 'keyVault' + params: { + name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' + location: location + tags: tags + enabledForDeployment: enabledForDeployment + enabledForTemplateDeployment: enabledForTemplateDeployment + enableRbacAuthorization: enableRbacAuthorization + } +} + +// Provision Storage Account +module storageAccount './core/storage/storage-account.bicep' = { + name: 'storageAccount' + params: { + name: !empty(storageAccountName) ? storageAccountName : '${abbrs.storageStorageAccounts}${resourceToken}' + location: location + tags: tags + tables: tables + keyVaultName: keyVault.outputs.name + } +} + +// TODO: Key vault Secret 권한부여, 생성한 사람에게 관리자 권한을, 그 외에는 secret user 권한을 부여 +//resource keyVaultSecretRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { +// name: guid(resourceGroup().id, resolvedKeyVaultName, 'secret-role-assignment') +// properties: { +// principalId: creatorAdminPrincipalId +// roleDefinitionId: '00482A5A-887F-4FB3-B363-3B7FE8E74483' // administrator role +// } +//} +// +//resource keyVaultSecretApiAppRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { +// name: guid(resourceGroup().id, resolvedKeyVaultName, 'secret-apiapp-role-assignment') +// properties: { +// principalId: apiAppUserPrincipalId +// roleDefinitionId: '4633458B-17DE-408A-B874-0445C86B69E6' // secret user role +// } +//} + +// Add outputs from the deployment here, if needed. +// +// This allows the outputs to be referenced by other bicep deployments in the deployment pipeline, +// or by the local machine as a way to reference created resources in Azure for local development. +// Secrets should not be added here. +// +// Outputs are automatically saved in the local azd environment .env file. +// To see these outputs, run `azd env get-values`, or `azd env get-values --output json` for json output. +output AZURE_LOCATION string = location +output AZURE_TENANT_ID string = tenant().tenantId + +output AZURE_KEYVAULT_NAME string = keyVault.outputs.name +output AZURE_KEYVAULT_ENDPOINT string = keyVault.outputs.endpoint From 55db6d695117c02b7ba7ba6bd4edfb5d187ae065 Mon Sep 17 00:00:00 2001 From: tae0y Date: Wed, 25 Sep 2024 21:45:51 +0900 Subject: [PATCH 23/35] =?UTF-8?q?update=20:=20=EB=B3=91=ED=95=A9=ED=9B=84?= =?UTF-8?q?=20repository=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=ED=81=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8,=20=ED=8C=8C=ED=8B=B0?= =?UTF-8?q?=EC=85=98=ED=82=A4=20=EB=A1=9C=EC=A7=81=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/AdminEventRepository.cs | 4 ++-- src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs index 5d0ce581..9b34045f 100644 --- a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs +++ b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs @@ -52,12 +52,12 @@ public class AdminEventRepository(TableServiceClient tableServiceClient, Storage public async Task CreateEvent(AdminEventDetails eventDetails) { // 테이블 클라이언트 - var tableClient = await GetEventTableClientAsync().ConfigureAwait(false); + TableClient tableClient = await GetTableClientAsync(); // 데이터 저장 var createResponse = await tableClient.AddEntityAsync(eventDetails).ConfigureAwait(false); - // 조회한 엔티티 반환 + // 저장한 엔티티 반환 return eventDetails; } diff --git a/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs b/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs index ce768c7c..a5289023 100644 --- a/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs +++ b/src/AzureOpenAIProxy.ApiApp/Services/AdminEventService.cs @@ -48,7 +48,7 @@ public class AdminEventService(IAdminEventRepository repository) : IAdminEventSe public async Task CreateEvent(AdminEventDetails eventDetails) { // Validate - eventDetails.PartitionKey = "event-details"; + eventDetails.PartitionKey = PartitionKeys.EventDetails; eventDetails.RowKey = eventDetails.EventId.ToString(); // Save From 473da19ee9b31f477565943a1936c5b8d264c333 Mon Sep 17 00:00:00 2001 From: tae0y Date: Wed, 25 Sep 2024 22:57:43 +0900 Subject: [PATCH 24/35] =?UTF-8?q?update=20:=20=EB=8B=A8=EC=9C=84=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/AdminEventRepositoryTests.cs | 207 +++--------------- .../Services/AdminEventServiceTests.cs | 190 +++------------- 2 files changed, 55 insertions(+), 342 deletions(-) diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs index 70ffba60..dbe68126 100644 --- a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs @@ -58,7 +58,7 @@ public void Given_Null_StorageAccountSettings_When_Creating_AdminEventRepository } [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_Same_Instance() { // Arrange var settings = Substitute.For(); @@ -67,10 +67,33 @@ public void Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Throw_Excepti var repository = new AdminEventRepository(tableServiceClient, settings); // Act - Func func = async () => await repository.CreateEvent(eventDetails); + var result = await repository.CreateEvent(eventDetails); // Assert - func.Should().ThrowAsync(); + result.Should().BeSameAs(eventDetails); + } + + [Fact] + public async Task Given_Failure_In_Add_Entity_When_CreateEvent_Invoked_Then_It_Should_Throw_Exception() + { + // Arrange + var settings = Substitute.For(); + var tableServiceClient = Substitute.For(); + var repository = new AdminEventRepository(tableServiceClient, settings); + var eventDetails = new AdminEventDetails(); + + var exception = new InvalidOperationException("Invalid Operation Error : check duplicate, null or empty values"); + + var tableClient = Substitute.For(); + tableServiceClient.GetTableClient(Arg.Any()).Returns(tableClient); + tableClient.AddEntityAsync(Arg.Any()) + .ThrowsAsync(exception); + + // Act + Func func = () => repository.CreateEvent(eventDetails); + + // Assert + await func.Should().ThrowAsync(); } [Fact] @@ -159,182 +182,4 @@ public void Given_Instance_When_UpdateEvent_Invoked_Then_It_Should_Throw_Excepti // Assert func.Should().ThrowAsync(); } - - // TODO: [tae0y] Add tests for other methods - [Fact] - public async Task Given_TableServiceClientIsNull_When_CreateEvent_Invoked_Then_ItShould_Throw_NullReferenceException() - { - // Arrange - var eventDetails = createRandomEventDetails( - new AdminEventDetails(), - new HashSet { } - ); - var settings = Substitute.For(); - var tableServiceClient = Substitute.For(); - var repository = new AdminEventRepository(tableServiceClient, settings); - - // Act - Func func = async () => await repository.CreateEvent(eventDetails); - - // Assert - await func.Should().ThrowAsync(); - } - - [Fact] - public async Task Given_AddEntityAsyncIsSuccessful_When_CreateEvent_Invoked_Then_ItShould_WorkProperly() - { - // Arrange - var eventDetails = createRandomEventDetails( - new AdminEventDetails(), - new HashSet { } - ); - // TODO: [tae0y] TableServiceClient를 Mocking하지 않고 실제 의존성을 주입 - var settings = Substitute.For(); - var tableServiceClient = Substitute.For(); - var repository = new AdminEventRepository(tableServiceClient, settings); - - // Act - var result = await repository.CreateEvent(eventDetails); - - // Assert - result.Should().NotBeNull(); - } - - [Fact] - public async Task Given_AddEntityAsyncIsNotSuccessful_When_CreateEvent_Invoked_Then_ItShould_ThrowException() - { - // Arrange - // duplicate eventDetails - var eventDetails = createRandomEventDetails( - new AdminEventDetails(), - new HashSet { } - ); - var settings = Substitute.For(); - var tableServiceClient = Substitute.For(); - var repository = new AdminEventRepository(tableServiceClient, settings); - - // Act - Func func = async () => await repository.CreateEvent(eventDetails); - - // Assert - await func.Should().ThrowAsync(); - } - - [Fact] - public async Task Given_AddEntityAsyncIsSuccessful_When_CreateEvent_Invoked_Then_GetEntityAsyncShould_ReturnCorrectValue() - { - // Arrange - var eventDetails = createRandomEventDetails( - new AdminEventDetails(), - new HashSet { } - ); - var settings = Substitute.For(); - var tableServiceClient = Substitute.For(); - var repository = new AdminEventRepository(tableServiceClient, settings); - - // Act - var result = await repository.CreateEvent(eventDetails); - - // Assert - // result Should BeEquivalentTo eventDetails except for PartitionKey, RowKey, Timestamp, ETag - result.Should().BeEquivalentTo(eventDetails, options => options - .Excluding(e => e.Timestamp) - .Excluding(e => e.ETag) - ); - } - - // Helper method to create random event details - private static AdminEventDetails createRandomEventDetails(AdminEventDetails details, HashSet keepNullFields) - { - Random random = new Random(); - - // Check if field should be skipped by checking if it exists in the hashset - bool ShouldSkip(string fieldName) - { - return keepNullFields.Contains(fieldName); - } - - // Fill in null or empty string properties with random values unless they should be skipped - if (!ShouldSkip(nameof(details.Title)) && string.IsNullOrEmpty(details.Title)) - { - details.Title = "Event Title " + random.Next(1000, 9999); - } - - if (!ShouldSkip(nameof(details.Summary)) && string.IsNullOrEmpty(details.Summary)) - { - details.Summary = "This is a summary for event " + random.Next(1000, 9999); - } - - if (!ShouldSkip(nameof(details.Description)) && string.IsNullOrEmpty(details.Description)) - { - details.Description = "Description for event " + random.Next(1000, 9999); - } - - if (!ShouldSkip(nameof(details.EventId)) && details.EventId == Guid.Empty) - { - details.EventId = Guid.NewGuid(); - } - - if (!ShouldSkip(nameof(details.MaxTokenCap)) && details.MaxTokenCap == 0) - { - details.MaxTokenCap = random.Next(100, 1000); // Assign random token cap - } - - if (!ShouldSkip(nameof(details.DailyRequestCap)) && details.DailyRequestCap == 0) - { - details.DailyRequestCap = random.Next(10, 100); // Assign random daily request cap - } - - if (!ShouldSkip(nameof(details.DateStart)) && details.DateStart == DateTimeOffset.MinValue) - { - details.DateStart = DateTimeOffset.Now.AddDays(random.Next(-10, 10)); // Random start date - } - - if (!ShouldSkip(nameof(details.DateEnd)) && (details.DateEnd == DateTimeOffset.MinValue || details.DateEnd <= details.DateStart)) - { - details.DateEnd = details.DateStart.AddHours(random.Next(1, 72)); // End date should be after start date - } - - if (!ShouldSkip(nameof(details.TimeZone)) && string.IsNullOrEmpty(details.TimeZone)) - { - details.TimeZone = "KST"; // Default timezone - } - - if (!ShouldSkip(nameof(details.IsActive))) - { - details.IsActive = true; // Default is active - } - - if (!ShouldSkip(nameof(details.OrganizerName)) && string.IsNullOrEmpty(details.OrganizerName)) - { - details.OrganizerName = "Organizer " + random.Next(1000, 9999); - } - - if (!ShouldSkip(nameof(details.OrganizerEmail)) && string.IsNullOrEmpty(details.OrganizerEmail)) - { - details.OrganizerEmail = $"organizer{random.Next(1000, 9999)}@example.com"; - } - - if (!ShouldSkip(nameof(details.CoorganizerName)) && string.IsNullOrEmpty(details.CoorganizerName)) - { - details.CoorganizerName = "Co-organizer " + random.Next(1000, 9999); - } - - if (!ShouldSkip(nameof(details.CoorganizerEmail)) && string.IsNullOrEmpty(details.CoorganizerEmail)) - { - details.CoorganizerEmail = $"coorganizer{random.Next(1000, 9999)}@example.com"; - } - - if (!ShouldSkip(nameof(details.PartitionKey)) && string.IsNullOrEmpty(details.PartitionKey)) - { - details.PartitionKey = details.TimeZone; - } - - if (!ShouldSkip(nameof(details.RowKey)) && string.IsNullOrEmpty(details.RowKey)) - { - details.RowKey = details.EventId.ToString(); - } - - return details; - } } diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs index 869bfc59..5851d642 100644 --- a/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs @@ -6,6 +6,8 @@ using FluentAssertions; +using Microsoft.AspNetCore.Authentication; + using Microsoft.Extensions.DependencyInjection; using NSubstitute; @@ -29,18 +31,42 @@ public void Given_ServiceCollection_When_AddAdminEventService_Invoked_Then_It_Sh } [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_Same_Instance() { // Arrange var eventDetails = new AdminEventDetails(); var repository = Substitute.For(); var service = new AdminEventService(repository); + repository.CreateEvent(Arg.Any()).Returns(eventDetails); + // Act - Func func = async () => await service.CreateEvent(eventDetails); + var result = await service.CreateEvent(eventDetails); // Assert - func.Should().ThrowAsync(); + result.Should().BeEquivalentTo( + eventDetails, + options => options.Excluding(x => x.PartitionKey) + .Excluding(x => x.RowKey) + ); + } + + [Fact] + public async Task Given_Failure_In_Add_Entity_When_CreateEvent_Invoked_Then_It_Should_Throw_Exception() + { + // Arrange + var eventDetails = new AdminEventDetails(); + var repository = Substitute.For(); + var service = new AdminEventService(repository); + + var exception = new InvalidOperationException("Invalid Operation Error : check duplicate, null or empty values"); + repository.CreateEvent(Arg.Any()).ThrowsAsync(exception); + + // Act + Func func = () => service.CreateEvent(eventDetails); + + // Assert + await func.Should().ThrowAsync(); } [Fact] @@ -119,162 +145,4 @@ public void Given_Instance_When_UpdateEvent_Invoked_Then_It_Should_Throw_Excepti // Assert func.Should().ThrowAsync(); } - - // TODO: [tae0y] Add tests for other methods - [Fact] - public async Task Given_TimeZoneIsEmpty_When_CreateEvent_Invoked_Then_Throw_ArgumentNullException() - { - // Arrange - var repository = Substitute.For(); - var service = new AdminEventService(repository); - var eventDetails = createRandomEventDetails( - new AdminEventDetails(), - new HashSet { nameof(AdminEventDetails.TimeZone) } - ); - - // Act - Func func = async () => await service.CreateEvent(eventDetails); - - // Assert - await func.Should().ThrowAsync(); - } - - [Fact] - public async Task Given_TimeZoneIsNotEmpty_When_CreateEvent_Invoked_Then_PartitionKeyShould_Be_TimeZone() - { - // Arrange - var repository = Substitute.For(); - var service = new AdminEventService(repository); - var eventDetails = createRandomEventDetails( - new AdminEventDetails(), - new HashSet() - ); - - // Act - var result = await service.CreateEvent(eventDetails); - - // Assert - result.PartitionKey.Should().Be(eventDetails.TimeZone); - } - - [Fact] - public async Task Given_EventIdIsNull_When_CreateEvent_Invoked_Then_Throw_ArgumentNullException() - { - // Arrange - var repository = Substitute.For(); - var service = new AdminEventService(repository); - var eventDetails = createRandomEventDetails( - new AdminEventDetails(), - new HashSet { nameof(AdminEventDetails.EventId) } - ); - - // Act - Func func = async () => await service.CreateEvent(eventDetails); - - // Assert - await func.Should().ThrowAsync(); - } - - [Fact] - public async Task Given_EventIdIsNotEmpty_When_CreateEvent_Invoked_Then_RowKeyShould_Be_EventId() - { - // Arrange - var repository = Substitute.For(); - var service = new AdminEventService(repository); - var eventDetails = createRandomEventDetails( - new AdminEventDetails(), - new HashSet() - ); - - // Act - var result = await service.CreateEvent(eventDetails); - - // Assert - result.RowKey.Should().Be(eventDetails.RowKey); - } - - // Helper method to create random event details - private static AdminEventDetails createRandomEventDetails(AdminEventDetails details, HashSet keepNullFields) - { - Random random = new Random(); - - // Check if field should be skipped by checking if it exists in the hashset - bool ShouldSkip(string fieldName) - { - return keepNullFields.Contains(fieldName); - } - - // Fill in null or empty string properties with random values unless they should be skipped - if (!ShouldSkip(nameof(details.Title)) && string.IsNullOrEmpty(details.Title)) - { - details.Title = "Event Title " + random.Next(1000, 9999); - } - - if (!ShouldSkip(nameof(details.Summary)) && string.IsNullOrEmpty(details.Summary)) - { - details.Summary = "This is a summary for event " + random.Next(1000, 9999); - } - - if (!ShouldSkip(nameof(details.Description)) && string.IsNullOrEmpty(details.Description)) - { - details.Description = "Description for event " + random.Next(1000, 9999); - } - - if (!ShouldSkip(nameof(details.EventId)) && details.EventId == Guid.Empty) - { - details.EventId = Guid.NewGuid(); - } - - if (!ShouldSkip(nameof(details.MaxTokenCap)) && details.MaxTokenCap == 0) - { - details.MaxTokenCap = random.Next(100, 1000); // Assign random token cap - } - - if (!ShouldSkip(nameof(details.DailyRequestCap)) && details.DailyRequestCap == 0) - { - details.DailyRequestCap = random.Next(10, 100); // Assign random daily request cap - } - - if (!ShouldSkip(nameof(details.DateStart)) && details.DateStart == DateTimeOffset.MinValue) - { - details.DateStart = DateTimeOffset.Now.AddDays(random.Next(-10, 10)); // Random start date - } - - if (!ShouldSkip(nameof(details.DateEnd)) && (details.DateEnd == DateTimeOffset.MinValue || details.DateEnd <= details.DateStart)) - { - details.DateEnd = details.DateStart.AddHours(random.Next(1, 72)); // End date should be after start date - } - - if (!ShouldSkip(nameof(details.TimeZone)) && string.IsNullOrEmpty(details.TimeZone)) - { - details.TimeZone = "KST"; // Default timezone - } - - if (!ShouldSkip(nameof(details.IsActive))) - { - details.IsActive = true; // Default is active - } - - if (!ShouldSkip(nameof(details.OrganizerName)) && string.IsNullOrEmpty(details.OrganizerName)) - { - details.OrganizerName = "Organizer " + random.Next(1000, 9999); - } - - if (!ShouldSkip(nameof(details.OrganizerEmail)) && string.IsNullOrEmpty(details.OrganizerEmail)) - { - details.OrganizerEmail = $"organizer{random.Next(1000, 9999)}@example.com"; - } - - if (!ShouldSkip(nameof(details.CoorganizerName)) && string.IsNullOrEmpty(details.CoorganizerName)) - { - details.CoorganizerName = "Co-organizer " + random.Next(1000, 9999); - } - - if (!ShouldSkip(nameof(details.CoorganizerEmail)) && string.IsNullOrEmpty(details.CoorganizerEmail)) - { - details.CoorganizerEmail = $"coorganizer{random.Next(1000, 9999)}@example.com"; - } - - return details; - } } From 2b466c77f8a0d2869e78cc2b8aea4e17b139d91f Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 28 Sep 2024 13:26:10 +0900 Subject: [PATCH 25/35] =?UTF-8?q?update=20:=20=EB=B9=8C=EB=93=9C=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EB=B0=A9=EC=A7=80=20=EC=9E=84=EC=8B=9C=EC=A1=B0?= =?UTF-8?q?=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AzureOpenAIProxy.PlaygroundApp/Clients/OpenAIApiClient.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Clients/OpenAIApiClient.cs b/src/AzureOpenAIProxy.PlaygroundApp/Clients/OpenAIApiClient.cs index 44856e0a..58b15a31 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Clients/OpenAIApiClient.cs +++ b/src/AzureOpenAIProxy.PlaygroundApp/Clients/OpenAIApiClient.cs @@ -38,7 +38,8 @@ public async Task CompleteChatAsync(OpenAIApiClientOptions clientOptions }; var options = new ChatCompletionOptions { - MaxTokens = clientOptions.MaxTokens, + // TODO: [tae0y] 빌드 오류 방지를 위한 임시조치 + MaxOutputTokenCount = clientOptions.MaxTokens, Temperature = clientOptions.Temperature, }; From d77ca2cd6a83268f69e1d038d15649aad020a316 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 28 Sep 2024 13:40:55 +0900 Subject: [PATCH 26/35] =?UTF-8?q?update=20:=20OpenAIApiClient=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=EC=98=A4=EB=A5=98=20=EC=9E=84=EC=8B=9C=EC=A1=B0?= =?UTF-8?q?=EC=B9=98=20=EC=9B=90=EB=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AzureOpenAIProxy.PlaygroundApp/Clients/OpenAIApiClient.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Clients/OpenAIApiClient.cs b/src/AzureOpenAIProxy.PlaygroundApp/Clients/OpenAIApiClient.cs index 58b15a31..44856e0a 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Clients/OpenAIApiClient.cs +++ b/src/AzureOpenAIProxy.PlaygroundApp/Clients/OpenAIApiClient.cs @@ -38,8 +38,7 @@ public async Task CompleteChatAsync(OpenAIApiClientOptions clientOptions }; var options = new ChatCompletionOptions { - // TODO: [tae0y] 빌드 오류 방지를 위한 임시조치 - MaxOutputTokenCount = clientOptions.MaxTokens, + MaxTokens = clientOptions.MaxTokens, Temperature = clientOptions.Temperature, }; From 74d967440711570f50e943392a2a834bb7f36388 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 5 Oct 2024 00:15:11 +0900 Subject: [PATCH 27/35] =?UTF-8?q?update=20:=20repository=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=20=EA=B0=81=EC=A3=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/AdminEventRepository.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs index 9b34045f..d5aa58ae 100644 --- a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs +++ b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs @@ -86,10 +86,6 @@ public async Task UpdateEvent(Guid eventId, AdminEventDetails throw new NotImplementedException(); } - /// - /// Gets the instance. - /// - /// TableClient private async Task GetTableClientAsync() { TableClient tableClient = _tableServiceClient.GetTableClient(_storageAccountSettings.TableStorage.TableName); From 951643609906b32bbeafb6c25e7e3ef3f809d816 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 5 Oct 2024 00:35:50 +0900 Subject: [PATCH 28/35] =?UTF-8?q?update=20:=20unit=20test=20tableclient=20?= =?UTF-8?q?=EB=AA=A9=ED=82=B9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/AdminEventRepositoryTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs index dbe68126..59519b2e 100644 --- a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs @@ -63,6 +63,8 @@ public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Return_ // Arrange var settings = Substitute.For(); var tableServiceClient = Substitute.For(); + var tableClient = Substitute.For(); + tableServiceClient.GetTableClient(Arg.Any()).Returns(tableClient); var eventDetails = new AdminEventDetails(); var repository = new AdminEventRepository(tableServiceClient, settings); From a083391dbe91b676a5995e14700e653be8c5f58a Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 5 Oct 2024 00:45:02 +0900 Subject: [PATCH 29/35] =?UTF-8?q?update=20:=20unit=20test=20AddEntityAsync?= =?UTF-8?q?=20=EB=AA=A9=ED=82=B9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/AdminEventRepositoryTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs index 59519b2e..68bc286d 100644 --- a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs @@ -65,6 +65,8 @@ public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Return_ var tableServiceClient = Substitute.For(); var tableClient = Substitute.For(); tableServiceClient.GetTableClient(Arg.Any()).Returns(tableClient); + tableClient.AddEntityAsync(Arg.Any()) + .Returns(Task.FromResult(default(Response))); var eventDetails = new AdminEventDetails(); var repository = new AdminEventRepository(tableServiceClient, settings); From 6274ead03a7e66acd5a936a098ef212ff72f6afd Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 5 Oct 2024 01:07:27 +0900 Subject: [PATCH 30/35] =?UTF-8?q?remote=20build=20=EC=9E=AC=EC=8B=9C?= =?UTF-8?q?=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/AdminEventRepositoryTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs index 68bc286d..d454448d 100644 --- a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs @@ -70,7 +70,7 @@ public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Return_ var eventDetails = new AdminEventDetails(); var repository = new AdminEventRepository(tableServiceClient, settings); - // Act + // Act var result = await repository.CreateEvent(eventDetails); // Assert From bbdbd680ace00e127f10871aeb791ca4c8aefb4b Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 12 Oct 2024 14:24:22 +0900 Subject: [PATCH 31/35] =?UTF-8?q?update=20:=20=EC=9E=91=EC=97=85=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=AA=A9=EB=A1=9D=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs | 2 ++ .../Repositories/AdminEventRepository.cs | 2 +- .../Repositories/AdminEventRepositoryTests.cs | 1 + .../Services/AdminEventServiceTests.cs | 4 ++++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs index 6df29391..79bcddb5 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs @@ -33,6 +33,8 @@ public static RouteHandlerBuilder AddNewAdminEvent(this WebApplication app) return Results.BadRequest("Payload is null"); } + // TODO: [tae0y] [테스트] 필수값이 null일때 언마샬 가능한가? 언마샬이 통과돼버리면.. 조치 필요 + // fluent validation try { diff --git a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs index d5aa58ae..3adb60a2 100644 --- a/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs +++ b/src/AzureOpenAIProxy.ApiApp/Repositories/AdminEventRepository.cs @@ -55,7 +55,7 @@ public async Task CreateEvent(AdminEventDetails eventDetails) TableClient tableClient = await GetTableClientAsync(); // 데이터 저장 - var createResponse = await tableClient.AddEntityAsync(eventDetails).ConfigureAwait(false); + await tableClient.AddEntityAsync(eventDetails).ConfigureAwait(false); // 저장한 엔티티 반환 return eventDetails; diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs index d454448d..23f354ca 100644 --- a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs @@ -75,6 +75,7 @@ public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Return_ // Assert result.Should().BeSameAs(eventDetails); + // TODO: [tae0y] 테스트관점이 다름, 예외없음을 검사 } [Fact] diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs index 5851d642..10f50941 100644 --- a/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs @@ -35,6 +35,10 @@ public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Return_ { // Arrange var eventDetails = new AdminEventDetails(); + // TODO: [tae0y] payload validation에 대해 더 고민, 필요시 fluent validation 적용 + eventDetails.PartitionKey = null; + eventDetails.RowKey = null; + var repository = Substitute.For(); var service = new AdminEventService(repository); From f695a7c3321c38d7bff25810c203123b038c37b2 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 12 Oct 2024 14:35:52 +0900 Subject: [PATCH 32/35] =?UTF-8?q?update=20:=20Endpoint=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=EA=B2=80=EC=82=AC=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs index 79bcddb5..6df29391 100644 --- a/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs +++ b/src/AzureOpenAIProxy.ApiApp/Endpoints/AdminEventEndpoints.cs @@ -33,8 +33,6 @@ public static RouteHandlerBuilder AddNewAdminEvent(this WebApplication app) return Results.BadRequest("Payload is null"); } - // TODO: [tae0y] [테스트] 필수값이 null일때 언마샬 가능한가? 언마샬이 통과돼버리면.. 조치 필요 - // fluent validation try { From 4b5a1bd80b4667bb73f67e7751022f1f02c52e38 Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 12 Oct 2024 14:44:57 +0900 Subject: [PATCH 33/35] =?UTF-8?q?update=20:=20RepositoryTests=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=EB=B0=9C=EC=83=9D=20=EA=B4=80=EC=A0=90=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/AdminEventRepositoryTests.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs index 23f354ca..e2d2038a 100644 --- a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs @@ -58,7 +58,7 @@ public void Given_Null_StorageAccountSettings_When_Creating_AdminEventRepository } [Fact] - public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Return_Same_Instance() + public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Not_Throw_Exception() { // Arrange var settings = Substitute.For(); @@ -71,11 +71,10 @@ public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Return_ var repository = new AdminEventRepository(tableServiceClient, settings); // Act - var result = await repository.CreateEvent(eventDetails); + Func func = () => repository.CreateEvent(eventDetails); // Assert - result.Should().BeSameAs(eventDetails); - // TODO: [tae0y] 테스트관점이 다름, 예외없음을 검사 + await func.Should().NotThrowAsync(); } [Fact] From 78ffc92ba17e2d088a8becfb7114da7e9ce9bdbe Mon Sep 17 00:00:00 2001 From: tae0y Date: Sat, 12 Oct 2024 14:47:44 +0900 Subject: [PATCH 34/35] =?UTF-8?q?update=20:=20ServiceTests=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=EB=B0=9C=EC=83=9D=20=EA=B4=80=EC=A0=90=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9E=91=EC=84=B1,=20?= =?UTF-8?q?=ED=95=84=EC=88=98=EA=B0=92=20=EC=97=86=EC=9D=84=20=EB=95=8C?= =?UTF-8?q?=EB=8A=94=20payload=20=EC=96=B8=EB=A7=88=EC=83=AC=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EB=A1=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B6=88?= =?UTF-8?q?=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/AdminEventServiceTests.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs index 10f50941..0a51edea 100644 --- a/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs @@ -31,13 +31,10 @@ public void Given_ServiceCollection_When_AddAdminEventService_Invoked_Then_It_Sh } [Fact] - public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Return_Same_Instance() + public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Not_Throw_Exception() { // Arrange var eventDetails = new AdminEventDetails(); - // TODO: [tae0y] payload validation에 대해 더 고민, 필요시 fluent validation 적용 - eventDetails.PartitionKey = null; - eventDetails.RowKey = null; var repository = Substitute.For(); var service = new AdminEventService(repository); @@ -45,14 +42,10 @@ public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Return_ repository.CreateEvent(Arg.Any()).Returns(eventDetails); // Act - var result = await service.CreateEvent(eventDetails); + Func func = () => service.CreateEvent(eventDetails); // Assert - result.Should().BeEquivalentTo( - eventDetails, - options => options.Excluding(x => x.PartitionKey) - .Excluding(x => x.RowKey) - ); + await func.Should().NotThrowAsync(); } [Fact] From 2587bd66ba07df9ea5a34802a00dc373b0ef6ea9 Mon Sep 17 00:00:00 2001 From: tae0y Date: Thu, 17 Oct 2024 22:33:39 +0900 Subject: [PATCH 35/35] =?UTF-8?q?update=20:=20repo/service=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=EA=B0=92=20=EA=B2=80=EC=82=AC=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/AdminEventRepositoryTests.cs | 8 ++++---- .../Services/AdminEventServiceTests.cs | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs index e2d2038a..3278cca4 100644 --- a/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Repositories/AdminEventRepositoryTests.cs @@ -58,7 +58,7 @@ public void Given_Null_StorageAccountSettings_When_Creating_AdminEventRepository } [Fact] - public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Not_Throw_Exception() + public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Return_Passed_Argument() { // Arrange var settings = Substitute.For(); @@ -70,11 +70,11 @@ public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Not_Thr var eventDetails = new AdminEventDetails(); var repository = new AdminEventRepository(tableServiceClient, settings); - // Act - Func func = () => repository.CreateEvent(eventDetails); + // Act + var result = await repository.CreateEvent(eventDetails); // Assert - await func.Should().NotThrowAsync(); + result.Should().BeEquivalentTo(eventDetails); } [Fact] diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs index 0a51edea..4ba483a1 100644 --- a/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Services/AdminEventServiceTests.cs @@ -31,7 +31,7 @@ public void Given_ServiceCollection_When_AddAdminEventService_Invoked_Then_It_Sh } [Fact] - public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Not_Throw_Exception() + public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Return_Passed_Argument() { // Arrange var eventDetails = new AdminEventDetails(); @@ -42,10 +42,10 @@ public async Task Given_Instance_When_CreateEvent_Invoked_Then_It_Should_Not_Thr repository.CreateEvent(Arg.Any()).Returns(eventDetails); // Act - Func func = () => service.CreateEvent(eventDetails); + var result = await service.CreateEvent(eventDetails); // Assert - await func.Should().NotThrowAsync(); + result.Should().BeEquivalentTo(eventDetails); } [Fact]