Files
WebDrop/server/tests/WebDrop.Core.Tests/PrimitivesTests.cs
T
justme c858bd2a09 feat(server)!: require a logged-in account for every channel request
Drop unauthenticated guests from channels: the allow_guests setting, the
/guests endpoints and the guest seat table are gone, and requests without an
account session get 401 account_required. Share links are unaffected.
Contract version is now 8.0.0.
2026-10-16 06:46:15 +08:00

176 lines
6.6 KiB
C#

using System.Buffers.Binary;
using WebDrop.Core;
namespace WebDrop.Core.Tests;
public class Id16Tests
{
[Fact]
public void RoundTrips()
{
var id = TestEnv.NewId();
Assert.True(Id16.TryParse(id.ToBase64Url(), out var parsed));
Assert.Equal(id, parsed);
Assert.Equal(22, id.ToBase64Url().Length);
Assert.Equal(32, id.ToHex().Length);
}
[Theory]
[InlineData("")]
[InlineData("short")]
[InlineData("AAAAAAAAAAAAAAAAAAAAAAA")] // 23 字符
[InlineData("AAAAAAAAAAAAAAAAAAAA+/")] // 非 url 字母表
public void RejectsMalformed(string text) => Assert.False(Id16.TryParse(text, out _));
[Fact]
public void RejectsNonCanonicalEncoding()
{
// 末字符的低 2 位在 16 字节编码中必须为 0;AAAAAAAAAAAAAAAAAAAAAB 解码后与 ...AA 同值
Assert.True(Id16.TryParse("AAAAAAAAAAAAAAAAAAAAAA", out var zero));
Assert.False(Id16.TryParse("AAAAAAAAAAAAAAAAAAAAAB", out _));
Assert.Equal(new byte[16], zero.ToArray());
}
}
public class EnvelopeTests
{
[Fact]
public void ParsesValidHeader()
{
var item = TestEnv.NewId();
var bytes = TestEnv.Frame(item, 7, FrameFlags.Final | FrameFlags.Gated | FrameFlags.Enc, 1040);
Assert.True(Envelope.TryParse(bytes, out var env));
Assert.Equal(item, env.ItemId);
Assert.Equal(7u, env.FrameIndex);
Assert.Equal(1040, env.PayloadLength);
Assert.True(env.IsFinal && env.IsGated && env.IsEnc && !env.IsSnapshot);
Assert.Equal(FrameFlags.Gated, env.ItemFlags);
}
[Fact]
public void RejectsBadVersion()
{
var bytes = TestEnv.Frame(TestEnv.NewId(), 0, FrameFlags.Final, 4);
bytes[0] = 2;
Assert.False(Envelope.TryParse(bytes, out _));
}
[Fact]
public void RejectsReservedFlagBits()
{
var bytes = TestEnv.Frame(TestEnv.NewId(), 0, FrameFlags.Final, 4);
bytes[1] |= 0x20;
Assert.False(Envelope.TryParse(bytes, out _));
}
[Fact]
public void RejectsLengthMismatch()
{
var bytes = TestEnv.Frame(TestEnv.NewId(), 0, FrameFlags.Final, 8);
Assert.False(Envelope.TryParse(bytes.AsSpan(0, bytes.Length - 1), out _));
Assert.False(Envelope.TryParse([.. bytes, 0], out _));
Assert.False(Envelope.TryParse(bytes.AsSpan(0, 25), out _));
}
[Fact]
public void RejectsHugePayloadLength()
{
var bytes = TestEnv.Frame(TestEnv.NewId(), 0, FrameFlags.Final, 4);
BinaryPrimitives.WriteUInt32BigEndian(bytes.AsSpan(22), uint.MaxValue);
Assert.False(Envelope.TryParse(bytes, out _));
}
[Theory]
[InlineData(1024 + 16, true)]
[InlineData(4096 + 16, true)]
[InlineData(1048576 + 16, true)]
[InlineData(1024, false)]
[InlineData(16, false)]
[InlineData(1000 + 16, false)]
[InlineData(0, false)]
public void EncPayloadMustBeTierPlusTag(int length, bool valid) =>
Assert.Equal(valid, PayloadTiers.IsValidEncPayloadLength(length));
}
public class AuthTests
{
[Fact]
public void TokenHashMatchesOnlyTheSameToken()
{
var token = TestEnv.Random(32);
var hash = TokenHash.Compute(token);
Assert.True(TokenHash.Matches(token, hash));
Assert.False(TokenHash.Matches(TestEnv.Random(32), hash));
Assert.False(TokenHash.Matches(token, null));
Assert.False(TokenHash.Matches(token, hash[..16]));
}
}
public class PolicyTests
{
[Fact]
public void AuthenticatedAuthorsCanManageOwnItemsAcrossDevices()
{
var first = TestEnv.Endpoint(); var second = TestEnv.Endpoint();
var item = ItemBy(first) with { AuthorAccount = "member" };
var channel = Chan(new ChannelSettings { DeletePolicy = DeletePolicy.Own });
Assert.True(ChannelPolicy.CanDeleteItem(channel, item, new Principal(Role.Read, "member")));
Assert.False(ChannelPolicy.CanDeleteItem(channel, item, new Principal(Role.Read, "other-account")));
}
private static Channel Chan(ChannelSettings s, bool writeHash = true) => new(
TestEnv.NewId(), ChannelMode.Enc, s, new byte[32], writeHash ? new byte[32] : null, new byte[32], 0, 0, 0, 0, 0, "owner");
private static Item ItemBy(byte[] author) => new(
TestEnv.NewId(), TestEnv.NewId(), author, FrameFlags.None, 0, 0, 0, -1, null, 0, 0, 0, null, null);
[Fact]
public void WritePermission()
{
var ep = TestEnv.Endpoint();
Assert.True(ChannelPolicy.CanWrite(Chan(new()), new Principal(Role.Owner, "someone")));
Assert.True(ChannelPolicy.CanWrite(Chan(new()), new Principal(Role.Write, "someone")));
Assert.False(ChannelPolicy.CanWrite(Chan(new()), new Principal(Role.Read, "someone")));
Assert.False(ChannelPolicy.CanWrite(Chan(new() { MembersCanUpload = false }), new Principal(Role.Write, "someone")));
Assert.False(ChannelPolicy.CanWrite(Chan(new(), writeHash: false), new Principal(Role.Write, "someone")));
Assert.True(ChannelPolicy.CanWrite(Chan(new() { MembersCanUpload = false }), new Principal(Role.Owner, "someone")));
}
[Theory]
[InlineData(DeletePolicy.OwnerOnly, Role.Write, true, false)]
[InlineData(DeletePolicy.OwnerOnly, Role.Owner, false, true)]
[InlineData(DeletePolicy.Own, Role.Write, true, true)]
[InlineData(DeletePolicy.Own, Role.Write, false, false)]
[InlineData(DeletePolicy.Own, Role.Owner, false, true)]
[InlineData(DeletePolicy.Anyone, Role.Read, false, true)]
public void DeletePermission(DeletePolicy policy, Role role, bool isAuthor, bool expected)
{
var item = ItemBy(TestEnv.Endpoint()) with { AuthorAccount = isAuthor ? "me" : "someone-else" };
var channel = Chan(new() { DeletePolicy = policy });
Assert.Equal(expected, ChannelPolicy.CanDeleteItem(channel, item, new Principal(role, "me")));
}
[Fact]
public void SettingsPatchOnlyChangesProvidedFields()
{
var before = new ChannelSettings { MaxMembers = 2, TtlSeconds = 100 };
var after = new ChannelSettingsPatch { MaxMembers = 5 }.ApplyTo(before);
Assert.Equal(5, after.MaxMembers);
Assert.Equal(100, after.TtlSeconds);
}
[Theory]
[InlineData(0, 86400, 10)]
[InlineData(999, 86400, 10)]
[InlineData(2, 10, 10)]
[InlineData(2, 99999999, 10)]
[InlineData(2, 86400, 0)]
public void SettingsValidationRejectsOutOfRange(int seats, int ttl, int maxItems)
{
var s = new ChannelSettings { MaxMembers = seats, TtlSeconds = ttl, MaxItems = maxItems };
var ex = Assert.Throws<WebDropException>(() => s.Validate(new ServerLimits()));
Assert.Equal(ErrorCode.BadRequest, ex.Code);
}
}