An account on a server whose handles are not its host's (Mastodon's LOCAL_DOMAIN apart from WEB_DOMAIN) showed as user@host. The actor's `webfinger` names the handle; its domain is kept once WebFinger there points back to the actor, and asked again when the name changes. A persona's blocked servers match a handle's domain as well as the actor's host, as the lists Mastodon exports name handles' domains. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
118 lines
4.6 KiB
C#
118 lines
4.6 KiB
C#
using Microsoft.Extensions.Caching.Memory;
|
|
|
|
using MongoDB.Entities;
|
|
|
|
using PrivaPub.Domain.Relationships;
|
|
using PrivaPub.Federation.Actors;
|
|
using PrivaPub.Infrastructure.Http;
|
|
using PrivaPub.Models.Social;
|
|
using PrivaPub.Tests.Support;
|
|
using PrivaPub.Tests.Support.Host;
|
|
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace PrivaPub.Tests.Federation
|
|
{
|
|
// an account's handle: user@host, or the domain its `webfinger` names (FEP-2c59; Mastodon's LOCAL_DOMAIN apart from
|
|
// its WEB_DOMAIN) once that domain's WebFinger says the handle is this account
|
|
[Trait("Category", "Integration")]
|
|
public sealed class RemoteHandleTests : IAsyncLifetime
|
|
{
|
|
Harness _harness;
|
|
RemoteActorService _remote;
|
|
|
|
public async ValueTask InitializeAsync()
|
|
{
|
|
Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip);
|
|
_harness = await Harness.Start();
|
|
_remote = new RemoteActorService(Peer.Http(), _harness.Local, new MemoryCache(new MemoryCacheOptions()), _harness.Db,
|
|
new StaticOptions<FederationOptions>(new FederationOptions { AllowPrivateNetworks = true, AllowPlainHttp = true }));
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_harness != default)
|
|
await _harness.DisposeAsync();
|
|
}
|
|
|
|
// the actor at the peer's first address, its handle at the second
|
|
RemoteActor Actor(string name) => Serve(new RemoteActor(_harness.Peer, name));
|
|
|
|
RemoteActor Serve(RemoteActor actor, string preferredUsername = default)
|
|
{
|
|
var document = actor.Document();
|
|
if (preferredUsername != default)
|
|
document["preferredUsername"] = preferredUsername;
|
|
document["webfinger"] = $"acct:{preferredUsername ?? actor.Name}@{new Uri(_harness.Peer.B).Authority}";
|
|
_harness.Peer.Serve(new Uri(actor.Id).AbsolutePath, document.ToJsonString());
|
|
return actor;
|
|
}
|
|
|
|
void WebFinger(string self) => _harness.Peer.ServeText("/.well-known/webfinger", new JsonObject
|
|
{
|
|
["links"] = new JsonArray(new JsonObject { ["rel"] = "self", ["type"] = "application/activity+json", ["href"] = self })
|
|
}.ToJsonString(), "application/jrd+json");
|
|
|
|
[Fact]
|
|
public async Task A_handle_on_another_domain_is_kept_once_its_webfinger_agrees()
|
|
{
|
|
var actor = Actor("split");
|
|
WebFinger(actor.Id);
|
|
|
|
var stored = await _remote.GetActor(actor.Id, refresh: true, TestContext.Current.CancellationToken);
|
|
|
|
Assert.Equal(new Uri(_harness.Peer.B).Authority, stored.Domain);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_handle_its_webfinger_does_not_confirm_stays_on_the_actors_host()
|
|
{
|
|
var actor = Actor("claims");
|
|
WebFinger("http://localhost/users/someone-else");
|
|
|
|
var stored = await _remote.GetActor(actor.Id, refresh: true, TestContext.Current.CancellationToken);
|
|
|
|
Assert.Equal(new Uri(actor.Id).Authority, stored.Domain);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_renamed_account_is_asked_again()
|
|
{
|
|
var actor = Actor("renames");
|
|
WebFinger(actor.Id);
|
|
await _remote.GetActor(actor.Id, refresh: true, TestContext.Current.CancellationToken);
|
|
|
|
Serve(actor, "renamed");
|
|
WebFinger("http://localhost/users/someone-else");
|
|
// (another cache: the first fetch holds the actor back for a while)
|
|
var later = new RemoteActorService(Peer.Http(), _harness.Local, new MemoryCache(new MemoryCacheOptions()), _harness.Db,
|
|
new StaticOptions<FederationOptions>(new FederationOptions { AllowPrivateNetworks = true, AllowPlainHttp = true }));
|
|
var stored = await later.GetActor(actor.Id, refresh: true, TestContext.Current.CancellationToken);
|
|
|
|
Assert.Equal("renamed", stored.UserName);
|
|
Assert.Equal(new Uri(actor.Id).Authority, stored.Domain);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_persona_blocking_the_handles_server_hides_the_account()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var (_, alice) = await _harness.Persona("alice");
|
|
var split = Actor("split");
|
|
WebFinger(split.Id);
|
|
await _remote.GetActor(split.Id, refresh: true, token);
|
|
var claims = Actor("claims");
|
|
WebFinger("http://localhost/users/someone-else");
|
|
await new RemoteActorService(Peer.Http(), _harness.Local, new MemoryCache(new MemoryCacheOptions()), _harness.Db,
|
|
new StaticOptions<FederationOptions>(new FederationOptions { AllowPrivateNetworks = true, AllowPlainHttp = true }))
|
|
.GetActor(claims.Id, refresh: true, token);
|
|
|
|
await DB.Default.SaveAsync(new AccountDomainBlock { AvatarId = alice.Id, Domain = new Uri(_harness.Peer.B).Host }, token);
|
|
|
|
Assert.Equal([split.Id], await Hidden.AuthorsHiddenFrom(alice.Id, new[] { split.Id, claims.Id }, forNotifications: false, token));
|
|
Assert.Contains(alice.Id, await Hidden.RecipientsHiding(new[] { alice.Id }, split.Id, token));
|
|
Assert.Empty(await Hidden.RecipientsHiding(new[] { alice.Id }, claims.Id, token));
|
|
}
|
|
}
|
|
}
|