Files
2026-09-21 03:53:15 +03:00

165 lines
5.6 KiB
C#

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<UnauthenticatedException>(
() => 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<User?> FindByIdAsync(Guid id, CancellationToken ct)
=> Task.FromResult(id == user.Id ? user : null);
public Task<User?> FindByYandexIdAsync(string yandexId, CancellationToken ct)
=> Task.FromResult(yandexId == user.YandexId ? user : null);
public Task<User?> FindBySlugAsync(string slug, CancellationToken ct)
=> Task.FromResult(slug == user.Slug ? user : null);
public Task<bool> 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<IReadOnlyList<ApiToken>> ListByUserAsync(Guid userId, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<ApiToken>>(token.UserId == userId ? [token] : []);
public Task<ApiToken?> FindByIdAsync(Guid userId, Guid tokenId, CancellationToken ct)
=> Task.FromResult(token.UserId == userId && token.Id == tokenId ? token : null);
public Task<ApiToken?> FindByHashAsync(string hash, CancellationToken ct)
=> Task.FromResult(token.Hash == hash ? token : null);
public Task<int> 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());
}
}