209 lines
7.5 KiB
C#
209 lines
7.5 KiB
C#
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);
|
|
}
|
|
}
|