133 lines
4.6 KiB
C#
133 lines
4.6 KiB
C#
using System.Net;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using TheLivingWorld.Core.Geo;
|
|
using TheLivingWorld.Osm;
|
|
using TheLivingWorld.Osm.Overpass;
|
|
|
|
namespace TheLivingWorld.Tests;
|
|
|
|
public class OverpassClientTests : IDisposable
|
|
{
|
|
private static readonly GeoBounds Bounds = GeoBounds.FromCenter(new GeoPoint(31.8966010, -100.4858591), 1000);
|
|
|
|
private readonly string _cacheDirectory =
|
|
Path.Combine(Path.GetTempPath(), $"tlw-osm-cache-{Guid.NewGuid():n}");
|
|
|
|
[Fact]
|
|
public async Task Caches_the_response_and_serves_it_again_without_a_second_request()
|
|
{
|
|
var handler = new StubHandler((_, _) => Json("""{"elements":[]}"""));
|
|
var client = CreateClient(handler);
|
|
|
|
var first = await client.FetchAsync(Bounds, forceRefresh: false);
|
|
var second = await client.FetchAsync(Bounds, forceRefresh: false);
|
|
|
|
Assert.Equal(first, second);
|
|
Assert.Equal(1, handler.Requests);
|
|
Assert.Equal("""{"elements":[]}""", await File.ReadAllTextAsync(first));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Refetches_when_asked_to_refresh()
|
|
{
|
|
var handler = new StubHandler((_, _) => Json("""{"elements":[]}"""));
|
|
var client = CreateClient(handler);
|
|
|
|
await client.FetchAsync(Bounds, forceRefresh: false);
|
|
await client.FetchAsync(Bounds, forceRefresh: true);
|
|
|
|
Assert.Equal(2, handler.Requests);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Falls_back_to_the_next_mirror_after_a_retryable_failure()
|
|
{
|
|
var handler = new StubHandler((request, _) => request.RequestUri!.Host == "first.example"
|
|
? new HttpResponseMessage(HttpStatusCode.TooManyRequests) { Content = new StringContent("busy") }
|
|
: Json("""{"elements":[]}"""));
|
|
|
|
var client = CreateClient(handler, "https://first.example/api", "https://second.example/api");
|
|
|
|
var path = await client.FetchAsync(Bounds, forceRefresh: false);
|
|
|
|
Assert.True(File.Exists(path));
|
|
Assert.Equal(2, handler.Requests);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Rejects_an_html_error_page_served_under_a_200()
|
|
{
|
|
// An overloaded Overpass instance answers "the server is probably too busy" as HTML with status 200.
|
|
var handler = new StubHandler((_, _) => new HttpResponseMessage(HttpStatusCode.OK)
|
|
{
|
|
Content = new StringContent("<html><body>too busy</body></html>", System.Text.Encoding.UTF8, "text/html"),
|
|
});
|
|
|
|
var client = CreateClient(handler);
|
|
|
|
var failure = await Assert.ThrowsAsync<OverpassException>(() => client.FetchAsync(Bounds, forceRefresh: false));
|
|
|
|
Assert.Contains("text/html", failure.Message);
|
|
Assert.False(Directory.EnumerateFiles(_cacheDirectory, "*.json").Any());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Reports_the_last_error_when_every_mirror_fails()
|
|
{
|
|
var handler = new StubHandler((_, _) => new HttpResponseMessage(HttpStatusCode.BadGateway)
|
|
{
|
|
Content = new StringContent("<p>gateway is down</p>"),
|
|
});
|
|
|
|
var client = CreateClient(handler);
|
|
|
|
var failure = await Assert.ThrowsAsync<OverpassException>(() => client.FetchAsync(Bounds, forceRefresh: false));
|
|
|
|
Assert.Contains("gateway is down", failure.Message);
|
|
}
|
|
|
|
private static HttpResponseMessage Json(string body) => new(HttpStatusCode.OK)
|
|
{
|
|
Content = new StringContent(body, System.Text.Encoding.UTF8, "application/json"),
|
|
};
|
|
|
|
private OverpassClient CreateClient(StubHandler handler, params string[] endpoints)
|
|
{
|
|
var options = new OsmOptions
|
|
{
|
|
CacheDirectory = _cacheDirectory,
|
|
MaxAttemptsPerEndpoint = 1,
|
|
RequestTimeoutSeconds = 5,
|
|
};
|
|
|
|
if (endpoints.Length > 0) options.Endpoints = endpoints;
|
|
else options.Endpoints = ["https://only.example/api"];
|
|
|
|
return new OverpassClient(
|
|
new HttpClient(handler),
|
|
Options.Create(options),
|
|
NullLogger<OverpassClient>.Instance);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (Directory.Exists(_cacheDirectory)) Directory.Delete(_cacheDirectory, recursive: true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
private sealed class StubHandler(Func<HttpRequestMessage, CancellationToken, HttpResponseMessage> respond)
|
|
: HttpMessageHandler
|
|
{
|
|
public int Requests { get; private set; }
|
|
|
|
protected override Task<HttpResponseMessage> SendAsync(
|
|
HttpRequestMessage request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
Requests++;
|
|
return Task.FromResult(respond(request, cancellationToken));
|
|
}
|
|
}
|
|
}
|