Arrival carries a verdict that handlers set with one line before an existing return (Arrival.Drop, Reject, Accept, About), so no handler signature changes. InboxProcessor records it as an 'in' event, with the time taken, the wait since the inbox accepted it, the attempt, the audience (from the post's visibility, or else the activity's addressing), the local actor kind, the object's age for updates, deletes and reactions, and the FEP features of a delivered object. It also records types nothing handles (unknown-type), actors that cannot be loaded (deferred) and handler failures. Drop reasons: fetch-failed, unparseable, misattributed, duplicate, deleted, not-addressed, not-followed, not-public, not-visible, not-deleted, unknown-object, unknown-recipient, cross-origin, unsupported. Accepted sub-reasons: stored, poll-vote, edit, refresh, actor-refresh, actor-delete, removed, tombstone-only, auto-accepted, pending, follow-answer, quote-answer, quote-granted, undone, reaction, reported. Rejected: blocked, ignored, quote-refused. A circle's traffic is private and kindless, and a stranger posting into one is just not-addressed, so the event store cannot reveal a circle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
106 lines
3.9 KiB
C#
106 lines
3.9 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
using PrivaPub.Infrastructure.Statistics;
|
|
using PrivaPub.Models.Statistics;
|
|
using PrivaPub.Tests.Support;
|
|
|
|
namespace PrivaPub.Tests.Statistics
|
|
{
|
|
public class InteractionsTests
|
|
{
|
|
[Fact]
|
|
public void The_local_kind_is_kept_only_on_public_and_unlisted_traffic()
|
|
{
|
|
Assert.Equal("person", Interactions.Sanitize(new InteractionEvent { Audience = "public", LocalKind = "person" }).LocalKind);
|
|
Assert.Equal("group", Interactions.Sanitize(new InteractionEvent { Audience = "unlisted", LocalKind = "group" }).LocalKind);
|
|
Assert.Null(Interactions.Sanitize(new InteractionEvent { Audience = "private", LocalKind = "group" }).LocalKind);
|
|
Assert.Null(Interactions.Sanitize(new InteractionEvent { Audience = "none", LocalKind = "person" }).LocalKind);
|
|
Assert.Null(Interactions.Sanitize(new InteractionEvent { LocalKind = "person" }).LocalKind);
|
|
Assert.Null(Interactions.Sanitize(new InteractionEvent { Audience = "public", LocalKind = "circle" }).LocalKind);
|
|
}
|
|
|
|
[Fact]
|
|
public void Junk_becomes_other_and_hosts_are_normalised()
|
|
{
|
|
var e = Interactions.Sanitize(new InteractionEvent
|
|
{
|
|
Host = "Mastodon.Social.",
|
|
Channel = "sideways",
|
|
Activity = "https://www.w3.org/ns/activitystreams#Create",
|
|
Object = "Note",
|
|
Outcome = "Dropped!",
|
|
Reason = "not-addressed",
|
|
Audience = "everyone",
|
|
Inbox = "front-door",
|
|
Signature = "cavage:rsa-sha256",
|
|
Features = new() { "fep-044f", "Bad Feature", "fep-044f", "source:text/markdown" }
|
|
});
|
|
|
|
Assert.Equal("mastodon.social", e.Host);
|
|
Assert.Equal("other", e.Channel);
|
|
Assert.Equal("other", e.Activity);
|
|
Assert.Equal("Note", e.Object);
|
|
Assert.Equal("other", e.Outcome);
|
|
Assert.Equal("not-addressed", e.Reason);
|
|
Assert.Null(e.Audience);
|
|
Assert.Null(e.Inbox);
|
|
Assert.Equal("cavage:rsa-sha256", e.Signature);
|
|
Assert.Equal(new[] { "fep-044f", "other", "source:text/markdown" }, e.Features);
|
|
Assert.Equal("-", Interactions.Host("evil host/with path"));
|
|
Assert.Equal("-", Interactions.Host(default));
|
|
Assert.Equal("social.example", Interactions.HostOf("https://Social.Example:8443/users/x"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Latencies_and_waits_fall_into_buckets()
|
|
{
|
|
Assert.Equal("le50ms", Interactions.Latency(0));
|
|
Assert.Equal("le250ms", Interactions.Latency(101));
|
|
Assert.Equal("inf", Interactions.Latency(31_000));
|
|
Assert.Equal("le1s", Interactions.Wait(900));
|
|
Assert.Equal("le60s", Interactions.Wait(59_000));
|
|
Assert.Equal("inf", Interactions.Wait(100_000_000));
|
|
}
|
|
|
|
[Fact]
|
|
public void An_actor_hash_is_stable_for_a_salt_and_unlinkable_across_salts()
|
|
{
|
|
var monday = new byte[32];
|
|
var tuesday = Enumerable.Repeat((byte)1, 32).ToArray();
|
|
const string actor = "https://social.example/users/alice";
|
|
|
|
var first = InteractionSalts.Hash(monday, actor);
|
|
|
|
Assert.Equal(first, InteractionSalts.Hash(monday, actor));
|
|
Assert.NotEqual(first, InteractionSalts.Hash(tuesday, actor));
|
|
Assert.NotEqual(first, InteractionSalts.Hash(monday, actor + "2"));
|
|
Assert.Equal(16, first.Length);
|
|
Assert.DoesNotContain("alice", first);
|
|
Assert.Null(InteractionSalts.Hash(default, actor));
|
|
}
|
|
|
|
[Fact]
|
|
public void A_full_ledger_drops_and_counts_but_never_blocks_or_throws()
|
|
{
|
|
var ledger = new InteractionLedger(new InteractionSalts(), new StaticOptions<StatisticsOptions>(new StatisticsOptions()), NullLogger<InteractionLedger>.Instance);
|
|
|
|
for (var i = 0; i < 10_005; i++)
|
|
ledger.Record(new InteractionEvent { Channel = Interactions.In });
|
|
ledger.Record(default);
|
|
|
|
Assert.Equal(5, ledger.Dropped);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_disabled_ledger_records_nothing()
|
|
{
|
|
var ledger = new InteractionLedger(new InteractionSalts(), new StaticOptions<StatisticsOptions>(new StatisticsOptions { Enabled = false }), NullLogger<InteractionLedger>.Instance);
|
|
|
|
for (var i = 0; i < 10_005; i++)
|
|
ledger.Record(new InteractionEvent { Channel = Interactions.In });
|
|
|
|
Assert.Equal(0, ledger.Dropped);
|
|
}
|
|
}
|
|
}
|