Edit history from formerRepresentations

Pleroma and Akkoma send a post's earlier versions with it. A post PrivaPub first meets after its edits now has its
history, and an edit that carries them brings the versions missed in between; each is read as the post itself is. The
Akkoma scenario checks a post fetched after two edits by an account no persona follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-06 12:07:49 +02:00
1 parent 02928caac1
commit 679463eb3f
12 files changed
+288 -24

No files matched your search

@@ -0,0 +1,97 @@
using MongoDB.Entities;
using PrivaPub.Federation.Objects;
using PrivaPub.Models.Post;
using PrivaPub.Models.Social;
using PrivaPub.Tests.Support;
using System.Text.Json.Nodes;
namespace PrivaPub.Tests.Federation
{
// Pleroma's and Akkoma's edited posts carry their earlier versions (formerRepresentations): a post first met after its
// edits has its history, and an edit tells the ones missed in between
[Trait("Category", "Integration")]
public sealed class FormerRepresentationsTests : 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);
static readonly DateTime Start = DateTime.UtcNow.AddHours(-1);
static JsonObject Version(string text, int minutes) => new()
{
["type"] = "Note", ["content"] = $"<p>{text}</p>", ["published"] = Start.ToString("O"),
["updated"] = minutes == 0 ? default : Start.AddMinutes(minutes).ToString("O")
};
async Task<(RemoteActor Carol, JsonObject Note)> Followed(string text, int minutes, params JsonObject[] former)
{
var token = TestContext.Current.CancellationToken;
var (_, alice) = await _harness.Persona("alice");
var carol = new RemoteActor(_harness.Peer, "carol");
await DB.Default.SaveAsync(new Following { AvatarId = alice.Id, TargetActorURI = carol.Id, State = FollowState.Accepted }, token);
var note = Version(text, minutes);
note["id"] = $"{Origin(carol)}/objects/{Guid.NewGuid():N}";
note["attributedTo"] = carol.Id;
note["to"] = new JsonArray(Addressing.Public);
note["cc"] = new JsonArray($"{carol.Id}/followers");
if (former.Length > 0)
note["formerRepresentations"] = new JsonObject { ["type"] = "OrderedCollection", ["totalItems"] = former.Length, ["orderedItems"] = new JsonArray(former) };
return (carol, note);
}
Task Deliver(RemoteActor carol, string type, JsonObject note) => _harness.Deliver(carol, "/human-centipede", new JsonObject
{
["id"] = $"{Origin(carol)}/activities/{Guid.NewGuid():N}", ["type"] = type, ["actor"] = carol.Id,
["to"] = new JsonArray(Addressing.Public), ["object"] = note.DeepClone()
});
Task<Post> Stored(JsonObject note) =>
DB.Default.Find<Post>().Match(p => p.ObjectURI == note["id"]!.GetValue<string>()).ExecuteSingleAsync(TestContext.Current.CancellationToken);
[Fact]
public async Task A_post_first_met_after_its_edits_has_its_history()
{
var (carol, note) = await Followed("third", 40, Version("second", 20), Version("first", 0));
await Deliver(carol, "Create", note);
var post = await Stored(note);
Assert.Equal(["<p>first</p>", "<p>second</p>"], post.Revisions.Select(r => r.ContentHtml));
Assert.Equal("<p>third</p>", post.ContentHtml);
}
[Fact]
public async Task An_edit_tells_the_versions_missed_in_between()
{
var (carol, note) = await Followed("first", 0);
await Deliver(carol, "Create", note);
var edited = (JsonObject)note.DeepClone();
edited["content"] = "<p>third</p>";
edited["updated"] = Start.AddMinutes(40).ToString("O");
edited["formerRepresentations"] = new JsonObject
{
["type"] = "OrderedCollection", ["totalItems"] = 2, ["orderedItems"] = new JsonArray(Version("second", 20), Version("first", 0))
};
await Deliver(carol, "Update", edited);
var post = await Stored(note);
Assert.Equal(["<p>first</p>", "<p>second</p>"], post.Revisions.Select(r => r.ContentHtml));
Assert.Equal("<p>third</p>", post.ContentHtml);
}
}
}
@@ -90,6 +90,36 @@ namespace PrivaPub.Tests.Federation
Assert.Equal(["https://privapub.test/peasants/flor_pp98"], note.Cc);
}
// Pleroma's edited post carries its earlier versions (formerRepresentations, newest first)
[Fact]
public void Reads_the_earlier_versions_of_a_pleroma_edit()
{
var note = NoteParser.Parse(JsonNode.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "pleroma", "edited.json"))));
var before = Assert.Single(note.FormerRepresentations);
Assert.StartsWith("tomorrow library garden bridge", before.ContentHtml);
Assert.Equal(new DateTime(2026, 10, 5, 0, 49, 58, DateTimeKind.Utc).AddTicks(8_600_710), before.EditedAt);
Assert.Null(before.SpoilerText);
Assert.EndsWith("(edited)", note.ContentHtml);
}
[Fact]
public void Earlier_versions_come_oldest_first_each_read_as_the_post_is()
{
var note = NoteParser.Parse(JsonNode.Parse("""
{"id":"https://pl.example/objects/1","type":"Note","attributedTo":"https://pl.example/users/carol","content":"<p>third</p>",
"published":"2026-10-06T08:00:00Z","updated":"2026-10-06T09:00:00Z",
"formerRepresentations":{"type":"OrderedCollection","totalItems":2,"orderedItems":[
{"type":"Note","content":"<p>second</p>","summary":"careful","published":"2026-10-06T08:00:00Z","updated":"2026-10-06T08:30:00Z"},
{"type":"Note","content":"<p>first<script>x</script></p>","summary":"","published":"2026-10-06T08:00:00Z"}]}}
"""));
Assert.Equal(["<p>first</p>", "<p>second</p>"], note.FormerRepresentations.Select(r => r.ContentHtml));
Assert.Equal([new DateTime(2026, 10, 6, 8, 0, 0, DateTimeKind.Utc), new DateTime(2026, 10, 6, 8, 30, 0, DateTimeKind.Utc)],
note.FormerRepresentations.Select(r => r.EditedAt));
Assert.Equal([(default(string), false), ("careful", true)], note.FormerRepresentations.Select(r => (r.SpoilerText, r.HasContentWarning)));
}
[Fact]
public void Reads_a_lemmy_page_with_a_title()
{
@@ -0,0 +1,71 @@
{
"actor": "https://pleroma.test/users/flor_pl97",
"attachment": [],
"attributedTo": "https://pleroma.test/users/flor_pl97",
"cc": [
"https://pleroma.test/users/flor_pl97/followers"
],
"content": "mountain late window map bright postcard (p10) (edited)",
"context": "https://pleroma.test/contexts/891653c5-91ba-43f3-b3ef-b066caa3fa4d",
"conversation": "https://pleroma.test/contexts/891653c5-91ba-43f3-b3ef-b066caa3fa4d",
"formerRepresentations": {
"orderedItems": [
{
"actor": "https://pleroma.test/users/flor_pl97",
"attachment": [],
"attributedTo": "https://pleroma.test/users/flor_pl97",
"cc": [
"https://pleroma.test/users/flor_pl97/followers"
],
"content": "tomorrow library garden bridge the festival bicycle together garden early quiet morning library the postcard tomorrow mountain the small rain postcard warm garden market map mountain map (p10)",
"contentMap": {
"en": "tomorrow library garden bridge the festival bicycle together garden early quiet morning library the postcard tomorrow mountain the small rain postcard warm garden market map mountain map (p10)"
},
"context": "https://pleroma.test/contexts/891653c5-91ba-43f3-b3ef-b066caa3fa4d",
"conversation": "https://pleroma.test/contexts/891653c5-91ba-43f3-b3ef-b066caa3fa4d",
"published": "2026-10-05T00:49:58.860071Z",
"repliesCount": 2,
"source": {
"content": "tomorrow library garden bridge the festival bicycle together garden early quiet morning library the postcard tomorrow mountain the small rain postcard warm garden market map mountain map (p10)",
"mediaType": "text/plain"
},
"summary": "",
"tag": [],
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Note"
}
],
"totalItems": 1,
"type": "OrderedCollection"
},
"id": "https://pleroma.test/objects/6101c4fd-ed46-461c-a3fe-9742852f47da",
"published": "2026-10-05T00:49:58.860071Z",
"replies": {
"first": {
"id": "https://pleroma.test/objects/6101c4fd-ed46-461c-a3fe-9742852f47da/replies?page=true",
"orderedItems": [
"https://pleroma.test/objects/b7b7c066-51a8-46d1-8919-07b87f158887",
"https://privapub.test/peasants/vera_pp40/scribbles/6ac2f4520c61eaf3ab241d35"
],
"partOf": "https://pleroma.test/objects/6101c4fd-ed46-461c-a3fe-9742852f47da/replies",
"type": "OrderedCollectionPage"
},
"id": "https://pleroma.test/objects/6101c4fd-ed46-461c-a3fe-9742852f47da/replies",
"totalItems": 2,
"type": "OrderedCollection"
},
"repliesCount": 2,
"source": {
"content": "mountain late window map bright postcard (p10) (edited)",
"mediaType": "text/plain"
},
"summary": "",
"tag": [],
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Note",
"updated": "2026-10-05T00:52:40.105051Z"
}