diff --git a/src/BirdsiteLive.Domain/TheFedInfoService.cs b/src/BirdsiteLive.Domain/TheFedInfoService.cs new file mode 100644 index 0000000..cbb8491 --- /dev/null +++ b/src/BirdsiteLive.Domain/TheFedInfoService.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace BirdsiteLive.Domain +{ + public interface ITheFedInfoService + { + Task> GetBslInstanceListAsync(); + } + + public class TheFedInfoService : ITheFedInfoService + { + private readonly IHttpClientFactory _httpClientFactory; + + #region Ctor + public TheFedInfoService(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + #endregion + + public async Task> GetBslInstanceListAsync() + { + var cancellationToken = CancellationToken.None; + + var result = await CallGraphQlAsync( + new Uri("https://the-federation.info/graphql"), + HttpMethod.Get, + "query ($platform: String!) { nodes(platform: $platform) { host, version } }", + new + { + platform = "birdsitelive", + }, + cancellationToken); + + var convertedResults = ConvertResults(result); + return convertedResults; + } + + private List ConvertResults(GraphQLResponse qlData) + { + var results = new List(); + + foreach (var instanceInfo in qlData.Data.Nodes) + { + var rawVersion = instanceInfo.Version.Split('+').FirstOrDefault(); + var version = Version.Parse(rawVersion); + + var instance = new BslInstanceInfo + { + Host = instanceInfo.Host, + Version = version + }; + results.Add(instance); + } + + return results; + } + + private async Task> CallGraphQlAsync(Uri endpoint, HttpMethod method, string query, object variables, CancellationToken cancellationToken) + { + var content = new StringContent(SerializeGraphQlCall(query, variables), Encoding.UTF8, "application/json"); + var httpRequestMessage = new HttpRequestMessage + { + Method = method, + Content = content, + RequestUri = endpoint, + }; + //add authorization headers if necessary here + httpRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var httpClient = _httpClientFactory.CreateClient(); + using (var response = await httpClient.SendAsync(httpRequestMessage, cancellationToken)) + { + //if (response.IsSuccessStatusCode) + if (response?.Content.Headers.ContentType?.MediaType == "application/json") + { + var responseString = await response.Content.ReadAsStringAsync(); //cancellationToken supported for .NET 5/6 + return DeserializeGraphQlCall(responseString); + } + else + { + throw new ApplicationException($"Unable to contact '{endpoint}': {response.StatusCode} - {response.ReasonPhrase}"); + } + } + } + + private string SerializeGraphQlCall(string query, object variables) + { + var sb = new StringBuilder(); + var textWriter = new StringWriter(sb); + var serializer = new JsonSerializer(); + serializer.Serialize(textWriter, new + { + query = query, + variables = variables, + }); + return sb.ToString(); + } + + private GraphQLResponse DeserializeGraphQlCall(string response) + { + var serializer = new JsonSerializer(); + var stringReader = new StringReader(response); + var jsonReader = new JsonTextReader(stringReader); + var result = serializer.Deserialize>(jsonReader); + return result; + } + + private class GraphQLResponse + { + public List Errors { get; set; } + public TResponse Data { get; set; } + } + + private class GraphQLError + { + public string Message { get; set; } + public List Locations { get; set; } + public List Path { get; set; } //either int or string + } + + private class GraphQLErrorLocation + { + public int Line { get; set; } + public int Column { get; set; } + } + + private class MyResponseData + { + public Node[] Nodes { get; set; } + } + + private class Node + { + public string Host { get; set; } + public string Version { get; set; } + } + } + + public class BslInstanceInfo + { + public string Host { get; set; } + public Version Version { get; set; } + } +} \ No newline at end of file diff --git a/src/Tests/BirdsiteLive.Domain.Tests/TheFedInfoServiceTests.cs b/src/Tests/BirdsiteLive.Domain.Tests/TheFedInfoServiceTests.cs new file mode 100644 index 0000000..37f4572 --- /dev/null +++ b/src/Tests/BirdsiteLive.Domain.Tests/TheFedInfoServiceTests.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace BirdsiteLive.Domain.Tests +{ + [TestClass] + public class TheFedInfoServiceTests + { + [TestMethod] + public async Task GetBslInstanceListAsyncTest() + { + var httpClientFactoryMock = new Mock(); + httpClientFactoryMock + .Setup(x => x.CreateClient(It.IsAny())) + .Returns(new HttpClient()); + + var service = new TheFedInfoService(httpClientFactoryMock.Object); + + var bslInstanceList = await service.GetBslInstanceListAsync(); + + Assert.IsTrue(bslInstanceList.Count > 0); + + foreach (var instanceInfo in bslInstanceList) + { + Assert.IsFalse(string.IsNullOrWhiteSpace(instanceInfo.Host)); + Assert.IsTrue(instanceInfo.Version > new Version(0, 1, 0)); + } + } + } +} \ No newline at end of file