Contract 7.0.0 (breaking). Devices become an attribute of the account;
members are managed per user, and only unauthenticated guests keep a
per-terminal registration.
- rename seats -> max_members, seats_used -> members_used,
seats_full -> members_full, SeatJoined -> JoinResult, the `seats`
stream control message -> `members`, and POST /channels/{id}/seats ->
POST /channels/{id}/join (stored channel settings are migrated on start)
- drop per-terminal encrypted device labels and the seat list/release
endpoints; add GET/DELETE /channels/{id}/guests for guest terminals
- authenticate account members by account session only: the terminal key
is optional for logged-in accounts, no seat row is written for them,
and gated-download grants are counted per user (per terminal for guests)
- GET /channels/{id}/members now returns online, devices and
online_devices per account; presence is tracked in memory from active
WebSocket/SSE subscriptions and is not pushed over the stream
- owner invitation list no longer returns accounts that already joined
65 lines
5.2 KiB
C#
65 lines
5.2 KiB
C#
using System.Net;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using WebDrop.Core;
|
|
using WebDrop.Storage;
|
|
using static WebDrop.Api.Tests.ServerFixture;
|
|
|
|
namespace WebDrop.Api.Tests;
|
|
|
|
public sealed class WorkspaceApiTests(ServerFixture server) : IClassFixture<ServerFixture>
|
|
{
|
|
[Fact]
|
|
public async Task AccountProfileIsServerOwnedAndDoesNotChangeTheEncryptedVault()
|
|
{
|
|
var store = server.App.Services.GetRequiredService<SqliteAccountStore>();
|
|
var revision = store.GetVault("fixture_owner")!.Value.Revision;
|
|
using var patch = new HttpRequestMessage(HttpMethod.Patch, "/api/v1/accounts/profile") { Content = JsonBody(new { displayName = "资料测试", avatar = "data:image/png;base64,AA==" }) };
|
|
patch.Headers.Add("X-WD-Account", server.OwnerAccountToken);
|
|
using var response = await server.Http.SendAsync(patch);
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
var body = await ReadJsonAsync(response); Assert.Equal("资料测试", body.GetProperty("displayName").GetString());
|
|
Assert.Equal(revision, store.GetVault("fixture_owner")!.Value.Revision);
|
|
using var read = new HttpRequestMessage(HttpMethod.Get, "/api/v1/accounts/profile"); read.Headers.Add("X-WD-Account", server.OwnerAccountToken);
|
|
using var loaded = await server.Http.SendAsync(read);
|
|
Assert.Equal("资料测试", (await ReadJsonAsync(loaded)).GetProperty("displayName").GetString());
|
|
using var bad = new HttpRequestMessage(HttpMethod.Patch, "/api/v1/accounts/profile") { Content = JsonBody(new { displayName = "test", avatar = "https://tracker.test/avatar.png" }) };
|
|
bad.Headers.Add("X-WD-Account", server.OwnerAccountToken);
|
|
using var rejected = await server.Http.SendAsync(bad); Assert.Equal(HttpStatusCode.BadRequest, rejected.StatusCode);
|
|
using var anonymous = await server.Http.GetAsync("/api/v1/accounts/profile"); Assert.Equal(HttpStatusCode.Unauthorized, anonymous.StatusCode);
|
|
}
|
|
[Fact]
|
|
public async Task StructureEndpointsPersistAndRejectCrossSpaceGroups()
|
|
{
|
|
var a = await server.CreateAsync(); var b = await server.CreateAsync();
|
|
using var createGroup = server.Req(HttpMethod.Post, $"/api/v1/spaces/{b.Id}/groups", b.Owner, JsonBody(new { name = "另一空间" }));
|
|
using var groupResponse = await server.Http.SendAsync(createGroup); Assert.Equal(HttpStatusCode.Created, groupResponse.StatusCode);
|
|
var groupBody = await ReadJsonAsync(groupResponse);
|
|
var groupId = groupBody.GetProperty("group").GetProperty("id").GetString();
|
|
using var wrong = server.Req(HttpMethod.Post, $"/api/v1/spaces/{a.Id}/channels", a.Owner, JsonBody(new { name = "频道", groupId }));
|
|
using var wrongResponse = await server.Http.SendAsync(wrong); Assert.Equal(HttpStatusCode.BadRequest, wrongResponse.StatusCode);
|
|
using var valid = server.Req(HttpMethod.Post, $"/api/v1/spaces/{a.Id}/channels", a.Owner, JsonBody(new { name = "讨论" }));
|
|
using var response = await server.Http.SendAsync(valid); Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
|
var createdBody = await ReadJsonAsync(response);
|
|
Assert.Equal("讨论", createdBody.GetProperty("channel").GetProperty("name").GetString());
|
|
using var read = server.Req(HttpMethod.Get, $"/api/v1/spaces/{a.Id}/structure", a.Owner);
|
|
using var loaded = await server.Http.SendAsync(read); Assert.Equal(2, (await ReadJsonAsync(loaded)).GetProperty("structure").GetProperty("channels").GetArrayLength());
|
|
}
|
|
[Fact]
|
|
public async Task AdvertisedDefaultsRespectDeploymentLimitsAndMatchCreation()
|
|
{
|
|
await using var isolated = WithSettings(new() { ["WebDrop:Limits:MaxMembers"] = "3", ["WebDrop:Limits:MinTtlSeconds"] = "120", ["WebDrop:Limits:MaxTtlSeconds"] = "3600" });
|
|
await isolated.InitializeAsync();
|
|
using var infoResponse = await isolated.Http.GetAsync("/api/v1/info"); var info = await ReadJsonAsync(infoResponse);
|
|
var defaults = info.GetProperty("defaults").GetProperty("channel");
|
|
Assert.Equal("enc", info.GetProperty("defaults").GetProperty("channel_mode").GetString());
|
|
Assert.Equal(3, defaults.GetProperty("max_members").GetInt32()); Assert.Equal(3600, defaults.GetProperty("ttl_seconds").GetInt32());
|
|
var ch = new Chan(NewId(), ChannelMode.Enc);
|
|
using var create = isolated.Req(HttpMethod.Post, "/api/v1/channels", ch.Owner, JsonBody(new { id = ch.Id, mode = "enc", settings = new { name = "Limited space", max_members = 3, ttl_seconds = 3600 }, read_hash = B64(TokenHash.Compute(ch.ReadToken)), owner_hash = B64(TokenHash.Compute(ch.OwnerToken)), write_hash = B64(TokenHash.Compute(ch.WriteToken)) }));
|
|
using var result = await isolated.Http.SendAsync(create); Assert.Equal(HttpStatusCode.Created, result.StatusCode);
|
|
var settings = (await ReadJsonAsync(result)).GetProperty("settings");
|
|
Assert.Equal("Limited space", settings.GetProperty("name").GetString());
|
|
Assert.Equal(defaults.GetProperty("max_members").GetInt32(), settings.GetProperty("max_members").GetInt32());
|
|
Assert.Equal(defaults.GetProperty("ttl_seconds").GetInt32(), settings.GetProperty("ttl_seconds").GetInt32());
|
|
}
|
|
}
|