feat(proj): init
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user