feat(api): add account workspaces and signed content events

This commit is contained in:
justme committed 2026-10-14 04:05:30 +08:00
1 parent 937b774a93
commit 7b8d61d1ba
63 files changed
+5407 -370

No files matched your search

@@ -41,6 +41,7 @@ public class ChannelServiceTests : IDisposable
var dup = new CreateChannelRequest
{
Id = ch.Id.ToBase64Url(), Mode = ChannelMode.Enc,
Settings = new ChannelSettings { Name = "Duplicate" },
ReadHash = Codec.Encode(TokenHash.Compute(ch.ReadToken)),
OwnerHash = Codec.Encode(TokenHash.Compute(ch.OwnerToken)),
};
@@ -51,9 +52,9 @@ public class ChannelServiceTests : IDisposable
public void CreateEnforcesModeSpecificHashes()
{
string h() => Codec.Encode(TokenHash.Compute(TestEnv.Random(32)));
var rawWithRead = new CreateChannelRequest { Id = TestEnv.NewId().ToBase64Url(), Mode = ChannelMode.Raw, ReadHash = h(), OwnerHash = h() };
var encNoRead = new CreateChannelRequest { Id = TestEnv.NewId().ToBase64Url(), Mode = ChannelMode.Enc, OwnerHash = h() };
var noOwner = new CreateChannelRequest { Id = TestEnv.NewId().ToBase64Url(), Mode = ChannelMode.Raw };
var rawWithRead = new CreateChannelRequest { Id = TestEnv.NewId().ToBase64Url(), Mode = ChannelMode.Raw, Settings = new ChannelSettings { Name = "Raw space" }, ReadHash = h(), OwnerHash = h() };
var encNoRead = new CreateChannelRequest { Id = TestEnv.NewId().ToBase64Url(), Mode = ChannelMode.Enc, Settings = new ChannelSettings { Name = "Enc space" }, OwnerHash = h() };
var noOwner = new CreateChannelRequest { Id = TestEnv.NewId().ToBase64Url(), Mode = ChannelMode.Raw, Settings = new ChannelSettings { Name = "No owner space" } };
AssertCode(ErrorCode.BadRequest, () => Svc.CreateChannel(rawWithRead, _ownerEp, "owner"));
AssertCode(ErrorCode.BadRequest, () => Svc.CreateChannel(encNoRead, _ownerEp, "owner"));
@@ -106,6 +107,32 @@ public class ChannelServiceTests : IDisposable
Assert.Equal(1, _env.Events.Seats); // 只有真正新增时才通知
}
[Fact]
public void AccountMembersUseOneLimitAcrossUnlimitedDevices()
{
var ch = _env.Create(_ownerEp, settings: new ChannelSettings { Seats = 2 });
var memberOne = TestEnv.Endpoint();
var memberTwo = TestEnv.Endpoint();
var ownerTwo = TestEnv.Endpoint();
Credentials alice(byte[] endpoint, string device) => new(Role.Read, ch.ReadToken, endpoint, "alice", device);
Assert.Equal(2, Svc.Join(ch.Id, alice(memberOne, "alice-1")).SeatsUsed);
Assert.Equal(2, Svc.Join(ch.Id, alice(memberTwo, "alice-2")).SeatsUsed);
Assert.Equal(2, Svc.Join(ch.Id, ch.Owner(ownerTwo)).SeatsUsed);
Assert.Equal(4, Svc.ListSeats(ch.Id, ch.Owner(_ownerEp)).Count); // 设备仍单独登记,但不计入成员人数
var bob = new Credentials(Role.Read, ch.ReadToken, TestEnv.Endpoint(), "bob", "bob-1");
AssertCode(ErrorCode.SeatsFull, () => Svc.Join(ch.Id, bob));
Svc.ReleaseSeat(ch.Id, ch.Owner(_ownerEp), memberOne);
Assert.Equal(2, Svc.GetChannel(ch.Id, ch.Owner(_ownerEp)).SeatsUsed); // 移除一台设备不会释放账号成员名额
AssertCode(ErrorCode.SeatsFull, () => Svc.Join(ch.Id, bob with { EndpointHash = TestEnv.Endpoint() }));
Svc.RevokeInvitation(ch.Id, ch.Owner(_ownerEp), "alice");
Assert.Equal(1, Svc.GetChannel(ch.Id, ch.Owner(_ownerEp)).SeatsUsed);
Assert.Equal(2, Svc.Join(ch.Id, bob with { EndpointHash = TestEnv.Endpoint() }).SeatsUsed);
}
[Fact]
public async Task DataOperationsRequireASeat()
{
@@ -114,6 +141,28 @@ public class ChannelServiceTests : IDisposable
await AssertCode(ErrorCode.NotJoined, () => LogOf(ch, ch.Reader(_memberEp)));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void CreateRequiresSpaceName(string? name)
{
var request = new CreateChannelRequest
{
Id = TestEnv.NewId().ToBase64Url(), Mode = ChannelMode.Raw,
Settings = new ChannelSettings { Name = name },
OwnerHash = Codec.Encode(TokenHash.Compute(TestEnv.Random(32))),
};
AssertCode(ErrorCode.SpaceNameRequired, () => Svc.CreateChannel(request, _ownerEp, "owner"));
}
[Fact]
public void CreateTrimsSpaceName()
{
var channel = _env.Create(_ownerEp, settings: new ChannelSettings { Name = " Team space " });
Assert.Equal("Team space", Svc.GetChannel(channel.Id, channel.Owner(_ownerEp)).Settings.Name);
}
[Fact]
public async Task ItemIdsAndFramesAreScopedToTheirChannel()
{