trends/tags, trends/statuses and trends/links replace the stubs. Hashtags and links rise with the number of different authors using them today against the week before, two at least; posts with their favourites, boosts and replies, halving every twelve hours. Only public posts PrivaPub already holds count, nothing behind a content warning, and a post trends only if its author is discoverable. Computed at most every ten minutes. The directory lists discoverable accounts by their latest public post, or by the day they say they joined: a persona's published day, never its creation, with ties broken by a hash of the name, so personas made together are not told apart by their order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
129 lines
5.9 KiB
C#
129 lines
5.9 KiB
C#
using PrivaPub.Api.Mastodon.Controllers;
|
|
using PrivaPub.Tests.Support;
|
|
using PrivaPub.Tests.Support.Host;
|
|
|
|
using System.Net;
|
|
|
|
namespace PrivaPub.Tests.Http
|
|
{
|
|
[Trait("Category", "Integration")]
|
|
public sealed class MastodonInstanceTests : IAsyncLifetime
|
|
{
|
|
PrivaPubHost _host;
|
|
|
|
public async ValueTask InitializeAsync()
|
|
{
|
|
Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip);
|
|
_host = await PrivaPubHost.Shared();
|
|
}
|
|
|
|
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
|
|
|
[Fact]
|
|
public async Task Both_instance_documents_advertise_mastodon_4_2_and_the_domain()
|
|
{
|
|
var anonymous = _host.Client();
|
|
|
|
var v1 = (await anonymous.Get("/api/v1/instance")).Ok();
|
|
Assert.Equal("4.2.0 (compatible; PrivaPub)", v1.Body.Text("version"));
|
|
Assert.Equal(InstanceController.Version, v1.Body.Text("version"));
|
|
Assert.Equal(PrivaPubHost.Host, v1.Body.Text("uri"));
|
|
Assert.True(v1.Body.Flag("registrations"));
|
|
Assert.True(v1.Body.Flag("invites_enabled"));
|
|
Assert.True(v1.Body["stats"].Number("user_count") >= 0);
|
|
Assert.Equal(5000, v1.Body["configuration"]!["statuses"].Number("max_characters"));
|
|
Assert.Equal(4, v1.Body["configuration"]!["polls"].Number("max_options"));
|
|
Assert.Contains("image/jpeg", v1.Body["configuration"]!["media_attachments"]!["supported_mime_types"]!.AsArray().Select(t => t!.GetValue<string>()));
|
|
Assert.Empty(v1.Body["rules"]!.AsArray());
|
|
Assert.Null(v1.Body["contact_account"]);
|
|
Assert.True(v1.Body.AsObject().ContainsKey("contact_account"));
|
|
|
|
var v2 = (await anonymous.Get("/api/v2/instance")).Ok();
|
|
Assert.Equal(InstanceController.Version, v2.Body.Text("version"));
|
|
Assert.Equal(PrivaPubHost.Host, v2.Body.Text("domain"));
|
|
Assert.Null(v2.Body["configuration"]!["urls"]!["streaming"]);//nothing streams yet, so nothing is advertised
|
|
Assert.Equal(PrivaPub.Api.Mastodon.Controllers.StatusesController.MaxPins, v2.Body["configuration"]!["accounts"].Number("max_pinned_statuses"));
|
|
Assert.Contains("image/avif", v2.Body["configuration"]!["media_attachments"]!["supported_mime_types"]!.AsArray().Select(t => t!.GetValue<string>()));
|
|
Assert.True(v2.Body["registrations"].Flag("enabled"));
|
|
Assert.True((await anonymous.Get("/nodeinfo/2.1")).Ok().Body.Flag("openRegistrations"));
|
|
Assert.False(v2.Body["configuration"]!["translation"].Flag("enabled"));
|
|
Assert.Equal(7, v2.Body["api_versions"].Number("mastodon"));
|
|
|
|
var signedIn = await _host.Mastodon("reader");
|
|
Assert.Equal(InstanceController.Version, (await signedIn.Client.Get("/api/v1/instance")).Ok().Body.Text("version"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task The_instance_side_routes_answer_without_a_login()
|
|
{
|
|
var anonymous = _host.Client();
|
|
|
|
Assert.Empty((await anonymous.Get("/api/v1/instance/peers")).Ok().Array);
|
|
Assert.Empty((await anonymous.Get("/api/v1/instance/activity")).Ok().Array);
|
|
Assert.Empty((await anonymous.Get("/api/v1/instance/rules")).Ok().Array);
|
|
var extended = (await anonymous.Get("/api/v1/instance/extended_description")).Ok();
|
|
Assert.StartsWith("<p>", extended.Body.Text("content"));
|
|
Assert.EndsWith("T00:00:00.000Z", extended.Body.Text("updated_at"));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/api/v1/custom_emojis")]
|
|
[InlineData("/api/v1/announcements")]
|
|
public async Task Public_stub_routes_answer_empty_lists_to_anyone(string path)
|
|
{
|
|
Assert.Empty((await _host.Client().Get(path)).Ok().Array);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/api/v1/suggestions")]
|
|
[InlineData("/api/v2/suggestions")]
|
|
[InlineData("/api/v1/endorsements")]
|
|
[InlineData("/api/v1/featured_tags")]
|
|
public async Task Signed_in_stub_routes_answer_empty_lists(string path)
|
|
{
|
|
var reader = await _host.Mastodon("reader");
|
|
|
|
Assert.Empty((await reader.Client.Get(path)).Ok().Array);
|
|
Assert.Equal(HttpStatusCode.Unauthorized, (await _host.Client().Get(path)).Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Apps_are_registered_validated_and_verified()
|
|
{
|
|
var anonymous = _host.Client();
|
|
|
|
var app = (await anonymous.Post("/api/v1/apps", ("client_name", "tusky-ish"), ("redirect_uris", "urn:ietf:wg:oauth:2.0:oob"), ("scopes", "read write"),
|
|
("website", "https://example.com"))).Ok();
|
|
|
|
Assert.Equal("tusky-ish", app.Body.Text("name"));
|
|
Assert.Equal(new[] { "read", "write" }, app.Body["scopes"]!.AsArray().Select(s => s!.GetValue<string>()));
|
|
Assert.False(string.IsNullOrEmpty(app.Body.Text("client_id")));
|
|
Assert.False(string.IsNullOrEmpty(app.Body.Text("client_secret")));
|
|
Assert.Equal(HttpStatusCode.UnprocessableEntity, (await anonymous.Post("/api/v1/apps", ("redirect_uris", "urn:ietf:wg:oauth:2.0:oob"))).Status);
|
|
Assert.Equal(HttpStatusCode.UnprocessableEntity, (await anonymous.Post("/api/v1/apps", ("client_name", "x"), ("redirect_uris", "javascript:alert(1)"))).Status);
|
|
Assert.Equal(HttpStatusCode.UnprocessableEntity, (await anonymous.Post("/api/v1/apps", ("client_name", "x"))).Status);
|
|
|
|
var reader = await _host.Mastodon("reader");
|
|
var verified = (await reader.Client.Get("/api/v1/apps/verify_credentials")).Ok();
|
|
Assert.Equal("privapub-tests", verified.Body.Text("name"));
|
|
Assert.Equal(HttpStatusCode.Unauthorized, (await anonymous.Get("/api/v1/apps/verify_credentials")).Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Preferences_follow_the_persona_and_push_has_no_subscription()
|
|
{
|
|
var reader = await _host.Mastodon("reader", scopes: "read write follow push");
|
|
(await reader.Client.Patch("/api/v1/accounts/update_credentials", ("source[privacy]", "unlisted"), ("source[sensitive]", "true"))).Ok();
|
|
|
|
var preferences = (await reader.Client.Get("/api/v1/preferences")).Ok();
|
|
|
|
Assert.Equal("unlisted", preferences.Body.Text("posting:default:visibility"));
|
|
Assert.True(preferences.Body.Flag("posting:default:sensitive"));
|
|
Assert.False(preferences.Body.Flag("reading:expand:spoilers"));
|
|
var push = await reader.Client.Get("/api/v1/push/subscription");
|
|
Assert.Equal(HttpStatusCode.NotFound, push.Status);
|
|
Assert.Equal("Record not found", push.Body.Text("error"));
|
|
}
|
|
}
|
|
}
|