Skip to content

Commit e702672

Browse files
authored
[Backend API] Implement endpoint for list of events (#330)
* add PlaygroundService Related to: 180 * add EventRepository Related to: #180 * implement EventRepository Related to: #180 * update PlaygroundEndpoints Related to: #180 * add EventRepositoryExtensions Related to: #180 * add PlaygroundServiceExtensions Related to: #180 * inject dependencies of PlaygroundService and EventRepository Related to: #180 * add ConfigureAwait(false) Related to: #180 * add 404 return to PlaygroundEndpoints Related to: #180 * fix comment * add PlaygroundServiceTests Related to: #180 * add EventRepositoryTests Related to: #180 * Revert "add 404 return to PlaygroundEndpoints" This reverts commit f307930. * add 404 return to PlaygroundEndpoints Related to: #180 * update test method Related to: #180 * update structures of EventDetails and AdminEventDetails Related to: #180 * sort event details list Related to: #180 * Revert "update structures of EventDetails and AdminEventDetails" This reverts commit a093fee. * fix event details sorting criteria Related to: #180 * remove 404 return in /events endpoint QueryAsync does not throw RequestFailedException with status code 404. Related to: #180 * update test code Related to: #180
1 parent f0658a5 commit e702672

File tree

6 files changed

+368
-3
lines changed

6 files changed

+368
-3
lines changed

src/AzureOpenAIProxy.ApiApp/Endpoints/PlaygroundEndpoints.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
using Azure;
2+
3+
using AzureOpenAIProxy.ApiApp.Services;
4+
15
using Microsoft.AspNetCore.Mvc;
26

37
namespace AzureOpenAIProxy.ApiApp.Endpoints;
@@ -14,10 +18,24 @@ public static class PlaygroundEndpoints
1418
/// <returns>Returns <see cref="RouteHandlerBuilder"/> instance.</returns>
1519
public static RouteHandlerBuilder AddListEvents(this WebApplication app)
1620
{
17-
var builder = app.MapGet(PlaygroundEndpointUrls.Events, () =>
21+
// ASSUMPTION: User has already logged in
22+
var builder = app.MapGet(PlaygroundEndpointUrls.Events, async (
23+
IPlaygroundService service,
24+
ILoggerFactory loggerFactory) =>
1825
{
19-
// TODO: Issue #179 https://github.com/aliencube/azure-openai-sdk-proxy/issues/179
20-
return Results.Ok();
26+
var logger = loggerFactory.CreateLogger(nameof(AdminEventEndpoints));
27+
logger.LogInformation("Received request to fetch events list");
28+
29+
try
30+
{
31+
var eventDetailsList = await service.GetEvents();
32+
return Results.Ok(eventDetailsList);
33+
}
34+
catch (Exception ex)
35+
{
36+
logger.LogError(ex, $"Error occurred while fetching events list");
37+
return Results.Problem(ex.Message, statusCode: StatusCodes.Status500InternalServerError);
38+
}
2139
})
2240
.Produces<List<EventDetails>>(statusCode: StatusCodes.Status200OK, contentType: "application/json")
2341
.Produces(statusCode: StatusCodes.Status401Unauthorized)

src/AzureOpenAIProxy.ApiApp/Program.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@
2828
// Add admin repositories
2929
builder.Services.AddAdminEventRepository();
3030

31+
// Add playground services
32+
builder.Services.AddPlaygroundService();
33+
34+
// Add playground repositories
35+
builder.Services.AddEventRepository();
36+
3137
var app = builder.Build();
3238

3339
app.MapDefaultEndpoints();
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using Azure.Data.Tables;
2+
3+
using AzureOpenAIProxy.ApiApp.Configurations;
4+
5+
namespace AzureOpenAIProxy.ApiApp.Repositories;
6+
7+
/// <summary>
8+
/// This provides interfaces to the <see cref="EventRepository"/> class.
9+
/// </summary>
10+
public interface IEventRepository
11+
{
12+
/// <summary>
13+
/// Gets the list of events.
14+
/// </summary>
15+
/// <returns>Returns the list of events.</returns>
16+
Task<List<EventDetails>> GetEvents();
17+
}
18+
19+
public class EventRepository(TableServiceClient tableServiceClient, StorageAccountSettings storageAccountSettings) : IEventRepository
20+
{
21+
private readonly TableServiceClient _tableServiceClient = tableServiceClient ?? throw new ArgumentNullException(nameof(tableServiceClient));
22+
private readonly StorageAccountSettings _storageAccountSettings = storageAccountSettings ?? throw new ArgumentNullException(nameof(storageAccountSettings));
23+
24+
/// <inheritdoc/>
25+
/// <remarks>
26+
/// The results are sorted based on the following criteria:
27+
/// Lexical order of event titles.
28+
/// </remarks>
29+
public async Task<List<EventDetails>> GetEvents()
30+
{
31+
TableClient tableClient = await GetTableClientAsync();
32+
33+
List<EventDetails> events = [];
34+
35+
await foreach(EventDetails eventDetails in tableClient.QueryAsync<EventDetails>(e => e.PartitionKey.Equals(PartitionKeys.EventDetails)).ConfigureAwait(false))
36+
{
37+
events.Add(eventDetails);
38+
}
39+
40+
events.Sort((e1, e2) => e1.Title.CompareTo(e2.Title));
41+
42+
return events;
43+
}
44+
45+
private async Task<TableClient> GetTableClientAsync()
46+
{
47+
TableClient tableClient = _tableServiceClient.GetTableClient(_storageAccountSettings.TableStorage.TableName);
48+
49+
await tableClient.CreateIfNotExistsAsync().ConfigureAwait(false);
50+
51+
return tableClient;
52+
}
53+
}
54+
55+
/// <summary>
56+
/// This represents the extension class for <see cref="IServiceCollection"/>
57+
/// </summary>
58+
public static class EventRepositoryExtensions
59+
{
60+
/// <summary>
61+
/// Adds the <see cref="EventRepository"/> instance to the service collection.
62+
/// </summary>
63+
/// <param name="services"><see cref="IServiceCollection"/> instance.</param>
64+
/// <returns>Returns <see cref="IServiceCollection"/> instance.</returns>
65+
public static IServiceCollection AddEventRepository(this IServiceCollection services)
66+
{
67+
services.AddScoped<IEventRepository, EventRepository>();
68+
69+
return services;
70+
}
71+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using AzureOpenAIProxy.ApiApp.Repositories;
2+
3+
namespace AzureOpenAIProxy.ApiApp.Services;
4+
5+
/// <summary>
6+
/// This provides interfaces to <see cref="PlaygroundService"/> class.
7+
/// </summary>
8+
public interface IPlaygroundService
9+
{
10+
/// <summary>
11+
/// Get the list of deployment model.
12+
/// </summary>
13+
/// <returns>Returns the list of deployment models.</returns>
14+
Task<List<DeploymentModelDetails>> GetDeploymentModels(string eventId);
15+
16+
/// <summary>
17+
/// Get the list of events.
18+
/// </summary>
19+
/// <returns>Returns the list of events.</returns>
20+
Task<List<EventDetails>> GetEvents();
21+
}
22+
23+
public class PlaygroundService(IEventRepository eventRepository) : IPlaygroundService
24+
{
25+
private readonly IEventRepository _eventRepository = eventRepository ?? throw new ArgumentNullException(nameof(eventRepository));
26+
/// <inheritdoc/>
27+
public async Task<List<DeploymentModelDetails>> GetDeploymentModels(string eventId)
28+
{
29+
throw new NotImplementedException();
30+
}
31+
32+
/// <inheritdoc/>
33+
public async Task<List<EventDetails>> GetEvents()
34+
{
35+
var result = await _eventRepository.GetEvents().ConfigureAwait(false);
36+
37+
return result;
38+
}
39+
}
40+
41+
/// <summary>
42+
/// This represents the extension class for <see cref="IServiceCollection"/>
43+
/// </summary>
44+
public static class PlaygroundServiceExtensions
45+
{
46+
/// <summary>
47+
/// Adds the <see cref="PlaygroundService"/> instance to the service collection.
48+
/// </summary>
49+
/// <param name="services"><see cref="IServiceCollection"/> instance.</param>
50+
/// <returns>Returns <see cref="IServiceCollection"/> instance.</returns>
51+
public static IServiceCollection AddPlaygroundService(this IServiceCollection services)
52+
{
53+
services.AddScoped<IPlaygroundService, PlaygroundService>();
54+
55+
return services;
56+
}
57+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using System.Linq.Expressions;
2+
3+
using Azure;
4+
using Azure.Data.Tables;
5+
6+
using AzureOpenAIProxy.ApiApp.Configurations;
7+
using AzureOpenAIProxy.ApiApp.Repositories;
8+
9+
using FluentAssertions;
10+
11+
using Microsoft.Extensions.DependencyInjection;
12+
13+
using NSubstitute;
14+
using NSubstitute.ExceptionExtensions;
15+
16+
namespace AzureOpenAIProxy.ApiApp.Tests.Repositories;
17+
18+
public class EventRepositoryTests
19+
{
20+
[Fact]
21+
public void Given_ServiceCollection_When_AddEventRepository_Invoked_Then_It_Should_Contain_EventRepository()
22+
{
23+
// Arrange
24+
var services = new ServiceCollection();
25+
26+
// Act
27+
services.AddEventRepository();
28+
29+
// Assert
30+
services.SingleOrDefault(p => p.ServiceType == typeof(IEventRepository)).Should().NotBeNull();
31+
}
32+
33+
[Fact]
34+
public void Given_Null_TableServiceClient_When_Creating_EventRepository_Then_It_Should_Throw_Exception()
35+
{
36+
// Arrange
37+
var settings = Substitute.For<StorageAccountSettings>();
38+
var tableServiceClient = default(TableServiceClient);
39+
40+
// Act
41+
Action action = () => new EventRepository(tableServiceClient, settings);
42+
43+
// Assert
44+
action.Should().Throw<ArgumentNullException>();
45+
}
46+
47+
[Fact]
48+
public void Given_Null_StorageAccountSettings_When_Creating_EventRepository_Then_It_Should_Throw_Exception()
49+
{
50+
// Arrange
51+
var settings = default(StorageAccountSettings);
52+
var tableServiceClient = Substitute.For<TableServiceClient>();
53+
54+
// Act
55+
Action action = () => new EventRepository(tableServiceClient, settings);
56+
57+
// Assert
58+
action.Should().Throw<ArgumentNullException>();
59+
}
60+
61+
[Fact]
62+
public async Task Given_Failure_In_Get_Entities_When_GetEvents_Invoked_Then_It_Should_Throw_Exception()
63+
{
64+
// Arrange
65+
var settings = Substitute.For<StorageAccountSettings>();
66+
var tableServiceClient = Substitute.For<TableServiceClient>();
67+
var repository = new EventRepository(tableServiceClient, settings);
68+
69+
var tableClient = Substitute.For<TableClient>();
70+
tableServiceClient.GetTableClient(Arg.Any<string>()).Returns(tableClient);
71+
tableClient.QueryAsync(Arg.Any<Expression<Func<EventDetails, bool>>>()).Throws(new Exception("error occurred"));
72+
73+
// Act
74+
Func<Task> func = repository.GetEvents;
75+
76+
// Assert
77+
var assertion = await func.Should().ThrowAsync<Exception>();
78+
}
79+
80+
[Fact]
81+
public async Task Given_Exist_Events_When_GetEvents_Invoked_Then_It_Should_Return_EventDetails_List()
82+
{
83+
// Arrange
84+
Random rand = new();
85+
int listSize = rand.Next(1, 20);
86+
Guid eventId = new();
87+
88+
var settings = Substitute.For<StorageAccountSettings>();
89+
var tableServiceClient = Substitute.For<TableServiceClient>();
90+
var repository = new EventRepository(tableServiceClient, settings);
91+
92+
var eventDetails = new EventDetails
93+
{
94+
RowKey = eventId.ToString(),
95+
PartitionKey = PartitionKeys.EventDetails
96+
};
97+
98+
List<EventDetails> events = [];
99+
100+
for(int i = 0; i < listSize; ++i)
101+
{
102+
events.Add(eventDetails);
103+
}
104+
105+
var pages = Page<EventDetails>.FromValues(events, default, Substitute.For<Response>());
106+
var asyncPages = AsyncPageable<EventDetails>.FromPages([pages]);
107+
108+
var tableClient = Substitute.For<TableClient>();
109+
tableServiceClient.GetTableClient(Arg.Any<string>()).Returns(tableClient);
110+
tableClient.QueryAsync(Arg.Any<Expression<Func<EventDetails, bool>>>()).Returns(asyncPages);
111+
112+
// Act
113+
var result = await repository.GetEvents();
114+
115+
// Assert
116+
result.Count.Should().Be(listSize);
117+
result.First().Should().BeEquivalentTo(eventDetails);
118+
}
119+
120+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using Azure;
2+
3+
using AzureOpenAIProxy.ApiApp.Repositories;
4+
using AzureOpenAIProxy.ApiApp.Services;
5+
6+
using FluentAssertions;
7+
8+
using Microsoft.Extensions.DependencyInjection;
9+
10+
using NSubstitute;
11+
using NSubstitute.ExceptionExtensions;
12+
13+
namespace AzureOpenAIProxy.ApiApp.Tests.Services;
14+
15+
public class PlaygroundServiceTests
16+
{
17+
[Fact]
18+
public void Given_ServiceCollection_When_AddPlaygroundService_Invoked_Then_It_Should_Contain_PlaygroundService()
19+
{
20+
// Arrange
21+
var services = new ServiceCollection();
22+
23+
// Act
24+
services.AddPlaygroundService();
25+
26+
// Assert
27+
services.SingleOrDefault(p => p.ServiceType == typeof(IPlaygroundService)).Should().NotBeNull();
28+
}
29+
30+
[Fact]
31+
public void Given_Instance_When_GetDeploymentModels_Invoked_Then_It_Should_Throw_Exception()
32+
{
33+
// Arrange
34+
string eventId = "test-id";
35+
var repository = Substitute.For<IEventRepository>();
36+
var service = new PlaygroundService(repository);
37+
38+
// Act
39+
Func<Task> func = async () => await service.GetDeploymentModels(eventId);
40+
41+
// Assert
42+
func.Should().ThrowAsync<NotImplementedException>();
43+
}
44+
45+
[Fact]
46+
public async Task Given_Failure_In_Get_Entities_When_GetEvents_Invoked_Then_It_Should_Throw_Exception()
47+
{
48+
// Arrange
49+
var eventId = Guid.NewGuid();
50+
var repository = Substitute.For<IEventRepository>();
51+
var service = new PlaygroundService(repository);
52+
53+
repository.GetEvents().ThrowsAsync(new Exception("Error occurred"));
54+
55+
// Act
56+
Func<Task> func = service.GetEvents;
57+
58+
// Assert
59+
var assertion = await func.Should().ThrowAsync<Exception>();
60+
}
61+
62+
[Fact]
63+
public async Task Given_Exist_Events_When_GetEvents_Invoked_Then_It_Should_Return_EventDetails_List()
64+
{
65+
// Arrange
66+
Random rand = new();
67+
int listSize = rand.Next(1, 20);
68+
Guid eventId = new();
69+
var repository = Substitute.For<IEventRepository>();
70+
var service = new PlaygroundService(repository);
71+
72+
var eventDetails = new EventDetails
73+
{
74+
RowKey = eventId.ToString()
75+
};
76+
77+
List<EventDetails> events = [];
78+
for(int i = 0; i < listSize; ++i)
79+
{
80+
events.Add(eventDetails);
81+
}
82+
83+
repository.GetEvents().Returns(events);
84+
85+
// Act
86+
var result = await service.GetEvents();
87+
88+
// Assert
89+
result.Count.Should().Be(listSize);
90+
result.First().Should().BeEquivalentTo(eventDetails);
91+
}
92+
93+
}

0 commit comments

Comments
 (0)