Followed hashtags, and a tag's week of use
tags/:name shows the tag's last seven days in public posts PrivaPub holds (uses and authors, as Mastodon gives them) and whether the persona follows it; follow, unfollow and followed_tags replace the stubs. A public post that is neither a boost nor a reply comes, as it arrives, to the homes of those following one of its tags. Nothing is fetched for a followed tag and no other server hears of it. Posts are indexed by tag. 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
864dae4469
commit
7bfd7ce285
9 files changed
+229
-13
No files matched your search
@@ -81,7 +81,6 @@ namespace PrivaPub.Tests.Http
|
||||
[Theory]
|
||||
[InlineData("/api/v1/suggestions")]
|
||||
[InlineData("/api/v2/suggestions")]
|
||||
[InlineData("/api/v1/followed_tags")]
|
||||
[InlineData("/api/v1/endorsements")]
|
||||
[InlineData("/api/v1/featured_tags")]
|
||||
public async Task Signed_in_stub_routes_answer_empty_lists(string path)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
using PrivaPub.Tests.Support;
|
||||
using PrivaPub.Tests.Support.Host;
|
||||
|
||||
using System.Net;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace PrivaPub.Tests.Http
|
||||
{
|
||||
// Hashtags: a tag's week of use in public posts, and followed tags, whose public posts come to the persona's home
|
||||
[Trait("Category", "Integration")]
|
||||
public sealed class MastodonTagsTests : IAsyncLifetime
|
||||
{
|
||||
PrivaPubHost _host;
|
||||
Peer _peer;
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip);
|
||||
_host = await PrivaPubHost.Shared();
|
||||
_peer = await Peer.Start();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_peer != default)
|
||||
await _peer.DisposeAsync();
|
||||
}
|
||||
|
||||
static string NewTag() => $"t{Guid.NewGuid():N}"[..12];
|
||||
|
||||
[Fact]
|
||||
public async Task A_tag_shows_its_week_of_public_use_and_whether_the_persona_follows_it()
|
||||
{
|
||||
var alice = await _host.Mastodon("alice");
|
||||
var bob = await _host.Mastodon("bob");
|
||||
var tag = NewTag();
|
||||
await alice.Status($"about #{tag}");
|
||||
await alice.Status($"more about #{tag.ToUpperInvariant()}");
|
||||
await bob.Status($"also #{tag}");
|
||||
await bob.Status($"quietly #{tag}", ("visibility", "unlisted"));
|
||||
|
||||
var shown = (await _host.Client().Get($"/api/v1/tags/{tag}")).Ok().Object;
|
||||
Assert.Equal(tag, shown.Text("name"));
|
||||
Assert.False(shown.Flag("following"));
|
||||
var history = shown["history"]!.AsArray();
|
||||
Assert.Equal(7, history.Count);
|
||||
Assert.Equal(("3", "2"), (history[0].Text("uses"), history[0].Text("accounts")));
|
||||
Assert.Equal(new DateTimeOffset(DateTime.UtcNow.Date, TimeSpan.Zero).ToUnixTimeSeconds().ToString(), history[0].Text("day"));
|
||||
|
||||
Assert.True((await alice.Client.Post($"/api/v1/tags/{tag}/follow")).Ok().Body.Flag("following"));
|
||||
Assert.True((await alice.Client.Get($"/api/v1/tags/%23{tag.ToUpperInvariant()}")).Ok().Body.Flag("following"));
|
||||
Assert.Equal(new[] { tag }, (await alice.Client.Get("/api/v1/followed_tags")).Ok().Array.Select(t => t.Text("name")));
|
||||
Assert.False((await bob.Client.Get($"/api/v1/tags/{tag}")).Ok().Body.Flag("following"));
|
||||
Assert.False((await alice.Client.Post($"/api/v1/tags/{tag}/unfollow")).Ok().Body.Flag("following"));
|
||||
Assert.Empty((await alice.Client.Get("/api/v1/followed_tags")).Ok().Array);
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, (await _host.Client().Post($"/api/v1/tags/{tag}/follow")).Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_followed_tags_public_posts_come_home_from_here_and_from_other_servers()
|
||||
{
|
||||
var alice = await _host.Mastodon("alice");
|
||||
var bob = await _host.Mastodon("bob");
|
||||
var carol = await _host.Mastodon("carol");
|
||||
var remote = new RemoteActor(_peer, "dora");
|
||||
var tag = NewTag();
|
||||
(await alice.Client.Post($"/api/v1/tags/{tag}/follow")).Ok();
|
||||
|
||||
var open = (await bob.Status($"news about #{tag}")).Text("id");
|
||||
var quiet = (await bob.Status($"quiet news #{tag}", ("visibility", "unlisted"))).Text("id");
|
||||
var answer = (await bob.Status($"and #{tag} again", ("in_reply_to_id", open))).Text("id");
|
||||
var untagged = (await bob.Status("nothing tagged")).Text("id");
|
||||
var far = await _host.PublicPostFrom(remote, carol, $"<p>far away #{tag}</p>", note => note["tag"]!.AsArray().Add(new JsonObject
|
||||
{
|
||||
["type"] = "Hashtag",
|
||||
["name"] = "#" + tag,
|
||||
["href"] = $"{remote.Origin()}/tags/{tag}"
|
||||
}));
|
||||
|
||||
var home = (await alice.Client.Get("/api/v1/timelines/home?limit=40")).Ok().Ids.ToList();
|
||||
Assert.Contains(open, home);
|
||||
Assert.Contains(far.ID, home);
|
||||
Assert.DoesNotContain(quiet, home);
|
||||
Assert.DoesNotContain(answer, home);
|
||||
Assert.DoesNotContain(untagged, home);
|
||||
Assert.DoesNotContain(open, (await carol.Client.Get("/api/v1/timelines/home?limit=40")).Ok().Ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user