Social bar v2: fediverse icons resolved via nodeinfo (with redirect-following), persisted software column, icon colors — 112 tests passing, 0 warnings
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using ytLive.Models;
|
||||
using ytLive.Services;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
public class SocialValidatorTests
|
||||
{
|
||||
private sealed class StubHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly Func<HttpRequestMessage, HttpResponseMessage> _respond;
|
||||
|
||||
public StubHandler(HttpResponseMessage response) : this(_ => response) { }
|
||||
|
||||
public StubHandler(Func<HttpRequestMessage, HttpResponseMessage> respond) => _respond = respond;
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
return Task.FromCanceled<HttpResponseMessage>(ct);
|
||||
return Task.FromResult(_respond(request));
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpSocialValidator Validator(HttpMessageHandler handler)
|
||||
=> new(new HttpClient(handler));
|
||||
|
||||
[Fact]
|
||||
public async Task FediverseHandle_BuildsHostUrl()
|
||||
{
|
||||
var validator = Validator(new StubHandler(new HttpResponseMessage(HttpStatusCode.OK)));
|
||||
|
||||
var result = await validator.LookupAsync(SocialService.Fediverse, "@user@site.tld", CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("https://site.tld/@user", result.ProfileUrl);
|
||||
Assert.Equal("@user@site.tld", result.Handle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FediverseHandle_FetchesInstanceSoftware()
|
||||
{
|
||||
var validator = Validator(new StubHandler(RespondFediverse("peertube")));
|
||||
|
||||
var result = await validator.LookupAsync(SocialService.Fediverse, "@gramps@llamachile.tube", CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("peertube", result.FediverseSoftware);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FediverseHandle_RootRedirectToSubdomain_ResolvesSoftware()
|
||||
{
|
||||
var validator = Validator(new StubHandler(request =>
|
||||
{
|
||||
var uri = request.RequestUri!;
|
||||
if (uri.Host == "site.tld")
|
||||
{
|
||||
if (uri.AbsolutePath == "/")
|
||||
{
|
||||
// Identity domain's default app redirects to the real instance.
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
RequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://social.site.tld/"),
|
||||
};
|
||||
}
|
||||
if (uri.AbsolutePath.StartsWith("/@"))
|
||||
return new HttpResponseMessage(HttpStatusCode.OK); // profile validates
|
||||
return new HttpResponseMessage(HttpStatusCode.NotFound); // nodeinfo SSO-blocked
|
||||
}
|
||||
if (uri.Host == "social.site.tld")
|
||||
{
|
||||
if (uri.AbsolutePath.StartsWith("/.well-known"))
|
||||
return Json(new { links = new[] { new { rel = "http://nodeinfo.diaspora.software/ns/schema/2.0", href = "https://social.site.tld/nodeinfo/2.0" } } });
|
||||
if (uri.AbsolutePath.StartsWith("/nodeinfo"))
|
||||
return Json(new { software = new { name = "mastodon", version = "4.6.3" } });
|
||||
}
|
||||
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||
}));
|
||||
|
||||
var result = await validator.LookupAsync(SocialService.Fediverse, "@user@site.tld", CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("mastodon", result.FediverseSoftware);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FediverseHandle_NodeInfoUnavailable_StillSucceeds()
|
||||
{
|
||||
var validator = Validator(new StubHandler(request =>
|
||||
request.RequestUri!.AbsolutePath.StartsWith("/.well-known")
|
||||
? new HttpResponseMessage(HttpStatusCode.NotFound)
|
||||
: new HttpResponseMessage(HttpStatusCode.OK)));
|
||||
|
||||
var result = await validator.LookupAsync(SocialService.Fediverse, "@user@site.tld", CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Null(result.FediverseSoftware);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FediverseHandle_CanceledDuringNodeInfo_IsCanceled()
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
var request = 0;
|
||||
var validator = Validator(new StubHandler(_ =>
|
||||
{
|
||||
if (++request >= 2) cts.Cancel();
|
||||
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||
}));
|
||||
|
||||
var result = await validator.LookupAsync(SocialService.Fediverse, "@user@site.tld", cts.Token);
|
||||
|
||||
Assert.True(result.Canceled);
|
||||
Assert.False(result.Success);
|
||||
}
|
||||
|
||||
private static Func<HttpRequestMessage, HttpResponseMessage> RespondFediverse(string software)
|
||||
{
|
||||
return request =>
|
||||
{
|
||||
if (request.RequestUri!.AbsolutePath.StartsWith("/.well-known"))
|
||||
{
|
||||
return Json(new
|
||||
{
|
||||
links = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
rel = "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
href = $"https://{request.RequestUri.Host}/nodeinfo/2.0",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (request.RequestUri!.AbsolutePath.StartsWith("/nodeinfo"))
|
||||
{
|
||||
return Json(new { software = new { name = software, version = "1.0.0" } });
|
||||
}
|
||||
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||
};
|
||||
}
|
||||
|
||||
private static HttpResponseMessage Json(object payload)
|
||||
=> new(HttpStatusCode.OK) { Content = new StringContent(System.Text.Json.JsonSerializer.Serialize(payload)) };
|
||||
|
||||
[Fact]
|
||||
public async Task KnownServiceHandle_CanonicalUrl()
|
||||
{
|
||||
var validator = Validator(new StubHandler(new HttpResponseMessage(HttpStatusCode.OK)));
|
||||
|
||||
var result = await validator.LookupAsync(SocialService.X, "creator", CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("https://x.com/creator", result.ProfileUrl);
|
||||
Assert.Equal("creator", result.Handle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NotFound_IsRejected()
|
||||
{
|
||||
var validator = Validator(new StubHandler(new HttpResponseMessage(HttpStatusCode.NotFound)));
|
||||
|
||||
var result = await validator.LookupAsync(SocialService.X, "doesnotexist", CancellationToken.None);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("NotFound", result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Redirect_CountsAsExists()
|
||||
{
|
||||
var validator = Validator(new StubHandler(new HttpResponseMessage(HttpStatusCode.Found)));
|
||||
|
||||
var result = await validator.LookupAsync(SocialService.GitHub, "dev", CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectionFailure_IsFriendlyError()
|
||||
{
|
||||
var validator = Validator(new StubHandler(_ => throw new HttpRequestException("boom")));
|
||||
|
||||
var result = await validator.LookupAsync(SocialService.X, "user", CancellationToken.None);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("Couldn't reach", result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanceledToken_AbortsRequest()
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
var validator = Validator(new StubHandler(new HttpResponseMessage(HttpStatusCode.OK)));
|
||||
|
||||
var result = await validator.LookupAsync(SocialService.X, "creator", cts.Token);
|
||||
|
||||
Assert.True(result.Canceled);
|
||||
Assert.False(result.Success);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user