using MongoDB.Entities; using PrivaPub.Models.Jobs; using PrivaPub.Tests.Support; using PrivaPub.Tests.Support.Host; using System.Net; using System.Text.Json.Nodes; namespace PrivaPub.Tests.Http { // Where a server is, on the public instance API: where it was located (city, coordinates and network, whatever its size), // only the CDN's name for a CDN-fronted one, the batch form, and this server itself. [Trait("Category", "Integration")] public sealed class InstanceLocationTests : IAsyncLifetime { PrivaPubHost _host; public async ValueTask InitializeAsync() { Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip); _host = await PrivaPubHost.Shared(); } public ValueTask DisposeAsync() => ValueTask.CompletedTask; static CancellationToken Token => TestContext.Current.CancellationToken; static async Task Instance(int users, InstanceGeo geo, string name = "located") { var host = $"{name}{Guid.NewGuid():N}.example"; await DB.Default.SaveAsync(new RemoteInstance { Host = host, Software = "mastodon", UsersTotal = users, Geo = geo }, Token); return host; } static InstanceGeo Milan(string cdn = default) => new() { Country = "IT", City = "Milan", Latitude = 45.5, Longitude = 9.2, Asn = 12345, AsnOrg = "Example Hosting", Cdn = cdn, Ipv6 = true, Source = "DB-IP Lite 2026-10", LocatedAt = DateTime.UtcNow.AddDays(-2) }; async Task Geo(string host) => (await _host.Client().Get($"/api/privapub/v1/instances/{host}")).Ok().Body["geo"]; [Fact] public async Task A_located_server_shows_its_city_coordinates_and_network() { var geo = await Geo(await Instance(50, Milan())); Assert.Equal("city", geo.Text("precision")); Assert.Equal(("IT", "Milan", "Example Hosting"), (geo.Text("country"), geo.Text("city"), geo.Text("network"))); Assert.Equal(45.5, geo["latitude"]!.GetValue()); Assert.Equal(9.2, geo["longitude"]!.GetValue()); Assert.Equal("DB-IP Lite 2026-10", geo.Text("source")); Assert.NotNull(geo.Text("located_at")); Assert.Null(geo["asn"]); Assert.Null(geo["ipv6"]); } [Fact] public async Task A_one_user_server_shows_its_city_too() { var geo = await Geo(await Instance(1, Milan())); Assert.Equal("city", geo.Text("precision")); Assert.Equal(("IT", "Milan", "Example Hosting"), (geo.Text("country"), geo.Text("city"), geo.Text("network"))); Assert.Equal(45.5, geo["latitude"]!.GetValue()); } [Fact] public async Task A_server_located_only_to_its_country_shows_the_country() { var geo = await Geo(await Instance(50, new InstanceGeo { Country = "IT", Source = "DB-IP Lite 2026-10", LocatedAt = DateTime.UtcNow })); Assert.Equal("country", geo.Text("precision")); Assert.Equal("IT", geo.Text("country")); Assert.Null(geo["city"]); Assert.Null(geo["latitude"]); } [Fact] public async Task A_cdn_fronted_server_shows_only_its_cdn() { var geo = await Geo(await Instance(5000, Milan(cdn: "Cloudflare"))); Assert.Equal("cdn", geo.Text("precision")); Assert.Equal("Cloudflare", geo.Text("cdn")); Assert.Null(geo["country"]); Assert.Null(geo["latitude"]); Assert.Null(geo["network"]); } [Fact] public async Task An_unlocated_server_has_no_place() { Assert.Null(await Geo(await Instance(50, default))); } [Fact] public async Task The_provenance_of_a_post_carries_the_same_projection() { var host = await Instance(3, Milan()); var described = (await _host.Client().Get($"/api/privapub/v1/instances/{host}")).Ok().Body; Assert.Equal("city", described["geo"].Text("precision")); Assert.Equal(described["geo"]!.ToJsonString(), (await Geo(host))!.ToJsonString()); } [Fact] public async Task Several_servers_answer_at_once_in_the_order_asked_without_the_unknown_ones() { var big = await Instance(50, Milan(), "big"); var small = await Instance(3, Milan(), "small"); var unknown = $"unknown{Guid.NewGuid():N}.example"; var answer = (await _host.Client().Get($"/api/privapub/v1/instances?host[]={small}&host[]={unknown}&host[]={big.ToUpperInvariant()}&host[]={small}")).Ok(); var hosts = answer.Body!.AsArray().Select(i => i.Text("host")).ToList(); Assert.Equal(new[] { small, big }, hosts); Assert.Equal("city", answer.Body![0]["geo"].Text("precision")); Assert.Equal("city", answer.Body![1]["geo"].Text("precision")); Assert.Empty((await _host.Client().Get("/api/privapub/v1/instances")).Ok().Body!.AsArray()); } [Fact] public async Task At_most_forty_servers_answer_at_once() { var hosts = new List(); for (var i = 0; i < 42; i++) hosts.Add(await Instance(1, default, "many")); var answer = (await _host.Client().Get("/api/privapub/v1/instances?" + string.Join("&", hosts.Select(h => "host[]=" + h)))).Ok(); Assert.Equal(hosts.Take(40), answer.Body!.AsArray().Select(i => i.Text("host"))); } [Fact] public async Task This_server_describes_itself() { var self = (await _host.Client().Get($"/api/privapub/v1/instances/{PrivaPubHost.Host}")).Ok().Body; Assert.Equal(PrivaPubHost.Host, self.Text("host")); Assert.Equal("privapub", self.Text("software")); Assert.Equal("activitypub", Assert.Single(self["protocols"]!.AsArray())!.GetValue()); Assert.Null(self["geo"]); var batch = (await _host.Client().Get($"/api/privapub/v1/instances?host[]={PrivaPubHost.Host}")).Ok().Body!.AsArray(); Assert.Equal("privapub", Assert.Single(batch).Text("software")); } [Fact] public async Task The_owner_may_place_this_server() { var placed = await SelfPlacedHost.Shared(); var geo = (await placed.Client().Get($"/api/privapub/v1/instances/{PrivaPubHost.Host}")).Ok().Body["geo"]; Assert.Equal("city", geo.Text("precision")); Assert.Equal(("IT", "Milan", "configured"), (geo.Text("country"), geo.Text("city"), geo.Text("source"))); Assert.Equal(45.5, geo["latitude"]!.GetValue()); Assert.Equal(9.2, geo["longitude"]!.GetValue()); } [Fact] public async Task Junk_hosts_are_not_an_error() { Assert.Equal(HttpStatusCode.NotFound, (await _host.Client().Get($"/api/privapub/v1/instances/{new string('a', 300)}")).Status); Assert.Empty((await _host.Client().Get($"/api/privapub/v1/instances?host[]=&host[]={new string('a', 300)}")).Ok().Body!.AsArray()); } } public sealed class SelfPlacedHost : PrivaPubHost { static readonly SemaphoreSlim Boot = new(1, 1); static SelfPlacedHost _shared; public static new async Task Shared() { await PrivaPubHost.Shared(); await Boot.WaitAsync(); try { if (_shared == default) { var host = new SelfPlacedHost(); _ = host.Services; _shared = host; } return _shared; } finally { Boot.Release(); } } protected override IEnumerable> Settings() => base.Settings().Concat(new Dictionary { ["Statistics:Geo:Self:Latitude"] = "45.4642", ["Statistics:Geo:Self:Longitude"] = "9.19", ["Statistics:Geo:Self:City"] = "Milan", ["Statistics:Geo:Self:Country"] = "IT" }); } }