using MaxMind.Db; using Microsoft.Extensions.Logging.Abstractions; using PrivaPub.Infrastructure.Geo; using PrivaPub.Infrastructure.Statistics; using PrivaPub.Tests.Support; using System.IO.Compression; namespace PrivaPub.Tests.Infrastructure { public sealed class GeoUpdaterTests : IAsyncLifetime { static readonly DateTime Now = new(2026, 10, 4, 12, 0, 0, DateTimeKind.Utc); static readonly DateTime LastMonth = new(2026, 9, 2, 0, 0, 0, DateTimeKind.Utc); readonly string _directory = Path.Combine(Path.GetTempPath(), $"privapub-geo-{Guid.NewGuid():N}"); Peer _peer; GeoUpdater _updater; DbIpLocator _locator; public async ValueTask InitializeAsync() { _peer = await Peer.Start(); var options = new StaticOptions(new StatisticsOptions { GeoDirectory = _directory, Geo = new GeoOptions { DownloadBase = $"{_peer.A}/free" } }); _locator = new DbIpLocator(options, NullLogger.Instance); _updater = new GeoUpdater(new Clients(), options, _locator, NullLogger.Instance); } public async ValueTask DisposeAsync() { _locator?.Dispose(); await _peer.DisposeAsync(); if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true); } sealed class Clients : IHttpClientFactory { public HttpClient CreateClient(string name) => new(); } static byte[] Gzip(byte[] bytes) { using var output = new MemoryStream(); using (var gzip = new GZipStream(output, CompressionLevel.Fastest)) gzip.Write(bytes); return output.ToArray(); } void Publish(string kind, DateTime month, byte[] database) => _peer.ServeFile($"/free/dbip-{kind}-lite-{month:yyyy-MM}.mmdb.gz", Gzip(database), "application/gzip"); static byte[] City(DateTime built) => MiniMmdb.Build("DBIP-City-Lite", built); static byte[] Asn(DateTime built) => MiniMmdb.Build("DBIP-ASN-Lite (compat=GeoLite2-ASN)", built); DateTime Built(string kind) { using var reader = new Reader(Path.Combine(_directory, GeoUpdater.FileOf(kind))); return reader.Metadata.BuildDate; } [Fact] public async Task Missing_databases_are_fetched_checked_and_read_at_once() { Publish("city", Now, City(Now)); Publish("asn", Now, Asn(Now)); Assert.Equal(2, await _updater.Update(Now, TestContext.Current.CancellationToken)); Assert.Equal(Now.Date, Built("city").Date); Assert.Equal(Now.Date, Built("asn").Date); Assert.Equal("DB-IP Lite 2026-10", _locator.Source); Assert.Empty(Directory.GetFiles(_directory).Where(f => f.EndsWith(".part") || f.EndsWith(".new"))); } [Fact] public async Task Early_in_the_month_last_months_database_is_used_and_not_fetched_again() { Publish("city", LastMonth, City(LastMonth)); Publish("asn", LastMonth, Asn(LastMonth)); Assert.Equal(2, await _updater.Update(Now, TestContext.Current.CancellationToken)); var asked = _peer.Requests.Count; Assert.Equal(0, await _updater.Update(Now, TestContext.Current.CancellationToken)); Assert.Equal(LastMonth.Date, Built("city").Date); Assert.DoesNotContain(_peer.Requests.Skip(asked), r => r.Path.Contains("2026-09")); } [Fact] public async Task A_current_database_asks_nothing() { Publish("city", Now, City(Now)); Publish("asn", Now, Asn(Now)); await _updater.Update(Now, TestContext.Current.CancellationToken); var asked = _peer.Requests.Count; Assert.Equal(0, await _updater.Update(Now.AddDays(1), TestContext.Current.CancellationToken)); Assert.Equal(asked, _peer.Requests.Count); } [Fact] public async Task A_broken_or_wrong_database_is_refused_and_the_old_one_kept() { Publish("city", LastMonth, City(LastMonth)); Publish("asn", LastMonth, Asn(LastMonth)); await _updater.Update(LastMonth.AddDays(1), TestContext.Current.CancellationToken); Publish("city", Now, "not a database at all"u8.ToArray()); Publish("asn", Now, City(Now)); Assert.Equal(0, await _updater.Update(Now, TestContext.Current.CancellationToken)); Assert.Equal(LastMonth.Date, Built("city").Date); Assert.Equal(LastMonth.Date, Built("asn").Date); Assert.Empty(Directory.GetFiles(_directory).Where(f => f.EndsWith(".part") || f.EndsWith(".new"))); } } }