118 lines
5.4 KiB
C#
118 lines
5.4 KiB
C#
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|