Files

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));
}
}
}