88 integration tests drive the Mastodon client API through the whole server
(PrivaPubHost), with remote actors on an in-process Peer and deliveries read
from the job queue. Helpers live in Support/Host/MastodonHelpers.cs.
Coverage:
- Accounts: verify_credentials (no root id or login name); update_credentials
with indexed and array fields_attributes (form and JSON), source[*],
quote_policy, locked/bot, avatar and header uploads resized and stripped of
EXIF and XMP; lookup (local, @domain, remote; a circle, the instance actor
and a circle's id answer 404); search with and without resolve (only a
signed-in persona resolves, the Peer is untouched otherwise), by post and
actor address, hashtags, undiscoverable personas; account statuses with
pinned, exclude_replies, exclude_reblogs, only_media, tagged and Link paging
both ways; followers-only posts for followers (local and remote authors);
community accounts; followers/following only to their owner; follow (open,
locked, remote Follow delivery), unfollow and Undo; follow requests from
local and remote followers answered with the original Follow;
remove_from_followers; blocks with Reject and Block/Undo deliveries; mutes
with duration and the notifications choice, never federated; domain blocks;
relationships with junk ids; a banned login's tokens; reports forwarded as a
Flag from the instance actor only; account stub routes.
- Statuses: each visibility's to/cc as delivered; CW as summary; replies to
local and remote posts (mention, inReplyTo, the author's inbox); polls and
votes (local, and remote votes only to the author without published); media
attached only by its owner; quotes, the quotes list and revocation; edit
history, source and the Update delivery; delete for redraft, the 410
Tombstone and the Delete delivery; favourite/reblog counts with Like,
Announce and their Undos; favourited_by, reblogged_by; bookmarks; pins;
interaction_policy matching canQuote in the note and in the Update;
strangers get 404 for followers-only and direct posts; a located post is
unreachable by id for anyone else on every route; statuses?id[];
Idempotency-Key; scopes; deleting a reblog.
- Timelines: home paging with max_id, since_id and min_id; public local and
remote; tag (anonymous); list stub; favourites; conversations and read;
markers; notifications with types[], exclude_types[], account_id, paging,
get, dismiss, clear and unread_count.
- Instance: v1 and v2 (4.2.0 (compatible; PrivaPub)), peers, activity, rules,
extended_description, apps and every stub route.
- Media: v1 and v2 uploads, owner-only GET and PUT, 422 for unsupported or
unreadable files, video and audio made with ffmpeg lavfi sources and checked
with ffprobe; every remote media address goes through the proxy; the proxy
refuses unsigned URLs, streams ranges as 206 without caching, caches whole
downloads and serves them with ranges, and streams anything over
Media:MaxProxiedBytes (a SmallProxyHost) without caching.
- Provenance of local, delivered (signature) and fetched (instance actor,
no signature, the trigger as activity) posts, visibility of provenance,
instance descriptions; reading any of them makes no outbound request.
Pleroma reactions with EmojiReact and Undo deliveries, and local reaction
notifications.
Bugs fixed:
- remove_from_followers deleted the Follower row but never told a remote
follower. It now sends Reject{Follow} with the stored Follow id, through
RelationshipService.RemoveFollower, which Block now shares.
- VisibilityPolicy.CanSee refused followers-only posts to accepted followers,
so a post in their home timeline answered 404 to GET, context, favourite and
reply. Followers of the author (local or remote) may now see them.
- Account statuses of a remote account hid followers-only posts from
personas that follow it.
- exclude_replies dropped the author's own threads; like Mastodon it now
drops only replies to other accounts.
- A community account's statuses were always empty: they are now the posts
addressed to the community.
- Pinning someone else's visible post answered 404; it answers 422 like
Mastodon.
- GET /api/v1/notifications/:id answered 200 with null when the notification's
post was gone; it answers 404.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
629 lines
35 KiB
C#
629 lines
35 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_instance_actor()
|
|
{
|
|
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=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);
|
|
}
|
|
|
|
[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 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);
|
|
Assert.Empty((await alice.Client.Get($"/api/v1/accounts/{alice.Id}/lists")).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());
|
|
}
|
|
}
|
|
}
|