Compare commits
20 Commits
take-a-num
...
main
Author | SHA1 | Date |
---|---|---|
|
c12a6465f9 | |
|
713d8c9956 | |
|
18a5a64d4e | |
|
a540221b7f | |
|
365e4a882c | |
|
ffedc09471 | |
|
a96a3c3a82 | |
|
261490ec74 | |
|
cca0976faf | |
|
0cc1913d98 | |
|
cf71bcbbc0 | |
|
a41daddb11 | |
|
18ddc200eb | |
|
8ae97efd41 | |
|
1de9b57c5c | |
|
42f97fadd3 | |
|
5dbde2c847 | |
|
295c91a05c | |
|
d6aa832f61 | |
|
44a3628489 |
|
@ -0,0 +1,17 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Blazor.Bootstrap" Version="3.3.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\AccessQueueService\AccessQueueService.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<base href="/" />
|
||||||
|
<link rel="stylesheet" href="app.css" />
|
||||||
|
<link rel="stylesheet" href="AccessQueuePlayground.styles.css" />
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet" />
|
||||||
|
<link href="_content/Blazor.Bootstrap/blazor.bootstrap.css" rel="stylesheet" />
|
||||||
|
<HeadOutlet @rendermode="InteractiveServer" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<Routes @rendermode="InteractiveServer" />
|
||||||
|
<script src="_framework/blazor.web.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
|
||||||
|
<script src="_content/Blazor.Bootstrap/blazor.bootstrap.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,9 @@
|
||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
@Body
|
||||||
|
|
||||||
|
<div id="blazor-error-ui">
|
||||||
|
An unhandled error has occurred.
|
||||||
|
<a href="" class="reload">Reload</a>
|
||||||
|
<a class="dismiss">🗙</a>
|
||||||
|
</div>
|
|
@ -0,0 +1,18 @@
|
||||||
|
#blazor-error-ui {
|
||||||
|
background: lightyellow;
|
||||||
|
bottom: 0;
|
||||||
|
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
display: none;
|
||||||
|
left: 0;
|
||||||
|
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui .dismiss {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
@page "/Error"
|
||||||
|
@using System.Diagnostics
|
||||||
|
|
||||||
|
<PageTitle>Error</PageTitle>
|
||||||
|
|
||||||
|
<h1 class="text-danger">Error.</h1>
|
||||||
|
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||||
|
|
||||||
|
@if (ShowRequestId)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>Development Mode</h3>
|
||||||
|
<p>
|
||||||
|
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||||
|
It can result in displaying sensitive information from exceptions to end users.
|
||||||
|
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||||
|
and restarting the app.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@code{
|
||||||
|
[CascadingParameter]
|
||||||
|
private HttpContext? HttpContext { get; set; }
|
||||||
|
|
||||||
|
private string? RequestId { get; set; }
|
||||||
|
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||||
|
|
||||||
|
protected override void OnInitialized() =>
|
||||||
|
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
@page "/"
|
||||||
|
@using AccessQueuePlayground.Models
|
||||||
|
@using AccessQueuePlayground.Services
|
||||||
|
@using BlazorBootstrap
|
||||||
|
|
||||||
|
@inject IAccessQueueManager Manager
|
||||||
|
|
||||||
|
<PageTitle>AccessQueue Playground</PageTitle>
|
||||||
|
@if (Config != null)
|
||||||
|
{
|
||||||
|
<h4>Config</h4>
|
||||||
|
<p>
|
||||||
|
<b>Expiration Seconds:</b> @Config.ExpirationSeconds,
|
||||||
|
<b>Activity Seconds:</b> @Config.ActivitySeconds,
|
||||||
|
<b>Capacity Limit:</b> @Config.CapacityLimit
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
<p>
|
||||||
|
<Button Color="ButtonColor.Success" @onclick="AddUser">Add User</Button>
|
||||||
|
<Button Color="ButtonColor.Danger" @onclick="RevokeAllAccess">Revoke All</Button>
|
||||||
|
<Button Color="ButtonColor.Warning" @onclick="Reset">Reset Data</Button>
|
||||||
|
</p>
|
||||||
|
@if (Status != null)
|
||||||
|
{
|
||||||
|
<h2>Users with access</h2>
|
||||||
|
<Grid TItem="User" Data="Status.AccessUsers" Class="table table-bordered mt-3" AllowSorting>
|
||||||
|
<GridColumns>
|
||||||
|
<GridColumn TItem="User" HeaderText="Id" PropertyName="Id" SortKeySelector="item => item.Id">
|
||||||
|
@context.Id
|
||||||
|
</GridColumn>
|
||||||
|
<GridColumn TItem="User" HeaderText="Expiration" PropertyName="LatestResponse?.ExpiresOn" SortKeySelector="item => item.LatestResponse.ExpiresOn">
|
||||||
|
@context.LatestResponse?.ExpiresOn
|
||||||
|
</GridColumn>
|
||||||
|
<GridColumn TItem="User" HeaderText="Active">
|
||||||
|
<ChildContent>
|
||||||
|
<Switch Value="context.Active" ValueExpression="() => context.Active" ValueChanged="(value) => SetUserActive(context.Id, value)" />
|
||||||
|
</ChildContent>
|
||||||
|
</GridColumn>
|
||||||
|
<GridColumn TItem="User" HeaderText="Revoke">
|
||||||
|
<ChildContent>
|
||||||
|
<Button Size="ButtonSize.ExtraSmall" Color="ButtonColor.Danger" @onclick="() => RevokeAccess(context.Id)">Revoke Access</Button>
|
||||||
|
</ChildContent>
|
||||||
|
</GridColumn>
|
||||||
|
</GridColumns>
|
||||||
|
</Grid>
|
||||||
|
<h2>Users in queue</h2>
|
||||||
|
<Grid TItem="User" Data="Status.QueuedUsers" Class="table table-bordered mt-3">
|
||||||
|
<GridColumns>
|
||||||
|
<GridColumn TItem="User" HeaderText="Id" PropertyName="Id">
|
||||||
|
@context.Id
|
||||||
|
</GridColumn>
|
||||||
|
<GridColumn TItem="User" HeaderText="Queue Postition" PropertyName="LatestResponse?.RequestsAhead">
|
||||||
|
@(context.LatestResponse?.RequestsAhead ?? 0 + 1)
|
||||||
|
</GridColumn>
|
||||||
|
<GridColumn TItem="User" HeaderText="Active">
|
||||||
|
<ChildContent>
|
||||||
|
<Switch Value="context.Active" ValueExpression="() => context.Active" ValueChanged="(value) => SetUserActive(context.Id, value)" />
|
||||||
|
</ChildContent>
|
||||||
|
</GridColumn>
|
||||||
|
<GridColumn TItem="User" HeaderText="Revoke">
|
||||||
|
<ChildContent>
|
||||||
|
<Button Size="ButtonSize.ExtraSmall" Color="ButtonColor.Danger" @onclick="() => RevokeAccess(context.Id)">Revoke Access</Button>
|
||||||
|
</ChildContent>
|
||||||
|
</GridColumn>
|
||||||
|
</GridColumns>
|
||||||
|
</Grid>
|
||||||
|
<h2>Inactive users</h2>
|
||||||
|
<Grid TItem="User" Data="Status.InactiveUsers" Class="table table-bordered mt-3" AllowSorting>
|
||||||
|
<GridColumns>
|
||||||
|
<GridColumn TItem="User" HeaderText="Id" PropertyName="Id" SortKeySelector="item => item.Id">
|
||||||
|
@context.Id
|
||||||
|
</GridColumn>
|
||||||
|
<GridColumn TItem="User" HeaderText="Active">
|
||||||
|
<ChildContent>
|
||||||
|
<Switch Value="context.Active" ValueExpression="() => context.Active" ValueChanged="(value) => SetUserActive(context.Id, value)" />
|
||||||
|
</ChildContent>
|
||||||
|
</GridColumn>
|
||||||
|
</GridColumns>
|
||||||
|
</Grid>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
public AccessQueueStatus? Status;
|
||||||
|
public AccessQueueConfig? Config;
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Manager.StatusUpdated += OnStatusUpdated;
|
||||||
|
Status = Manager.GetStatus();
|
||||||
|
Config = Manager.GetConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStatusUpdated()
|
||||||
|
{
|
||||||
|
InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
Status = Manager.GetStatus();
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddUser()
|
||||||
|
{
|
||||||
|
Manager.AddUser();
|
||||||
|
Status = Manager.GetStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetUserActive(Guid userId, bool isActive)
|
||||||
|
{
|
||||||
|
Manager.SetUserActive(userId, isActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RevokeAccess(Guid userId)
|
||||||
|
{
|
||||||
|
Manager.RevokeAccess(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RevokeAllAccess()
|
||||||
|
{
|
||||||
|
Manager.RevokeAllAccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
Manager.Reset();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
<Router AppAssembly="typeof(Program).Assembly">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||||
|
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||||
|
</Found>
|
||||||
|
</Router>
|
|
@ -0,0 +1,10 @@
|
||||||
|
@using System.Net.Http
|
||||||
|
@using System.Net.Http.Json
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using AccessQueuePlayground
|
||||||
|
@using AccessQueuePlayground.Components
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace AccessQueuePlayground.Models
|
||||||
|
{
|
||||||
|
public class AccessQueueConfig
|
||||||
|
{
|
||||||
|
public int ActivitySeconds { get; set; }
|
||||||
|
public int ExpirationSeconds { get; set; }
|
||||||
|
public int CapacityLimit { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
using AccessQueueService.Models;
|
||||||
|
|
||||||
|
namespace AccessQueuePlayground.Models
|
||||||
|
{
|
||||||
|
public class AccessQueueStatus
|
||||||
|
{
|
||||||
|
public List<User> AccessUsers { get; set; } = [];
|
||||||
|
public List<User> QueuedUsers { get; set; } = [];
|
||||||
|
public List<User> InactiveUsers { get; set; } = [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
using AccessQueueService.Models;
|
||||||
|
|
||||||
|
namespace AccessQueuePlayground.Models
|
||||||
|
{
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public bool Active { get; set; }
|
||||||
|
public AccessResponse? LatestResponse { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
using AccessQueuePlayground.Components;
|
||||||
|
using AccessQueuePlayground.Services;
|
||||||
|
using AccessQueueService.Data;
|
||||||
|
using AccessQueueService.Services;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Add services to the container.
|
||||||
|
builder.Services.AddRazorComponents()
|
||||||
|
.AddInteractiveServerComponents();
|
||||||
|
builder.Services.AddSingleton<IAccessService, AccessService>();
|
||||||
|
builder.Services.AddSingleton<IAccessQueueRepo, TakeANumberAccessQueueRepo>();
|
||||||
|
builder.Services.AddSingleton<IAccessQueueManager, AccessQueueManager>();
|
||||||
|
builder.Services.AddHostedService<AccessQueueBackgroundService>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (!app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||||
|
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||||
|
app.UseHsts();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseStaticFiles();
|
||||||
|
app.UseAntiforgery();
|
||||||
|
|
||||||
|
app.MapRazorComponents<App>()
|
||||||
|
.AddInteractiveServerRenderMode();
|
||||||
|
|
||||||
|
app.Run();
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:25310",
|
||||||
|
"sslPort": 44353
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "http://localhost:5108",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "https://localhost:7211;http://localhost:5108",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace AccessQueuePlayground.Services
|
||||||
|
{
|
||||||
|
public class AccessQueueBackgroundService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly IAccessQueueManager _accessQueueManager;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
|
||||||
|
public AccessQueueBackgroundService(IAccessQueueManager accessQueueManager, IConfiguration config)
|
||||||
|
{
|
||||||
|
_accessQueueManager = accessQueueManager;
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
int refreshRate = _config.GetValue<int>("AccessQueuePlayground:RefreshRateMilliseconds");
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await _accessQueueManager.RecalculateStatus();
|
||||||
|
await Task.Delay(refreshRate, stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using AccessQueuePlayground.Models;
|
||||||
|
using AccessQueueService.Models;
|
||||||
|
using AccessQueueService.Services;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace AccessQueuePlayground.Services
|
||||||
|
{
|
||||||
|
public class AccessQueueManager : IAccessQueueManager
|
||||||
|
{
|
||||||
|
private readonly IAccessService _accessService;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
private ConcurrentDictionary<Guid, User> _users;
|
||||||
|
private AccessQueueStatus _status;
|
||||||
|
public event Action? StatusUpdated;
|
||||||
|
|
||||||
|
private void NotifyStatusUpdated()
|
||||||
|
{
|
||||||
|
StatusUpdated?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccessQueueManager(IAccessService accessService, IConfiguration config)
|
||||||
|
{
|
||||||
|
_accessService = accessService;
|
||||||
|
_users = new ConcurrentDictionary<Guid, User>();
|
||||||
|
_status = new AccessQueueStatus();
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccessQueueStatus GetStatus() => _status;
|
||||||
|
|
||||||
|
public AccessQueueConfig GetConfig() => new AccessQueueConfig
|
||||||
|
{
|
||||||
|
ActivitySeconds = _config.GetValue<int>("AccessQueue:ActivitySeconds"),
|
||||||
|
ExpirationSeconds = _config.GetValue<int>("AccessQueue:ExpirationSeconds"),
|
||||||
|
CapacityLimit = _config.GetValue<int>("AccessQueue:CapacityLimit")
|
||||||
|
};
|
||||||
|
|
||||||
|
public Guid AddUser()
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
_users[id] = new User
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Active = false,
|
||||||
|
};
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetUserActive(Guid userId, bool isActive)
|
||||||
|
{
|
||||||
|
if (_users.TryGetValue(userId, out var user))
|
||||||
|
{
|
||||||
|
user.Active = isActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RecalculateStatus()
|
||||||
|
{
|
||||||
|
var userList = _users.Values.ToList();
|
||||||
|
var newStatus = new AccessQueueStatus();
|
||||||
|
foreach (var user in userList)
|
||||||
|
{
|
||||||
|
if (user.Active)
|
||||||
|
{
|
||||||
|
user.LatestResponse = await _accessService.RequestAccess(user.Id.ToString());
|
||||||
|
if (user.LatestResponse?.HasAccess ?? false)
|
||||||
|
{
|
||||||
|
newStatus.AccessUsers.Add(user);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newStatus.QueuedUsers.Add(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(user.LatestResponse?.ExpiresOn != null && user.LatestResponse.ExpiresOn > DateTime.UtcNow)
|
||||||
|
{
|
||||||
|
newStatus.AccessUsers.Add(user);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newStatus.InactiveUsers.Add(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newStatus.QueuedUsers.Sort((user1, user2) => user1.LatestResponse!.RequestsAhead - user2.LatestResponse!.RequestsAhead);
|
||||||
|
_status = newStatus;
|
||||||
|
NotifyStatusUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RevokeAccess(Guid userId)
|
||||||
|
{
|
||||||
|
var user = _users[userId];
|
||||||
|
user.Active = false;
|
||||||
|
user.LatestResponse = null;
|
||||||
|
_accessService.RevokeAccess(userId.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RevokeAllAccess()
|
||||||
|
{
|
||||||
|
foreach (var user in _users.Values)
|
||||||
|
{
|
||||||
|
RevokeAccess(user.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
RevokeAllAccess();
|
||||||
|
_users = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
using AccessQueuePlayground.Models;
|
||||||
|
|
||||||
|
namespace AccessQueuePlayground.Services
|
||||||
|
{
|
||||||
|
public interface IAccessQueueManager
|
||||||
|
{
|
||||||
|
public event Action? StatusUpdated;
|
||||||
|
public AccessQueueConfig GetConfig();
|
||||||
|
public Task RecalculateStatus();
|
||||||
|
public AccessQueueStatus GetStatus();
|
||||||
|
public Guid AddUser();
|
||||||
|
public void SetUserActive(Guid userId, bool isActive);
|
||||||
|
public void RevokeAccess(Guid userId);
|
||||||
|
public void RevokeAllAccess();
|
||||||
|
public void Reset();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AccessQueue": {
|
||||||
|
"CapacityLimit": 3, // Maximum number of active users
|
||||||
|
"ActivitySeconds": 2, // Time since last access before a user is considered inactive
|
||||||
|
"ExpirationSeconds": 10, // 12 hours - Time before a user access is revoked
|
||||||
|
"RollingExpiration": true // Whether to extend expiration time on access
|
||||||
|
},
|
||||||
|
"AccessQueuePlayground": {
|
||||||
|
"RefreshRateMilliseconds": 200 // How often to re-request access and update the UI
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
h1:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valid.modified:not([type=checkbox]) {
|
||||||
|
outline: 1px solid #26b050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid {
|
||||||
|
outline: 1px solid #e50000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message {
|
||||||
|
color: #e50000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blazor-error-boundary {
|
||||||
|
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||||
|
padding: 1rem 1rem 1rem 3.7rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blazor-error-boundary::after {
|
||||||
|
content: "An error has occurred."
|
||||||
|
}
|
||||||
|
|
||||||
|
.darker-border-checkbox.form-check-input {
|
||||||
|
border-color: #929292;
|
||||||
|
}
|
|
@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AccessQueueService", "Acces
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AccessQueueServiceTests", "AccessQueueServiceTests\AccessQueueServiceTests.csproj", "{1DF48A19-A2B3-4B0C-B726-E65B8E023760}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AccessQueueServiceTests", "AccessQueueServiceTests\AccessQueueServiceTests.csproj", "{1DF48A19-A2B3-4B0C-B726-E65B8E023760}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AccessQueuePlayground", "AccessQueuePlayground\AccessQueuePlayground.csproj", "{65D5E841-7B02-4A55-89C6-12082FA1BCAF}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -21,6 +23,10 @@ Global
|
||||||
{1DF48A19-A2B3-4B0C-B726-E65B8E023760}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{1DF48A19-A2B3-4B0C-B726-E65B8E023760}.Release|Any CPU.Build.0 = Release|Any CPU
|
{1DF48A19-A2B3-4B0C-B726-E65B8E023760}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{65D5E841-7B02-4A55-89C6-12082FA1BCAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{65D5E841-7B02-4A55-89C6-12082FA1BCAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{65D5E841-7B02-4A55-89C6-12082FA1BCAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{65D5E841-7B02-4A55-89C6-12082FA1BCAF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
@ -17,14 +17,14 @@ namespace AccessQueueService.Controllers
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{id}")]
|
[Route("{id}")]
|
||||||
public async Task<AccessResponse> Get(Guid id)
|
public async Task<AccessResponse> Get(string id)
|
||||||
{
|
{
|
||||||
return await _accessService.RequestAccess(id);
|
return await _accessService.RequestAccess(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete]
|
[HttpDelete]
|
||||||
[Route("{id}")]
|
[Route("{id}")]
|
||||||
public async Task<bool> Delete(Guid id)
|
public async Task<bool> Delete(string id)
|
||||||
{
|
{
|
||||||
return await _accessService.RevokeAccess(id);
|
return await _accessService.RevokeAccess(id);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,99 +0,0 @@
|
||||||
using AccessQueueService.Models;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
namespace AccessQueueService.Data
|
|
||||||
{
|
|
||||||
public class DictionaryAccessQueueRepo : IAccessQueueRepo
|
|
||||||
{
|
|
||||||
private readonly Dictionary<Guid, AccessTicket> _accessTickets = new();
|
|
||||||
private readonly Queue<AccessTicket> _accessQueue = new();
|
|
||||||
|
|
||||||
public int GetUnexpiredTicketsCount() => _accessTickets.Count(t => t.Value.ExpiresOn > DateTime.UtcNow);
|
|
||||||
public int GetActiveTicketsCount(DateTime activeCutoff) => _accessTickets
|
|
||||||
.Count(t => t.Value.ExpiresOn > DateTime.UtcNow && t.Value.LastActive >activeCutoff);
|
|
||||||
public int GetQueueCount() => _accessQueue.Count;
|
|
||||||
public int GetRequestsAhead(Guid userId)
|
|
||||||
{
|
|
||||||
var index = 0;
|
|
||||||
foreach (var ticket in _accessQueue)
|
|
||||||
{
|
|
||||||
if (ticket.UserId == userId)
|
|
||||||
{
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Enqueue(AccessTicket ticket)
|
|
||||||
{
|
|
||||||
_accessQueue.Enqueue(ticket);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int DeleteExpiredTickets()
|
|
||||||
{
|
|
||||||
var cutoff = DateTime.UtcNow;
|
|
||||||
var expiredTickets = _accessTickets.Where(t => t.Value.ExpiresOn < cutoff);
|
|
||||||
int count = 0;
|
|
||||||
foreach (var ticket in expiredTickets)
|
|
||||||
{
|
|
||||||
count++;
|
|
||||||
_accessTickets.Remove(ticket.Key);
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveUser(Guid userId)
|
|
||||||
{
|
|
||||||
_accessTickets.Remove(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool DidDequeueUntilFull(int activeSeconds, int expirationSeconds, int capacityLimit)
|
|
||||||
{
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
var activeCutoff = now.AddSeconds(-activeSeconds);
|
|
||||||
var numberOfActiveUsers = _accessTickets.Count(t => t.Value.ExpiresOn > now && t.Value.LastActive > activeCutoff);
|
|
||||||
var openSpots = capacityLimit - 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 = now.AddSeconds(expirationSeconds),
|
|
||||||
LastActive = now
|
|
||||||
};
|
|
||||||
filledSpots++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filledSpots == openSpots;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccessTicket? GetTicket(Guid userId)
|
|
||||||
{
|
|
||||||
return _accessTickets.TryGetValue(userId, out var ticket) ? ticket : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpsertTicket(AccessTicket ticket)
|
|
||||||
{
|
|
||||||
_accessTickets[ticket.UserId] = ticket;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IAccessQueueRepo.RemoveUser(Guid userId)
|
|
||||||
{
|
|
||||||
return _accessTickets.Remove(userId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,15 +8,12 @@ namespace AccessQueueService.Data
|
||||||
public int GetUnexpiredTicketsCount();
|
public int GetUnexpiredTicketsCount();
|
||||||
public int GetActiveTicketsCount(DateTime activeCutoff);
|
public int GetActiveTicketsCount(DateTime activeCutoff);
|
||||||
public int GetQueueCount();
|
public int GetQueueCount();
|
||||||
public AccessTicket? GetTicket(Guid userId);
|
public AccessTicket? GetTicket(string userId);
|
||||||
public void UpsertTicket(AccessTicket ticket);
|
public void UpsertTicket(AccessTicket ticket);
|
||||||
public int GetRequestsAhead(Guid userId);
|
public int GetRequestsAhead(string userId);
|
||||||
public void Enqueue(AccessTicket ticket);
|
public void Enqueue(AccessTicket ticket);
|
||||||
public int DeleteExpiredTickets();
|
public int DeleteExpiredTickets();
|
||||||
public bool RemoveUser(Guid userId);
|
public bool RemoveUser(string userId);
|
||||||
public bool DidDequeueUntilFull(int activeSeconds, int expirationSeconds, int capacityLimit);
|
public bool DidDequeueUntilFull(int activeSeconds, int expirationSeconds, int capacityLimit);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,8 @@ namespace AccessQueueService.Data
|
||||||
{
|
{
|
||||||
public class TakeANumberAccessQueueRepo : IAccessQueueRepo
|
public class TakeANumberAccessQueueRepo : IAccessQueueRepo
|
||||||
{
|
{
|
||||||
private readonly Dictionary<Guid, AccessTicket> _accessTickets = [];
|
private readonly Dictionary<string, AccessTicket> _accessTickets = [];
|
||||||
private readonly Dictionary<Guid, ulong> _queueNumbers = [];
|
private readonly Dictionary<string, ulong> _queueNumbers = [];
|
||||||
private readonly Dictionary<ulong, AccessTicket> _accessQueue = [];
|
private readonly Dictionary<ulong, AccessTicket> _accessQueue = [];
|
||||||
|
|
||||||
private ulong _nowServing = 0;
|
private ulong _nowServing = 0;
|
||||||
|
@ -16,12 +16,16 @@ namespace AccessQueueService.Data
|
||||||
public int GetActiveTicketsCount(DateTime activeCutoff) => _accessTickets
|
public int GetActiveTicketsCount(DateTime activeCutoff) => _accessTickets
|
||||||
.Count(t => t.Value.ExpiresOn > DateTime.UtcNow && t.Value.LastActive > activeCutoff);
|
.Count(t => t.Value.ExpiresOn > DateTime.UtcNow && t.Value.LastActive > activeCutoff);
|
||||||
public int GetQueueCount() => (int)(_nextUnusedTicket - _nowServing);
|
public int GetQueueCount() => (int)(_nextUnusedTicket - _nowServing);
|
||||||
public int GetRequestsAhead(Guid userId)
|
public int GetRequestsAhead(string userId)
|
||||||
{
|
{
|
||||||
if(_queueNumbers.TryGetValue(userId, out var queueNumber))
|
if(_queueNumbers.TryGetValue(userId, out var queueNumber))
|
||||||
{
|
{
|
||||||
|
if(_accessQueue.TryGetValue(queueNumber, out var ticket))
|
||||||
|
{
|
||||||
|
ticket.LastActive = DateTime.UtcNow;
|
||||||
return queueNumber >= _nowServing ? (int)(queueNumber - _nowServing) : -1;
|
return queueNumber >= _nowServing ? (int)(queueNumber - _nowServing) : -1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -46,23 +50,23 @@ namespace AccessQueueService.Data
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveUser(Guid userId)
|
|
||||||
{
|
|
||||||
_accessTickets.Remove(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool DidDequeueUntilFull(int activeSeconds, int expirationSeconds, int capacityLimit)
|
public bool DidDequeueUntilFull(int activeSeconds, int expirationSeconds, int capacityLimit)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var activeCutoff = now.AddSeconds(-activeSeconds);
|
var activeCutoff = now.AddSeconds(-activeSeconds);
|
||||||
var numberOfActiveUsers = _accessTickets.Count(t => t.Value.ExpiresOn > now && t.Value.LastActive > activeCutoff);
|
var numberOfActiveUsers = _accessTickets.Count(t => t.Value.ExpiresOn > now && t.Value.LastActive > activeCutoff);
|
||||||
var openSpots = capacityLimit - numberOfActiveUsers;
|
var openSpots = capacityLimit - numberOfActiveUsers;
|
||||||
|
if(openSpots <= 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
int filledSpots = 0;
|
int filledSpots = 0;
|
||||||
while (filledSpots < openSpots && _nowServing < _nextUnusedTicket)
|
while (filledSpots < openSpots && _nowServing < _nextUnusedTicket)
|
||||||
{
|
{
|
||||||
if (_accessQueue.TryGetValue(_nowServing, out var nextUser))
|
if (_accessQueue.TryGetValue(_nowServing, out var nextUser))
|
||||||
{
|
{
|
||||||
_nowServing++;
|
_accessQueue.Remove(_nowServing);
|
||||||
|
_queueNumbers.Remove(nextUser.UserId);
|
||||||
if (nextUser.LastActive < activeCutoff)
|
if (nextUser.LastActive < activeCutoff)
|
||||||
{
|
{
|
||||||
// User is inactive, throw away their ticket
|
// User is inactive, throw away their ticket
|
||||||
|
@ -76,15 +80,12 @@ namespace AccessQueueService.Data
|
||||||
};
|
};
|
||||||
filledSpots++;
|
filledSpots++;
|
||||||
}
|
}
|
||||||
else
|
_nowServing++;
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return filledSpots == openSpots;
|
return filledSpots == openSpots;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccessTicket? GetTicket(Guid userId)
|
public AccessTicket? GetTicket(string userId)
|
||||||
{
|
{
|
||||||
return _accessTickets.TryGetValue(userId, out var ticket) ? ticket : null;
|
return _accessTickets.TryGetValue(userId, out var ticket) ? ticket : null;
|
||||||
}
|
}
|
||||||
|
@ -94,8 +95,13 @@ namespace AccessQueueService.Data
|
||||||
_accessTickets[ticket.UserId] = ticket;
|
_accessTickets[ticket.UserId] = ticket;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IAccessQueueRepo.RemoveUser(Guid userId)
|
public bool RemoveUser(string userId)
|
||||||
{
|
{
|
||||||
|
if(_queueNumbers.TryGetValue(userId, out var queueNumber))
|
||||||
|
{
|
||||||
|
_accessQueue.Remove(queueNumber);
|
||||||
|
_queueNumbers.Remove(userId);
|
||||||
|
}
|
||||||
return _accessTickets.Remove(userId);
|
return _accessTickets.Remove(userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
{
|
{
|
||||||
public class AccessTicket
|
public class AccessTicket
|
||||||
{
|
{
|
||||||
public Guid UserId { get; set; }
|
public string UserId { get; set; }
|
||||||
public DateTime ExpiresOn { get; set; }
|
public DateTime ExpiresOn { get; set; }
|
||||||
public DateTime LastActive { get; set; }
|
public DateTime LastActive { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,8 @@ builder.Services.AddControllers();
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
builder.Services.AddSingleton<IAccessService, AccessService>();
|
builder.Services.AddSingleton<IAccessService, AccessService>();
|
||||||
builder.Services.AddSingleton<IAccessQueueRepo, DictionaryAccessQueueRepo>();
|
builder.Services.AddSingleton<IAccessQueueRepo, TakeANumberAccessQueueRepo>();
|
||||||
|
builder.Services.AddHostedService<AccessCleanupBackgroundService>();
|
||||||
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
|
||||||
|
namespace AccessQueueService.Services
|
||||||
|
{
|
||||||
|
public class AccessCleanupBackgroundService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly IAccessService _accessService;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
|
||||||
|
public AccessCleanupBackgroundService(IAccessService accessService, IConfiguration config)
|
||||||
|
{
|
||||||
|
_accessService = accessService;
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
var cleanupIntervalMillis = _config.GetValue<int>("AccessQueue:CleanupIntervalSeconds") * 1000;
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_accessService.DeleteExpiredTickets();
|
||||||
|
await Task.Delay(cleanupIntervalMillis, stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,8 +8,6 @@ namespace AccessQueueService.Services
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly IAccessQueueRepo _accessQueueRepo;
|
private readonly IAccessQueueRepo _accessQueueRepo;
|
||||||
|
|
||||||
//private readonly Dictionary<Guid, AccessTicket> _accessTickets = new();
|
|
||||||
//private readonly Queue<AccessTicket> _accessQueue = new();
|
|
||||||
private readonly SemaphoreSlim _queueLock = new(1, 1);
|
private readonly SemaphoreSlim _queueLock = new(1, 1);
|
||||||
private readonly int EXP_SECONDS;
|
private readonly int EXP_SECONDS;
|
||||||
private readonly int ACT_SECONDS;
|
private readonly int ACT_SECONDS;
|
||||||
|
@ -27,7 +25,7 @@ namespace AccessQueueService.Services
|
||||||
public int UnexpiredTicketsCount => _accessQueueRepo.GetUnexpiredTicketsCount();
|
public int UnexpiredTicketsCount => _accessQueueRepo.GetUnexpiredTicketsCount();
|
||||||
public int ActiveTicketsCount => _accessQueueRepo.GetActiveTicketsCount(DateTime.UtcNow.AddSeconds(-_configuration.GetValue<int>("AccessQueue:ActivitySeconds")));
|
public int ActiveTicketsCount => _accessQueueRepo.GetActiveTicketsCount(DateTime.UtcNow.AddSeconds(-_configuration.GetValue<int>("AccessQueue:ActivitySeconds")));
|
||||||
public int QueueCount => _accessQueueRepo.GetQueueCount();
|
public int QueueCount => _accessQueueRepo.GetQueueCount();
|
||||||
public async Task<AccessResponse> RequestAccess(Guid userId)
|
public async Task<AccessResponse> RequestAccess(string userId)
|
||||||
{
|
{
|
||||||
await _queueLock.WaitAsync();
|
await _queueLock.WaitAsync();
|
||||||
try
|
try
|
||||||
|
@ -36,6 +34,7 @@ namespace AccessQueueService.Services
|
||||||
var existingTicket = _accessQueueRepo.GetTicket(userId);
|
var existingTicket = _accessQueueRepo.GetTicket(userId);
|
||||||
if (existingTicket != null && existingTicket.ExpiresOn > DateTime.UtcNow)
|
if (existingTicket != null && existingTicket.ExpiresOn > DateTime.UtcNow)
|
||||||
{
|
{
|
||||||
|
// Already has access
|
||||||
var expiresOn = existingTicket.ExpiresOn;
|
var expiresOn = existingTicket.ExpiresOn;
|
||||||
if (ROLLING_EXPIRATION)
|
if (ROLLING_EXPIRATION)
|
||||||
{
|
{
|
||||||
|
@ -54,6 +53,7 @@ namespace AccessQueueService.Services
|
||||||
}
|
}
|
||||||
if (hasCapacity)
|
if (hasCapacity)
|
||||||
{
|
{
|
||||||
|
// Doesn't have access, but there's space available
|
||||||
var accessTicket = new AccessTicket
|
var accessTicket = new AccessTicket
|
||||||
{
|
{
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
|
@ -68,9 +68,11 @@ namespace AccessQueueService.Services
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// No access and no space, add to queue
|
||||||
var requestsAhead = _accessQueueRepo.GetRequestsAhead(userId);
|
var requestsAhead = _accessQueueRepo.GetRequestsAhead(userId);
|
||||||
if (requestsAhead == -1)
|
if (requestsAhead == -1)
|
||||||
{
|
{
|
||||||
|
requestsAhead = _accessQueueRepo.GetQueueCount();
|
||||||
_accessQueueRepo.Enqueue(new AccessTicket
|
_accessQueueRepo.Enqueue(new AccessTicket
|
||||||
{
|
{
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
|
@ -81,7 +83,7 @@ namespace AccessQueueService.Services
|
||||||
return new AccessResponse
|
return new AccessResponse
|
||||||
{
|
{
|
||||||
ExpiresOn = null,
|
ExpiresOn = null,
|
||||||
RequestsAhead = _accessQueueRepo.GetQueueCount() - 1
|
RequestsAhead = requestsAhead
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,7 +93,7 @@ namespace AccessQueueService.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> RevokeAccess(Guid userId)
|
public async Task<bool> RevokeAccess(string userId)
|
||||||
{
|
{
|
||||||
await _queueLock.WaitAsync();
|
await _queueLock.WaitAsync();
|
||||||
try
|
try
|
||||||
|
|
|
@ -4,8 +4,8 @@ namespace AccessQueueService.Services
|
||||||
{
|
{
|
||||||
public interface IAccessService
|
public interface IAccessService
|
||||||
{
|
{
|
||||||
public Task<AccessResponse> RequestAccess(Guid userId);
|
public Task<AccessResponse> RequestAccess(string userId);
|
||||||
public Task<bool> RevokeAccess(Guid userId);
|
public Task<bool> RevokeAccess(string userId);
|
||||||
public int DeleteExpiredTickets();
|
public int DeleteExpiredTickets();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
"CapacityLimit": 100, // Maximum number of active users
|
"CapacityLimit": 100, // Maximum number of active users
|
||||||
"ActivitySeconds": 900, // Time since last access before a user is considered inactive
|
"ActivitySeconds": 900, // Time since last access before a user is considered inactive
|
||||||
"ExpirationSeconds": 43200, // 12 hours - Time before a user access is revoked
|
"ExpirationSeconds": 43200, // 12 hours - Time before a user access is revoked
|
||||||
"RollingExpiration": true // Whether to extend expiration time on access
|
"RollingExpiration": true, // Whether to extend expiration time on access
|
||||||
|
"CleanupIntervalSeconds": 60 // Interval for cleaning up expired users
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,196 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AccessQueueService.Data;
|
||||||
|
using AccessQueueService.Models;
|
||||||
|
using AccessQueueService.Services;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace AccessQueueServiceTests
|
||||||
|
{
|
||||||
|
public class AccessQueueRepoTests
|
||||||
|
{
|
||||||
|
private readonly TakeANumberAccessQueueRepo _repo;
|
||||||
|
|
||||||
|
public AccessQueueRepoTests()
|
||||||
|
{
|
||||||
|
_repo = new TakeANumberAccessQueueRepo();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetUnexpiredTicketsCount_ReturnsCorrectCount()
|
||||||
|
{
|
||||||
|
|
||||||
|
_repo.UpsertTicket(new AccessTicket { UserId = "a", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow });
|
||||||
|
_repo.UpsertTicket(new AccessTicket { UserId = "b", ExpiresOn = DateTime.UtcNow.AddMinutes(-1), LastActive = DateTime.UtcNow });
|
||||||
|
Assert.Equal(1, _repo.GetUnexpiredTicketsCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetActiveTicketsCount_ReturnsCorrectCount()
|
||||||
|
{
|
||||||
|
|
||||||
|
var activeCutoff = DateTime.UtcNow.AddMinutes(-5);
|
||||||
|
_repo.UpsertTicket(new AccessTicket { UserId = "a", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow });
|
||||||
|
_repo.UpsertTicket(new AccessTicket { UserId = "b", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow.AddMinutes(-10) });
|
||||||
|
Assert.Equal(1, _repo.GetActiveTicketsCount(activeCutoff));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetQueueCount_ReturnsCorrectCount()
|
||||||
|
{
|
||||||
|
|
||||||
|
Assert.Equal(0, _repo.GetQueueCount());
|
||||||
|
_repo.Enqueue(new AccessTicket { UserId = "a", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow });
|
||||||
|
Assert.Equal(1, _repo.GetQueueCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetRequestsAhead_ReturnsMinusOneIfUserNotInQueue()
|
||||||
|
{
|
||||||
|
|
||||||
|
Assert.Equal(-1, _repo.GetRequestsAhead("notfound"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetRequestsAhead_ReturnsCorrectNumber()
|
||||||
|
{
|
||||||
|
|
||||||
|
var ticket = new AccessTicket { UserId = "a", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow };
|
||||||
|
_repo.Enqueue(ticket);
|
||||||
|
Assert.Equal(0, _repo.GetRequestsAhead("a"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Enqueue_AddsTicketToQueue()
|
||||||
|
{
|
||||||
|
|
||||||
|
var ticket = new AccessTicket { UserId = "a", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow };
|
||||||
|
_repo.Enqueue(ticket);
|
||||||
|
Assert.Equal(1, _repo.GetQueueCount());
|
||||||
|
Assert.Equal(0, _repo.GetRequestsAhead("a"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeleteExpiredTickets_RemovesExpiredTickets()
|
||||||
|
{
|
||||||
|
|
||||||
|
_repo.UpsertTicket(new AccessTicket { UserId = "a", ExpiresOn = DateTime.UtcNow.AddMinutes(-1), LastActive = DateTime.UtcNow });
|
||||||
|
_repo.UpsertTicket(new AccessTicket { UserId = "b", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow });
|
||||||
|
int removed = _repo.DeleteExpiredTickets();
|
||||||
|
Assert.Equal(1, removed);
|
||||||
|
Assert.NotNull(_repo.GetTicket("b"));
|
||||||
|
Assert.Null(_repo.GetTicket("a"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DidDequeueUntilFull_FillsOpenSpots()
|
||||||
|
{
|
||||||
|
|
||||||
|
var ticket = new AccessTicket { UserId = "a", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow };
|
||||||
|
_repo.Enqueue(ticket);
|
||||||
|
bool result = _repo.DidDequeueUntilFull(60, 60, 1);
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.NotNull(_repo.GetTicket("a"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DidDequeueUntilFull_ReturnsTrueIfNoOpenSpots()
|
||||||
|
{
|
||||||
|
|
||||||
|
_repo.UpsertTicket(new AccessTicket { UserId = "a", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow });
|
||||||
|
bool result = _repo.DidDequeueUntilFull(60, 60, 0);
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetTicket_ReturnsTicketIfExists()
|
||||||
|
{
|
||||||
|
|
||||||
|
var ticket = new AccessTicket { UserId = "a", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow };
|
||||||
|
_repo.UpsertTicket(ticket);
|
||||||
|
var found = _repo.GetTicket("a");
|
||||||
|
Assert.NotNull(found);
|
||||||
|
Assert.Equal("a", found.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetTicket_ReturnsNullIfNotExists()
|
||||||
|
{
|
||||||
|
|
||||||
|
Assert.Null(_repo.GetTicket("notfound"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpsertTicket_AddsOrUpdatesTicket()
|
||||||
|
{
|
||||||
|
|
||||||
|
var ticket = new AccessTicket { UserId = "a", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow };
|
||||||
|
_repo.UpsertTicket(ticket);
|
||||||
|
Assert.NotNull(_repo.GetTicket("a"));
|
||||||
|
var updated = new AccessTicket { UserId = "a", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow };
|
||||||
|
_repo.UpsertTicket(updated);
|
||||||
|
Assert.Equal(updated.ExpiresOn, _repo.GetTicket("a")!.ExpiresOn);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveUser_RemovesFromAllCollections()
|
||||||
|
{
|
||||||
|
|
||||||
|
var ticket = new AccessTicket { UserId = "a", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow };
|
||||||
|
_repo.Enqueue(ticket);
|
||||||
|
_repo.UpsertTicket(ticket);
|
||||||
|
bool removed = _repo.RemoveUser("a");
|
||||||
|
Assert.True(removed);
|
||||||
|
Assert.Null(_repo.GetTicket("a"));
|
||||||
|
Assert.Equal(-1, _repo.GetRequestsAhead("a"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DidDequeueUntilFull_SkipsInactiveUser()
|
||||||
|
{
|
||||||
|
var inactive = new AccessTicket { UserId = "inactive", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow.AddMinutes(-10) };
|
||||||
|
var active = new AccessTicket { UserId = "active", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow };
|
||||||
|
_repo.Enqueue(inactive);
|
||||||
|
_repo.Enqueue(active);
|
||||||
|
bool result = _repo.DidDequeueUntilFull(5 * 60, 60, 1);
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.Null(_repo.GetTicket("inactive"));
|
||||||
|
Assert.NotNull(_repo.GetTicket("active"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Enqueue_QueuesUsersInOrder()
|
||||||
|
{
|
||||||
|
var ticket1 = new AccessTicket { UserId = "first", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow };
|
||||||
|
var ticket2 = new AccessTicket { UserId = "second", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow };
|
||||||
|
var ticket3 = new AccessTicket { UserId = "third", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow };
|
||||||
|
_repo.Enqueue(ticket1);
|
||||||
|
_repo.Enqueue(ticket2);
|
||||||
|
_repo.Enqueue(ticket3);
|
||||||
|
Assert.Equal(0, _repo.GetRequestsAhead("first"));
|
||||||
|
Assert.Equal(1, _repo.GetRequestsAhead("second"));
|
||||||
|
Assert.Equal(2, _repo.GetRequestsAhead("third"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DidDequeueUntilFull_DequeuesUsersInOrder()
|
||||||
|
{
|
||||||
|
var ticket1 = new AccessTicket { UserId = "first", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow };
|
||||||
|
var ticket2 = new AccessTicket { UserId = "second", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow };
|
||||||
|
var ticket3 = new AccessTicket { UserId = "third", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow };
|
||||||
|
_repo.Enqueue(ticket1);
|
||||||
|
_repo.Enqueue(ticket2);
|
||||||
|
_repo.Enqueue(ticket3);
|
||||||
|
|
||||||
|
bool result = _repo.DidDequeueUntilFull(60 * 60, 60, 1);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.NotNull(_repo.GetTicket("first"));
|
||||||
|
Assert.Null(_repo.GetTicket("second"));
|
||||||
|
Assert.Null(_repo.GetTicket("third"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,9 +7,6 @@ namespace AccessQueueServiceTests
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace AccessQueueService.Tests
|
|
||||||
{
|
|
||||||
public class AccessServiceTests
|
public class AccessServiceTests
|
||||||
{
|
{
|
||||||
const int EXP_SECONDS = 5;
|
const int EXP_SECONDS = 5;
|
||||||
|
@ -18,14 +15,9 @@ namespace AccessQueueServiceTests
|
||||||
const int ACT_MILLIS = 1000 * ACT_SECONDS;
|
const int ACT_MILLIS = 1000 * ACT_SECONDS;
|
||||||
const int CAP_LIMIT = 5;
|
const int CAP_LIMIT = 5;
|
||||||
const int BULK_COUNT = 50000;
|
const int BULK_COUNT = 50000;
|
||||||
private AccessService _accessService;
|
private readonly AccessService _accessService;
|
||||||
public static IEnumerable<object[]> RepoImplementations()
|
|
||||||
{
|
|
||||||
yield return new object[] { new DictionaryAccessQueueRepo() };
|
|
||||||
yield return new object[] { new TakeANumberAccessQueueRepo() };
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateService(IAccessQueueRepo repo)
|
public AccessServiceTests()
|
||||||
{
|
{
|
||||||
var inMemorySettings = new Dictionary<string, string?>
|
var inMemorySettings = new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
|
@ -38,17 +30,15 @@ namespace AccessQueueServiceTests
|
||||||
var configuration = new ConfigurationBuilder()
|
var configuration = new ConfigurationBuilder()
|
||||||
.AddInMemoryCollection(inMemorySettings)
|
.AddInMemoryCollection(inMemorySettings)
|
||||||
.Build();
|
.Build();
|
||||||
|
var accessQueueRepo = new TakeANumberAccessQueueRepo();
|
||||||
|
|
||||||
|
_accessService = new AccessService(configuration, accessQueueRepo);
|
||||||
_accessService = new AccessService(configuration, repo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[MemberData(nameof(RepoImplementations))]
|
public async Task RequestAccess_ShouldGrantAccess_WhenCapacityIsAvailable()
|
||||||
public async Task RequestAccess_ShouldGrantAccess_WhenCapacityIsAvailable(IAccessQueueRepo repo)
|
|
||||||
{
|
{
|
||||||
CreateService(repo);
|
var userId = "user";
|
||||||
var userId = Guid.NewGuid();
|
|
||||||
|
|
||||||
var response = await _accessService.RequestAccess(userId);
|
var response = await _accessService.RequestAccess(userId);
|
||||||
|
|
||||||
|
@ -60,12 +50,10 @@ namespace AccessQueueServiceTests
|
||||||
Assert.Equal(0, _accessService.QueueCount);
|
Assert.Equal(0, _accessService.QueueCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[MemberData(nameof(RepoImplementations))]
|
public async Task RequestAccess_ShouldReturnAccessResponse_WhenUserAlreadyHasTicket()
|
||||||
public async Task RequestAccess_ShouldReturnAccessResponse_WhenUserAlreadyHasTicket(IAccessQueueRepo repo)
|
|
||||||
{
|
{
|
||||||
CreateService(repo);
|
var userId = "user";
|
||||||
var userId = Guid.NewGuid();
|
|
||||||
await _accessService.RequestAccess(userId);
|
await _accessService.RequestAccess(userId);
|
||||||
|
|
||||||
var response = await _accessService.RequestAccess(userId);
|
var response = await _accessService.RequestAccess(userId);
|
||||||
|
@ -78,16 +66,14 @@ namespace AccessQueueServiceTests
|
||||||
Assert.Equal(0, _accessService.QueueCount);
|
Assert.Equal(0, _accessService.QueueCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[MemberData(nameof(RepoImplementations))]
|
public async Task RequestAccess_ShouldQueueUser_WhenCapacityIsFull()
|
||||||
public async Task RequestAccess_ShouldQueueUser_WhenCapacityIsFull(IAccessQueueRepo repo)
|
|
||||||
{
|
{
|
||||||
CreateService(repo);
|
|
||||||
for (int i = 0; i < CAP_LIMIT * 2; i++) // Fill double capacity
|
for (int i = 0; i < CAP_LIMIT * 2; i++) // Fill double capacity
|
||||||
{
|
{
|
||||||
await _accessService.RequestAccess(Guid.NewGuid());
|
await _accessService.RequestAccess(Guid.NewGuid().ToString());
|
||||||
}
|
}
|
||||||
var userId = Guid.NewGuid();
|
var userId = "user";
|
||||||
|
|
||||||
var response = await _accessService.RequestAccess(userId);
|
var response = await _accessService.RequestAccess(userId);
|
||||||
|
|
||||||
|
@ -100,12 +86,10 @@ namespace AccessQueueServiceTests
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[MemberData(nameof(RepoImplementations))]
|
public async Task RevokeAccess_ShouldReturnTrue_WhenUserHasAccess()
|
||||||
public async Task RevokeAccess_ShouldReturnTrue_WhenUserHasAccess(IAccessQueueRepo repo)
|
|
||||||
{
|
{
|
||||||
CreateService(repo);
|
var userId = "user";
|
||||||
var userId = Guid.NewGuid();
|
|
||||||
await _accessService.RequestAccess(userId);
|
await _accessService.RequestAccess(userId);
|
||||||
|
|
||||||
var result = await _accessService.RevokeAccess(userId);
|
var result = await _accessService.RevokeAccess(userId);
|
||||||
|
@ -113,29 +97,25 @@ namespace AccessQueueServiceTests
|
||||||
Assert.True(result);
|
Assert.True(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[MemberData(nameof(RepoImplementations))]
|
public async Task RevokeAccess_ShouldReturnFalse_WhenUserDoesNotHaveAccess()
|
||||||
public async Task RevokeAccess_ShouldReturnFalse_WhenUserDoesNotHaveAccess(IAccessQueueRepo repo)
|
|
||||||
{
|
{
|
||||||
CreateService(repo);
|
var userId = "user";
|
||||||
var userId = Guid.NewGuid();
|
|
||||||
|
|
||||||
var result = await _accessService.RevokeAccess(userId);
|
var result = await _accessService.RevokeAccess(userId);
|
||||||
|
|
||||||
Assert.False(result);
|
Assert.False(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[MemberData(nameof(RepoImplementations))]
|
public async Task RequestAccess_ShouldQueueUser_AfterAccessRevoked()
|
||||||
public async Task RequestAccess_ShouldQueueUser_AfterAccessRevoked(IAccessQueueRepo repo)
|
|
||||||
{
|
{
|
||||||
CreateService(repo);
|
var userId = "user";
|
||||||
var userId = Guid.NewGuid();
|
|
||||||
await _accessService.RequestAccess(userId);
|
await _accessService.RequestAccess(userId);
|
||||||
|
|
||||||
for (int i = 0; i < CAP_LIMIT; i++) // Fill remaining slots
|
for (int i = 0; i < CAP_LIMIT; i++) // Fill remaining slots
|
||||||
{
|
{
|
||||||
await _accessService.RequestAccess(Guid.NewGuid());
|
await _accessService.RequestAccess(Guid.NewGuid().ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await _accessService.RequestAccess(userId); // Request access before revoking
|
var response = await _accessService.RequestAccess(userId); // Request access before revoking
|
||||||
|
@ -148,42 +128,36 @@ namespace AccessQueueServiceTests
|
||||||
Assert.False(responseAfterRevoke.HasAccess);
|
Assert.False(responseAfterRevoke.HasAccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[MemberData(nameof(RepoImplementations))]
|
public async Task RequestAccess_ShouldNotQueueUser_WhenMultipleRequestsForOtherUsersMade()
|
||||||
public async Task RequestAccess_ShouldNotQueueUser_WhenMultipleRequestsForOtherUsersMade(IAccessQueueRepo repo)
|
|
||||||
{
|
{
|
||||||
CreateService(repo);
|
|
||||||
for (int i = 0; i < CAP_LIMIT; i++) // Fill slots without awaiting
|
for (int i = 0; i < CAP_LIMIT; i++) // Fill slots without awaiting
|
||||||
{
|
{
|
||||||
_ = _accessService.RequestAccess(Guid.NewGuid());
|
_ = _accessService.RequestAccess(Guid.NewGuid().ToString());
|
||||||
}
|
}
|
||||||
var response = await _accessService.RequestAccess(Guid.NewGuid()); // Request access before revoking
|
var response = await _accessService.RequestAccess(Guid.NewGuid().ToString()); // Request access before revoking
|
||||||
Assert.NotNull(response);
|
Assert.NotNull(response);
|
||||||
Assert.False(response.HasAccess);
|
Assert.False(response.HasAccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[MemberData(nameof(RepoImplementations))]
|
public async Task RequestAccess_ShouldUpdateExpirationTime_WhenRollingExpirationTrue()
|
||||||
public async Task RequestAccess_ShouldUpdateExpirationTime_WhenRollingExpirationTrue(IAccessQueueRepo repo)
|
|
||||||
{
|
{
|
||||||
CreateService(repo);
|
var userId = "user";
|
||||||
var userId = Guid.NewGuid();
|
|
||||||
var initialResponse = await _accessService.RequestAccess(userId);
|
var initialResponse = await _accessService.RequestAccess(userId);
|
||||||
await Task.Delay(ACT_MILLIS);
|
await Task.Delay(ACT_MILLIS);
|
||||||
var updatedResponse = await _accessService.RequestAccess(userId);
|
var updatedResponse = await _accessService.RequestAccess(userId);
|
||||||
Assert.True(updatedResponse.ExpiresOn > initialResponse.ExpiresOn);
|
Assert.True(updatedResponse.ExpiresOn > initialResponse.ExpiresOn);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[MemberData(nameof(RepoImplementations))]
|
public async Task RequestAccess_ShouldGrantAccess_WhenUsersWithAccessInactive()
|
||||||
public async Task RequestAccess_ShouldGrantAccess_WhenUsersWithAccessInactive(IAccessQueueRepo repo)
|
|
||||||
{
|
{
|
||||||
CreateService(repo);
|
|
||||||
for (int i = 0; i < CAP_LIMIT; i++)
|
for (int i = 0; i < CAP_LIMIT; i++)
|
||||||
{
|
{
|
||||||
await _accessService.RequestAccess(Guid.NewGuid());
|
await _accessService.RequestAccess(Guid.NewGuid().ToString());
|
||||||
}
|
}
|
||||||
var userId = Guid.NewGuid();
|
var userId = "user";
|
||||||
var response = await _accessService.RequestAccess(userId);
|
var response = await _accessService.RequestAccess(userId);
|
||||||
Assert.False(response.HasAccess);
|
Assert.False(response.HasAccess);
|
||||||
await Task.Delay(ACT_MILLIS);
|
await Task.Delay(ACT_MILLIS);
|
||||||
|
@ -191,75 +165,66 @@ namespace AccessQueueServiceTests
|
||||||
Assert.True(response.HasAccess);
|
Assert.True(response.HasAccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[MemberData(nameof(RepoImplementations))]
|
public async Task RequestAccess_ShouldRevokeAccess_WhenExpired()
|
||||||
public async Task RequestAccess_ShouldRevokeAccess_WhenExpired(IAccessQueueRepo repo)
|
|
||||||
{
|
{
|
||||||
CreateService(repo);
|
var userId = "user";
|
||||||
var userId = Guid.NewGuid();
|
|
||||||
var response = await _accessService.RequestAccess(userId);
|
var response = await _accessService.RequestAccess(userId);
|
||||||
Assert.True(response.HasAccess);
|
Assert.True(response.HasAccess);
|
||||||
await Task.Delay(EXP_MILLIS);
|
await Task.Delay(EXP_MILLIS);
|
||||||
for (int i = 0; i < CAP_LIMIT; i++)
|
for (int i = 0; i < CAP_LIMIT; i++)
|
||||||
{
|
{
|
||||||
await _accessService.RequestAccess(Guid.NewGuid());
|
await _accessService.RequestAccess(Guid.NewGuid().ToString());
|
||||||
}
|
}
|
||||||
response = await _accessService.RequestAccess(userId);
|
response = await _accessService.RequestAccess(userId);
|
||||||
Assert.False(response.HasAccess);
|
Assert.False(response.HasAccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[MemberData(nameof(RepoImplementations))]
|
public async Task RequestAccess_ShouldRetailAccess_WhenNotExpired()
|
||||||
public async Task RequestAccess_ShouldRetailAccess_WhenNotExpired(IAccessQueueRepo repo)
|
|
||||||
{
|
{
|
||||||
CreateService(repo);
|
var userId = "user";
|
||||||
var userId = Guid.NewGuid();
|
|
||||||
var response = await _accessService.RequestAccess(userId);
|
var response = await _accessService.RequestAccess(userId);
|
||||||
Assert.True(response.HasAccess);
|
Assert.True(response.HasAccess);
|
||||||
await Task.Delay(ACT_MILLIS);
|
await Task.Delay(ACT_MILLIS);
|
||||||
for (int i = 0; i < CAP_LIMIT; i++)
|
for (int i = 0; i < CAP_LIMIT; i++)
|
||||||
{
|
{
|
||||||
response = await _accessService.RequestAccess(Guid.NewGuid());
|
response = await _accessService.RequestAccess(Guid.NewGuid().ToString());
|
||||||
Assert.True(response.HasAccess);
|
Assert.True(response.HasAccess);
|
||||||
}
|
}
|
||||||
response = await _accessService.RequestAccess(userId);
|
response = await _accessService.RequestAccess(userId);
|
||||||
Assert.True(response.HasAccess);
|
Assert.True(response.HasAccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[MemberData(nameof(RepoImplementations))]
|
public async Task RequestAccess_ShouldProcessBulkRequests()
|
||||||
public async Task RequestAccess_ShouldProcessBulkRequests(IAccessQueueRepo repo)
|
|
||||||
{
|
{
|
||||||
CreateService(repo);
|
var userId = "user";
|
||||||
var userId = Guid.NewGuid();
|
|
||||||
await _accessService.RequestAccess(userId);
|
await _accessService.RequestAccess(userId);
|
||||||
for (int i = 0; i < BULK_COUNT; i++)
|
for (int i = 0; i < BULK_COUNT; i++)
|
||||||
{
|
{
|
||||||
_ = _accessService.RequestAccess(Guid.NewGuid());
|
_ = _accessService.RequestAccess(Guid.NewGuid().ToString());
|
||||||
}
|
}
|
||||||
var response = await _accessService.RequestAccess(userId);
|
var response = await _accessService.RequestAccess(userId);
|
||||||
Assert.NotNull(response);
|
Assert.NotNull(response);
|
||||||
Assert.True(response.HasAccess);
|
Assert.True(response.HasAccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[MemberData(nameof(RepoImplementations))]
|
public async Task RequestAccess_ShouldReportLessInQueue_AsTicketsInactivate()
|
||||||
public async Task RequestAccess_ShouldReportLessInQueue_AsTicketsInactivate(IAccessQueueRepo repo)
|
|
||||||
{
|
{
|
||||||
CreateService(repo);
|
|
||||||
var start = DateTime.UtcNow;
|
var start = DateTime.UtcNow;
|
||||||
for (int i = 0; i < CAP_LIMIT; i++)
|
for (int i = 0; i < CAP_LIMIT; i++)
|
||||||
{
|
{
|
||||||
var elapsed = DateTime.UtcNow - start;
|
var elapsed = DateTime.UtcNow - start;
|
||||||
Console.WriteLine($"Elapsed time: {elapsed.TotalSeconds} s: Adding {i}");
|
await _accessService.RequestAccess(Guid.NewGuid().ToString());
|
||||||
await _accessService.RequestAccess(Guid.NewGuid());
|
|
||||||
await Task.Delay(ACT_MILLIS / CAP_LIMIT);
|
await Task.Delay(ACT_MILLIS / CAP_LIMIT);
|
||||||
}
|
}
|
||||||
var users = new[]
|
var users = new[]
|
||||||
{
|
{
|
||||||
Guid.NewGuid(),
|
"user1",
|
||||||
Guid.NewGuid(),
|
"user2",
|
||||||
Guid.NewGuid()
|
"user3"
|
||||||
};
|
};
|
||||||
|
|
||||||
await _accessService.RequestAccess(users[0]);
|
await _accessService.RequestAccess(users[0]);
|
||||||
|
@ -276,21 +241,35 @@ namespace AccessQueueServiceTests
|
||||||
Assert.Equal(0, response.RequestsAhead);
|
Assert.Equal(0, response.RequestsAhead);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[MemberData(nameof(RepoImplementations))]
|
public async Task RequestAccess_ShouldShowCorrectRequestsAhead_WhenAccessRerequested()
|
||||||
public async Task RequestAccess_ShouldShowCorrectRequestsAhead_WhenAccessRerequested(IAccessQueueRepo repo)
|
|
||||||
{
|
{
|
||||||
CreateService(repo);
|
for (int i = 0; i < CAP_LIMIT; i++)
|
||||||
for (int i = 0; i < CAP_LIMIT + 3; i++)
|
|
||||||
{
|
{
|
||||||
await _accessService.RequestAccess(Guid.NewGuid());
|
await _accessService.RequestAccess(Guid.NewGuid().ToString());
|
||||||
}
|
|
||||||
var id = Guid.NewGuid();
|
|
||||||
var response = await _accessService.RequestAccess(id);
|
|
||||||
Assert.Equal(3, response.RequestsAhead);
|
|
||||||
response = await _accessService.RequestAccess(id);
|
|
||||||
Assert.Equal(3, response?.RequestsAhead);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var id1 = "user1";
|
||||||
|
var id2 = "user2";
|
||||||
|
var id3 = "user3";
|
||||||
|
|
||||||
|
var response1 = await _accessService.RequestAccess(id1);
|
||||||
|
var response2 = await _accessService.RequestAccess(id2);
|
||||||
|
var response3 = await _accessService.RequestAccess(id3);
|
||||||
|
|
||||||
|
Assert.Equal(0, response1.RequestsAhead);
|
||||||
|
Assert.Equal(1, response2.RequestsAhead);
|
||||||
|
Assert.Equal(2, response3.RequestsAhead);
|
||||||
|
|
||||||
|
response1 = await _accessService.RequestAccess(id1);
|
||||||
|
response2 = await _accessService.RequestAccess(id2);
|
||||||
|
response3 = await _accessService.RequestAccess(id3);
|
||||||
|
|
||||||
|
Assert.Equal(0, response1.RequestsAhead);
|
||||||
|
Assert.Equal(1, response2.RequestsAhead);
|
||||||
|
Assert.Equal(2, response3.RequestsAhead);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue