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