Lemmy takes a report only from a person or a service, about one post or comment, addressed to its community, and it answered PrivaPub's Flag (the instance actor's, an Application, with no `to` and the account and posts as its object) 400. A report of a post or comment in a community on a server whose NodeInfo names Lemmy now leaves from `privapub_reports`, a Service with its own key that names nobody: one Flag per post, `to` the community (its own audience, else its thread's), with the persona's words, or the category, in `summary` and `content`, sent to the community's inbox. This is the second exception to "a server's software is for display" (owner decision 2026-10-06, `ReportService.ServiceReportTakers`). Every other server keeps the instance actor's report. An account alone is not reported to Lemmy, which takes no such report, and `forwarded` now says whether anything left. The reporter is read unsigned in SecureMode and answers WebFinger like the instance actor. Nobody follows or mentions it, the Mastodon API has no account for it, and a migration reserves its name. Checked live: Lemmy 1.0 and 0.19 keep the reports of a thread and of a comment, with alice's words, from "Reports from privapub.test", and none names her (69 checks). A sweep of every scenario with this and the next commit: 876 checks pass; Ghost's Network feed listed alice's post too late once, and Ghost passes alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
673 lines
37 KiB
C#
673 lines
37 KiB
C#
using MongoDB.Entities;
|
|
|
|
using PrivaPub.Models.Post;
|
|
using PrivaPub.Models.Social;
|
|
using PrivaPub.Models.User;
|
|
using PrivaPub.Tests.Support;
|
|
using PrivaPub.Tests.Support.Host;
|
|
|
|
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace PrivaPub.Tests.Http
|
|
{
|
|
[Trait("Category", "Integration")]
|
|
public sealed class MastodonAccountsTests : 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 CancellationToken Token => TestContext.Current.CancellationToken;
|
|
|
|
async Task<(RemoteActor Actor, string Id)> Remote(string name = "bob")
|
|
{
|
|
var actor = new RemoteActor(_peer, name);
|
|
var known = await _host.Known(actor);
|
|
return (actor, known.ID);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Verify_credentials_shows_the_persona_and_its_source_and_nothing_of_the_login()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
|
|
var me = (await alice.Client.Get("/api/v1/accounts/verify_credentials")).Ok();
|
|
|
|
Assert.Equal(alice.Id, me.Body.Text("id"));
|
|
Assert.Equal(alice.UserName, me.Body.Text("username"));
|
|
Assert.Equal(alice.UserName, me.Body.Text("acct"));
|
|
Assert.Equal(alice.Uri, me.Body.Text("uri"));
|
|
Assert.Equal("public", me.Body["source"].Text("privacy"));
|
|
Assert.Equal("public", me.Body["source"].Text("quote_policy"));
|
|
Assert.Equal(0, me.Body["source"].Number("follow_requests_count"));
|
|
Assert.DoesNotContain(alice.Persona.Root.Id, me.Text);
|
|
Assert.DoesNotContain(alice.Persona.Root.UserName, me.Text);
|
|
Assert.Equal(HttpStatusCode.Unauthorized, (await _host.Client().Get("/api/v1/accounts/verify_credentials")).Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Update_credentials_takes_fields_in_both_shapes_the_source_and_the_quote_policy()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
|
|
var updated = (await alice.Client.Patch("/api/v1/accounts/update_credentials",
|
|
("display_name", " Alice Liddell "), ("note", "down the hole"), ("locked", "true"), ("bot", "true"),
|
|
("fields_attributes[0][name]", "Site"), ("fields_attributes[0][value]", "https://example.com"),
|
|
("fields_attributes[1][name]", "Pronouns"), ("fields_attributes[1][value]", "she/her"),
|
|
("source[privacy]", "private"), ("source[sensitive]", "true"), ("source[language]", "it"), ("source[quote_policy]", "followers"))).Ok();
|
|
|
|
Assert.Equal("Alice Liddell", updated.Body.Text("display_name"));
|
|
Assert.True(updated.Body.Flag("locked"));
|
|
Assert.True(updated.Body.Flag("bot"));
|
|
Assert.Equal(new[] { "Site", "Pronouns" }, updated.Body["fields"]!.AsArray().Select(f => f.Text("name")));
|
|
Assert.Equal("down the hole", updated.Body["source"].Text("note"));
|
|
Assert.Equal("private", updated.Body["source"].Text("privacy"));
|
|
Assert.True(updated.Body["source"].Flag("sensitive"));
|
|
Assert.Equal("it", updated.Body["source"].Text("language"));
|
|
Assert.Equal("followers", updated.Body["source"].Text("quote_policy"));
|
|
|
|
var arrayShape = (await alice.Client.Json(HttpMethod.Patch, "/api/v1/accounts/update_credentials", new JsonObject
|
|
{
|
|
["fields_attributes"] = new JsonArray(new JsonObject { ["name"] = "Garden", ["value"] = "roses" }, new JsonObject { ["name"] = "Cat", ["value"] = "Dinah" }),
|
|
["source"] = new JsonObject { ["quote_policy"] = "everyone" }
|
|
})).Ok();
|
|
Assert.Equal(new[] { ("Garden", "roses"), ("Cat", "Dinah") },
|
|
arrayShape.Body["source"]!["fields"]!.AsArray().Select(f => (f.Text("name"), f.Text("value"))));
|
|
Assert.Equal("followers", arrayShape.Body["source"].Text("quote_policy"));
|
|
|
|
var formArray = (await alice.Client.Patch("/api/v1/accounts/update_credentials",
|
|
("fields_attributes[][name]", "One"), ("fields_attributes[][value]", "1"), ("fields_attributes[][name]", "Two"), ("fields_attributes[][value]", "2"))).Ok();
|
|
Assert.Equal(new[] { ("One", "1"), ("Two", "2") }, formArray.Body["source"]!["fields"]!.AsArray().Select(f => (f.Text("name"), f.Text("value"))));
|
|
|
|
var again = (await alice.Client.Get("/api/v1/accounts/verify_credentials")).Ok();
|
|
Assert.Equal(2, again.Body["fields"]!.AsArray().Count);
|
|
Assert.Equal("private", again.Body["source"].Text("privacy"));
|
|
var status = await alice.Status("with the default visibility");
|
|
Assert.Equal("private", status.Text("visibility"));
|
|
Assert.True(status.Flag("sensitive"));
|
|
Assert.Equal("it", status.Text("language"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Avatar_and_header_uploads_are_resized_and_lose_their_metadata()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
var picture = MastodonHelpers.JpegWithMetadata(900, 700);
|
|
using (var original = NetVips.Image.NewFromBuffer(picture))
|
|
Assert.Contains("exif-data", original.GetFields());
|
|
var form = MastodonHelpers.Multipart(("avatar", picture, "image/jpeg", "me.jpg"), ("header", MastodonHelpers.JpegWithMetadata(2000, 900), "image/jpeg", "header.jpg"));
|
|
form.Add(new StringContent("Alice"), "display_name");
|
|
|
|
var updated = (await alice.Client.Exchange(new HttpRequestMessage(HttpMethod.Patch, "/api/v1/accounts/update_credentials") { Content = form })).Ok();
|
|
|
|
Assert.Equal("Alice", updated.Body.Text("display_name"));
|
|
foreach (var (field, maxWidth, maxHeight) in new[] { ("avatar", 400, 400), ("header", 1500, 500) })
|
|
{
|
|
var url = updated.Body.Text(field);
|
|
Assert.StartsWith($"{PrivaPubHost.Base}/media/files/", url);
|
|
var bytes = await _host.Client().GetByteArrayAsync(url, Token);
|
|
MastodonHelpers.AssertNoMetadata(bytes);
|
|
using var stored = NetVips.Image.NewFromBuffer(bytes);
|
|
Assert.True(stored.Width <= maxWidth && stored.Height <= maxHeight, $"{field} is {stored.Width}x{stored.Height}");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Lookup_finds_local_and_remote_accounts_but_never_a_circle_or_the_servers_own_actors()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
var (bob, bobId) = await Remote();
|
|
var circleName = $"circle{Guid.NewGuid():N}"[..20];
|
|
string circleId;
|
|
using (var clientApi = _host.As(alice.Persona.Root.Jwt))
|
|
{
|
|
var circle = await clientApi.PostAsJsonAsync("/clientapi/group/insert", new { avatarId = alice.Id, userName = circleName, name = "inner", isCommunity = false }, Token);
|
|
Assert.Equal(HttpStatusCode.OK, circle.StatusCode);
|
|
circleId = (await circle.Content.ReadFromJsonAsync<JsonObject>(Token))!["id"]!.GetValue<string>();
|
|
}
|
|
var anonymous = _host.Client();
|
|
|
|
Assert.Equal(alice.Id, (await anonymous.Get($"/api/v1/accounts/lookup?acct={alice.UserName}")).Ok().Body.Text("id"));
|
|
Assert.Equal(alice.Id, (await anonymous.Get($"/api/v1/accounts/lookup?acct=@{alice.UserName}@{PrivaPubHost.Host}")).Ok().Body.Text("id"));
|
|
var remote = (await anonymous.Get($"/api/v1/accounts/lookup?acct={Uri.EscapeDataString(bob.Handle())}")).Ok();
|
|
Assert.Equal(bobId, remote.Body.Text("id"));
|
|
Assert.Equal(bob.Handle(), remote.Body.Text("acct"));
|
|
Assert.Equal(HttpStatusCode.NotFound, (await anonymous.Get($"/api/v1/accounts/lookup?acct={circleName}")).Status);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await alice.Client.Get($"/api/v1/accounts/lookup?acct={circleName}")).Status);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await anonymous.Get("/api/v1/accounts/lookup?acct=privapub")).Status);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await anonymous.Get("/api/v1/accounts/lookup?acct=privapub_reports")).Status);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await anonymous.Get($"/api/v1/accounts/lookup?acct=nobody{Guid.NewGuid():N}")).Status);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await anonymous.Get("/api/v1/accounts/lookup?acct=a@b@c")).Status);
|
|
|
|
Assert.Equal(alice.UserName, (await anonymous.Get($"/api/v1/accounts/{alice.Id}")).Ok().Body.Text("username"));
|
|
Assert.Equal(bob.Handle(), (await anonymous.Get($"/api/v1/accounts/{bobId}")).Ok().Body.Text("acct"));
|
|
Assert.Equal(HttpStatusCode.NotFound, (await anonymous.Get("/api/v1/accounts/000000000000000000000000")).Status);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await alice.Client.Get($"/api/v1/accounts/{circleId}")).Status);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await alice.Client.Get($"/api/v1/accounts/{circleId}/statuses")).Status);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await alice.Client.Post($"/api/v1/accounts/{circleId}/follow")).Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Search_resolves_a_remote_handle_only_when_asked_by_a_signed_in_persona()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
var bob = new RemoteActor(_peer, "bob");
|
|
_peer.WebFinger(bob);
|
|
var handle = Uri.EscapeDataString(bob.Handle());
|
|
|
|
Assert.Empty((await alice.Client.Get($"/api/v1/accounts/search?q={handle}")).Ok().Array);
|
|
Assert.Empty((await alice.Client.Get($"/api/v2/search?q={handle}")).Ok().Body["accounts"]!.AsArray());
|
|
Assert.Equal(HttpStatusCode.Unauthorized, (await _host.Client().Get($"/api/v1/accounts/search?q={handle}&resolve=true")).Status);
|
|
Assert.Equal(HttpStatusCode.Unauthorized, (await _host.Client().Get($"/api/v2/search?q={handle}&resolve=true")).Status);
|
|
Assert.Empty(_peer.Requests);
|
|
|
|
var resolved = (await alice.Client.Get($"/api/v1/accounts/search?q={handle}&resolve=true")).Ok();
|
|
|
|
var found = Assert.Single(resolved.Array);
|
|
Assert.Equal(bob.Handle(), found.Text("acct"));
|
|
Assert.Contains(_peer.Requests, r => r.Path == "/.well-known/webfinger");
|
|
Assert.Equal(found.Text("id"), Assert.Single((await alice.Client.Get($"/api/v2/search?q={handle}&type=accounts")).Ok().Body["accounts"]!.AsArray()).Text("id"));
|
|
Assert.Contains(alice.Id, (await alice.Client.Get($"/api/v1/accounts/search?q={alice.UserName}")).Ok().Ids);
|
|
}
|
|
|
|
// an account elsewhere: what it features, in its order, its public posts only
|
|
[Fact]
|
|
public async Task A_remote_accounts_pinned_statuses_are_what_it_features()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
var (bob, bobId) = await Remote();
|
|
Post Held(string text, PostVisibility visibility) => new()
|
|
{
|
|
ObjectURI = $"{bob.Id}/statuses/{Guid.NewGuid():N}", ActorURI = bob.Id, AuthorAccountId = bobId, IsFederatedCopy = true,
|
|
Visibility = visibility, ContentHtml = $"<p>{text}</p>", CreationDate = DateTime.UtcNow, UpdateDate = DateTime.UtcNow
|
|
};
|
|
var first = Held("first pin", PostVisibility.Public);
|
|
var second = Held("second pin", PostVisibility.Unlisted);
|
|
var hidden = Held("for followers", PostVisibility.FollowersOnly);
|
|
await DB.Default.SaveAsync(new[] { first, second, hidden }, Token);
|
|
await DB.Default.SaveAsync(new[]
|
|
{
|
|
new RemoteFeatured { ActorURI = bob.Id, ObjectURI = second.ObjectURI, PostId = second.ID, Position = 1 },
|
|
new RemoteFeatured { ActorURI = bob.Id, ObjectURI = hidden.ObjectURI, PostId = hidden.ID, Position = 2 },
|
|
new RemoteFeatured { ActorURI = bob.Id, ObjectURI = first.ObjectURI, PostId = first.ID, Position = 0 }
|
|
}, Token);
|
|
|
|
var pinned = (await alice.Client.Get($"/api/v1/accounts/{bobId}/statuses?pinned=true")).Ok();
|
|
|
|
Assert.Equal(new[] { first.ID, second.ID }, pinned.Ids);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Account_statuses_filter_pins_replies_reblogs_media_and_tags_and_page_both_ways()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
var carol = await _host.Mastodon("carol");
|
|
var tag = $"tag{Guid.NewGuid():N}"[..16];
|
|
var tagged = await alice.Status($"first #{tag}");
|
|
var thread = await alice.Status("and a thread", ("in_reply_to_id", tagged.Text("id")));
|
|
var upload = (await alice.Client.Exchange(new HttpRequestMessage(HttpMethod.Post, "/api/v2/media")
|
|
{
|
|
Content = MastodonHelpers.Multipart(("file", MastodonHelpers.JpegWithMetadata(64, 48), "image/jpeg", "a.jpg"))
|
|
})).Ok();
|
|
var withMedia = await alice.Status("a picture", ("media_ids[]", upload.Body.Text("id")));
|
|
var carolPost = await carol.Status("boost me");
|
|
var answer = await alice.Status("an answer", ("in_reply_to_id", carolPost.Text("id")));
|
|
var reblog = (await alice.Client.Post($"/api/v1/statuses/{carolPost.Text("id")}/reblog")).Ok().Body;
|
|
var pinned = await alice.Status("pinned");
|
|
(await alice.Client.Post($"/api/v1/statuses/{pinned.Text("id")}/pin")).Ok();
|
|
var path = $"/api/v1/accounts/{alice.Id}/statuses";
|
|
string[] Ids(params JsonObject[] statuses) => statuses.Select(s => s.Text("id")).ToArray();
|
|
|
|
Assert.Equal(Ids(pinned), (await carol.Client.Get(path + "?pinned=true")).Ok().Ids);
|
|
Assert.True((await carol.Client.Get(path + "?pinned=true")).Ok().Array[0].Flag("pinned"));
|
|
Assert.Equal(Ids(pinned, reblog.AsObject(), answer, withMedia, thread, tagged), (await carol.Client.Get(path)).Ok().Ids);
|
|
Assert.Equal(Ids(pinned, reblog.AsObject(), withMedia, thread, tagged), (await carol.Client.Get(path + "?exclude_replies=true")).Ok().Ids);
|
|
Assert.DoesNotContain(reblog.Text("id"), (await carol.Client.Get(path + "?exclude_reblogs=true")).Ok().Ids);
|
|
Assert.Equal(Ids(withMedia), (await carol.Client.Get(path + "?only_media=true")).Ok().Ids);
|
|
Assert.Equal(Ids(tagged), (await _host.Client().Get(path + $"?tagged={tag.ToUpperInvariant()}")).Ok().Ids);
|
|
|
|
var first = (await carol.Client.Get(path + "?limit=2")).Ok();
|
|
Assert.Equal(Ids(pinned, reblog.AsObject()), first.Ids);
|
|
var next = (await carol.Client.Get(first.Link("next"))).Ok();
|
|
Assert.Equal(Ids(answer, withMedia), next.Ids);
|
|
Assert.Contains("limit=2", next.Link("prev"));
|
|
Assert.Equal(first.Ids, (await carol.Client.Get(next.Link("prev"))).Ok().Ids);
|
|
var last = (await carol.Client.Get(next.Link("next"))).Ok();
|
|
Assert.Equal(Ids(thread, tagged), last.Ids);
|
|
Assert.Empty((await carol.Client.Get(last.Link("next"))).Ok().Array);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_community_account_lists_the_posts_addressed_to_it()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
var reader = await _host.Mastodon("reader");
|
|
var name = $"community{Guid.NewGuid():N}"[..20];
|
|
string communityId;
|
|
using (var clientApi = _host.As(alice.Persona.Root.Jwt))
|
|
{
|
|
var created = await clientApi.PostAsJsonAsync("/clientapi/group/insert", new { avatarId = alice.Id, userName = name, name = "Gardeners", isCommunity = true }, Token);
|
|
Assert.Equal(HttpStatusCode.OK, created.StatusCode);
|
|
communityId = (await created.Content.ReadFromJsonAsync<JsonObject>(Token))!["id"]!.GetValue<string>();
|
|
}
|
|
var addressed = await alice.Status($"@{name} first meeting on Sunday");
|
|
await alice.Status("not for the garden");
|
|
|
|
var community = (await reader.Client.Get($"/api/v1/accounts/lookup?acct={name}")).Ok();
|
|
Assert.Equal(communityId, community.Body.Text("id"));
|
|
Assert.True(community.Body.Flag("group"));
|
|
Assert.Equal(new[] { addressed.Text("id") }, (await reader.Client.Get($"/api/v1/accounts/{communityId}/statuses")).Ok().Ids);
|
|
Assert.Equal(1, (await _host.Client().Get($"/api/v1/accounts/{communityId}")).Ok().Body.Number("statuses_count"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_follower_sees_followers_only_posts_and_a_stranger_does_not()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
var follower = await _host.Mastodon("follower");
|
|
var stranger = await _host.Mastodon("stranger");
|
|
(await follower.Client.Post($"/api/v1/accounts/{alice.Id}/follow")).Ok();
|
|
var open = await alice.Status("for everyone");
|
|
var quiet = await alice.Status("for followers", ("visibility", "private"));
|
|
var direct = await alice.Status($"@{follower.UserName} for you", ("visibility", "direct"));
|
|
var path = $"/api/v1/accounts/{alice.Id}/statuses";
|
|
|
|
Assert.Equal(new[] { quiet.Text("id"), open.Text("id") }, (await follower.Client.Get(path)).Ok().Ids);
|
|
Assert.Equal(new[] { open.Text("id") }, (await stranger.Client.Get(path)).Ok().Ids);
|
|
Assert.Equal(new[] { open.Text("id") }, (await _host.Client().Get(path)).Ok().Ids);
|
|
Assert.Equal(new[] { direct.Text("id"), quiet.Text("id"), open.Text("id") }, (await alice.Client.Get(path)).Ok().Ids);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await stranger.Client.Get($"/api/v1/statuses/{quiet.Text("id")}")).Status);
|
|
Assert.Equal(HttpStatusCode.OK, (await follower.Client.Get($"/api/v1/statuses/{quiet.Text("id")}")).Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Followers_and_following_are_listed_only_to_their_owner()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
var bob = await _host.Mastodon("bob");
|
|
(await bob.Client.Post($"/api/v1/accounts/{alice.Id}/follow")).Ok();
|
|
|
|
Assert.Equal(new[] { bob.Id }, (await alice.Client.Get($"/api/v1/accounts/{alice.Id}/followers")).Ok().Ids);
|
|
Assert.Empty((await bob.Client.Get($"/api/v1/accounts/{alice.Id}/followers")).Ok().Array);
|
|
Assert.Empty((await _host.Client().Get($"/api/v1/accounts/{alice.Id}/followers")).Ok().Array);
|
|
Assert.Equal(new[] { alice.Id }, (await bob.Client.Get($"/api/v1/accounts/{bob.Id}/following")).Ok().Ids);
|
|
Assert.Empty((await alice.Client.Get($"/api/v1/accounts/{bob.Id}/following")).Ok().Array);
|
|
var account = (await bob.Client.Get($"/api/v1/accounts/{alice.Id}")).Ok();
|
|
Assert.Equal(1, account.Body.Number("followers_count"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Following_is_immediate_for_an_open_persona_requested_for_a_locked_one_and_queued_for_a_remote_one()
|
|
{
|
|
var since = DateTime.UtcNow.AddSeconds(-1);
|
|
var alice = await _host.Mastodon("alice");
|
|
var locked = await _host.Mastodon("locked");
|
|
var follower = await _host.Mastodon("follower");
|
|
(await locked.Client.Patch("/api/v1/accounts/update_credentials", ("locked", "true"))).Ok();
|
|
var (bob, bobId) = await Remote();
|
|
|
|
var open = (await follower.Client.Post($"/api/v1/accounts/{alice.Id}/follow")).Ok();
|
|
Assert.True(open.Body.Flag("following"));
|
|
Assert.False(open.Body.Flag("requested"));
|
|
Assert.True(open.Body.Flag("showing_reblogs"));
|
|
var notified = (await alice.Client.Get("/api/v1/notifications?types[]=follow")).Ok();
|
|
Assert.Equal(follower.Id, Assert.Single(notified.Array)!["account"].Text("id"));
|
|
|
|
var asked = (await follower.Client.Post($"/api/v1/accounts/{locked.Id}/follow", ("reblogs", "false"))).Ok();
|
|
Assert.False(asked.Body.Flag("following"));
|
|
Assert.True(asked.Body.Flag("requested"));
|
|
Assert.Equal(new[] { follower.Id }, (await locked.Client.Get("/api/v1/follow_requests")).Ok().Ids);
|
|
Assert.Equal(1, (await locked.Client.Get("/api/v1/accounts/verify_credentials")).Ok().Body["source"].Number("follow_requests_count"));
|
|
Assert.True((await locked.Client.Post($"/api/v1/follow_requests/{follower.Id}/authorize")).Ok().Body.Flag("followed_by"));
|
|
var accepted = Assert.Single((await follower.Client.Get($"/api/v1/accounts/relationships?id[]={locked.Id}")).Ok().Array);
|
|
Assert.True(accepted.Flag("following"));
|
|
Assert.False(accepted.Flag("showing_reblogs"));
|
|
Assert.Empty((await locked.Client.Get("/api/v1/follow_requests")).Ok().Array);
|
|
|
|
var remote = (await follower.Client.Post($"/api/v1/accounts/{bobId}/follow")).Ok();
|
|
Assert.True(remote.Body.Flag("requested"));
|
|
var follow = Assert.Single(await bob.Delivered(since), d => d.Type() == "Follow");
|
|
Assert.Equal(follower.Uri, follow.Text("actor"));
|
|
Assert.Equal(bob.Id, follow.Text("object"));
|
|
|
|
Assert.False((await follower.Client.Post($"/api/v1/accounts/{alice.Id}/unfollow")).Ok().Body.Flag("following"));
|
|
Assert.Empty((await alice.Client.Get($"/api/v1/accounts/{alice.Id}/followers")).Ok().Array);
|
|
Assert.False((await follower.Client.Post($"/api/v1/accounts/{bobId}/unfollow")).Ok().Body.Flag("requested"));
|
|
var undo = Assert.Single(await bob.Delivered(since), d => d.Type() == "Undo");
|
|
Assert.Equal(follow.Text("id"), undo["object"].Text("id"));
|
|
Assert.Equal(HttpStatusCode.NotFound, (await follower.Client.Post("/api/v1/accounts/000000000000000000000000/follow")).Status);
|
|
Assert.Equal(HttpStatusCode.Forbidden, (await follower.Client.Post($"/api/v1/accounts/{follower.Id}/follow")).Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Remote_follow_requests_are_answered_with_the_original_follow()
|
|
{
|
|
var since = DateTime.UtcNow.AddSeconds(-1);
|
|
var locked = await _host.Mastodon("locked");
|
|
(await locked.Client.Patch("/api/v1/accounts/update_credentials", ("locked", "true"))).Ok();
|
|
var bob = new RemoteActor(_peer, "bob");
|
|
var carol = new RemoteActor(_peer, "carol");
|
|
var bobFollow = await _host.FollowedBy(locked, bob);
|
|
var carolFollow = await _host.FollowedBy(locked, carol);
|
|
var bobId = (await _host.Known(bob)).ID;
|
|
var carolId = (await _host.Known(carol)).ID;
|
|
|
|
var requests = (await locked.Client.Get("/api/v1/follow_requests")).Ok();
|
|
Assert.Equal(new[] { carolId, bobId }, requests.Ids);
|
|
Assert.True(Assert.Single((await locked.Client.Get($"/api/v1/accounts/relationships?id[]={bobId}")).Ok().Array).Flag("requested_by"));
|
|
Assert.Empty(await bob.Delivered(since));
|
|
|
|
Assert.True((await locked.Client.Post($"/api/v1/follow_requests/{bobId}/authorize")).Ok().Body.Flag("followed_by"));
|
|
Assert.False((await locked.Client.Post($"/api/v1/follow_requests/{carolId}/reject")).Ok().Body.Flag("requested_by"));
|
|
|
|
var accept = Assert.Single(await bob.Delivered(since));
|
|
Assert.Equal("Accept", accept.Type());
|
|
Assert.Equal(locked.Uri, accept.Text("actor"));
|
|
Assert.Equal(bobFollow, accept["object"].Text("id"));
|
|
var reject = Assert.Single(await carol.Delivered(since));
|
|
Assert.Equal("Reject", reject.Type());
|
|
Assert.Equal(carolFollow, reject["object"].Text("id"));
|
|
Assert.Equal(carol.Id, reject["object"].Text("actor"));
|
|
Assert.Empty((await locked.Client.Get("/api/v1/follow_requests")).Ok().Array);
|
|
Assert.Equal(new[] { bobId }, (await locked.Client.Get($"/api/v1/accounts/{locked.Id}/followers")).Ok().Ids);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await locked.Client.Post($"/api/v1/follow_requests/{carolId}/authorize")).Status);
|
|
|
|
var local = await _host.Mastodon("local");
|
|
(await local.Client.Post($"/api/v1/accounts/{locked.Id}/follow")).Ok();
|
|
(await locked.Client.Post($"/api/v1/follow_requests/{local.Id}/reject")).Ok();
|
|
var refused = Assert.Single((await local.Client.Get($"/api/v1/accounts/relationships?id[]={locked.Id}")).Ok().Array);
|
|
Assert.False(refused.Flag("requested"));
|
|
Assert.False(refused.Flag("following"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Removing_a_follower_ends_the_follow_on_both_sides_and_tells_a_remote_follower()
|
|
{
|
|
var since = DateTime.UtcNow.AddSeconds(-1);
|
|
var alice = await _host.Mastodon("alice");
|
|
var local = await _host.Mastodon("local");
|
|
var bob = new RemoteActor(_peer, "bob");
|
|
var follow = await _host.FollowedBy(alice, bob);
|
|
var bobId = (await _host.Known(bob)).ID;
|
|
(await local.Client.Post($"/api/v1/accounts/{alice.Id}/follow")).Ok();
|
|
Assert.True(Assert.Single((await alice.Client.Get($"/api/v1/accounts/relationships?id[]={bobId}")).Ok().Array).Flag("followed_by"));
|
|
|
|
var removed = (await alice.Client.Post($"/api/v1/accounts/{bobId}/remove_from_followers")).Ok();
|
|
|
|
Assert.False(removed.Body.Flag("followed_by"));
|
|
var reject = Assert.Single(await bob.Delivered(since), d => d.Type() == "Reject");
|
|
Assert.Equal(alice.Uri, reject.Text("actor"));
|
|
Assert.Equal(follow, reject["object"].Text("id"));
|
|
Assert.Equal("Follow", reject["object"].Text("type"));
|
|
Assert.Equal(bob.Id, reject["object"].Text("actor"));
|
|
Assert.Equal(alice.Uri, reject["object"].Text("object"));
|
|
|
|
Assert.False((await alice.Client.Post($"/api/v1/accounts/{local.Id}/remove_from_followers")).Ok().Body.Flag("followed_by"));
|
|
Assert.False(Assert.Single((await local.Client.Get($"/api/v1/accounts/relationships?id[]={alice.Id}")).Ok().Array).Flag("following"));
|
|
Assert.Empty((await alice.Client.Get($"/api/v1/accounts/{alice.Id}/followers")).Ok().Array);
|
|
Assert.Empty((await local.Client.Get($"/api/v1/accounts/{local.Id}/following")).Ok().Array);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await alice.Client.Post("/api/v1/accounts/000000000000000000000000/remove_from_followers")).Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Blocks_federate_and_reject_a_remote_follower()
|
|
{
|
|
var since = DateTime.UtcNow.AddSeconds(-1);
|
|
var alice = await _host.Mastodon("alice");
|
|
var local = await _host.Mastodon("local");
|
|
var bob = new RemoteActor(_peer, "bob");
|
|
var follow = await _host.FollowedBy(alice, bob);
|
|
var bobId = (await _host.Known(bob)).ID;
|
|
(await local.Client.Post($"/api/v1/accounts/{alice.Id}/follow")).Ok();
|
|
|
|
var blocked = (await alice.Client.Post($"/api/v1/accounts/{bobId}/block")).Ok();
|
|
|
|
Assert.True(blocked.Body.Flag("blocking"));
|
|
Assert.False(blocked.Body.Flag("followed_by"));
|
|
var sent = await bob.Delivered(since);
|
|
var reject = Assert.Single(sent, d => d.Type() == "Reject");
|
|
Assert.Equal((follow, "Follow", bob.Id), (reject["object"].Text("id"), reject["object"].Text("type"), reject["object"].Text("actor")));
|
|
var block = Assert.Single(sent, d => d.Type() == "Block");
|
|
Assert.Equal((alice.Uri, bob.Id), (block.Text("actor"), block.Text("object")));
|
|
Assert.True((await alice.Client.Post($"/api/v1/accounts/{local.Id}/block")).Ok().Body.Flag("blocking"));
|
|
Assert.Equal(new[] { local.Id, bobId }, (await alice.Client.Get("/api/v1/blocks")).Ok().Ids);
|
|
var blockedBy = Assert.Single((await local.Client.Get($"/api/v1/accounts/relationships?id[]={alice.Id}")).Ok().Array);
|
|
Assert.True(blockedBy.Flag("blocked_by"));
|
|
Assert.False(blockedBy.Flag("following"));
|
|
Assert.Empty((await alice.Client.Get($"/api/v1/accounts/{alice.Id}/followers")).Ok().Array);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await alice.Client.Post($"/api/v1/accounts/{alice.Id}/block")).Status);
|
|
|
|
Assert.False((await alice.Client.Post($"/api/v1/accounts/{bobId}/unblock")).Ok().Body.Flag("blocking"));
|
|
var undo = Assert.Single(await bob.Delivered(since), d => d.Type() == "Undo");
|
|
Assert.Equal(block.Text("id"), undo["object"].Text("id"));
|
|
(await alice.Client.Post($"/api/v1/accounts/{local.Id}/unblock")).Ok();
|
|
Assert.Empty((await alice.Client.Get("/api/v1/blocks")).Ok().Array);
|
|
Assert.Equal(new[] { "Accept", "Block", "Reject", "Undo" }, (await bob.Delivered(since)).Select(d => d.Type()).Order());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutes_keep_their_duration_and_notification_choice_and_never_federate()
|
|
{
|
|
var since = DateTime.UtcNow.AddSeconds(-1);
|
|
var alice = await _host.Mastodon("alice");
|
|
var chatty = await _host.Mastodon("chatty");
|
|
var muffled = await _host.Mastodon("muffled");
|
|
var (bob, bobId) = await Remote();
|
|
(await alice.Client.Post($"/api/v1/accounts/{chatty.Id}/follow")).Ok();
|
|
var post = (await alice.Status("like me")).Text("id");
|
|
|
|
var muted = (await alice.Client.Post($"/api/v1/accounts/{chatty.Id}/mute", ("duration", "3600"), ("notifications", "false"))).Ok();
|
|
Assert.True(muted.Body.Flag("muting"));
|
|
Assert.False(muted.Body.Flag("muting_notifications"));
|
|
var mute = await DB.Default.Find<Mute>().Match(m => m.AvatarId == alice.Id && m.TargetAccountId == chatty.Id).ExecuteFirstAsync(Token);
|
|
Assert.InRange(mute.ExpiresAt!.Value, DateTime.UtcNow.AddMinutes(59), DateTime.UtcNow.AddMinutes(61));
|
|
var forever = (await alice.Client.Post($"/api/v1/accounts/{muffled.Id}/mute")).Ok();
|
|
Assert.True(forever.Body.Flag("muting_notifications"));
|
|
Assert.Null((await DB.Default.Find<Mute>().Match(m => m.AvatarId == alice.Id && m.TargetAccountId == muffled.Id).ExecuteFirstAsync(Token)).ExpiresAt);
|
|
(await alice.Client.Post($"/api/v1/accounts/{bobId}/mute")).Ok();
|
|
Assert.Equal(new[] { bobId, muffled.Id, chatty.Id }, (await alice.Client.Get("/api/v1/mutes")).Ok().Ids);
|
|
|
|
var hidden = (await chatty.Status("you will not see this")).Text("id");
|
|
(await chatty.Client.Post($"/api/v1/statuses/{post}/favourite")).Ok();
|
|
(await muffled.Client.Post($"/api/v1/statuses/{post}/favourite")).Ok();
|
|
Assert.DoesNotContain(hidden, (await alice.Client.Get("/api/v1/timelines/home")).Ok().Ids);
|
|
Assert.Equal(new[] { chatty.Id }, (await alice.Client.Get("/api/v1/notifications?types[]=favourite")).Ok().Array.Select(n => n!["account"].Text("id")));
|
|
Assert.Empty(await bob.Delivered(since));
|
|
|
|
Assert.False((await alice.Client.Post($"/api/v1/accounts/{chatty.Id}/unmute")).Ok().Body.Flag("muting"));
|
|
(await alice.Client.Post($"/api/v1/accounts/{muffled.Id}/unmute")).Ok();
|
|
(await alice.Client.Post($"/api/v1/accounts/{bobId}/unmute")).Ok();
|
|
Assert.Empty((await alice.Client.Get("/api/v1/mutes")).Ok().Array);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await alice.Client.Post($"/api/v1/accounts/{alice.Id}/mute")).Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Domain_blocks_are_listed_added_and_removed()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
var domain = $"blocked{Guid.NewGuid():N}.example";
|
|
|
|
Assert.Equal("{}", (await alice.Client.Post("/api/v1/domain_blocks", ("domain", domain.ToUpperInvariant()))).Ok().Text);
|
|
(await alice.Client.Post("/api/v1/domain_blocks", ("domain", "not a domain"))).Ok();
|
|
Assert.Equal(new[] { domain }, (await alice.Client.Get("/api/v1/domain_blocks")).Ok().Array.Select(d => d!.GetValue<string>()));
|
|
|
|
(await alice.Client.Delete("/api/v1/domain_blocks", ("domain", domain))).Ok();
|
|
Assert.Empty((await alice.Client.Get("/api/v1/domain_blocks")).Ok().Array);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Relationships_answer_every_asked_id_even_a_junk_one()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
var bob = await _host.Mastodon("bob");
|
|
|
|
var answer = (await alice.Client.Get($"/api/v1/accounts/relationships?id[]=junk&id[]={bob.Id}&id[]=junk")).Ok();
|
|
|
|
Assert.Equal(new[] { "junk", bob.Id }, answer.Ids);
|
|
Assert.All(answer.Array, r => Assert.False(r.Flag("following") || r.Flag("blocking") || r.Flag("muting") || r.Flag("followed_by")));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Following_again_sends_an_unanswered_request_once_more_and_no_more_than_hourly()
|
|
{
|
|
var since = DateTime.UtcNow.AddSeconds(-1);
|
|
var alice = await _host.Mastodon("alice");
|
|
var (bob, bobId) = await Remote();
|
|
Assert.True((await alice.Client.Post($"/api/v1/accounts/{bobId}/follow")).Ok().Body.Flag("requested"));
|
|
var first = Assert.Single(await bob.Delivered(since), d => d.Type() == "Follow");
|
|
|
|
Assert.True((await alice.Client.Post($"/api/v1/accounts/{bobId}/follow")).Ok().Body.Flag("requested"));
|
|
(await alice.Client.Post($"/api/v1/accounts/{bobId}/follow")).Ok();
|
|
|
|
var follows = (await bob.Delivered(since)).Where(d => d.Type() == "Follow").ToList();
|
|
Assert.Equal(2, follows.Count);
|
|
Assert.All(follows, f => Assert.Equal(first.Text("id"), f.Text("id")));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_remote_authors_followers_only_posts_show_only_to_the_personas_that_follow_them()
|
|
{
|
|
var since = DateTime.UtcNow.AddSeconds(-1);
|
|
var alice = await _host.Mastodon("alice");
|
|
var carol = await _host.Mastodon("carol");
|
|
var (bob, bobId) = await Remote();
|
|
(await alice.Client.Post($"/api/v1/accounts/{bobId}/follow")).Ok();
|
|
var follow = Assert.Single(await bob.Delivered(since), d => d.Type() == "Follow");
|
|
await _host.Deliver(bob, alice.Mouth, new JsonObject
|
|
{
|
|
["id"] = $"{bob.Origin()}/accepts/{Guid.NewGuid():N}",
|
|
["type"] = "Accept",
|
|
["actor"] = bob.Id,
|
|
["object"] = follow.DeepClone()
|
|
});
|
|
Assert.True(Assert.Single((await alice.Client.Get($"/api/v1/accounts/relationships?id[]={bobId}")).Ok().Array).Flag("following"));
|
|
var create = bob.Create("<p>for my followers</p>", new[] { bob.Id + "/followers" });
|
|
await _host.Deliver(bob, alice.Mouth, create);
|
|
var noteId = create["object"].Text("id");
|
|
var stored = await DB.Default.Find<Post>().Match(p => p.ObjectURI == noteId).ExecuteFirstAsync(Token);
|
|
Assert.Equal(PostVisibility.FollowersOnly, stored.Visibility);
|
|
var path = $"/api/v1/accounts/{bobId}/statuses";
|
|
|
|
Assert.Equal(new[] { stored.ID }, (await alice.Client.Get(path)).Ok().Ids);
|
|
Assert.Empty((await carol.Client.Get(path)).Ok().Array);
|
|
Assert.Empty((await _host.Client().Get(path)).Ok().Array);
|
|
Assert.Equal("private", (await alice.Client.Get($"/api/v1/statuses/{stored.ID}")).Ok().Body.Text("visibility"));
|
|
Assert.Equal(HttpStatusCode.NotFound, (await carol.Client.Get($"/api/v1/statuses/{stored.ID}")).Status);
|
|
Assert.Contains(stored.ID, (await alice.Client.Get("/api/v1/timelines/home")).Ok().Ids);
|
|
Assert.True((await alice.Client.Post($"/api/v1/statuses/{stored.ID}/favourite")).Ok().Body.Flag("favourited"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Search_by_address_finds_local_posts_and_personas_but_never_a_circle_or_a_hidden_post()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
var stranger = await _host.Mastodon("stranger");
|
|
var open = await alice.Status("findable");
|
|
var quiet = await alice.Status("not for strangers", ("visibility", "private"));
|
|
var circleName = $"circle{Guid.NewGuid():N}"[..20];
|
|
using (var clientApi = _host.As(alice.Persona.Root.Jwt))
|
|
Assert.Equal(HttpStatusCode.OK, (await clientApi.PostAsJsonAsync("/clientapi/group/insert",
|
|
new { avatarId = alice.Id, userName = circleName, name = "inner", isCommunity = false }, Token)).StatusCode);
|
|
|
|
var byPost = (await stranger.Client.Get($"/api/v2/search?q={Uri.EscapeDataString(open.Text("uri"))}")).Ok();
|
|
Assert.Equal(open.Text("id"), Assert.Single(byPost.Body["statuses"]!.AsArray()).Text("id"));
|
|
Assert.Equal(open.Text("id"), Assert.Single((await stranger.Client.Get($"/api/v2/search?q={Uri.EscapeDataString(open.Text("url"))}")).Ok().Body["statuses"]!.AsArray()).Text("id"));
|
|
var hidden = (await stranger.Client.Get($"/api/v2/search?q={Uri.EscapeDataString(quiet.Text("uri"))}&resolve=true")).Ok();
|
|
Assert.Empty(hidden.Body["statuses"]!.AsArray());
|
|
Assert.Equal(alice.Id, Assert.Single((await stranger.Client.Get($"/api/v2/search?q={Uri.EscapeDataString(alice.Uri)}")).Ok().Body["accounts"]!.AsArray()).Text("id"));
|
|
Assert.Empty((await stranger.Client.Get($"/api/v2/search?q={Uri.EscapeDataString($"{PrivaPubHost.Base}/peasants/{circleName}")}&resolve=true")).Ok().Body["accounts"]!.AsArray());
|
|
Assert.Empty((await stranger.Client.Get($"/api/v1/accounts/search?q={circleName}")).Ok().Array);
|
|
var tag = (await stranger.Client.Get("/api/v2/search?q=%23Cats&type=hashtags")).Ok();
|
|
Assert.Equal("cats", Assert.Single(tag.Body["hashtags"]!.AsArray()).Text("name"));
|
|
Assert.Empty(tag.Body["accounts"]!.AsArray());
|
|
|
|
(await alice.Client.Patch("/api/v1/accounts/update_credentials", ("discoverable", "false"))).Ok();
|
|
Assert.Empty((await stranger.Client.Get($"/api/v1/accounts/search?q={alice.UserName}")).Ok().Array);
|
|
Assert.Equal(alice.Id, (await stranger.Client.Get($"/api/v1/accounts/lookup?acct={alice.UserName}")).Ok().Body.Text("id"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_banned_login_loses_every_persona_token()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
Assert.Equal(HttpStatusCode.OK, (await alice.Client.Get("/api/v1/accounts/verify_credentials")).Status);
|
|
|
|
await DB.Default.Update<RootUser>().MatchID(alice.Persona.Root.Id).Modify(r => r.IsBanned, true).ExecuteAsync(Token);
|
|
|
|
Assert.Equal(HttpStatusCode.Unauthorized, (await alice.Client.Get("/api/v1/accounts/verify_credentials")).Status);
|
|
Assert.Equal(HttpStatusCode.Unauthorized, (await alice.Client.Post("/api/v1/statuses", ("status", "still here?"))).Status);
|
|
Assert.Equal(alice.Id, (await _host.Client().Get($"/api/v1/accounts/{alice.Id}")).Ok().Body.Text("id"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_forwarded_report_leaves_as_a_flag_from_the_instance_actor_never_from_the_reporter()
|
|
{
|
|
var since = DateTime.UtcNow.AddSeconds(-1);
|
|
var alice = await _host.Mastodon("alice");
|
|
var local = await _host.Mastodon("local");
|
|
var bob = new RemoteActor(_peer, "bob");
|
|
var post = await _host.PublicPostFrom(bob, alice, "<p>something nasty</p>");
|
|
var bobId = (await _host.Known(bob)).ID;
|
|
var unrelated = await alice.Status("mine");
|
|
|
|
var report = (await alice.Client.Post("/api/v1/reports", ("account_id", bobId), ("status_ids[]", post.ID), ("status_ids[]", unrelated.Text("id")),
|
|
("comment", "please look"), ("category", "spam"), ("forward", "true"))).Ok();
|
|
|
|
Assert.Equal(bobId, report.Body["target_account"].Text("id"));
|
|
Assert.Equal(new[] { post.ID }, report.Body["status_ids"]!.AsArray().Select(i => i!.GetValue<string>()));
|
|
Assert.True(report.Body.Flag("forwarded"));
|
|
Assert.Equal("spam", report.Body.Text("category"));
|
|
var flag = Assert.Single(await bob.Delivered(since));
|
|
Assert.Equal("Flag", flag.Type());
|
|
Assert.Equal($"{PrivaPubHost.Base}/peasants/privapub", flag.Text("actor"));
|
|
Assert.Equal(new[] { bob.Id, post.ObjectURI }, flag["object"]!.AsArray().Select(o => o!.GetValue<string>()));
|
|
Assert.DoesNotContain(alice.UserName, flag.ToJsonString());
|
|
Assert.DoesNotContain(alice.Id, flag.ToJsonString());
|
|
|
|
var quiet = (await alice.Client.Post("/api/v1/reports", ("account_id", bobId), ("comment", "just noting"))).Ok();
|
|
Assert.False(quiet.Body.Flag("forwarded"));
|
|
Assert.False((await alice.Client.Post("/api/v1/reports", ("account_id", local.Id), ("forward", "true"))).Ok().Body.Flag("forwarded"));
|
|
Assert.Single(await bob.Delivered(since));
|
|
Assert.Equal(HttpStatusCode.NotFound, (await alice.Client.Post("/api/v1/reports", ("account_id", "000000000000000000000000"))).Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Statuses_count_still_counts_located_posts_pending_an_owner_decision()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
await alice.Status("visible");
|
|
await _host.Located(alice, "by the river");
|
|
|
|
var account = (await _host.Client().Get($"/api/v1/accounts/{alice.Id}")).Ok();
|
|
|
|
Assert.Equal(2, account.Body.Number("statuses_count"));
|
|
Assert.Single((await _host.Client().Get($"/api/v1/accounts/{alice.Id}/statuses")).Ok().Array);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Account_stub_routes_answer_empty_lists()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
|
|
Assert.Empty((await _host.Client().Get($"/api/v1/accounts/{alice.Id}/featured_tags")).Ok().Array);
|
|
var familiar = (await alice.Client.Get($"/api/v1/accounts/familiar_followers?id[]={alice.Id}")).Ok();
|
|
Assert.Equal(alice.Id, Assert.Single(familiar.Array).Text("id"));
|
|
Assert.Empty(familiar.Array[0]!["accounts"]!.AsArray());
|
|
}
|
|
}
|
|
}
|