using TileServer.Application.Accounts; using TileServer.Domain; using TileServer.Domain.Accounts; using TileServer.Domain.Exceptions; namespace TileServer.UnitTests; public sealed class AccountSlugTests { [Theory] [InlineData("alice", "alice")] [InlineData("Alice.K", "alice-k")] [InlineData("user_name", "user-name")] [InlineData("42go", "u-42go")] public void FromLogin_ProducesResourceName(string login, string expected) { var slug = AccountSlug.FromLogin(login); Assert.Equal(expected, slug); Assert.True(ResourceName.IsValid(slug)); } [Fact] public void WithSuffix_StaysValid() { var slug = AccountSlug.WithSuffix("alice", 2); Assert.Equal("alice-2", slug); Assert.True(ResourceName.IsValid(slug)); } } public sealed class ApiTokenHashTests { [Fact] public void Compute_Verify_RoundTrip() { var plaintext = ApiTokenHash.CreatePlaintext(); Assert.StartsWith("ts_", plaintext); var hash = ApiTokenHash.Compute(plaintext); Assert.True(ApiTokenHash.Verify(plaintext, hash)); Assert.False(ApiTokenHash.Verify(plaintext + "x", hash)); Assert.Equal(plaintext[..11], ApiTokenHash.DisplayPrefix(plaintext)); } } public sealed class ApiTokenServiceTests { [Fact] public async Task Resolve_RejectsRevokedToken() { var user = new User { Id = Guid.NewGuid(), YandexId = "1", Login = "alice", DisplayName = "Alice", Slug = "alice" }; var plaintext = ApiTokenHash.CreatePlaintext(); var token = new ApiToken { Id = Guid.NewGuid(), UserId = user.Id, Name = "test", Hash = ApiTokenHash.Compute(plaintext), Prefix = ApiTokenHash.DisplayPrefix(plaintext), CreatedAt = DateTimeOffset.UtcNow, RevokedAt = DateTimeOffset.UtcNow }; var service = new ApiTokenService(new MemoryUsers(user), new MemoryTokens(token)); var error = await Assert.ThrowsAsync( () => service.ResolveAsync("alice", plaintext, null, CancellationToken.None)); Assert.Contains("revoked", error.Message, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task Resolve_AcceptsActiveTokenForSlug() { var user = new User { Id = Guid.NewGuid(), YandexId = "1", Login = "alice", DisplayName = "Alice", Slug = "alice" }; var plaintext = ApiTokenHash.CreatePlaintext(); var token = new ApiToken { Id = Guid.NewGuid(), UserId = user.Id, Name = "test", Hash = ApiTokenHash.Compute(plaintext), Prefix = ApiTokenHash.DisplayPrefix(plaintext), CreatedAt = DateTimeOffset.UtcNow }; var service = new ApiTokenService(new MemoryUsers(user), new MemoryTokens(token)); var access = await service.ResolveAsync("alice", null, plaintext, CancellationToken.None); Assert.Equal(user.Id, access.User.Id); Assert.Equal(token.Id, access.Token.Id); } private sealed class MemoryUsers(User user) : IUserRepository { public Task FindByIdAsync(Guid id, CancellationToken ct) => Task.FromResult(id == user.Id ? user : null); public Task FindByYandexIdAsync(string yandexId, CancellationToken ct) => Task.FromResult(yandexId == user.YandexId ? user : null); public Task FindBySlugAsync(string slug, CancellationToken ct) => Task.FromResult(slug == user.Slug ? user : null); public Task SlugExistsAsync(string slug, Guid? exceptUserId, CancellationToken ct) => Task.FromResult(slug == user.Slug && exceptUserId != user.Id); public Task AddAsync(User incoming, CancellationToken ct) => Task.CompletedTask; public Task SaveChangesAsync(CancellationToken ct) => Task.CompletedTask; } private sealed class MemoryTokens(ApiToken token) : IApiTokenRepository { public Task> ListByUserAsync(Guid userId, CancellationToken ct) => Task.FromResult>(token.UserId == userId ? [token] : []); public Task FindByIdAsync(Guid userId, Guid tokenId, CancellationToken ct) => Task.FromResult(token.UserId == userId && token.Id == tokenId ? token : null); public Task FindByHashAsync(string hash, CancellationToken ct) => Task.FromResult(token.Hash == hash ? token : null); public Task CountActiveAsync(Guid userId, CancellationToken ct) => Task.FromResult(token.UserId == userId && !token.IsRevoked ? 1 : 0); public Task AddAsync(ApiToken incoming, CancellationToken ct) => Task.CompletedTask; public Task SaveChangesAsync(CancellationToken ct) => Task.CompletedTask; } } public sealed class UsageAccumulatorTests { [Fact] public void Drain_SumsBatchedIncrements() { var userId = Guid.NewGuid(); var tokenId = Guid.NewGuid(); var acc = new UsageAccumulator(); acc.RecordTile(userId, tokenId, 100); acc.RecordTile(userId, tokenId, 40); acc.RecordStyle(userId, tokenId, 20); var deltas = acc.Drain(); var row = Assert.Single(deltas); Assert.Equal(userId, row.UserId); Assert.Equal(tokenId, row.TokenId); Assert.Equal(2, row.TileRequests); Assert.Equal(1, row.StyleRequests); Assert.Equal(160, row.Bytes); Assert.Empty(acc.Drain()); } }