Files
SocialPub/PrivaPub.Tests/Federation/ProvenanceTests.cs
T
thepraandClaude Opus 5.5 dcd024100a
Build / Build (push) Successful in 36s
Every remote object keeps its raw form and how it reached us, for the client's details view
- ObjectRecord, one per stored remote object: the raw JSON (up to 256 KB, always hashed), delivered or fetched,
  refetched from origin or not, the activity that brought it (or caused the fetch), shared or personal inbox, the
  signature's key, algorithm and signed headers, received time, published and updated, the delivering activity's
  @context, and up to ten later revisions from Update.
- Delivery details travel from InboxReceiver through the inbox job to the handlers as Arrival.Current.
- A host is described from its NodeInfo when we first hear from it, at most weekly (DescribeInstance job), never when
  someone opens the details view.
- GET /api/privapub/v1/statuses/:id/provenance and /api/privapub/v1/instances/:host, with the extensions an object
  used detected from its raw form (044f quotes, interaction policies, contexts, proofs, Misskey fields, MFM, FEP-8967
  links, emoji, polls, language maps, url variants, Markdown content).

Checked live: a GoToSocial reply shows as delivered to the shared inbox, signed hs2019 with GoToSocial's fragment-less
key id, with its interaction policy detected, and gts.test is described as gotosocial 0.22.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-01 17:49:22 +02:00

86 lines
3.2 KiB
C#

using MongoDB.Entities;
using PrivaPub.Federation.Objects;
using PrivaPub.Models.Federation;
using PrivaPub.Models.Jobs;
using PrivaPub.Tests.Support;
using System.Text.Json.Nodes;
namespace PrivaPub.Tests.Federation
{
[Trait("Category", "Integration")]
public sealed class ProvenanceTests : 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();
}
[Fact]
public async Task A_delivered_post_keeps_its_raw_object_how_it_came_and_every_edit()
{
var token = TestContext.Current.CancellationToken;
var (_, alice) = await _harness.Persona("alice");
var mallory = new RemoteActor(_harness.Peer, "mallory");
var origin = new Uri(mallory.Id).GetLeftPart(UriPartial.Authority);
var note = new JsonObject
{
["id"] = $"{origin}/notes/{Guid.NewGuid():N}",
["type"] = "Note",
["attributedTo"] = mallory.Id,
["content"] = "<p>hi</p>",
["_misskey_quote"] = $"{origin}/notes/elsewhere",
["to"] = new JsonArray(Addressing.Public),
["cc"] = new JsonArray(alice.Uri),
["tag"] = new JsonArray(new JsonObject { ["type"] = "Mention", ["href"] = alice.Uri, ["name"] = "@alice" })
};
var create = new JsonObject { ["id"] = $"{origin}/activities/{Guid.NewGuid():N}", ["type"] = "Create", ["actor"] = mallory.Id, ["object"] = note.DeepClone() };
await _harness.Deliver(mallory, "/human-centipede", create);
var edited = note.DeepClone().AsObject();
edited["content"] = "<p>hi again</p>";
edited["updated"] = DateTime.UtcNow.ToString("O");
await _harness.Deliver(mallory, "/human-centipede", new JsonObject
{
["id"] = $"{origin}/activities/{Guid.NewGuid():N}", ["type"] = "Update", ["actor"] = mallory.Id, ["object"] = edited
});
var record = await DB.Default.Find<ObjectRecord>().Match(r => r.ObjectURI == note["id"]!.GetValue<string>()).ExecuteFirstAsync(token);
Assert.NotNull(record);
Assert.Equal(ObjectPath.Delivered, record.Path);
Assert.False(record.Refetched);
Assert.Equal("shared", record.Inbox);
Assert.Equal(mallory.KeyId, record.KeyId);
Assert.Equal(create["id"]!.GetValue<string>(), record.ActivityId);
Assert.Contains("digest", record.SignedHeaders);
Assert.Equal("<p>hi</p>", JsonNode.Parse(record.Raw)!["content"]!.GetValue<string>());
Assert.Contains("legacy-quote", ObjectFeatures.Detect(JsonNode.Parse(record.Raw)!.AsObject()));
Assert.Equal("<p>hi again</p>", JsonNode.Parse(Assert.Single(record.Revisions).Raw)!["content"]!.GetValue<string>());
Assert.True(await DB.Default.Find<Job>().Match(j => j.Kind == JobKind.DescribeInstance && j.Payload == record.Host).ExecuteAnyAsync(token));
}
[Fact]
public void An_object_larger_than_the_cap_keeps_only_its_hash()
{
var big = new JsonObject { ["content"] = new string('x', ObjectRecords.MaxRawBytes + 1) };
var (text, hash, bytes, truncated) = ObjectRecords.Capture(big);
Assert.Null(text);
Assert.True(truncated);
Assert.True(bytes > ObjectRecords.MaxRawBytes);
Assert.Equal(64, hash.Length);
}
}
}