feat: implement default retrievers
This commit is contained in:
1 parent
b6017e1222
commit
d3a2794293
8 files changed
+544
-8
No files matched your search
+14
-8
@@ -1,17 +1,17 @@
|
|||||||
namespace StevanFreeborn.Atproto.IdentityResolver;
|
namespace StevanFreeborn.Atproto.IdentityResolver;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Represents an identifier for the atproto network.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class AtprotoId
|
public abstract class AtprotoId
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Gets the string representation of the underlying id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Value { get; protected set; } = string.Empty;
|
public string Value { get; protected set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Constructs a strongly typed representation of the given id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static AtprotoId From(string atprotoId)
|
public static AtprotoId From(string atprotoId)
|
||||||
{
|
{
|
||||||
@@ -37,14 +37,14 @@ public abstract class AtprotoId
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Represents a did on the atproto network.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Did : AtprotoId
|
public class Did : AtprotoId
|
||||||
{
|
{
|
||||||
private static readonly string[] ValidPrefixes = ["did:web:", "did:plc:"];
|
private static readonly string[] ValidPrefixes = ["did:web:", "did:plc:"];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Gets the method associated with the did.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Method { get; private set; }
|
public string Method { get; private set; }
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ public class Did : AtprotoId
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Constructs a strongly-typed representation of the did.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static new Did From(string did)
|
public static new Did From(string did)
|
||||||
{
|
{
|
||||||
@@ -103,7 +103,7 @@ public class Did : AtprotoId
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Represents a handle for an id on the atproto network.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Handle : AtprotoId
|
public class Handle : AtprotoId
|
||||||
{
|
{
|
||||||
@@ -123,8 +123,14 @@ public class Handle : AtprotoId
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Constructs a strongly-typed representation of the handle.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="handle">The handle for the id you'd like to represent</param>
|
||||||
|
/// <returns>An instance of a <see cref="Handle"/></returns>
|
||||||
|
/// <exception cref="ArgumentException">
|
||||||
|
/// Thrown when <paramref name="handle"></paramref> is null or empty.
|
||||||
|
/// Thrown when <paramref name="handle"></paramref> handle starts with '@'.
|
||||||
|
/// </exception>
|
||||||
public static new Handle From(string handle)
|
public static new Handle From(string handle)
|
||||||
{
|
{
|
||||||
return new(handle);
|
return new(handle);
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
using StevanFreeborn.Atproto.IdentityResolver.Json;
|
||||||
|
|
||||||
|
namespace StevanFreeborn.Atproto.IdentityResolver.Identity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DidDocument
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("@context")]
|
||||||
|
[JsonConverter(typeof(StringToCollectionConverter))]
|
||||||
|
public Collection<string> Context { get; private set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("alsoKnownAs")]
|
||||||
|
public Collection<string> AlsoKnownAs { get; private set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("verificationMethod")]
|
||||||
|
public Collection<VerificationMethod> VerificatonMethod { get; private set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("service")]
|
||||||
|
public Collection<Service> Service { get; private set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class VerificationMethod
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("controller")]
|
||||||
|
public string Controller { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("publicKeyMultiBase")]
|
||||||
|
public string PublicKeyMultiBase { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class Service
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("serviceEndpoint")]
|
||||||
|
public string ServiceEndpoint { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
using StevanFreeborn.Atproto.IdentityResolver.Exceptions;
|
||||||
|
using StevanFreeborn.Atproto.IdentityResolver.Identity;
|
||||||
|
using StevanFreeborn.Atproto.IdentityResolver.Json;
|
||||||
|
|
||||||
|
namespace StevanFreeborn.Atproto.IdentityResolver.Retrieval;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class DefaultDidDocumentRetriever(HttpClient httpClient) : IDidDocumentRetriever
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public Task<DidDocument> GetByPlcOrThrowAsync(Did did, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (did is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(did));
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestUri = new Uri($"https://plc.directory/{did.Value}", UriKind.Absolute);
|
||||||
|
return GetOrThrowAsync(requestUri, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public Task<DidDocument> GetByWellKnownOrThrowAsync(Handle handle, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (handle is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestUri = new Uri($"https://{handle.Value}/.well-known/did.json", UriKind.Absolute);
|
||||||
|
return GetOrThrowAsync(requestUri, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<DidDocument> GetOrThrowAsync(Uri requestUri, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var response = await httpClient.GetAsync(requestUri, ct).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
using var body = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
var didDocument = await JsonSerializer
|
||||||
|
.DeserializeAsync(body, JsonContext.Default.DidDocument, ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
return didDocument ?? throw new InvalidOperationException("Deserializing response body resulted in null value.");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new DidDocumentNotFoundException("Unable to retrieve did document. See inner exception", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
using StevanFreeborn.Atproto.IdentityResolver.Exceptions;
|
||||||
|
using StevanFreeborn.Atproto.IdentityResolver.Json;
|
||||||
|
|
||||||
|
namespace StevanFreeborn.Atproto.IdentityResolver.Retrieval;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class DefaultDidRetriever(HttpClient httpClient) : IDidRetriever
|
||||||
|
{
|
||||||
|
private static readonly MediaTypeWithQualityHeaderValue DnsJsonHeader = new("application/dns-json");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Did> GetByDnsOrThrowAsync(Handle handle, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (handle is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var requestUri = new Uri($"https://cloudflare-dns.com/dns-query?name=_atproto.{handle.Value}&type=TXT");
|
||||||
|
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
||||||
|
request.Headers.Accept.Add(DnsJsonHeader);
|
||||||
|
|
||||||
|
using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
using var body = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||||
|
var dnsResponse = await JsonSerializer.DeserializeAsync(body, JsonContext.Default.DnsQueryResponse, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var didString = ExtractDidFromDnsResponse(dnsResponse);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(didString))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"No valid atproto DID TXT record found for handle '{handle.Value}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Did.From(didString);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new DidNotFoundException("Unable to get did. See inner exception", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Did> GetByWellKnownOrThrowAsync(Handle handle, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (handle is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var requestUri = new Uri($"https://{handle.Value}/.well-known/atproto-did");
|
||||||
|
|
||||||
|
using var response = await httpClient.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var rawDid = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
var trimmedDid = rawDid?.Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(trimmedDid))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Endpoint '{requestUri}' returned an empty DID response.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Did.From(trimmedDid);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new DidNotFoundException("Unable to retrieve did. See inner exception", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ExtractDidFromDnsResponse(DnsQueryResponse? response)
|
||||||
|
{
|
||||||
|
if (response?.Answer is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var answer in response.Answer)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(answer.Data))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cloudflare wraps TXT records in quotes (e.g., "\"did=did:plc:...\"")
|
||||||
|
var text = answer.Data.Trim('\"');
|
||||||
|
|
||||||
|
if (text.StartsWith("did=", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return text.Substring(4).Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DnsQueryResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("Answer")]
|
||||||
|
public List<Answer> Answer { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Answer
|
||||||
|
{
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public string Data { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using StevanFreeborn.Atproto.IdentityResolver.Identity;
|
||||||
|
|
||||||
|
namespace StevanFreeborn.Atproto.IdentityResolver.Retrieval;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public interface IDidDocumentRetriever
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
Task<DidDocument> GetByWellKnownOrThrowAsync(Handle handle, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
Task<DidDocument> GetByPlcOrThrowAsync(Did did, CancellationToken ct);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace StevanFreeborn.Atproto.IdentityResolver.Retrieval;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public interface IDidRetriever
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
Task<Did> GetByWellKnownOrThrowAsync(Handle handle, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
Task<Did> GetByDnsOrThrowAsync(Handle handle, CancellationToken ct);
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
using RichardSzalay.MockHttp;
|
||||||
|
|
||||||
|
using StevanFreeborn.Atproto.IdentityResolver.Retrieval;
|
||||||
|
|
||||||
|
namespace StevanFreeborn.Atproto.IdentityResolver.Tests.Unit;
|
||||||
|
|
||||||
|
public sealed class DefaultDidDocumentRetrieverTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly MockHttpMessageHandler _mockHttpMessageHandler = new();
|
||||||
|
private readonly DefaultDidDocumentRetriever _sut;
|
||||||
|
|
||||||
|
public DefaultDidDocumentRetrieverTests()
|
||||||
|
{
|
||||||
|
_sut = new(_mockHttpMessageHandler.ToHttpClient());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_mockHttpMessageHandler.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByWellKnownOrThrowAsync_WhenResponse()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
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<ArgumentNullException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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<DidNotFoundException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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<DidNotFoundException>();
|
||||||
|
e.WithInnerException<JsonException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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<DidNotFoundException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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<DidNotFoundException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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<DidNotFoundException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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<DidNotFoundException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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<DidNotFoundException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user