- IConnectedAddresses remembers which address each host's last connection reached, set in SafeHttpHandlerFactory's connect callback. That is once per pooled connection, with no second DNS lookup, and the address is never stored. - IGeoLocator / DbIpLocator reads the offline DB-IP Lite city and ASN databases (MaxMind .mmdb, via MaxMind.Db). It maps memory, swaps to new files within ten minutes, rounds coordinates to one decimal, never looks up a private address, and answers nothing when the files are missing. CdnNetworks names the CDNs whose edge addresses say nothing about where a server is. - deploy/max/geo-update.sh fetches this or last month's databases, checks them and swaps them in atomically. The privapub-geo timer runs it monthly as www-data, and setup.sh installs the directory, the script, the units and a first download. Describing servers will use these in M8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
143 lines
5.4 KiB
C#
143 lines
5.4 KiB
C#
using Microsoft.AspNetCore.Http;
|
|
|
|
using PrivaPub.Infrastructure.Http;
|
|
using PrivaPub.Infrastructure.Statistics;
|
|
using PrivaPub.Tests.Support;
|
|
|
|
namespace PrivaPub.Tests.Statistics
|
|
{
|
|
public class HttpScopeTests
|
|
{
|
|
[Fact]
|
|
public async Task Scopes_nest_and_restore_across_awaits()
|
|
{
|
|
Assert.Null(HttpScope.Purpose);
|
|
Assert.Equal("request", HttpScope.Trigger);
|
|
|
|
using (HttpScope.Triggered("deliver"))
|
|
{
|
|
using (HttpScope.Default("object"))
|
|
{
|
|
Assert.Equal("object", HttpScope.Purpose);
|
|
using (HttpScope.For("actor"))
|
|
{
|
|
await Task.Yield();
|
|
Assert.Equal("actor", HttpScope.Purpose);
|
|
using (HttpScope.Default("object"))
|
|
Assert.Equal("actor", HttpScope.Purpose);
|
|
}
|
|
Assert.Equal("object", HttpScope.Purpose);
|
|
}
|
|
Assert.Null(HttpScope.Purpose);
|
|
Assert.Equal("deliver", HttpScope.Trigger);
|
|
Assert.False(HttpScope.Crawling);
|
|
using (HttpScope.Crawl())
|
|
Assert.True(HttpScope.Crawling);
|
|
}
|
|
Assert.Equal("request", HttpScope.Trigger);
|
|
}
|
|
}
|
|
|
|
public class TrafficMeterTests
|
|
{
|
|
[Theory]
|
|
[InlineData("api/v1/accounts/{id}/follow", "api/v1/accounts")]
|
|
[InlineData("/api/v1/statuses", "api/v1/statuses")]
|
|
[InlineData("/oauth/token", "oauth/token")]
|
|
[InlineData("/clientapi/avatar/private/insert", "clientapi/avatar/private")]
|
|
[InlineData("api/{x}", "api")]
|
|
[InlineData(null, "unmatched")]
|
|
public void Client_routes_group_by_their_first_literal_segments(string template, string group) =>
|
|
Assert.Equal(group, TrafficMeter.Group(template));
|
|
|
|
[Theory]
|
|
[InlineData("peasants/{actor}", "/peasants/alice", "actor")]
|
|
[InlineData("peasants/{actor}/anus", "/peasants/alice/anus", "outbox")]
|
|
[InlineData("peasants/{actor}/flock", "/peasants/c/flock", "collection")]
|
|
[InlineData("peasants/{actor}/groupies", "/peasants/alice/groupies", "collection")]
|
|
[InlineData("peasants/{actor}/scribbles/{postId}", "/peasants/alice/scribbles/1", "object")]
|
|
[InlineData("peasants/{actor}/grunts/{activityId}", "/peasants/alice/grunts/1", "activity")]
|
|
[InlineData("peasants/{actor}/parrot-licences/{licenceId}", "/peasants/alice/parrot-licences/1", "licence")]
|
|
[InlineData("users/{actor}", "/users/alice", "actor")]
|
|
[InlineData(".well-known/webfinger", "/.well-known/webfinger", "webfinger")]
|
|
[InlineData("nodeinfo/2.1", "/nodeinfo/2.1", "nodeinfo")]
|
|
[InlineData("@{user}", "/@alice", null)]
|
|
public void Served_documents_are_counted_by_kind_never_by_name(string template, string path, string kind) =>
|
|
Assert.Equal(kind, TrafficMeter.Served(template, new PathString(path)));
|
|
}
|
|
|
|
public sealed class OutboundRequestTests : IAsyncLifetime
|
|
{
|
|
Peer _peer;
|
|
|
|
public async ValueTask InitializeAsync() => _peer = await Peer.Start();
|
|
|
|
public async ValueTask DisposeAsync() => await _peer.DisposeAsync();
|
|
|
|
[Fact]
|
|
public async Task A_federation_fetch_is_recorded_with_its_purpose_trigger_and_size()
|
|
{
|
|
var ledger = new MemoryLedger();
|
|
var http = Peer.Http(ledger: ledger);
|
|
_peer.Serve("/users/a", "{\"id\":\"{A}/users/a\",\"type\":\"Person\"}");
|
|
|
|
using (HttpScope.Triggered("processinbox"))
|
|
using (HttpScope.For("actor"))
|
|
Assert.NotNull(await http.GetJson($"{_peer.A}/users/a", "application/activity+json", default, TestContext.Current.CancellationToken));
|
|
|
|
var e = Assert.Single(ledger.Of("http"));
|
|
Assert.Equal(("127.0.0.1", "actor", "processinbox", "ok", 200), (e.Host, e.Purpose, e.Trigger, e.Outcome, e.Status!.Value));
|
|
Assert.True(e.Bytes > 10);
|
|
Assert.Equal(0, e.Redirects);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Refusals_are_recorded_and_a_second_try_is_remembered()
|
|
{
|
|
var ledger = new MemoryLedger();
|
|
var http = Peer.Http(ledger: ledger);
|
|
_peer.Answer("/gone", 410);
|
|
_peer.ServeText("/page", "<html></html>", "text/html");
|
|
|
|
using (HttpScope.Triggered("fetchancestors"))
|
|
{
|
|
await http.GetJson($"{_peer.A}/gone", "application/activity+json", default, TestContext.Current.CancellationToken);
|
|
await http.GetJson($"{_peer.A}/gone", "application/activity+json", default, TestContext.Current.CancellationToken);
|
|
await http.GetJson($"{_peer.A}/page", "application/activity+json", default, TestContext.Current.CancellationToken);
|
|
await http.GetJson("ftp://example.org/x", "application/activity+json", default, TestContext.Current.CancellationToken);
|
|
}
|
|
|
|
var events = ledger.Of("http");
|
|
Assert.Equal(new[] { ("refused", "410"), ("refused", "remembered"), ("refused", "content-type"), ("refused", "disallowed") },
|
|
events.Select(e => (e.Outcome, e.Reason)));
|
|
Assert.Equal("object", events[0].Purpose);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_fetch_a_reader_caused_is_only_counted()
|
|
{
|
|
var ledger = new MemoryLedger();
|
|
var http = Peer.Http(ledger: ledger);
|
|
_peer.Serve("/users/b", "{\"id\":\"{A}/users/b\",\"type\":\"Person\"}");
|
|
|
|
using (HttpScope.For("webfinger"))
|
|
await http.GetJson($"{_peer.A}/users/b", "application/activity+json", default, TestContext.Current.CancellationToken);
|
|
|
|
Assert.Empty(ledger.Events);
|
|
Assert.Equal(1, ledger.Counts[("127.0.0.1", "http:webfinger:ok")]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_connection_remembers_the_address_it_reached_for_locating_the_server()
|
|
{
|
|
var connected = new ConnectedAddresses();
|
|
var http = Peer.Http(connected: connected);
|
|
_peer.Serve("/users/c", "{\"id\":\"{A}/users/c\",\"type\":\"Person\"}");
|
|
|
|
await http.GetJson($"{_peer.A}/users/c", "application/activity+json", default, TestContext.Current.CancellationToken);
|
|
|
|
Assert.Equal(System.Net.IPAddress.Loopback, connected.Of("127.0.0.1"));
|
|
}
|
|
}
|
|
}
|