Add project files.
This commit is contained in:
parent
33f394550d
commit
6c9da8e796
|
@ -0,0 +1,31 @@
|
||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.13.35931.197
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AccessQueueService", "AccessQueueService\AccessQueueService.csproj", "{6E556254-63D4-4CEB-9CAF-E912C00E0C30}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AccessQueueServiceTests", "AccessQueueServiceTests\AccessQueueServiceTests.csproj", "{1DF48A19-A2B3-4B0C-B726-E65B8E023760}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{6E556254-63D4-4CEB-9CAF-E912C00E0C30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6E556254-63D4-4CEB-9CAF-E912C00E0C30}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6E556254-63D4-4CEB-9CAF-E912C00E0C30}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6E556254-63D4-4CEB-9CAF-E912C00E0C30}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{1DF48A19-A2B3-4B0C-B726-E65B8E023760}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{1DF48A19-A2B3-4B0C-B726-E65B8E023760}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1DF48A19-A2B3-4B0C-B726-E65B8E023760}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{1DF48A19-A2B3-4B0C-B726-E65B8E023760}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {0B4CB38D-5CDA-4E77-97C8-41A555171F52}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
|
@ -0,0 +1,13 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,6 @@
|
||||||
|
@AccessQueueService_HostAddress = http://localhost:5199
|
||||||
|
|
||||||
|
GET {{AccessQueueService_HostAddress}}/weatherforecast/
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
|
@ -0,0 +1,32 @@
|
||||||
|
using AccessQueueService.Models;
|
||||||
|
using AccessQueueService.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace AccessQueueService.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("access")]
|
||||||
|
public class AccessController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IAccessService _accessService;
|
||||||
|
|
||||||
|
public AccessController(IAccessService accessService)
|
||||||
|
{
|
||||||
|
_accessService = accessService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{id}")]
|
||||||
|
public async Task<AccessResponse> Get(Guid id)
|
||||||
|
{
|
||||||
|
return await _accessService.RequestAccess(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete]
|
||||||
|
[Route("{id}")]
|
||||||
|
public async Task<bool> Delete(Guid id)
|
||||||
|
{
|
||||||
|
return await _accessService.RevokeAccess(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
namespace AccessQueueService.Models
|
||||||
|
{
|
||||||
|
public class AccessResponse
|
||||||
|
{
|
||||||
|
public DateTime? ExpiresOn { get; set; }
|
||||||
|
public int RequestsAhead { get; set; } = 0;
|
||||||
|
public bool HasAccess
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ExpiresOn != null && ExpiresOn > DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace AccessQueueService.Models
|
||||||
|
{
|
||||||
|
public class AccessTicket
|
||||||
|
{
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
public DateTime ExpiresOn { get; set; }
|
||||||
|
public DateTime LastActive { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
using AccessQueueService.Services;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Add services to the container.
|
||||||
|
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen();
|
||||||
|
builder.Services.AddSingleton<IAccessService, AccessService>();
|
||||||
|
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.Run();
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:53481",
|
||||||
|
"sslPort": 44342
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "http://localhost:5199",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "https://localhost:7291;http://localhost:5199",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
using AccessQueueService.Models;
|
||||||
|
|
||||||
|
namespace AccessQueueService.Services
|
||||||
|
{
|
||||||
|
public class AccessService : IAccessService
|
||||||
|
{
|
||||||
|
private readonly Dictionary<Guid, AccessTicket> _accessTickets = new();
|
||||||
|
private readonly Queue<AccessTicket> _accessQueue = new();
|
||||||
|
private static SemaphoreSlim _queueLock = new(1, 1);
|
||||||
|
private IConfiguration _configuration;
|
||||||
|
private readonly int EXP_SECONDS;
|
||||||
|
private readonly int ACT_SECONDS;
|
||||||
|
private readonly int CAPACITY_LIMIT;
|
||||||
|
private readonly bool ROLLING_EXPIRATION;
|
||||||
|
public AccessService(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
EXP_SECONDS = _configuration.GetValue<int>("AccessQueue:ExpirationSeconds");
|
||||||
|
ACT_SECONDS = _configuration.GetValue<int>("AccessQueue:ActivitySeconds");
|
||||||
|
CAPACITY_LIMIT = _configuration.GetValue<int>("AccessQueue:CapacityLimit");
|
||||||
|
ROLLING_EXPIRATION = _configuration.GetValue<bool>("AccessQueue:RollingExpiration");
|
||||||
|
}
|
||||||
|
public int UnexpiredTicketsCount => _accessTickets.Count(t => t.Value.ExpiresOn > DateTime.UtcNow);
|
||||||
|
public int ActiveTicketsCount => _accessTickets.Count(t => t.Value.ExpiresOn > DateTime.UtcNow && t.Value.LastActive > DateTime.UtcNow.AddSeconds(-_configuration.GetValue<int>("AccessQueue:ActivitySeconds")));
|
||||||
|
public int QueueCount => _accessQueue.Count;
|
||||||
|
public async Task<AccessResponse> RequestAccess(Guid userId)
|
||||||
|
{
|
||||||
|
await _queueLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var hasCapacity = !DidDequeueUntilFull();
|
||||||
|
var existingTicket = _accessTickets.GetValueOrDefault(userId);
|
||||||
|
if (existingTicket != null && existingTicket.ExpiresOn > DateTime.UtcNow)
|
||||||
|
{
|
||||||
|
var expiresOn = existingTicket.ExpiresOn;
|
||||||
|
if (ROLLING_EXPIRATION)
|
||||||
|
{
|
||||||
|
expiresOn = DateTime.UtcNow.AddSeconds(EXP_SECONDS);
|
||||||
|
}
|
||||||
|
_accessTickets[userId] = new AccessTicket
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
ExpiresOn = expiresOn,
|
||||||
|
LastActive = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
return new AccessResponse
|
||||||
|
{
|
||||||
|
ExpiresOn = expiresOn
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (hasCapacity)
|
||||||
|
{
|
||||||
|
var accessTicket = new AccessTicket
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
ExpiresOn = DateTime.UtcNow.AddSeconds(EXP_SECONDS),
|
||||||
|
LastActive = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
_accessTickets[userId] = accessTicket;
|
||||||
|
return new AccessResponse
|
||||||
|
{
|
||||||
|
ExpiresOn = _accessTickets[userId].ExpiresOn,
|
||||||
|
RequestsAhead = _accessQueue.Count
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var indexOfTicket = IndexOfTicket(userId);
|
||||||
|
var requestsAhead = _accessQueue.Count - indexOfTicket - 1;
|
||||||
|
if (indexOfTicket == -1)
|
||||||
|
{
|
||||||
|
_accessQueue.Enqueue(new AccessTicket
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
LastActive = DateTime.UtcNow,
|
||||||
|
ExpiresOn = DateTime.MaxValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new AccessResponse
|
||||||
|
{
|
||||||
|
ExpiresOn = null,
|
||||||
|
RequestsAhead = requestsAhead
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_queueLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> RevokeAccess(Guid userId)
|
||||||
|
{
|
||||||
|
await _queueLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _accessTickets.Remove(userId);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_queueLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int DeleteExpiredTickets()
|
||||||
|
{
|
||||||
|
var expiredTickets = _accessTickets.Where(t => t.Value.ExpiresOn < DateTime.UtcNow);
|
||||||
|
int count = 0;
|
||||||
|
foreach (var ticket in expiredTickets)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
_accessTickets.Remove(ticket.Key);
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DidDequeueUntilFull()
|
||||||
|
{
|
||||||
|
var activeCutoff = DateTime.UtcNow.AddSeconds(-ACT_SECONDS);
|
||||||
|
var numberOfActiveUsers = _accessTickets.Count(t => t.Value.ExpiresOn > DateTime.UtcNow && t.Value.LastActive > activeCutoff);
|
||||||
|
var openSpots = CAPACITY_LIMIT - numberOfActiveUsers;
|
||||||
|
int filledSpots = 0;
|
||||||
|
while (filledSpots < openSpots)
|
||||||
|
{
|
||||||
|
if (_accessQueue.TryDequeue(out var nextUser))
|
||||||
|
{
|
||||||
|
if (nextUser.LastActive < activeCutoff)
|
||||||
|
{
|
||||||
|
// User is inactive, throw away their ticket
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_accessTickets[nextUser.UserId] = new AccessTicket
|
||||||
|
{
|
||||||
|
UserId = nextUser.UserId,
|
||||||
|
ExpiresOn = DateTime.UtcNow.AddSeconds(EXP_SECONDS),
|
||||||
|
LastActive = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
filledSpots++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filledSpots == openSpots;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int IndexOfTicket(Guid userId)
|
||||||
|
{
|
||||||
|
var index = 0;
|
||||||
|
foreach (var ticket in _accessQueue)
|
||||||
|
{
|
||||||
|
if (ticket.UserId == userId)
|
||||||
|
{
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
using AccessQueueService.Models;
|
||||||
|
|
||||||
|
namespace AccessQueueService.Services
|
||||||
|
{
|
||||||
|
public interface IAccessService
|
||||||
|
{
|
||||||
|
public Task<AccessResponse> RequestAccess(Guid userId);
|
||||||
|
public Task<bool> RevokeAccess(Guid userId);
|
||||||
|
public int DeleteExpiredTickets();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
namespace AccessQueueService
|
||||||
|
{
|
||||||
|
public class WeatherForecast
|
||||||
|
{
|
||||||
|
public DateOnly Date { get; set; }
|
||||||
|
|
||||||
|
public int TemperatureC { get; set; }
|
||||||
|
|
||||||
|
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||||
|
|
||||||
|
public string? Summary { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AccessQueue": {
|
||||||
|
"CapacityLimit": 100, // Maximum number of active users
|
||||||
|
"ActivitySeconds": 900, // Time since last access before a user is considered inactive
|
||||||
|
"ExpirationSeconds": 43200, // 12 hours - Time before a user access is revoked
|
||||||
|
"RollingExpiration": true // Whether to extend expiration time on access
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
<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="xunit" Version="2.5.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\AccessQueueService\AccessQueueService.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,263 @@
|
||||||
|
namespace AccessQueueServiceTests
|
||||||
|
{
|
||||||
|
using global::AccessQueueService.Services;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AccessQueueService.Tests
|
||||||
|
{
|
||||||
|
public class AccessServiceTests
|
||||||
|
{
|
||||||
|
const int EXP_SECONDS = 5;
|
||||||
|
const int EXP_MILLIS = 1000 * EXP_SECONDS;
|
||||||
|
const int ACT_SECONDS = 1;
|
||||||
|
const int ACT_MILLIS = 1000 * ACT_SECONDS;
|
||||||
|
const int CAP_LIMIT = 5;
|
||||||
|
const int BULK_COUNT = 10000;
|
||||||
|
private readonly AccessService _accessService;
|
||||||
|
|
||||||
|
public AccessServiceTests()
|
||||||
|
{
|
||||||
|
var inMemorySettings = new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
{ "AccessQueue:ExpirationSeconds", $"{EXP_SECONDS}" },
|
||||||
|
{ "AccessQueue:ActivitySeconds", $"{ACT_SECONDS}" },
|
||||||
|
{ "AccessQueue:CapacityLimit", $"{CAP_LIMIT}" },
|
||||||
|
{ "AccessQueue:RollingExpiration", "true" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddInMemoryCollection(inMemorySettings) // Add in-memory settings
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_accessService = new AccessService(configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestAccess_ShouldGrantAccess_WhenCapacityIsAvailable()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await _accessService.RequestAccess(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(response);
|
||||||
|
Assert.NotNull(response.ExpiresOn);
|
||||||
|
Assert.True(response.RequestsAhead == 0);
|
||||||
|
Assert.Equal(1, _accessService.UnexpiredTicketsCount);
|
||||||
|
Assert.Equal(1, _accessService.ActiveTicketsCount);
|
||||||
|
Assert.Equal(0, _accessService.QueueCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestAccess_ShouldReturnAccessResponse_WhenUserAlreadyHasTicket()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
await _accessService.RequestAccess(userId); // First request to create a ticket
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await _accessService.RequestAccess(userId); // Second request for the same user
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(response);
|
||||||
|
Assert.NotNull(response.ExpiresOn);
|
||||||
|
Assert.True(response.RequestsAhead == 0);
|
||||||
|
Assert.Equal(1, _accessService.UnexpiredTicketsCount);
|
||||||
|
Assert.Equal(1, _accessService.ActiveTicketsCount);
|
||||||
|
Assert.Equal(0, _accessService.QueueCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestAccess_ShouldQueueUser_WhenCapacityIsFull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
for (int i = 0; i < CAP_LIMIT * 2; i++) // Fill double capacity
|
||||||
|
{
|
||||||
|
await _accessService.RequestAccess(Guid.NewGuid());
|
||||||
|
}
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await _accessService.RequestAccess(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(response);
|
||||||
|
Assert.Null(response.ExpiresOn);
|
||||||
|
Assert.True(response.RequestsAhead == 5);
|
||||||
|
Assert.Equal(5, _accessService.UnexpiredTicketsCount);
|
||||||
|
Assert.Equal(5, _accessService.ActiveTicketsCount);
|
||||||
|
Assert.Equal(6, _accessService.QueueCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RevokeAccess_ShouldReturnTrue_WhenUserHasAccess()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
await _accessService.RequestAccess(userId);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _accessService.RevokeAccess(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RevokeAccess_ShouldReturnFalse_WhenUserDoesNotHaveAccess()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _accessService.RevokeAccess(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestAccess_ShouldQueueUser_AfterAccessRevoked()
|
||||||
|
{
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
await _accessService.RequestAccess(userId);
|
||||||
|
|
||||||
|
for (int i = 0; i < CAP_LIMIT; i++) // Fill remaining slots
|
||||||
|
{
|
||||||
|
await _accessService.RequestAccess(Guid.NewGuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _accessService.RequestAccess(userId); // Request access before revoking
|
||||||
|
Assert.NotNull(response);
|
||||||
|
Assert.True(response.HasAccess);
|
||||||
|
|
||||||
|
await _accessService.RevokeAccess(userId); // Revoke access
|
||||||
|
var responseAfterRevoke = await _accessService.RequestAccess(userId); // Request access again
|
||||||
|
Assert.NotNull(responseAfterRevoke);
|
||||||
|
Assert.False(responseAfterRevoke.HasAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestAccess_ShouldNotQueueUser_WhenMultipleRequestsForOtherUsersMade()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < CAP_LIMIT; i++) // Fill slots without awaiting
|
||||||
|
{
|
||||||
|
_ = _accessService.RequestAccess(Guid.NewGuid());
|
||||||
|
}
|
||||||
|
var response = await _accessService.RequestAccess(Guid.NewGuid()); // Request access before revoking
|
||||||
|
Assert.NotNull(response);
|
||||||
|
Assert.False(response.HasAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestAccess_ShouldUpdateExpirationTime_WhenRollingExpirationTrue()
|
||||||
|
{
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
var initialResponse = await _accessService.RequestAccess(userId);
|
||||||
|
await Task.Delay(ACT_MILLIS);
|
||||||
|
var updatedResponse = await _accessService.RequestAccess(userId);
|
||||||
|
Assert.True(updatedResponse.ExpiresOn > initialResponse.ExpiresOn);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestAccess_ShouldGrantAccess_WhenUsersWithAccessInactive()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < CAP_LIMIT; i++)
|
||||||
|
{
|
||||||
|
await _accessService.RequestAccess(Guid.NewGuid());
|
||||||
|
}
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
var response = await _accessService.RequestAccess(userId);
|
||||||
|
Assert.False(response.HasAccess);
|
||||||
|
await Task.Delay(ACT_MILLIS);
|
||||||
|
response = await _accessService.RequestAccess(userId);
|
||||||
|
Assert.True(response.HasAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestAccess_ShouldRevokeAccess_WhenExpired()
|
||||||
|
{
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
var response = await _accessService.RequestAccess(userId);
|
||||||
|
Assert.True(response.HasAccess);
|
||||||
|
await Task.Delay(EXP_MILLIS);
|
||||||
|
for (int i = 0; i < CAP_LIMIT; i++)
|
||||||
|
{
|
||||||
|
await _accessService.RequestAccess(Guid.NewGuid());
|
||||||
|
}
|
||||||
|
response = await _accessService.RequestAccess(userId);
|
||||||
|
Assert.False(response.HasAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestAccess_ShouldRetailAccess_WhenNotExpired()
|
||||||
|
{
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
var response = await _accessService.RequestAccess(userId);
|
||||||
|
Assert.True(response.HasAccess);
|
||||||
|
await Task.Delay(ACT_MILLIS);
|
||||||
|
for (int i = 0; i < CAP_LIMIT; i++)
|
||||||
|
{
|
||||||
|
response = await _accessService.RequestAccess(Guid.NewGuid());
|
||||||
|
Assert.True(response.HasAccess);
|
||||||
|
}
|
||||||
|
response = await _accessService.RequestAccess(userId);
|
||||||
|
Assert.True(response.HasAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestAccess_ShouldProcessBulkRequests()
|
||||||
|
{
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
await _accessService.RequestAccess(userId);
|
||||||
|
for (int i = 0; i < BULK_COUNT; i++)
|
||||||
|
{
|
||||||
|
_ = _accessService.RequestAccess(Guid.NewGuid());
|
||||||
|
}
|
||||||
|
var response = await _accessService.RequestAccess(userId);
|
||||||
|
Assert.NotNull(response);
|
||||||
|
Assert.True(response.HasAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestAccess_ShouldReportLessInQueue_AsTicketsInactivate()
|
||||||
|
{
|
||||||
|
var start = DateTime.UtcNow;
|
||||||
|
for (int i = 0; i < CAP_LIMIT; i++) // Fill all slots
|
||||||
|
{
|
||||||
|
var elapsed = DateTime.UtcNow - start;
|
||||||
|
Console.WriteLine($"Elapsed time: {elapsed.TotalSeconds} s: Adding {i}");
|
||||||
|
await _accessService.RequestAccess(Guid.NewGuid());
|
||||||
|
await Task.Delay(ACT_MILLIS / CAP_LIMIT);
|
||||||
|
}
|
||||||
|
var users = new[]
|
||||||
|
{
|
||||||
|
Guid.NewGuid(),
|
||||||
|
Guid.NewGuid(),
|
||||||
|
Guid.NewGuid()
|
||||||
|
};
|
||||||
|
|
||||||
|
await _accessService.RequestAccess(users[0]);
|
||||||
|
await _accessService.RequestAccess(users[1]);
|
||||||
|
var response = await _accessService.RequestAccess(users[2]);
|
||||||
|
|
||||||
|
Assert.Equal(1, response.RequestsAhead);
|
||||||
|
await Task.Delay(ACT_MILLIS / CAP_LIMIT);
|
||||||
|
|
||||||
|
await _accessService.RequestAccess(users[0]);
|
||||||
|
await _accessService.RequestAccess(users[1]);
|
||||||
|
response = await _accessService.RequestAccess(users[2]);
|
||||||
|
|
||||||
|
Assert.Equal(0, response.RequestsAhead);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue