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.
176 lines
6.6 KiB
C#
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);
|
|
}
|
|
}
|