Update README with project details and setup instructions; add data directories to .gitignore
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user