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:
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"
|
||||
}
|
||||
Reference in new issue
Block a user