211 lines
7.9 KiB
C#
211 lines
7.9 KiB
C#
using MongoDB.Entities;
|
|||
|
|
|
||
|
|
using PrivaPub.Domain.Statuses;
|
||
|
|
using PrivaPub.Federation.Actors;
|
||
|
|
using PrivaPub.Federation.Inbox;
|
||
|
|
using PrivaPub.Federation.Objects;
|
||
|
|
using PrivaPub.Models.Federation;
|
||
|
|
using PrivaPub.Models.Group;
|
||
|
|
using PrivaPub.Models.Post;
|
||
|
|
using PrivaPub.Models.Statistics;
|
||
|
|
using PrivaPub.Tests.Support;
|
||
|
|
|
||
|
|
using System.Text.Json.Nodes;
|
||
|
|
|
||
|
|
using GroupEntity = PrivaPub.Models.Group.Group;
|
||
|
|
|
||
|
|
namespace PrivaPub.Tests.Statistics
|
||
|
|
{
|
||
|
|
public class ArrivalVerdictTests
|
||
|
|
{
|
||
|
|
[Fact]
|
||
|
|
public void The_first_decision_wins_and_a_circle_has_no_kind()
|
||
|
|
{
|
||
|
|
var verdict = new ArrivalVerdict();
|
||
|
|
|
||
|
|
verdict.Drop("not-addressed");
|
||
|
|
verdict.Accept("stored");
|
||
|
|
verdict.About("Note", PostVisibility.Circle, DateTime.UtcNow.AddMinutes(-2), new LocalActor { Kind = LocalActorKind.Group, IsCircle = true });
|
||
|
|
|
||
|
|
Assert.Equal(("dropped", "not-addressed"), (verdict.Outcome, verdict.Reason));
|
||
|
|
Assert.Equal("private", verdict.Audience);
|
||
|
|
Assert.Null(verdict.LocalKind);
|
||
|
|
Assert.InRange(verdict.AgeSeconds!.Value, 110, 130);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Theory]
|
||
|
|
[InlineData(PostVisibility.Public, "public")]
|
||
|
|
[InlineData(PostVisibility.Unlisted, "unlisted")]
|
||
|
|
[InlineData(PostVisibility.FollowersOnly, "private")]
|
||
|
|
[InlineData(PostVisibility.Direct, "private")]
|
||
|
|
[InlineData(PostVisibility.Circle, "private")]
|
||
|
|
[InlineData(PostVisibility.LocalGeo, "private")]
|
||
|
|
public void Every_visibility_but_public_and_unlisted_is_private(PostVisibility visibility, string audience) =>
|
||
|
|
Assert.Equal(audience, ArrivalVerdict.AudienceOf(visibility));
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Outside_an_arrival_nothing_happens()
|
||
|
|
{
|
||
|
|
Arrival.Current = default;
|
||
|
|
Arrival.Drop("x");
|
||
|
|
Arrival.About("Note");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
[Trait("Category", "Integration")]
|
||
|
|
public sealed class HandlerVerdictTests : 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();
|
||
|
|
}
|
||
|
|
|
||
|
|
InteractionEvent Processed(string activityType) =>
|
||
|
|
_harness.Ledger.Of("in").Last(e => e.Activity == activityType);
|
||
|
|
|
||
|
|
JsonObject Create(RemoteActor author, IEnumerable<string> to, JsonObject extra = default)
|
||
|
|
{
|
||
|
|
var origin = new Uri(author.Id).GetLeftPart(UriPartial.Authority);
|
||
|
|
var note = new JsonObject
|
||
|
|
{
|
||
|
|
["id"] = $"{origin}/notes/{Guid.NewGuid():N}",
|
||
|
|
["type"] = "Note",
|
||
|
|
["attributedTo"] = author.Id,
|
||
|
|
["content"] = "<p>hello</p>",
|
||
|
|
["to"] = new JsonArray(to.Select(t => (JsonNode)t).ToArray())
|
||
|
|
};
|
||
|
|
foreach (var (key, value) in extra ?? new JsonObject())
|
||
|
|
note[key] = value?.DeepClone();
|
||
|
|
return new JsonObject { ["id"] = $"{origin}/activities/{Guid.NewGuid():N}", ["type"] = "Create", ["actor"] = author.Id, ["object"] = note };
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task A_direct_message_is_stored_private_and_without_a_local_kind()
|
||
|
|
{
|
||
|
|
var (_, alice) = await _harness.Persona("alice");
|
||
|
|
var bob = new RemoteActor(_harness.Peer, "bob");
|
||
|
|
|
||
|
|
await _harness.Deliver(bob, "/human-centipede", Create(bob, new[] { alice.Uri }));
|
||
|
|
|
||
|
|
var e = Processed("Create");
|
||
|
|
Assert.Equal(("accepted", "stored", "private"), (e.Outcome, e.Reason, e.Audience));
|
||
|
|
Assert.Equal("Note", e.Object);
|
||
|
|
Assert.Null(e.LocalKind);
|
||
|
|
Assert.Equal("127.0.0.1", e.Host);
|
||
|
|
Assert.NotNull(e.Attempt);
|
||
|
|
Assert.NotNull(e.WaitMs);
|
||
|
|
Assert.Equal(bob.Id, _harness.Ledger.Events.Last(x => x.Event == e).ActorUri);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task A_public_reply_to_a_persona_names_its_kind_and_its_features()
|
||
|
|
{
|
||
|
|
var (_, alice) = await _harness.Persona("alice");
|
||
|
|
var bob = new RemoteActor(_harness.Peer, "bob");
|
||
|
|
var parent = await _harness.Statuses.Publish(alice, new StatusDraft { Text = "first" }, TestContext.Current.CancellationToken);
|
||
|
|
|
||
|
|
await _harness.Deliver(bob, "/human-centipede", Create(bob, new[] { Addressing.Public, alice.Uri },
|
||
|
|
new JsonObject { ["inReplyTo"] = parent.Post.ObjectURI, ["interactionPolicy"] = new JsonObject(), ["quoteUrl"] = "https://elsewhere.example/1" }));
|
||
|
|
|
||
|
|
var e = Processed("Create");
|
||
|
|
Assert.Equal(("accepted", "public", "person"), (e.Outcome, e.Audience, e.LocalKind));
|
||
|
|
Assert.Contains("interaction-policy", e.Features);
|
||
|
|
Assert.Contains("legacy-quote", e.Features);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Unwanted_things_are_dropped_with_a_reason()
|
||
|
|
{
|
||
|
|
var stranger = new RemoteActor(_harness.Peer, "stranger");
|
||
|
|
|
||
|
|
await _harness.Deliver(stranger, "/human-centipede", Create(stranger, new[] { Addressing.Public }));
|
||
|
|
var unaddressed = Processed("Create");
|
||
|
|
await _harness.Deliver(stranger, "/human-centipede", new JsonObject
|
||
|
|
{
|
||
|
|
["id"] = new Uri(stranger.Id).GetLeftPart(UriPartial.Authority) + "/likes/" + Guid.NewGuid().ToString("N"),
|
||
|
|
["type"] = "Like",
|
||
|
|
["actor"] = stranger.Id,
|
||
|
|
["object"] = $"{Harness.Base}/peasants/x/scribbles/{Guid.NewGuid():N}"
|
||
|
|
});
|
||
|
|
var like = Processed("Like");
|
||
|
|
await _harness.Deliver(stranger, "/human-centipede", new JsonObject
|
||
|
|
{
|
||
|
|
["id"] = new Uri(stranger.Id).GetLeftPart(UriPartial.Authority) + "/x/" + Guid.NewGuid().ToString("N"),
|
||
|
|
["type"] = "Bite",
|
||
|
|
["actor"] = stranger.Id,
|
||
|
|
["object"] = stranger.Id
|
||
|
|
});
|
||
|
|
var bite = Processed("Bite");
|
||
|
|
await _harness.Deliver(stranger, "/human-centipede", new JsonObject
|
||
|
|
{
|
||
|
|
["id"] = new Uri(stranger.Id).GetLeftPart(UriPartial.Authority) + "/undo/" + Guid.NewGuid().ToString("N"),
|
||
|
|
["type"] = "Undo",
|
||
|
|
["actor"] = stranger.Id,
|
||
|
|
["object"] = new JsonObject { ["id"] = stranger.Id + "/likes/never", ["type"] = "Like", ["actor"] = stranger.Id, ["object"] = "https://nowhere.example/1" }
|
||
|
|
});
|
||
|
|
var undo = Processed("Undo");
|
||
|
|
|
||
|
|
Assert.Equal(("dropped", "not-addressed", "public"), (unaddressed.Outcome, unaddressed.Reason, unaddressed.Audience));
|
||
|
|
Assert.Equal(("dropped", "unknown-object"), (like.Outcome, like.Reason));
|
||
|
|
Assert.Equal(("dropped", "unknown-type"), (bite.Outcome, bite.Reason));
|
||
|
|
Assert.Equal(("dropped", "unknown-object", "Like"), (undo.Outcome, undo.Reason, undo.Object));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task A_stranger_posting_into_a_circle_never_reveals_the_circle()
|
||
|
|
{
|
||
|
|
var (_, owner) = await _harness.Persona("owner");
|
||
|
|
var (privateKey, publicKey) = Keys.NewKeyPair();
|
||
|
|
var entity = new GroupEntity
|
||
|
|
{
|
||
|
|
UserName = $"circle{Guid.NewGuid():N}"[..20],
|
||
|
|
Kind = GroupKind.Circle,
|
||
|
|
PostingPolicy = PostingPolicy.Followers,
|
||
|
|
PrivateKey = privateKey,
|
||
|
|
PublicKey = publicKey,
|
||
|
|
Members = new() { new GroupMember { AvatarId = owner.Id, Role = GroupRole.Owner } }
|
||
|
|
};
|
||
|
|
await DB.Default.SaveAsync(entity);
|
||
|
|
var circle = _harness.Local.FromGroup(entity);
|
||
|
|
var outsider = new RemoteActor(_harness.Peer, "outsider");
|
||
|
|
|
||
|
|
await _harness.Deliver(outsider, "/human-centipede", Create(outsider, new[] { circle.Uri, circle.Flock }));
|
||
|
|
|
||
|
|
var e = Processed("Create");
|
||
|
|
Assert.Equal(("dropped", "not-addressed", "private"), (e.Outcome, e.Reason, e.Audience));
|
||
|
|
Assert.Null(e.LocalKind);
|
||
|
|
Assert.DoesNotContain(_harness.Ledger.Events, x => System.Text.Json.JsonSerializer.Serialize(x.Event).Contains("circle", StringComparison.OrdinalIgnoreCase));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task A_follow_is_accepted_and_its_undo_is_recorded()
|
||
|
|
{
|
||
|
|
var (_, alice) = await _harness.Persona("alice");
|
||
|
|
var bob = new RemoteActor(_harness.Peer, "bob");
|
||
|
|
var follow = new JsonObject { ["id"] = bob.Id + "/follows/" + Guid.NewGuid().ToString("N"), ["type"] = "Follow", ["actor"] = bob.Id, ["object"] = alice.Uri };
|
||
|
|
|
||
|
|
await _harness.Deliver(bob, "/human-centipede", follow);
|
||
|
|
var followed = Processed("Follow");
|
||
|
|
await _harness.Deliver(bob, "/human-centipede", new JsonObject
|
||
|
|
{
|
||
|
|
["id"] = bob.Id + "/undo/" + Guid.NewGuid().ToString("N"),
|
||
|
|
["type"] = "Undo",
|
||
|
|
["actor"] = bob.Id,
|
||
|
|
["object"] = follow.DeepClone()
|
||
|
|
});
|
||
|
|
var undone = Processed("Undo");
|
||
|
|
|
||
|
|
Assert.Equal(("accepted", "auto-accepted", "none"), (followed.Outcome, followed.Reason, followed.Audience));
|
||
|
|
Assert.Equal(("accepted", "undone", "Follow"), (undone.Outcome, undone.Reason, undone.Object));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|