using System.Net; using System.Text.Json; using StevanFreeborn.Atproto.IdentityResolver.Exceptions; using RichardSzalay.MockHttp; using StevanFreeborn.Atproto.IdentityResolver.Retrieval; namespace StevanFreeborn.Atproto.IdentityResolver.Tests.Unit; public sealed class DefaultDidRetrieverTests : IDisposable { private readonly MockHttpMessageHandler _mockHttpMessageHandler = new(); private readonly DefaultDidRetriever _sut; public DefaultDidRetrieverTests() { _sut = new(_mockHttpMessageHandler.ToHttpClient()); } public void Dispose() { _mockHttpMessageHandler.Dispose(); } [Fact] public async Task GetByDnsOrThrowAsync_WhenHandleIsNull_ItShouldThrowArgumentNullException() { var act = async () => await _sut.GetByDnsOrThrowAsync(null!, TestContext.Current.CancellationToken); var e = await act.Should().ThrowAsync(); } [Fact] public async Task GetByDnsOrThrowAsync_WhenResponseIsNotSuccessful_ItShouldThrowDidNotFoundException() { var handle = "stevan.cc"; _mockHttpMessageHandler .When(HttpMethod.Get, $"https://cloudflare-dns.com/dns-query?name=_atproto.{handle}&type=TXT") .Respond(HttpStatusCode.InternalServerError); var act = async () => await _sut.GetByDnsOrThrowAsync(Handle.From(handle), TestContext.Current.CancellationToken); await act.Should().ThrowAsync(); } [Fact] public async Task GetByDnsOrThrowAsync_WhenResponseIsSuccessfulButDeserialziationFails_ItShouldThrowDidNotFoundException() { var handle = "stevan.cc"; _mockHttpMessageHandler .When(HttpMethod.Get, $"https://cloudflare-dns.com/dns-query?name=_atproto.{handle}&type=TXT") .Respond("application/json", JsonSerializer.Serialize("null")); var act = async () => await _sut.GetByDnsOrThrowAsync(Handle.From(handle), TestContext.Current.CancellationToken); var e = await act.Should().ThrowAsync(); e.WithInnerException(); } [Fact] public async Task GetByDnsOrThrowAsync_WhenResponseIsSuccessfulButBodyIsNull_ItShouldThrowDidNotFoundException() { var handle = "stevan.cc"; _mockHttpMessageHandler .When(HttpMethod.Get, $"https://cloudflare-dns.com/dns-query?name=_atproto.{handle}&type=TXT") .Respond("application/json", "null"); var act = async () => await _sut.GetByDnsOrThrowAsync(Handle.From(handle), TestContext.Current.CancellationToken); await act.Should().ThrowAsync(); } [Fact] public async Task GetByDnsOrThrowAsync_WhenResponseIsSuccessfulButNoAnswers_ItShouldThrowDidNotFoundException() { var handle = "stevan.cc"; var response = new DnsQueryResponse(); _mockHttpMessageHandler .When(HttpMethod.Get, $"https://cloudflare-dns.com/dns-query?name=_atproto.{handle}&type=TXT") .Respond("application/json", JsonSerializer.Serialize(response)); var act = async () => await _sut.GetByDnsOrThrowAsync(Handle.From(handle), TestContext.Current.CancellationToken); await act.Should().ThrowAsync(); } [Fact] public async Task GetByDnsOrThrowAsync_WhenResponseIsSuccessfulButTextRecordValueFormattedIncorrectly_ItShouldThrowDidNotFoundException() { var handle = "stevan.cc"; var response = new DnsQueryResponse() { Answer = [new() { Data = $"did:web:{handle}" }] }; _mockHttpMessageHandler .When(HttpMethod.Get, $"https://cloudflare-dns.com/dns-query?name=_atproto.{handle}&type=TXT") .Respond("application/json", JsonSerializer.Serialize(response)); var act = async () => await _sut.GetByDnsOrThrowAsync(Handle.From(handle), TestContext.Current.CancellationToken); await act.Should().ThrowAsync(); } [Fact] public async Task GetByDnsOrThrowAsync_WhenResponseIsSuccessfulAndTextRecordValueFormattedCorrectly_ItShouldReturnDid() { var handle = "stevan.cc"; var response = new DnsQueryResponse() { Answer = [new() { Data = $@"did=did:web:{handle}" }] }; _mockHttpMessageHandler .When(HttpMethod.Get, $"https://cloudflare-dns.com/dns-query?name=_atproto.{handle}&type=TXT") .Respond("application/json", JsonSerializer.Serialize(response)); var result = await _sut.GetByDnsOrThrowAsync(Handle.From(handle), TestContext.Current.CancellationToken); result.Should().BeEquivalentTo(Did.From($"did:web:{handle}")); } [Fact] public async Task GetByWellKnownOrThrowAsync_WhenResponseIsUnsuccessful_ItShouldThrowDidNotFoundException() { var handle = "stevan.cc"; var did = $"did:web:{handle}"; _mockHttpMessageHandler .When(HttpMethod.Get, $"https://{handle}/.well-known/atproto-did") .Respond(HttpStatusCode.InternalServerError); var act = async () => await _sut.GetByWellKnownOrThrowAsync(Handle.From(handle), TestContext.Current.CancellationToken); await act.Should().ThrowAsync(); } [Fact] public async Task GetByWellKnownOrThrowAsync_WhenResponseIsSuccessfulAndButDoesNotContainDid_ItShouldThrowDidNotFoundException() { var handle = "stevan.cc"; var did = $"did:web:{handle}"; _mockHttpMessageHandler .When(HttpMethod.Get, $"https://{handle}/.well-known/atproto-did") .Respond("application/json", JsonSerializer.Serialize(new { did })); var act = async () => await _sut.GetByWellKnownOrThrowAsync(Handle.From(handle), TestContext.Current.CancellationToken); await act.Should().ThrowAsync(); } [Fact] public async Task GetByWellKnownOrThrowAsync_WhenResponseIsSuccessfulAndContainsDid_ItShouldReturnDid() { var handle = "stevan.cc"; var did = $"did:web:{handle}"; _mockHttpMessageHandler .When(HttpMethod.Get, $"https://{handle}/.well-known/atproto-did") .Respond("text/plain", did); var result = await _sut.GetByWellKnownOrThrowAsync(Handle.From(handle), TestContext.Current.CancellationToken); result.Should().BeEquivalentTo(Did.From(did)); } }