Files

118 lines
5.4 KiB
C#
Raw Permalink Normal View History

using MongoDB.Entities;
using PrivaPub.Models.Federation;
using PrivaPub.Models.User;
using PrivaPub.Tests.Support;
using PrivaPub.Tests.Support.Host;
using System.Net;
using System.Net.Http.Json;
using GroupEntity = PrivaPub.Models.Group.Group;
using PostEntity = PrivaPub.Models.Post.Post;
namespace PrivaPub.Tests.Http
{
// Deleting a root deletes its personas and their groups everywhere (owner decision 2026-10-04).
[Trait("Category", "Integration")]
public sealed class RootRemovalTests : IAsyncLifetime
{
PrivaPubHost _host;
HttpClient _client;
Peer _peer;
public async ValueTask InitializeAsync()
{
Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip);
_host = await PrivaPubHost.Shared();
_client = _host.Client();
_peer = await Peer.Start();
}
public async ValueTask DisposeAsync()
{
_client?.Dispose();
if (_peer != default)
await _peer.DisposeAsync();
}
async Task<HttpResponseMessage> AdminRemoves(params string[] rootIds)
{
var admin = await _host.Admin();
using var client = _host.As(admin.Jwt);
using var request = new HttpRequestMessage(HttpMethod.Delete, "/clientapi/admin/remove/users") { Content = JsonContent.Create(new { userIdList = rootIds }) };
return await client.SendAsync(request, TestContext.Current.CancellationToken);
}
[Fact]
public async Task A_removed_roots_personas_and_groups_are_deleted_everywhere_and_say_so()
{
var token = TestContext.Current.CancellationToken;
var root = await _host.SignUp("leaving");
var persona = await _host.Persona(root, "leaving");
var follower = new RemoteActor(_peer, "fan");
var member = new RemoteActor(_peer, "member");
await DB.Default.SaveAsync(new Follower { LocalActorId = persona.Id, LocalActorKind = LocalActorKind.Person, ActorURI = follower.Id, InboxURL = follower.Id + "/inbox" }, token);
var group = await _host.Group(persona, community: true);
var groupId = group["id"]!.GetValue<string>();
await DB.Default.SaveAsync(new Follower { LocalActorId = groupId, LocalActorKind = LocalActorKind.Group, ActorURI = member.Id, InboxURL = member.Id + "/inbox" }, token);
var post = await _host.Publish(persona, "goodbye soon");
var apiToken = await _host.MastodonToken(persona);
var since = DateTime.UtcNow.AddSeconds(-1);
Assert.Equal(HttpStatusCode.OK, (await AdminRemoves(root.Id)).StatusCode);
var toFollower = Assert.Single(await Jobs.Deliveries(follower.Id + "/inbox", since, token), d => d["type"]!.GetValue<string>() == "Delete");
Assert.Equal(persona.ActorUri(), toFollower["object"]!.GetValue<string>());
Assert.Equal(persona.ActorUri(), toFollower["actor"]!.GetValue<string>());
var groupName = group["userName"]!.GetValue<string>();
Assert.Contains(await Jobs.Deliveries(member.Id + "/inbox", since, token),
d => d["type"]!.GetValue<string>() == "Delete" && d["object"]!.GetValue<string>() == $"{PrivaPubHost.Base}/peasants/{groupName}");
var gone = await _client.Fetch($"/peasants/{persona.UserName}");
Assert.Equal(HttpStatusCode.Gone, gone.Status);
Assert.Equal("Person", gone.Json["formerType"]!.GetValue<string>());
Assert.Equal(HttpStatusCode.Gone, (await _client.Fetch($"/peasants/{groupName}")).Status);
Assert.Equal(HttpStatusCode.Gone, (await _client.GetAsync($"/.well-known/webfinger?resource=acct:{persona.UserName}@{PrivaPubHost.Host}", token)).StatusCode);
var stored = await DB.Default.Find<PostEntity>().MatchID(post.ID).ExecuteFirstAsync(token);
Assert.NotNull(stored.DeletedAt);
Assert.Null(stored.ContentHtml);
Assert.NotNull((await DB.Default.Find<GroupEntity>().MatchID(groupId).ExecuteFirstAsync(token)).DeletionAt);
var removed = await DB.Default.Find<RootUser>().MatchID(root.Id).ExecuteFirstAsync(token);
Assert.Equal($"deleted-{root.Id}", removed.UserName);
Assert.Null(removed.HashedPassword);
using var app = _host.Client();
app.DefaultRequestHeaders.Authorization = new("Bearer", apiToken);
Assert.Equal(HttpStatusCode.Unauthorized, (await app.GetAsync("/api/v1/accounts/verify_credentials", token)).StatusCode);
}
[Fact]
public async Task A_second_removal_does_not_collide_with_the_first()
{
var first = await _host.SignUp("first");
var second = await _host.SignUp("second");
Assert.Equal(HttpStatusCode.OK, (await AdminRemoves(first.Id)).StatusCode);
Assert.Equal(HttpStatusCode.OK, (await AdminRemoves(second.Id)).StatusCode);
Assert.NotNull((await DB.Default.Find<RootUser>().MatchID(second.Id).ExecuteFirstAsync(TestContext.Current.CancellationToken)).DeletedAt);
}
[Fact]
public async Task A_root_deletes_itself_only_with_its_password()
{
var token = TestContext.Current.CancellationToken;
var root = await _host.SignUp("self");
var persona = await _host.Persona(root, "self");
using var client = _host.As(root.Jwt);
var refused = await client.PostJson("/clientapi/user/delete", new { password = "Not-The-Password-1" });
Assert.Equal(HttpStatusCode.Forbidden, refused.StatusCode);
Assert.Null((await DB.Default.Find<Avatar>().MatchID(persona.Id).ExecuteFirstAsync(token)).DeletionAt);
var accepted = await client.PostJson("/clientapi/user/delete", new { password = root.Password });
Assert.Equal(HttpStatusCode.OK, accepted.StatusCode);
Assert.NotNull((await DB.Default.Find<Avatar>().MatchID(persona.Id).ExecuteFirstAsync(token)).DeletionAt);
Assert.Equal(HttpStatusCode.Unauthorized, (await client.GetAsync("/clientapi/user/sniff/again", token)).StatusCode);
}
}
}