Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 0 additions & 4 deletions .github/workflows/dotnet-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ jobs:
build:
runs-on: ubuntu-latest

defaults:
run:
working-directory: ./src

steps:
- uses: actions/checkout@v3
- name: Setup .NET
Expand Down
6 changes: 3 additions & 3 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"type": "process",
"args": [
"build",
"${workspaceFolder}/src/AzureAIProxy.sln",
"${workspaceFolder}/AzureAIProxy.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
Expand All @@ -35,7 +35,7 @@
"type": "process",
"args": [
"publish",
"${workspaceFolder}/src/AzureAIProxy.sln",
"${workspaceFolder}/AzureAIProxy.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
Expand All @@ -49,7 +49,7 @@
"watch",
"run",
"--project",
"${workspaceFolder}/src/AzureAIProxy.sln"
"${workspaceFolder}/AzureAIProxy.sln"
],
"problemMatcher": "$msCompile"
},
Expand Down
17 changes: 14 additions & 3 deletions src/AzureAIProxy.sln → AzureAIProxy.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureAIProxy.Management", "AzureAIProxy.Management\AzureAIProxy.Management.csproj", "{928EAFD1-4179-453D-9367-1BC8E6934ACB}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureAIProxy.Management", "src\AzureAIProxy.Management\AzureAIProxy.Management.csproj", "{928EAFD1-4179-453D-9367-1BC8E6934ACB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureAIProxy", "AzureAIProxy\AzureAIProxy.csproj", "{0D53C13A-DFC7-499C-A9EC-D9C4277FED9E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureAIProxy", "src\AzureAIProxy\AzureAIProxy.csproj", "{0D53C13A-DFC7-499C-A9EC-D9C4277FED9E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureAIProxy.Shared", "AzureAIProxy.Shared\AzureAIProxy.Shared.csproj", "{2797D4D7-7450-43CE-8866-5B1C54E66553}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{3BF40B75-6359-476D-8A5B-0A1A6D7F1235}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureAIProxy.Tests", "tests\AzureAIProxy.Tests\AzureAIProxy.Tests.csproj", "{21E054B7-AFA4-42D3-BB60-02567B722BF1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureAIProxy.Shared", "src\AzureAIProxy.Shared\AzureAIProxy.Shared.csproj", "{2797D4D7-7450-43CE-8866-5B1C54E66553}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -23,6 +27,10 @@ Global
{0D53C13A-DFC7-499C-A9EC-D9C4277FED9E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0D53C13A-DFC7-499C-A9EC-D9C4277FED9E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0D53C13A-DFC7-499C-A9EC-D9C4277FED9E}.Release|Any CPU.Build.0 = Release|Any CPU
{21E054B7-AFA4-42D3-BB60-02567B722BF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{21E054B7-AFA4-42D3-BB60-02567B722BF1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{21E054B7-AFA4-42D3-BB60-02567B722BF1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{21E054B7-AFA4-42D3-BB60-02567B722BF1}.Release|Any CPU.Build.0 = Release|Any CPU
{2797D4D7-7450-43CE-8866-5B1C54E66553}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2797D4D7-7450-43CE-8866-5B1C54E66553}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2797D4D7-7450-43CE-8866-5B1C54E66553}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -31,4 +39,7 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{21E054B7-AFA4-42D3-BB60-02567B722BF1} = {3BF40B75-6359-476D-8A5B-0A1A6D7F1235}
EndGlobalSection
EndGlobal
3 changes: 3 additions & 0 deletions src/AzureAIProxy/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("AzureAIProxy.Tests")]
18 changes: 11 additions & 7 deletions src/AzureAIProxy/Routes/Attendee.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using AzureAIProxy.Services;
using System.Text.Json.Serialization;

namespace AzureAIProxy.Routes;

Expand All @@ -8,25 +9,25 @@ public static class Attendee
public static RouteGroupBuilder MapAttendeeRoutes(this RouteGroupBuilder builder)
{
var attendeeGroup = builder.MapGroup("/attendee/event/{eventId}");
attendeeGroup.MapPost("/register", AttendeeAdd);
attendeeGroup.MapGet("/", AttendeeGetKey);
attendeeGroup.MapPost("/register", AddAttendee);
attendeeGroup.MapGet("/", GetAttendeeKey).WithName(nameof(GetAttendeeKey));
return builder;
}

[JwtAuthorize]
private static async Task<IResult> AttendeeAdd(
internal static async Task<IResult> AddAttendee(
[FromServices] IAttendeeService attendeeService,
HttpContext context,
string eventId
)
{
string userId = (string)context.Items["RequestContext"]!;
string api_key = await attendeeService.AddAttendeeAsync(userId, eventId);
return TypedResults.Created(context.Request.Path, new { api_key });
string attendeeApiKey = await attendeeService.AddAttendeeAsync(userId, eventId);
return TypedResults.CreatedAtRoute(new AttendeeAdded(attendeeApiKey), nameof(GetAttendeeKey));
}

[JwtAuthorize]
private static async Task<IResult> AttendeeGetKey(
internal static async Task<IResult> GetAttendeeKey(
[FromServices] IAttendeeService attendeeService,
HttpContext context,
string eventId
Expand All @@ -38,6 +39,9 @@ string eventId
if (attendee is null)
return TypedResults.NotFound("Attendee not found.");

return TypedResults.Ok(new { attendee.ApiKey, attendee.Active });
return TypedResults.Ok(new AttendeeStatus(attendee.ApiKey, attendee.Active));
}

internal record AttendeeAdded([property: JsonPropertyName("api_key")] string ApiKey);
internal record AttendeeStatus(string ApiKey, bool Active);
}
4 changes: 2 additions & 2 deletions src/AzureAIProxy/Routes/Event.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public static RouteGroupBuilder MapEventRoutes(this RouteGroupBuilder builder)
}

[ApiKeyAuthorize]
private static async Task<IResult> EventInfoAsync(
internal static async Task<IResult> EventInfoAsync(
[FromServices] ICatalogService catalogService,
HttpContext context
)
Expand All @@ -37,7 +37,7 @@ HttpContext context
return TypedResults.Ok(eventInfo);
}

private static async Task<IResult> EventRegistrationInfoAsync(
internal static async Task<IResult> EventRegistrationInfoAsync(
[FromServices] IEventService eventService,
HttpContext context,
string eventId
Expand Down
78 changes: 78 additions & 0 deletions tests/AzureAIProxy.Tests/AttendeeRoutesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using static AzureAIProxy.Routes.Attendee;

namespace AzureAIProxy.Tests;

public class AttendeeRoutesTests
{
[Fact]
public async Task AddAttendeeCreatedNewApiKey()
{
const string attendeeMockKey = "api-key";
const string userId = "user-id";
const string eventId = "event-id";

HttpContext httpContext = Substitute.For<HttpContext>();
httpContext.Items["RequestContext"] = userId;
IAttendeeService attendeeService = Substitute.For<IAttendeeService>();
attendeeService.AddAttendeeAsync(userId, eventId).Returns(attendeeMockKey);

IResult result = await AddAttendee(attendeeService, httpContext, eventId);

Assert.IsType<CreatedAtRoute<AttendeeAdded>>(result);

CreatedAtRoute<AttendeeAdded> created = (CreatedAtRoute<AttendeeAdded>)result;

Assert.Equal(nameof(GetAttendeeKey), created.RouteName);
Assert.NotNull(created.Value);
Assert.Equal(attendeeMockKey, created.Value.ApiKey);

Received.InOrder(() =>
{
attendeeService.AddAttendeeAsync(userId, eventId);
});
}

[Fact]
public async Task NoAttendeeReturnsNotFound()
{
const string userId = "user-id";
const string eventId = "event-id";

HttpContext httpContext = Substitute.For<HttpContext>();
httpContext.Items["RequestContext"] = userId;
IAttendeeService attendeeService = Substitute.For<IAttendeeService>();
attendeeService.GetAttendeeKeyAsync(userId, eventId).Returns((AttendeeKey?)null);

IResult result = await GetAttendeeKey(attendeeService, httpContext, eventId);

Assert.IsType<NotFound<string>>(result);

Received.InOrder(() =>
{
attendeeService.GetAttendeeKeyAsync(userId, eventId);
});
}

[Fact]
public async Task AttendeeReturnsOk()
{
const string userId = "user-id";
const string eventId = "event-id";
const string apiKey = "api-key";

HttpContext httpContext = Substitute.For<HttpContext>();
httpContext.Items["RequestContext"] = userId;
IAttendeeService attendeeService = Substitute.For<IAttendeeService>();
attendeeService.GetAttendeeKeyAsync(userId, eventId).Returns(new AttendeeKey(apiKey, true));

IResult result = await GetAttendeeKey(attendeeService, httpContext, eventId);

Assert.IsType<Ok<AttendeeStatus>>(result);

Ok<AttendeeStatus> ok = (Ok<AttendeeStatus>)result;

Assert.NotNull(ok.Value);
Assert.Equal(apiKey, ok.Value.ApiKey);
Assert.True(ok.Value.Active);
}
}
33 changes: 33 additions & 0 deletions tests/AzureAIProxy.Tests/AzureAIProxy.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="NSubstitute.Analyzers.CSharp" Version="1.0.17">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\AzureAIProxy\AzureAIProxy.csproj" />
<ProjectReference Include="..\..\src\AzureAIProxy.Shared\AzureAIProxy.Shared.csproj" />
</ItemGroup>

</Project>
103 changes: 103 additions & 0 deletions tests/AzureAIProxy.Tests/EventRoutesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using AzureAIProxy.Models;
using static AzureAIProxy.Routes.Event;

namespace AzureAIProxy.Tests;

public class EventRoutesTests
{
[Fact]
public async Task EventInfoReturnsCapabilities()
{
HttpContext httpContext = Substitute.For<HttpContext>();
RequestContext requestContext = new()
{
EventId = "event-id",
IsAuthorized = true,
MaxTokenCap = 100,
EventCode = "event-code",
EventImageUrl = "event-image-url",
OrganizerName = "organizer-name",
OrganizerEmail = "organizer-email"
};
httpContext.Items["RequestContext"] = requestContext;

ICatalogService catalogService = Substitute.For<ICatalogService>();

Dictionary<string, List<string>> capabilities = [];
capabilities.Add("capability", ["value"]);

catalogService.GetCapabilities(requestContext.EventId).Returns(capabilities);

IResult result = await EventInfoAsync(catalogService, httpContext);

Assert.IsType<Ok<EventInfoResponse>>(result);

Ok<EventInfoResponse> ok = (Ok<EventInfoResponse>)result;
Assert.NotNull(ok.Value);
Assert.Equal(requestContext.IsAuthorized, ok.Value.IsAuthorized);
Assert.Equal(requestContext.MaxTokenCap, ok.Value.MaxTokenCap);
Assert.Equal(requestContext.EventCode, ok.Value.EventCode);
Assert.Equal(requestContext.EventImageUrl, ok.Value.EventImageUrl);
Assert.Equal(requestContext.OrganizerName, ok.Value.OrganizerName);
Assert.Equal(requestContext.OrganizerEmail, ok.Value.OrganizerEmail);
Assert.Equal(capabilities, ok.Value.Capabilities);
}

[Fact]
public async Task InvalidEventIdReturnsNotFound()
{
HttpContext httpContext = Substitute.For<HttpContext>();

IEventService eventService = Substitute.For<IEventService>();

eventService.GetEventRegistrationInfoAsync("invalid-event-id").Returns((EventRegistration?)null);

IResult result = await EventRegistrationInfoAsync(eventService, httpContext, "invalid-event-id");

Assert.IsType<NotFound<string>>(result);

NotFound<string> notFound = (NotFound<string>)result;
Assert.Equal("Event not found.", notFound.Value);
}

[Fact]
public async Task ValidEventIdReturnsEventRegistrationInfo()
{
HttpContext httpContext = Substitute.For<HttpContext>();

IEventService eventService = Substitute.For<IEventService>();

EventRegistration eventRegistrationInfo = new()
{
EventId = "event-id",
EventCode = "event-name",
EventImageUrl = "event-image-url",
OrganizerName = "organizer-name",
OrganizerEmail = "organizer-email",
EventMarkdown = "event-markdown",
StartTimestamp = DateTime.UtcNow,
EndTimestamp = DateTime.UtcNow,
TimeZoneLabel = "time-zone-label",
TimeZoneOffset = 0
};

eventService.GetEventRegistrationInfoAsync("event-id").Returns(eventRegistrationInfo);

IResult result = await EventRegistrationInfoAsync(eventService, httpContext, "event-id");

Assert.IsType<Ok<EventRegistration>>(result);

Ok<EventRegistration> ok = (Ok<EventRegistration>)result;
Assert.NotNull(ok.Value);
Assert.Equal(eventRegistrationInfo.EventId, ok.Value.EventId);
Assert.Equal(eventRegistrationInfo.EventCode, ok.Value.EventCode);
Assert.Equal(eventRegistrationInfo.EventImageUrl, ok.Value.EventImageUrl);
Assert.Equal(eventRegistrationInfo.OrganizerName, ok.Value.OrganizerName);
Assert.Equal(eventRegistrationInfo.OrganizerEmail, ok.Value.OrganizerEmail);
Assert.Equal(eventRegistrationInfo.EventMarkdown, ok.Value.EventMarkdown);
Assert.Equal(eventRegistrationInfo.StartTimestamp, ok.Value.StartTimestamp);
Assert.Equal(eventRegistrationInfo.EndTimestamp, ok.Value.EndTimestamp);
Assert.Equal(eventRegistrationInfo.TimeZoneLabel, ok.Value.TimeZoneLabel);
Assert.Equal(eventRegistrationInfo.TimeZoneOffset, ok.Value.TimeZoneOffset);
}
}
5 changes: 5 additions & 0 deletions tests/AzureAIProxy.Tests/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
global using AzureAIProxy.Services;
global using AzureAIProxy.Shared.Database;
global using Microsoft.AspNetCore.Http;
global using Microsoft.AspNetCore.Http.HttpResults;
global using NSubstitute;