diff --git a/AccessQueueService.sln b/AccessQueueService.sln
new file mode 100644
index 0000000..a3ecab9
--- /dev/null
+++ b/AccessQueueService.sln
@@ -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
diff --git a/AccessQueueService/AccessQueueService.csproj b/AccessQueueService/AccessQueueService.csproj
new file mode 100644
index 0000000..5419ef0
--- /dev/null
+++ b/AccessQueueService/AccessQueueService.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/AccessQueueService/AccessQueueService.http b/AccessQueueService/AccessQueueService.http
new file mode 100644
index 0000000..499b4c0
--- /dev/null
+++ b/AccessQueueService/AccessQueueService.http
@@ -0,0 +1,6 @@
+@AccessQueueService_HostAddress = http://localhost:5199
+
+GET {{AccessQueueService_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/AccessQueueService/Controllers/AccessController.cs b/AccessQueueService/Controllers/AccessController.cs
new file mode 100644
index 0000000..fdc63c4
--- /dev/null
+++ b/AccessQueueService/Controllers/AccessController.cs
@@ -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 Get(Guid id)
+ {
+ return await _accessService.RequestAccess(id);
+ }
+
+ [HttpDelete]
+ [Route("{id}")]
+ public async Task Delete(Guid id)
+ {
+ return await _accessService.RevokeAccess(id);
+ }
+ }
+}
diff --git a/AccessQueueService/Models/AccessResponse.cs b/AccessQueueService/Models/AccessResponse.cs
new file mode 100644
index 0000000..101523a
--- /dev/null
+++ b/AccessQueueService/Models/AccessResponse.cs
@@ -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;
+ }
+ }
+ }
+}
diff --git a/AccessQueueService/Models/AccessTicket.cs b/AccessQueueService/Models/AccessTicket.cs
new file mode 100644
index 0000000..faf99e4
--- /dev/null
+++ b/AccessQueueService/Models/AccessTicket.cs
@@ -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; }
+ }
+}
diff --git a/AccessQueueService/Program.cs b/AccessQueueService/Program.cs
new file mode 100644
index 0000000..700d2ab
--- /dev/null
+++ b/AccessQueueService/Program.cs
@@ -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();
+
+
+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();
diff --git a/AccessQueueService/Properties/launchSettings.json b/AccessQueueService/Properties/launchSettings.json
new file mode 100644
index 0000000..b49fa74
--- /dev/null
+++ b/AccessQueueService/Properties/launchSettings.json
@@ -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"
+ }
+ }
+ }
+}
diff --git a/AccessQueueService/Services/AccessService.cs b/AccessQueueService/Services/AccessService.cs
new file mode 100644
index 0000000..dd9b042
--- /dev/null
+++ b/AccessQueueService/Services/AccessService.cs
@@ -0,0 +1,162 @@
+using AccessQueueService.Models;
+
+namespace AccessQueueService.Services
+{
+ public class AccessService : IAccessService
+ {
+ private readonly Dictionary _accessTickets = new();
+ private readonly Queue _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("AccessQueue:ExpirationSeconds");
+ ACT_SECONDS = _configuration.GetValue("AccessQueue:ActivitySeconds");
+ CAPACITY_LIMIT = _configuration.GetValue("AccessQueue:CapacityLimit");
+ ROLLING_EXPIRATION = _configuration.GetValue("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("AccessQueue:ActivitySeconds")));
+ public int QueueCount => _accessQueue.Count;
+ public async Task 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 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;
+ }
+ }
+}
diff --git a/AccessQueueService/Services/IAccessService.cs b/AccessQueueService/Services/IAccessService.cs
new file mode 100644
index 0000000..c11da36
--- /dev/null
+++ b/AccessQueueService/Services/IAccessService.cs
@@ -0,0 +1,11 @@
+using AccessQueueService.Models;
+
+namespace AccessQueueService.Services
+{
+ public interface IAccessService
+ {
+ public Task RequestAccess(Guid userId);
+ public Task RevokeAccess(Guid userId);
+ public int DeleteExpiredTickets();
+ }
+}
diff --git a/AccessQueueService/WeatherForecast.cs b/AccessQueueService/WeatherForecast.cs
new file mode 100644
index 0000000..d23d200
--- /dev/null
+++ b/AccessQueueService/WeatherForecast.cs
@@ -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; }
+ }
+}
diff --git a/AccessQueueService/appsettings.Development.json b/AccessQueueService/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/AccessQueueService/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/AccessQueueService/appsettings.json b/AccessQueueService/appsettings.json
new file mode 100644
index 0000000..6ba2637
--- /dev/null
+++ b/AccessQueueService/appsettings.json
@@ -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": "*"
+}
diff --git a/AccessQueueServiceTests/AccessQueueServiceTests.csproj b/AccessQueueServiceTests/AccessQueueServiceTests.csproj
new file mode 100644
index 0000000..a1b7476
--- /dev/null
+++ b/AccessQueueServiceTests/AccessQueueServiceTests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AccessQueueServiceTests/AccessServiceTests.cs b/AccessQueueServiceTests/AccessServiceTests.cs
new file mode 100644
index 0000000..c97d654
--- /dev/null
+++ b/AccessQueueServiceTests/AccessServiceTests.cs
@@ -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
+ {
+ { "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);
+ }
+
+ }
+ }
+}
\ No newline at end of file