Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ca20e13
add PlaygroundService
sikutisa Sep 23, 2024
1a62e87
add EventRepository
sikutisa Sep 23, 2024
5379d7e
implement EventRepository
sikutisa Sep 23, 2024
21f163b
update PlaygroundEndpoints
sikutisa Sep 23, 2024
434a8d6
add EventRepositoryExtensions
sikutisa Sep 23, 2024
8196da1
add PlaygroundServiceExtensions
sikutisa Sep 23, 2024
8ba1f06
inject dependencies of PlaygroundService and EventRepository
sikutisa Sep 23, 2024
c7558a6
add ConfigureAwait(false)
sikutisa Sep 23, 2024
cb6039e
sync fork to upstream
sikutisa Oct 3, 2024
f307930
add 404 return to PlaygroundEndpoints
sikutisa Oct 3, 2024
c3bc030
fix comment
sikutisa Oct 3, 2024
bbac70a
add PlaygroundServiceTests
sikutisa Oct 3, 2024
77a5870
add EventRepositoryTests
sikutisa Oct 3, 2024
7d69927
Revert "add 404 return to PlaygroundEndpoints"
sikutisa Oct 3, 2024
4803bcf
add 404 return to PlaygroundEndpoints
sikutisa Oct 3, 2024
7bb624a
sync fork to upstream
sikutisa Oct 6, 2024
184aac6
update test method
sikutisa Oct 6, 2024
a093fee
update structures of EventDetails and AdminEventDetails
sikutisa Oct 6, 2024
0dc5db5
sort event details list
sikutisa Oct 6, 2024
b16f6e8
sync fork to upstream
sikutisa Oct 10, 2024
a245264
Revert "update structures of EventDetails and AdminEventDetails"
sikutisa Oct 10, 2024
b2f7a50
fix event details sorting criteria
sikutisa Oct 10, 2024
6604e11
remove 404 return in /events endpoint
sikutisa Oct 10, 2024
00361b8
update test code
sikutisa Oct 10, 2024
dc2acb7
sync fork to upstream
sikutisa Oct 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions src/AzureOpenAIProxy.ApiApp/Endpoints/PlaygroundEndpoints.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
using Azure;

using AzureOpenAIProxy.ApiApp.Services;

using Microsoft.AspNetCore.Mvc;

namespace AzureOpenAIProxy.ApiApp.Endpoints;
Expand All @@ -14,10 +18,24 @@ public static class PlaygroundEndpoints
/// <returns>Returns <see cref="RouteHandlerBuilder"/> instance.</returns>
public static RouteHandlerBuilder AddListEvents(this WebApplication app)
{
var builder = app.MapGet(PlaygroundEndpointUrls.Events, () =>
// ASSUMPTION: User has already logged in
var builder = app.MapGet(PlaygroundEndpointUrls.Events, async (
IPlaygroundService service,
ILoggerFactory loggerFactory) =>
{
// TODO: Issue #179 https://github.com/aliencube/azure-openai-sdk-proxy/issues/179
return Results.Ok();
var logger = loggerFactory.CreateLogger(nameof(AdminEventEndpoints));
logger.LogInformation("Received request to fetch events list");

try
{
var eventDetailsList = await service.GetEvents();
return Results.Ok(eventDetailsList);
}
catch (Exception ex)
{
logger.LogError(ex, $"Error occurred while fetching events list");
return Results.Problem(ex.Message, statusCode: StatusCodes.Status500InternalServerError);
}
})
.Produces<List<EventDetails>>(statusCode: StatusCodes.Status200OK, contentType: "application/json")
.Produces(statusCode: StatusCodes.Status401Unauthorized)
Expand Down
6 changes: 6 additions & 0 deletions src/AzureOpenAIProxy.ApiApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@
// Add admin repositories
builder.Services.AddAdminEventRepository();

// Add playground services
builder.Services.AddPlaygroundService();

// Add playground repositories
builder.Services.AddEventRepository();

var app = builder.Build();

app.MapDefaultEndpoints();
Expand Down
71 changes: 71 additions & 0 deletions src/AzureOpenAIProxy.ApiApp/Repositories/EventRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using Azure.Data.Tables;

using AzureOpenAIProxy.ApiApp.Configurations;

namespace AzureOpenAIProxy.ApiApp.Repositories;

/// <summary>
/// This provides interfaces to the <see cref="EventRepository"/> class.
/// </summary>
public interface IEventRepository
{
/// <summary>
/// Gets the list of events.
/// </summary>
/// <returns>Returns the list of events.</returns>
Task<List<EventDetails>> GetEvents();
}

public class EventRepository(TableServiceClient tableServiceClient, StorageAccountSettings storageAccountSettings) : IEventRepository
{
private readonly TableServiceClient _tableServiceClient = tableServiceClient ?? throw new ArgumentNullException(nameof(tableServiceClient));
private readonly StorageAccountSettings _storageAccountSettings = storageAccountSettings ?? throw new ArgumentNullException(nameof(storageAccountSettings));

/// <inheritdoc/>
/// <remarks>
/// The results are sorted based on the following criteria:
/// Lexical order of event titles.
/// </remarks>
public async Task<List<EventDetails>> GetEvents()
{
TableClient tableClient = await GetTableClientAsync();

List<EventDetails> events = [];

await foreach(EventDetails eventDetails in tableClient.QueryAsync<EventDetails>(e => e.PartitionKey.Equals(PartitionKeys.EventDetails)).ConfigureAwait(false))
{
events.Add(eventDetails);
}

events.Sort((e1, e2) => e1.Title.CompareTo(e2.Title));

return events;
}

private async Task<TableClient> GetTableClientAsync()
{
TableClient tableClient = _tableServiceClient.GetTableClient(_storageAccountSettings.TableStorage.TableName);

await tableClient.CreateIfNotExistsAsync().ConfigureAwait(false);

return tableClient;
}
}

/// <summary>
/// This represents the extension class for <see cref="IServiceCollection"/>
/// </summary>
public static class EventRepositoryExtensions
{
/// <summary>
/// Adds the <see cref="EventRepository"/> instance to the service collection.
/// </summary>
/// <param name="services"><see cref="IServiceCollection"/> instance.</param>
/// <returns>Returns <see cref="IServiceCollection"/> instance.</returns>
public static IServiceCollection AddEventRepository(this IServiceCollection services)
{
services.AddScoped<IEventRepository, EventRepository>();

return services;
}
}
57 changes: 57 additions & 0 deletions src/AzureOpenAIProxy.ApiApp/Services/PlaygroundService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using AzureOpenAIProxy.ApiApp.Repositories;

namespace AzureOpenAIProxy.ApiApp.Services;

/// <summary>
/// This provides interfaces to <see cref="PlaygroundService"/> class.
/// </summary>
public interface IPlaygroundService
{
/// <summary>
/// Get the list of deployment model.
/// </summary>
/// <returns>Returns the list of deployment models.</returns>
Task<List<DeploymentModelDetails>> GetDeploymentModels(string eventId);

/// <summary>
/// Get the list of events.
/// </summary>
/// <returns>Returns the list of events.</returns>
Task<List<EventDetails>> GetEvents();
}

public class PlaygroundService(IEventRepository eventRepository) : IPlaygroundService
{
private readonly IEventRepository _eventRepository = eventRepository ?? throw new ArgumentNullException(nameof(eventRepository));
/// <inheritdoc/>
public async Task<List<DeploymentModelDetails>> GetDeploymentModels(string eventId)

Check warning on line 27 in src/AzureOpenAIProxy.ApiApp/Services/PlaygroundService.cs

View workflow job for this annotation

GitHub Actions / build-test

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
throw new NotImplementedException();
}

/// <inheritdoc/>
public async Task<List<EventDetails>> GetEvents()
{
var result = await _eventRepository.GetEvents().ConfigureAwait(false);

return result;
}
}

/// <summary>
/// This represents the extension class for <see cref="IServiceCollection"/>
/// </summary>
public static class PlaygroundServiceExtensions
{
/// <summary>
/// Adds the <see cref="PlaygroundService"/> instance to the service collection.
/// </summary>
/// <param name="services"><see cref="IServiceCollection"/> instance.</param>
/// <returns>Returns <see cref="IServiceCollection"/> instance.</returns>
public static IServiceCollection AddPlaygroundService(this IServiceCollection services)
{
services.AddScoped<IPlaygroundService, PlaygroundService>();

return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using System.Linq.Expressions;

using Azure;
using Azure.Data.Tables;

using AzureOpenAIProxy.ApiApp.Configurations;
using AzureOpenAIProxy.ApiApp.Repositories;

using FluentAssertions;

using Microsoft.Extensions.DependencyInjection;

using NSubstitute;
using NSubstitute.ExceptionExtensions;

namespace AzureOpenAIProxy.ApiApp.Tests.Repositories;

public class EventRepositoryTests
{
[Fact]
public void Given_ServiceCollection_When_AddEventRepository_Invoked_Then_It_Should_Contain_EventRepository()
{
// Arrange
var services = new ServiceCollection();

// Act
services.AddEventRepository();

// Assert
services.SingleOrDefault(p => p.ServiceType == typeof(IEventRepository)).Should().NotBeNull();
}

[Fact]
public void Given_Null_TableServiceClient_When_Creating_EventRepository_Then_It_Should_Throw_Exception()
{
// Arrange
var settings = Substitute.For<StorageAccountSettings>();
var tableServiceClient = default(TableServiceClient);

// Act
Action action = () => new EventRepository(tableServiceClient, settings);

Check warning on line 41 in test/AzureOpenAIProxy.ApiApp.Tests/Repositories/EventRepositoryTests.cs

View workflow job for this annotation

GitHub Actions / build-test

Possible null reference argument for parameter 'tableServiceClient' in 'EventRepository.EventRepository(TableServiceClient tableServiceClient, StorageAccountSettings storageAccountSettings)'.

// Assert
action.Should().Throw<ArgumentNullException>();
}

[Fact]
public void Given_Null_StorageAccountSettings_When_Creating_EventRepository_Then_It_Should_Throw_Exception()
{
// Arrange
var settings = default(StorageAccountSettings);
var tableServiceClient = Substitute.For<TableServiceClient>();

// Act
Action action = () => new EventRepository(tableServiceClient, settings);

Check warning on line 55 in test/AzureOpenAIProxy.ApiApp.Tests/Repositories/EventRepositoryTests.cs

View workflow job for this annotation

GitHub Actions / build-test

Possible null reference argument for parameter 'storageAccountSettings' in 'EventRepository.EventRepository(TableServiceClient tableServiceClient, StorageAccountSettings storageAccountSettings)'.

// Assert
action.Should().Throw<ArgumentNullException>();
}

[Fact]
public async Task Given_Failure_In_Get_Entities_When_GetEvents_Invoked_Then_It_Should_Throw_Exception()
{
// Arrange
var settings = Substitute.For<StorageAccountSettings>();
var tableServiceClient = Substitute.For<TableServiceClient>();
var repository = new EventRepository(tableServiceClient, settings);

var tableClient = Substitute.For<TableClient>();
tableServiceClient.GetTableClient(Arg.Any<string>()).Returns(tableClient);
tableClient.QueryAsync(Arg.Any<Expression<Func<EventDetails, bool>>>()).Throws(new Exception("error occurred"));

// Act
Func<Task> func = repository.GetEvents;

// Assert
var assertion = await func.Should().ThrowAsync<Exception>();
}

[Fact]
public async Task Given_Exist_Events_When_GetEvents_Invoked_Then_It_Should_Return_EventDetails_List()
{
// Arrange
Random rand = new();
int listSize = rand.Next(1, 20);
Guid eventId = new();

var settings = Substitute.For<StorageAccountSettings>();
var tableServiceClient = Substitute.For<TableServiceClient>();
var repository = new EventRepository(tableServiceClient, settings);

var eventDetails = new EventDetails
{
RowKey = eventId.ToString(),
PartitionKey = PartitionKeys.EventDetails
};

List<EventDetails> events = [];

for(int i = 0; i < listSize; ++i)
{
events.Add(eventDetails);
}

var pages = Page<EventDetails>.FromValues(events, default, Substitute.For<Response>());
var asyncPages = AsyncPageable<EventDetails>.FromPages([pages]);

var tableClient = Substitute.For<TableClient>();
tableServiceClient.GetTableClient(Arg.Any<string>()).Returns(tableClient);
tableClient.QueryAsync(Arg.Any<Expression<Func<EventDetails, bool>>>()).Returns(asyncPages);

// Act
var result = await repository.GetEvents();

// Assert
result.Count.Should().Be(listSize);
result.First().Should().BeEquivalentTo(eventDetails);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using Azure;

using AzureOpenAIProxy.ApiApp.Repositories;
using AzureOpenAIProxy.ApiApp.Services;

using FluentAssertions;

using Microsoft.Extensions.DependencyInjection;

using NSubstitute;
using NSubstitute.ExceptionExtensions;

namespace AzureOpenAIProxy.ApiApp.Tests.Services;

public class PlaygroundServiceTests
{
[Fact]
public void Given_ServiceCollection_When_AddPlaygroundService_Invoked_Then_It_Should_Contain_PlaygroundService()
{
// Arrange
var services = new ServiceCollection();

// Act
services.AddPlaygroundService();

// Assert
services.SingleOrDefault(p => p.ServiceType == typeof(IPlaygroundService)).Should().NotBeNull();
}

[Fact]
public void Given_Instance_When_GetDeploymentModels_Invoked_Then_It_Should_Throw_Exception()
{
// Arrange
string eventId = "test-id";
var repository = Substitute.For<IEventRepository>();
var service = new PlaygroundService(repository);

// Act
Func<Task> func = async () => await service.GetDeploymentModels(eventId);

// Assert
func.Should().ThrowAsync<NotImplementedException>();
}

[Fact]
public async Task Given_Failure_In_Get_Entities_When_GetEvents_Invoked_Then_It_Should_Throw_Exception()
{
// Arrange
var eventId = Guid.NewGuid();
var repository = Substitute.For<IEventRepository>();
var service = new PlaygroundService(repository);

repository.GetEvents().ThrowsAsync(new Exception("Error occurred"));

// Act
Func<Task> func = service.GetEvents;

// Assert
var assertion = await func.Should().ThrowAsync<Exception>();
}

[Fact]
public async Task Given_Exist_Events_When_GetEvents_Invoked_Then_It_Should_Return_EventDetails_List()
{
// Arrange
Random rand = new();
int listSize = rand.Next(1, 20);
Guid eventId = new();
var repository = Substitute.For<IEventRepository>();
var service = new PlaygroundService(repository);

var eventDetails = new EventDetails
{
RowKey = eventId.ToString()
};

List<EventDetails> events = [];
for(int i = 0; i < listSize; ++i)
{
events.Add(eventDetails);
}

repository.GetEvents().Returns(events);

// Act
var result = await service.GetEvents();

// Assert
result.Count.Should().Be(listSize);
result.First().Should().BeEquivalentTo(eventDetails);
}

}
Loading