124 lines
4.0 KiB
C#
124 lines
4.0 KiB
C#
using Fleet.Core;
|
|
|
|
namespace Fleet.Tests;
|
|
|
|
public sealed class CoreTests
|
|
{
|
|
[Theory]
|
|
[InlineData(0UL)]
|
|
[InlineData(1UL)]
|
|
[InlineData(127UL)]
|
|
[InlineData(128UL)]
|
|
[InlineData(300UL)]
|
|
[InlineData(ulong.MaxValue)]
|
|
public void VarInt_RoundTrips(ulong value)
|
|
{
|
|
Span<byte> buffer = stackalloc byte[VarInt.MaxBytes];
|
|
int written = VarInt.Write(buffer, value);
|
|
Assert.True(VarInt.TryRead(buffer[..written], out ulong read, out int consumed));
|
|
Assert.Equal(value, read);
|
|
Assert.Equal(written, consumed);
|
|
}
|
|
|
|
[Fact]
|
|
public void VarInt_RejectsTruncatedAndOverlongInput()
|
|
{
|
|
Assert.False(VarInt.TryRead([0x80], out _, out _));
|
|
Assert.False(VarInt.TryRead([], out _, out _));
|
|
byte[] overlong = [.. Enumerable.Repeat((byte)0x80, 11), 0x01];
|
|
Assert.False(VarInt.TryRead(overlong, out _, out _));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0L)]
|
|
[InlineData(-1L)]
|
|
[InlineData(1L)]
|
|
[InlineData(long.MinValue)]
|
|
[InlineData(long.MaxValue)]
|
|
public void ZigZag_RoundTrips_AndKeepsSmallMagnitudesSmall(long value)
|
|
{
|
|
Assert.Equal(value, VarInt.UnZigZag(VarInt.ZigZag(value)));
|
|
if (value is >= -63 and <= 63)
|
|
{
|
|
Assert.True(VarInt.ZigZag(value) < 128);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Ulid_IsTwentySixCharacters_SortsByTime_AndDoesNotRepeat()
|
|
{
|
|
var time = new FakeClock(new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
|
string a = Ulid.New(time);
|
|
time.Advance(TimeSpan.FromMilliseconds(5));
|
|
string b = Ulid.New(time);
|
|
Assert.Equal(26, a.Length);
|
|
Assert.True(string.CompareOrdinal(a, b) < 0);
|
|
Assert.Equal(1000, Enumerable.Range(0, 1000).Select(_ => Ulid.New(time)).Distinct().Count());
|
|
Assert.StartsWith("agt_", Ulid.NewPrefixed("agt", time));
|
|
}
|
|
|
|
[Fact]
|
|
public void Tokens_AreHighEntropy_AndOnlyTheHashIsComparable()
|
|
{
|
|
string a = TokenHash.NewToken("jt");
|
|
string b = TokenHash.NewToken("jt");
|
|
Assert.NotEqual(a, b);
|
|
Assert.StartsWith("jt_", a);
|
|
Assert.Equal(3 + 43, a.Length);
|
|
Assert.True(TokenHash.FixedTimeEquals(TokenHash.Sha256(a), TokenHash.Sha256(a)));
|
|
Assert.False(TokenHash.FixedTimeEquals(TokenHash.Sha256(a), TokenHash.Sha256(b)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Base64Url_RoundTrips_AndRejectsGarbage()
|
|
{
|
|
byte[] data = [0xFB, 0xFF, 0xFE, 0x00, 0x01];
|
|
string text = Base64Url.Encode(data);
|
|
Assert.DoesNotContain('+', text);
|
|
Assert.DoesNotContain('/', text);
|
|
Assert.DoesNotContain('=', text);
|
|
Assert.True(Base64Url.TryDecode(text, out byte[] back));
|
|
Assert.Equal(data, back);
|
|
Assert.False(Base64Url.TryDecode("***", out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void Result_CarriesEitherAValueOrAnError_AndRefusesTheWrongAccess()
|
|
{
|
|
Result<int> ok = Result<int>.Ok(5);
|
|
Result<int> failed = Result<int>.Fail(FleetErrors.NotFound, "missing");
|
|
Assert.True(ok.IsSuccess);
|
|
Assert.Equal(10, ok.Map(v => v * 2).Value);
|
|
Assert.False(failed.IsSuccess);
|
|
Assert.Equal(FleetErrors.NotFound, failed.Error.Code);
|
|
Assert.Throws<InvalidOperationException>(() => failed.Value);
|
|
Assert.Throws<InvalidOperationException>(() => ok.Error);
|
|
Assert.Equal("fleet_not_found", failed.Map(v => v).Error.Code.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void FrameworkErrorCodes_ArePrefixedAndSnakeCase()
|
|
{
|
|
foreach (System.Reflection.FieldInfo field in typeof(FleetErrors).GetFields())
|
|
{
|
|
string value = ((ErrorCode)field.GetValue(null)!).Value;
|
|
Assert.StartsWith("fleet_", value);
|
|
Assert.Matches("^[a-z]+(_[a-z]+)*$", value);
|
|
}
|
|
}
|
|
|
|
private sealed class FakeClock : TimeProvider
|
|
{
|
|
private DateTimeOffset _now;
|
|
|
|
public FakeClock(DateTimeOffset now)
|
|
{
|
|
_now = now;
|
|
}
|
|
|
|
public override DateTimeOffset GetUtcNow() => _now;
|
|
|
|
public void Advance(TimeSpan by) => _now += by;
|
|
}
|
|
}
|