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 { [Fact] public async Task AccountProfileIsServerOwnedAndDoesNotChangeTheEncryptedVault() { var store = server.App.Services.GetRequiredService(); 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 DeletingAGroupMovesItsChannelsBackToUngrouped() { var a = await server.CreateAsync(); using var createGroup = server.Req(HttpMethod.Post, $"/api/v1/spaces/{a.Id}/groups", a.Owner, JsonBody(new { name = "待删分组" })); using var groupResponse = await server.Http.SendAsync(createGroup); var groupId = (await ReadJsonAsync(groupResponse)).GetProperty("group").GetProperty("id").GetString(); using var createChannel = server.Req(HttpMethod.Post, $"/api/v1/spaces/{a.Id}/channels", a.Owner, JsonBody(new { name = "组内频道", groupId })); using var channelResponse = await server.Http.SendAsync(createChannel); Assert.Equal(HttpStatusCode.Created, channelResponse.StatusCode); using var delete = server.Req(HttpMethod.Delete, $"/api/v1/spaces/{a.Id}/groups/{groupId}", a.Owner); using var deleted = await server.Http.SendAsync(delete); Assert.Equal(HttpStatusCode.OK, deleted.StatusCode); var structure = (await ReadJsonAsync(deleted)).GetProperty("structure"); Assert.DoesNotContain(structure.GetProperty("groups").EnumerateArray(), g => g.GetProperty("id").GetString() == groupId); var moved = structure.GetProperty("channels").EnumerateArray().Single(ch => ch.GetProperty("name").GetString() == "组内频道"); Assert.True(!moved.TryGetProperty("groupId", out var remaining) || remaining.ValueKind == System.Text.Json.JsonValueKind.Null); using var again = server.Req(HttpMethod.Delete, $"/api/v1/spaces/{a.Id}/groups/{groupId}", a.Owner); using var missing = await server.Http.SendAsync(again); Assert.Equal(HttpStatusCode.NotFound, missing.StatusCode); } [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()); } }