using System; using System.Collections.Generic; using System.Linq; using System.Net.Sockets; using System.Text; using System.Text.Json; 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; private readonly AccessQueueConfig _simpleConfig = new() { ExpirationSeconds = 60, ActivitySeconds = 60, CapacityLimit = 1, RollingExpiration = false, CacheMilliseconds = null }; private readonly AccessQueueConfig _configWithCache = new() { ExpirationSeconds = 60, ActivitySeconds = 60, CapacityLimit = 1, RollingExpiration = false, CacheMilliseconds = 100 }; private AccessQueueConfig SimpleConfigWithCapacity(int capacity) => new() { ExpirationSeconds = 60, ActivitySeconds = 60, CapacityLimit = capacity, RollingExpiration = false, CacheMilliseconds = null }; public AccessQueueRepoTests() { _repo = new TakeANumberAccessQueueRepo(); } private List GetSimpleTicketList() => new() { new() { UserId = "first", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow }, new() { UserId = "second", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow }, new() { UserId = "third", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow } }; [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(_simpleConfig); 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(SimpleConfigWithCapacity(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(_simpleConfig); Assert.True(result); Assert.Null(_repo.GetTicket("inactive")); Assert.NotNull(_repo.GetTicket("active")); } [Fact] public void Enqueue_QueuesUsersInOrder() { foreach (var ticket in GetSimpleTicketList()) { _repo.Enqueue(ticket); } Assert.Equal(0, _repo.GetRequestsAhead("first")); Assert.Equal(1, _repo.GetRequestsAhead("second")); Assert.Equal(2, _repo.GetRequestsAhead("third")); } [Fact] public void DidDequeueUntilFull_DequeuesUsersInOrder() { foreach (var ticket in GetSimpleTicketList()) { _repo.Enqueue(ticket); } bool result = _repo.DidDequeueUntilFull(_simpleConfig); Assert.True(result); Assert.NotNull(_repo.GetTicket("first")); Assert.Null(_repo.GetTicket("second")); Assert.Null(_repo.GetTicket("third")); } [Fact] public void DidDequeuedUntilFull_CachesActiveTickets_WhenCacheMillisSet() { foreach (var ticket in GetSimpleTicketList()) { _repo.Enqueue(ticket); } Assert.True(_repo.DidDequeueUntilFull(_configWithCache)); Assert.NotNull(_repo.GetTicket("first")); Assert.Null(_repo.GetTicket("second")); Assert.Null(_repo.GetTicket("third")); Assert.True(_repo.DidDequeueUntilFull(_configWithCache)); Assert.Null(_repo.GetTicket("second")); Assert.Null(_repo.GetTicket("third")); } [Fact] public void Optimize_MaintainsQueueOrder() { foreach (var ticket in GetSimpleTicketList()) { _repo.Enqueue(ticket); } _repo.DidDequeueUntilFull(_simpleConfig); _repo.Optimize(); Assert.NotNull(_repo.GetTicket("first")); Assert.Equal(0, _repo.GetRequestsAhead("second")); Assert.Equal(1, _repo.GetRequestsAhead("third")); Assert.Equal(0ul, _repo._nowServing); _repo.DidDequeueUntilFull(SimpleConfigWithCapacity(2)); Assert.NotNull(_repo.GetTicket("second")); Assert.Equal(0, _repo.GetRequestsAhead("third")); } [Fact] public void Enqueue_MaintainsQueueOrder_WhenMaxValueExceeded() { _repo._nowServing = long.MaxValue - 1; _repo._nextUnusedTicket = long.MaxValue - 1; foreach (var ticket in GetSimpleTicketList()) { _repo.Enqueue(ticket); } Assert.Equal(0ul, _repo._nowServing); Assert.Equal(3ul, _repo._nextUnusedTicket); Assert.Equal(0, _repo.GetRequestsAhead("first")); Assert.Equal(2, _repo.GetRequestsAhead("third")); } [Fact] public void ToState_ReturnsAccurateJson() { var ticketWithAccess = new AccessTicket { UserId = "access", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow }; var ticketWithoutAccess = new AccessTicket { UserId = "noAccess", LastActive = DateTime.UtcNow }; _repo.UpsertTicket(ticketWithAccess); _repo.Enqueue(ticketWithoutAccess); string stateJson = _repo.ToState(); var state = JsonSerializer.Deserialize(stateJson); Assert.NotNull(state?.AccessQueue); Assert.NotNull(state?.AccessTickets); Assert.Single(state!.AccessTickets); Assert.Single(state!.AccessQueue); Assert.Equal(ticketWithAccess.UserId, state.AccessTickets.First().Key); Assert.Equal(ticketWithAccess.ExpiresOn, state.AccessTickets.First().Value.ExpiresOn); Assert.Equal(ticketWithAccess.LastActive, state.AccessTickets.First().Value.LastActive); Assert.Equal(ticketWithoutAccess.UserId, state.AccessQueue.First().Value.UserId); Assert.Equal(ticketWithoutAccess.LastActive, state.AccessQueue.First().Value.LastActive); } [Fact] public void FromState_DeserializesJsonCorrectly() { var ticketWithAccess = new AccessTicket { UserId = "access", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow }; var ticketWithoutAccess = new AccessTicket { UserId = "noAccess", LastActive = DateTime.UtcNow }; _repo.UpsertTicket(ticketWithAccess); _repo.Enqueue(ticketWithoutAccess); string stateJson = _repo.ToState(); var deserializedRepo = TakeANumberAccessQueueRepo.FromState(stateJson); Assert.Equal(1, deserializedRepo.GetUnexpiredTicketsCount()); Assert.Equal(1, deserializedRepo.GetQueueCount()); Assert.Equal(deserializedRepo.GetTicket("access")!.ExpiresOn, ticketWithAccess.ExpiresOn); Assert.Null(deserializedRepo.GetTicket("noAccess")); Assert.Equal(0, deserializedRepo.GetRequestsAhead("noAccess")); } } }