using Microsoft.Extensions.Logging.Abstractions; using MongoDB.Entities; using PrivaPub.Domain.Statistics; using PrivaPub.Federation.Moderation; using PrivaPub.Federation.Objects; using PrivaPub.Infrastructure.Data.Migrations; using PrivaPub.Infrastructure.Geo; using PrivaPub.Infrastructure.Http; using PrivaPub.Infrastructure.Jobs; using PrivaPub.Infrastructure.Statistics; using PrivaPub.Models.Federation; using PrivaPub.Models.Jobs; using PrivaPub.Models.Statistics; using PrivaPub.Tests.Support; using System.Net; using System.Text.Json; namespace PrivaPub.Tests.Statistics { public class InstanceDocumentsTests { static JsonElement Json(string text) => JsonDocument.Parse(text).RootElement; [Fact] public void NodeInfo_gives_software_usage_and_languages_whatever_their_shape() { var facts = InstanceDocuments.NodeInfo(Json(""" {"software":{"name":"mastodon","version":"4.7.3"},"protocols":["activitypub"],"openRegistrations":false, "usage":{"users":{"total":"1234","activeMonth":56.0,"activeHalfyear":-3},"localPosts":99999999999,"localComments":7}, "metadata":{"nodeName":"Example","nodeDescription":"
A small\n place
","languages":["en","DE","klingon!"]}} """), "2.1"); Assert.Equal(("mastodon", "4.7.3", "Example", "2.1"), (facts.Software, facts.SoftwareVersion, facts.NodeName, facts.Version)); Assert.Equal("A small place", facts.NodeDescription); Assert.Equal(new[] { "activitypub" }, facts.Protocols); Assert.False(facts.OpenRegistrations); Assert.Equal(1234, facts.UsersTotal); Assert.Equal(56, facts.UsersActiveMonth); Assert.Equal(0, facts.UsersActiveHalfyear); Assert.Equal(1_000_000_000, facts.LocalPosts); Assert.Equal(7, facts.LocalComments); Assert.Equal(new[] { "en", "de" }, facts.Languages); Assert.Null(InstanceDocuments.NodeInfo(Json("{}"), "2.0").UsersTotal); } [Fact] public void The_newest_https_nodeinfo_link_is_chosen() { var links = Json(""" {"links":[{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.0","href":"https://a.example/2.0"}, {"rel":"http://nodeinfo.diaspora.software/ns/schema/2.1","href":"http://a.example/2.1"}, {"rel":"http://nodeinfo.diaspora.software/ns/schema/2.2","href":"https://a.example/2.2"}]} """); Assert.Equal(("https://a.example/2.2", "2.2"), InstanceDocuments.NodeInfoLink(links)); Assert.Equal(default, InstanceDocuments.NodeInfoLink(Json("[]"))); } [Fact] public void The_instance_api_gives_registration_mode_limits_and_never_a_contact() { var v2 = InstanceDocuments.InstanceApi(Json(""" {"title":"Example","languages":["en"],"registrations":{"enabled":true,"approval_required":true}, "configuration":{"statuses":{"max_characters":500}},"api_versions":{"mastodon":7},"source_url":"https://github.com/mastodon/mastodon", "contact":{"email":"admin@a.example","account":{"username":"admin"}}} """)); var v1 = InstanceDocuments.InstanceApi(Json("""{"title":"Old","registrations":false,"max_toot_chars":"5000"}""")); Assert.Equal(("Example", "approval", 500, 7, "https://github.com/mastodon/mastodon"), (v2.Title, v2.RegistrationMode, v2.MaxPostChars!.Value, v2.ApiVersion!.Value, v2.SourceUrl)); Assert.Equal(new[] { "en" }, v2.Languages); Assert.DoesNotContain("admin", System.Text.Json.JsonSerializer.Serialize(v2)); Assert.Equal(("closed", 5000), (v1.RegistrationMode, v1.MaxPostChars!.Value)); Assert.True(InstanceDocuments.SpeaksMastodonApi("GoToSocial")); Assert.False(InstanceDocuments.SpeaksMastodonApi("lemmy")); } [Fact] public void The_public_location_is_where_the_server_was_located_unless_a_cdn_hides_it() { var geo = new InstanceGeo { Country = "DE", City = "Berlin", Latitude = 52.5, Longitude = 13.4, AsnOrg = "Hetzner", Asn = 24940 }; Assert.Equal(new PublicLocation("DE", "Berlin", 52.5, 13.4, "Hetzner", null), PublicGeo.Project(geo)); Assert.Equal(new PublicLocation("DE", null, null, null, null, null), PublicGeo.Project(new InstanceGeo { Country = "DE" })); Assert.Equal(new PublicLocation(null, null, null, null, null, "Cloudflare"), PublicGeo.Project(new InstanceGeo { Country = "US", City = "SF", Latitude = 37.8, Longitude = -122.4, Cdn = "Cloudflare" })); Assert.Null(PublicGeo.Project(new InstanceGeo())); Assert.Null(PublicGeo.Project(null)); } } [Trait("Category", "Integration")] public sealed class DescribeTests : IAsyncLifetime { Peer _peer; public async ValueTask InitializeAsync() { Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip); _peer = await Peer.Start(); } public async ValueTask DisposeAsync() { if (_peer != default) await _peer.DisposeAsync(); } sealed class PeerDescriber : InstanceDescriber { readonly string _peer; public PeerDescriber(IFederationHttp http, IGeoLocator geo, IConnectedAddresses addresses, string peer) : base(http, geo, addresses) => _peer = peer; protected override string Address(string url) => _peer + new Uri(url).PathAndQuery; } sealed class FixedGeo : IGeoLocator { public GeoFix Locate(IPAddress address) => new("NL", "Amsterdam", 52.4, 4.9, 60781, "LeaseWeb"); public string Source => "test"; } void ServeServer(string prefix, string software, bool v2 = true) { _peer.Serve($"{prefix}/.well-known/nodeinfo", $$"""{"links":[{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.1","href":"https://ignored.example/nodeinfo/2.1"}]}"""); _peer.Serve($"{prefix}/nodeinfo/2.1", $$$"""{"software":{"name":"{{{software}}}","version":"1.0"},"usage":{"users":{"total":42,"activeMonth":7}},"openRegistrations":true}"""); if (v2) _peer.Serve($"{prefix}/api/v2/instance", """{"title":"A test server","languages":["it"],"registrations":{"enabled":true,"approval_required":false}}"""); _peer.Serve($"{prefix}/api/v1/instance", """{"title":"An old test server","registrations":true}"""); } [Fact] public async Task A_server_is_described_located_and_snapshotted_weekly() { var token = TestContext.Current.CancellationToken; var host = $"described{Guid.NewGuid():N}.example"; ServeServer("/" + host, "mastodon"); var addresses = new ConnectedAddresses(); var describer = new PeerDescriber(Peer.Http(connected: addresses), new FixedGeo(), addresses, _peer.A + "/" + host); Assert.True(await describer.Describe(host, crawled: false, default, token)); var instance = await DB.Default.Find