- A remote `summary` is a content warning only on a Note or Question, or when `sensitive` is set. On Articles,
Events, Videos, Pages and Audio it is an excerpt (WordPress teasers, Mobilizon dates and places, Mbin titles) and
is now kept as `Post.Excerpt` instead of hiding the post. `Post.ObjectType` records the remote type, and the
Mastodon API shows a remote non-Note object's title above its body again.
- Persona usernames must match `^[a-z0-9_]+$`, as groups already did; a name outside it was unreachable from
Mastodon and Misskey.
- `postingRestrictedToMods` is defined in our JSON-LD context (Iceshrimp.NET drops undefined terms).
- `Vary: Accept` on actor and object URLs.
- Owner decision: blocks federate. A block sends `Block`, an unblock `Undo{Block}`; checked live against GoToSocial.
- Owner decision: a persona's and a group's `published`, and the day in new ids, is a random day up to two weeks
before creation, so personas made the same day no longer share a date. Migration _007 gives existing ones theirs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
147 lines
6.6 KiB
C#
147 lines
6.6 KiB
C#
using MongoDB.Entities;
|
|
|
|
using PrivaPub.ClientModels.Post;
|
|
using PrivaPub.ClientModels.Social;
|
|
using PrivaPub.Federation.Objects;
|
|
using PrivaPub.Models.Federation;
|
|
using PrivaPub.Models.Social;
|
|
using PrivaPub.Tests.Support;
|
|
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace PrivaPub.Tests.Domain
|
|
{
|
|
[Trait("Category", "Integration")]
|
|
public sealed class RelationshipTests : IAsyncLifetime
|
|
{
|
|
Harness _harness;
|
|
|
|
public async ValueTask InitializeAsync()
|
|
{
|
|
Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip);
|
|
_harness = await Harness.Start();
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_harness != default)
|
|
await _harness.DisposeAsync();
|
|
}
|
|
|
|
static string Origin(RemoteActor actor) => new Uri(actor.Id).GetLeftPart(UriPartial.Authority);
|
|
|
|
[Fact]
|
|
public async Task A_block_cuts_follows_both_ways_and_hides_the_account()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var (aliceRoot, alice) = await _harness.Persona("alice");
|
|
var (bobRoot, bob) = await _harness.Persona("bob");
|
|
await _harness.Follows.Follow(aliceRoot, new FollowForm { AvatarId = alice.Id, Target = bob.UserName }, token);
|
|
await _harness.Follows.Follow(bobRoot, new FollowForm { AvatarId = bob.Id, Target = alice.UserName }, token);
|
|
|
|
await _harness.Relationships.Block(alice, bob.Uri, bob.Id, token);
|
|
await _harness.Posts.InsertPost(bobRoot, new InsertPostForm { AvatarId = bob.Id, Text = $"hey @{alice.UserName}" }, token);
|
|
|
|
Assert.False(await DB.Default.Find<Following>().Match(f => f.AvatarId == alice.Id || f.AvatarId == bob.Id).ExecuteAnyAsync(token));
|
|
Assert.False(await DB.Default.Find<Follower>().Match(f => f.LocalActorId == alice.Id || f.LocalActorId == bob.Id).ExecuteAnyAsync(token));
|
|
Assert.False(await DB.Default.Find<TimelineEntry>().Match(e => e.AvatarId == alice.Id && e.AuthorAccountId == bob.Id).ExecuteAnyAsync(token));
|
|
Assert.False(await DB.Default.Find<Notification>().Match(n => n.AvatarId == alice.Id).ExecuteAnyAsync(token));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_blocked_remote_account_is_refused_when_it_follows()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var (_, alice) = await _harness.Persona("alice");
|
|
var mallory = new RemoteActor(_harness.Peer, "mallory");
|
|
await _harness.Relationships.Block(alice, mallory.Id, default, token);
|
|
|
|
await _harness.Deliver(mallory, "/human-centipede", new JsonObject
|
|
{
|
|
["id"] = $"{Origin(mallory)}/follows/{Guid.NewGuid():N}", ["type"] = "Follow", ["actor"] = mallory.Id, ["object"] = alice.Uri
|
|
});
|
|
|
|
Assert.False(await DB.Default.Find<Follower>().Match(f => f.LocalActorId == alice.Id).ExecuteAnyAsync(token));
|
|
var sent = (await _harness.Outgoing(mallory.Id + "/inbox")).Select(a => a["type"]!.GetValue<string>()).ToList();
|
|
Assert.Equal(new[] { "Block", "Reject" }, sent.Order());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_block_is_told_to_the_blocked_server_and_so_is_the_unblock()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var (_, alice) = await _harness.Persona("alice");
|
|
var mallory = new RemoteActor(_harness.Peer, "mallory");
|
|
await _harness.Deliver(mallory, "/human-centipede", new JsonObject
|
|
{
|
|
["id"] = $"{Origin(mallory)}/follows/{Guid.NewGuid():N}", ["type"] = "Follow", ["actor"] = mallory.Id, ["object"] = alice.Uri
|
|
});
|
|
|
|
await _harness.Relationships.Block(alice, mallory.Id, default, token);
|
|
await _harness.Relationships.Unblock(alice, mallory.Id, token);
|
|
|
|
var sent = await _harness.Outgoing(mallory.Id + "/inbox");
|
|
var block = Assert.Single(sent, a => a["type"]!.GetValue<string>() == "Block");
|
|
Assert.Equal(mallory.Id, block["object"]!.GetValue<string>());
|
|
Assert.Equal(alice.Uri, block["actor"]!.GetValue<string>());
|
|
var undo = Assert.Single(sent, a => a["type"]!.GetValue<string>() == "Undo");
|
|
Assert.Equal(block["id"]!.GetValue<string>(), undo["object"]!["id"]!.GetValue<string>());
|
|
Assert.False(await DB.Default.Find<Block>().Match(b => b.AvatarId == alice.Id).ExecuteAnyAsync(token));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_muted_account_stays_out_of_home_and_notifications()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var (aliceRoot, alice) = await _harness.Persona("alice");
|
|
var (bobRoot, bob) = await _harness.Persona("bob");
|
|
await _harness.Follows.Follow(aliceRoot, new FollowForm { AvatarId = alice.Id, Target = bob.UserName }, token);
|
|
await _harness.Relationships.Mute(alice, bob.Uri, bob.Id, hideNotifications: true, default, token);
|
|
|
|
await _harness.Posts.InsertPost(bobRoot, new InsertPostForm { AvatarId = bob.Id, Text = $"hey @{alice.UserName}" }, token);
|
|
|
|
Assert.False(await DB.Default.Find<TimelineEntry>().Match(e => e.AvatarId == alice.Id).ExecuteAnyAsync(token));
|
|
Assert.False(await DB.Default.Find<Notification>().Match(n => n.AvatarId == alice.Id && n.Type == NotificationType.Mention).ExecuteAnyAsync(token));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_report_is_forwarded_by_the_instance_not_the_persona()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var (_, alice) = await _harness.Persona("alice");
|
|
var mallory = new RemoteActor(_harness.Peer, "mallory");
|
|
var foreign = await _harness.Remote.GetActor(mallory.Id, refresh: false, token);
|
|
|
|
var report = await _harness.Reports.File(alice, foreign.ID, Array.Empty<string>(), "spam everywhere", "spam", forward: true, token);
|
|
|
|
Assert.True(report.Forwarded);
|
|
var flag = Assert.Single(await _harness.Outgoing(mallory.Id + "/inbox"));
|
|
Assert.Equal("Flag", flag["type"]!.GetValue<string>());
|
|
Assert.EndsWith("/peasants/privapub", flag["actor"]!.GetValue<string>());
|
|
Assert.DoesNotContain(alice.UserName, flag.ToJsonString());
|
|
Assert.DoesNotContain(alice.Id, flag.ToJsonString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task An_inbound_flag_becomes_a_report_for_moderators()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var (root, alice) = await _harness.Persona("alice");
|
|
var reporter = new RemoteActor(_harness.Peer, "reporter");
|
|
await _harness.Posts.InsertPost(root, new InsertPostForm { AvatarId = alice.Id, Text = "bad post" }, token);
|
|
var post = await DB.Default.Find<PrivaPub.Models.Post.Post>().Match(p => p.GroupUserId == alice.Id).ExecuteSingleAsync(token);
|
|
|
|
await _harness.Deliver(reporter, "/human-centipede", new JsonObject
|
|
{
|
|
["id"] = $"{Origin(reporter)}/flags/{Guid.NewGuid():N}", ["type"] = "Flag", ["actor"] = reporter.Id,
|
|
["content"] = "please look", ["object"] = new JsonArray(alice.Uri, post.ObjectURI)
|
|
});
|
|
|
|
var report = await DB.Default.Find<Report>().Match(r => r.TargetAccountId == alice.Id).ExecuteSingleAsync(token);
|
|
Assert.Equal(reporter.Id, report.ReporterActorURI);
|
|
Assert.Equal(new[] { post.ID }, report.PostIds);
|
|
Assert.Equal("please look", report.Comment);
|
|
}
|
|
}
|
|
}
|