Wetterdienst
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Api.Tests;
|
||||
|
||||
public sealed class DwdWeatherServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void ParseStationCatalog_LiestFestbreitenformatUndFindetNaechsteStation()
|
||||
{
|
||||
const string catalog = """
|
||||
ID ICAO NAME LAT LON ELEV
|
||||
----- ---- -------------------- ----- ------- -----
|
||||
10641 EDDF OFFENBACH-WETTERPARK 50.08 8.78 119
|
||||
10382 EDDT BERLIN-TEGEL 52.56 13.31 37
|
||||
""";
|
||||
|
||||
var stations = DwdWeatherService.ParseStationCatalog(catalog);
|
||||
var nearest = DwdWeatherService.FindNearestStation(stations, 50.1, 8.7);
|
||||
|
||||
Assert.Equal(2, stations.Count);
|
||||
Assert.Equal("10641", nearest!.Id);
|
||||
Assert.Equal("OFFENBACH-WETTERPARK", nearest.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseMosmix_KonvertiertEinheitenUndBehaeltFehlwerteAlsNull()
|
||||
{
|
||||
var now = DateTime.UtcNow.AddHours(1).ToString("yyyy-MM-dd'T'HH:00:00.000'Z'");
|
||||
var kml = $$"""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml:kml xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:dwd="https://opendata.dwd.de/weather/lib/pointforecast_dwd_extension_V1_0.xsd">
|
||||
<kml:Document><kml:ExtendedData><dwd:ProductDefinition>
|
||||
<dwd:IssueTime>2026-08-23T15:00:00Z</dwd:IssueTime>
|
||||
<dwd:ForecastTimeSteps><dwd:TimeStep>{{now}}</dwd:TimeStep></dwd:ForecastTimeSteps>
|
||||
</dwd:ProductDefinition></kml:ExtendedData><kml:Placemark><kml:ExtendedData>
|
||||
<dwd:Forecast dwd:elementName="TTT"><dwd:value>293.15</dwd:value></dwd:Forecast>
|
||||
<dwd:Forecast dwd:elementName="FF"><dwd:value>5</dwd:value></dwd:Forecast>
|
||||
<dwd:Forecast dwd:elementName="FX1"><dwd:value>-</dwd:value></dwd:Forecast>
|
||||
<dwd:Forecast dwd:elementName="RR1c"><dwd:value>1.25</dwd:value></dwd:Forecast>
|
||||
<dwd:Forecast dwd:elementName="Neff"><dwd:value>75</dwd:value></dwd:Forecast>
|
||||
<dwd:Forecast dwd:elementName="ww"><dwd:value>61</dwd:value></dwd:Forecast>
|
||||
</kml:ExtendedData></kml:Placemark></kml:Document>
|
||||
</kml:kml>
|
||||
""";
|
||||
|
||||
var parsed = DwdWeatherService.ParseMosmix(Zip("forecast.kml", kml));
|
||||
var hour = Assert.Single(parsed.Hours);
|
||||
|
||||
Assert.Equal(20, hour.TemperatureC);
|
||||
Assert.Equal(18, hour.WindSpeedKmh);
|
||||
Assert.Null(hour.WindGustKmh);
|
||||
Assert.Equal(1.3, hour.PrecipitationMm);
|
||||
Assert.Equal(61, hour.WeatherCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseCapArchive_OrdnetWarnpolygonGeografischZu()
|
||||
{
|
||||
var xml = """
|
||||
<alert xmlns="urn:oasis:names:tc:emergency:cap:1.2">
|
||||
<identifier>warnung-1</identifier><status>Actual</status>
|
||||
<info><language>de-DE</language><event>GEWITTER</event><severity>Severe</severity>
|
||||
<onset>2026-08-23T18:00:00Z</onset><expires>2099-08-23T20:00:00Z</expires>
|
||||
<headline>Amtliche Warnung</headline><description>Test</description>
|
||||
<area><polygon>50.0,8.0 50.0,9.0 51.0,9.0 51.0,8.0 50.0,8.0</polygon></area>
|
||||
</info>
|
||||
</alert>
|
||||
""";
|
||||
|
||||
var warning = Assert.Single(DwdWeatherService.ParseCapArchive(
|
||||
Zip("warning.xml", xml), DateTime.UtcNow));
|
||||
|
||||
Assert.True(DwdWeatherService.PointInPolygon(warning.Polygons[0], 50.5, 8.5));
|
||||
Assert.False(DwdWeatherService.PointInPolygon(warning.Polygons[0], 52.0, 8.5));
|
||||
Assert.Equal("GEWITTER", warning.Warning.Event);
|
||||
}
|
||||
|
||||
private static byte[] Zip(string fileName, string content)
|
||||
{
|
||||
using var memory = new MemoryStream();
|
||||
using (var archive = new ZipArchive(memory, ZipArchiveMode.Create, leaveOpen: true))
|
||||
{
|
||||
var entry = archive.CreateEntry(fileName);
|
||||
using var writer = new StreamWriter(entry.Open(), new UTF8Encoding(false));
|
||||
writer.Write(content);
|
||||
}
|
||||
return memory.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using LehrerApp.Core.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Api.Tests;
|
||||
|
||||
public sealed class NominatimGeocoderTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GeocodeAsync_NutztStrukturierteDeutschlandSucheUndLiestDisplayName()
|
||||
{
|
||||
Uri? requestedUri = null;
|
||||
var handler = new RecordingHandler(request =>
|
||||
{
|
||||
requestedUri = request.RequestUri;
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(
|
||||
"""[{"lat":"50.105","lon":"8.761","display_name":"Testschule, Offenbach, Deutschland"}]""",
|
||||
Encoding.UTF8, "application/json"),
|
||||
};
|
||||
});
|
||||
var http = new HttpClient(handler) { BaseAddress = new Uri("https://nominatim.openstreetmap.org/") };
|
||||
var geocoder = new NominatimGeocoder(http);
|
||||
|
||||
var result = await geocoder.GeocodeAsync(new SchoolLocationRequest
|
||||
{
|
||||
SchoolName = "Testschule", Street = "Schulweg 1", PostalCode = "63000", City = "Offenbach",
|
||||
}, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(50.105, result!.Latitude);
|
||||
Assert.Equal("Testschule, Offenbach, Deutschland", result.DisplayName);
|
||||
Assert.Contains("countrycodes=de", requestedUri!.Query);
|
||||
Assert.Contains("street=Schulweg%201", requestedUri.Query);
|
||||
Assert.Contains("postalcode=63000", requestedUri.Query);
|
||||
}
|
||||
|
||||
private sealed class RecordingHandler(Func<HttpRequestMessage, HttpResponseMessage> response)
|
||||
: HttpMessageHandler
|
||||
{
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
|
||||
CancellationToken cancellationToken) => Task.FromResult(response(request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Api.Tests;
|
||||
|
||||
public sealed class SchoolWeatherStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public void SaveLocation_TrenntNutzerUndErsetztVorhandenenStandort()
|
||||
{
|
||||
using var temp = new TempStore();
|
||||
temp.Store.SaveLocation("eins", Location("Köln", 50.9, 6.9));
|
||||
temp.Store.SaveLocation("zwei", Location("Berlin", 52.5, 13.4));
|
||||
temp.Store.SaveLocation("eins", Location("Bonn", 50.7, 7.1));
|
||||
|
||||
Assert.Equal("Bonn", temp.Store.GetLocation("eins")!.City);
|
||||
Assert.Equal("Berlin", temp.Store.GetLocation("zwei")!.City);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveLocation_AenderungEntferntWettercacheDesAltenOrts()
|
||||
{
|
||||
using var temp = new TempStore();
|
||||
temp.Store.SaveLocation("eins", Location("Köln", 50.9, 6.9));
|
||||
temp.Store.SaveWeather("eins", new WeatherSnapshot { StationId = "alt", RetrievedAt = DateTime.UtcNow });
|
||||
|
||||
temp.Store.SaveLocation("eins", Location("Bonn", 50.7, 7.1));
|
||||
|
||||
Assert.Null(temp.Store.GetWeather("eins"));
|
||||
}
|
||||
|
||||
private static SchoolLocationProfile Location(string city, double lat, double lon) => new()
|
||||
{
|
||||
SchoolName = "Testschule", Street = "Schulweg 1", PostalCode = "12345", City = city,
|
||||
Latitude = lat, Longitude = lon, ResolvedAddress = city, UpdatedAt = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
private sealed class TempStore : IDisposable
|
||||
{
|
||||
private readonly string _path = Path.Combine(Path.GetTempPath(), $"lehrerapp-weather-tests-{Guid.NewGuid():N}");
|
||||
public SchoolWeatherStore Store { get; }
|
||||
public TempStore() { Directory.CreateDirectory(_path); Store = new(_path); }
|
||||
public void Dispose()
|
||||
{
|
||||
Store.Dispose();
|
||||
if (Directory.Exists(_path)) Directory.Delete(_path, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user