134 lines
5.1 KiB
PHP
134 lines
5.1 KiB
PHP
<?php
|
|
|
|
app_require('routes/moduleWeatherAPIRoute.php');
|
|
|
|
use routes\moduleWeatherAPIRoute;
|
|
|
|
function department_weather_fallback_invoke_private(moduleWeatherAPIRoute $route, string $method, array $args = []): mixed
|
|
{
|
|
$reflection = new ReflectionClass($route);
|
|
$target = $reflection->getMethod($method);
|
|
$target->setAccessible(true);
|
|
|
|
return $target->invokeArgs($route, $args);
|
|
}
|
|
|
|
function department_weather_coordinate_property(mixed $value): object
|
|
{
|
|
return new class($value) {
|
|
public function __construct(private mixed $value)
|
|
{
|
|
}
|
|
|
|
public function value(): mixed
|
|
{
|
|
return $this->value;
|
|
}
|
|
};
|
|
}
|
|
|
|
function department_weather_fake_department(mixed $latitude, mixed $longitude): object
|
|
{
|
|
return (object)[
|
|
'latitude' => department_weather_coordinate_property($latitude),
|
|
'longitude' => department_weather_coordinate_property($longitude),
|
|
];
|
|
}
|
|
|
|
beforeEach(function (): void {
|
|
$_SERVER['REQUEST_URI'] = '/departments/weather';
|
|
});
|
|
|
|
it('resolves weather coordinates from valid coordinates only', function (): void {
|
|
$route = new moduleWeatherAPIRoute();
|
|
$coordinates = department_weather_fallback_invoke_private($route, 'resolveWeatherCoordinates', [[
|
|
department_weather_fake_department('', ''),
|
|
department_weather_fake_department('0', '0'),
|
|
department_weather_fake_department('91.0', '12.0'),
|
|
department_weather_fake_department('55.6761', '181.0'),
|
|
department_weather_fake_department('55.6761', '12.5683'),
|
|
department_weather_fake_department(56.2639, 9.5018),
|
|
]]);
|
|
|
|
expect($coordinates)->toBeArray();
|
|
expect(round($coordinates['lat'], 4))->toBe(round((55.6761 + 56.2639) / 2, 4));
|
|
expect(round($coordinates['lon'], 4))->toBe(round((12.5683 + 9.5018) / 2, 4));
|
|
});
|
|
|
|
it('returns null weather coordinates when all department locations are invalid', function (): void {
|
|
$route = new moduleWeatherAPIRoute();
|
|
$coordinates = department_weather_fallback_invoke_private($route, 'resolveWeatherCoordinates', [[
|
|
department_weather_fake_department('', ''),
|
|
department_weather_fake_department('0', '0'),
|
|
department_weather_fake_department('95.0', '12.0'),
|
|
department_weather_fake_department('55.0', '-181.0'),
|
|
]]);
|
|
|
|
expect($coordinates)->toBeNull();
|
|
});
|
|
|
|
it('classifies weatherapi no matching location errors as location lookup failures', function (): void {
|
|
$route = new moduleWeatherAPIRoute();
|
|
|
|
$is_location_error = department_weather_fallback_invoke_private($route, 'isWeatherLocationLookupFailure', [
|
|
new Exception('WeatherAPI request failed with HTTP 400: No matching location found.'),
|
|
]);
|
|
|
|
expect($is_location_error)->toBeTrue();
|
|
});
|
|
|
|
it('does not classify non-location weatherapi errors as location lookup failures', function (): void {
|
|
$route = new moduleWeatherAPIRoute();
|
|
|
|
$is_location_error = department_weather_fallback_invoke_private($route, 'isWeatherLocationLookupFailure', [
|
|
new Exception('WeatherAPI request failed with HTTP 401: API key is invalid.'),
|
|
]);
|
|
|
|
expect($is_location_error)->toBeFalse();
|
|
});
|
|
|
|
it('returns an empty forecast fallback when coordinates are unavailable', function (): void {
|
|
$route = new moduleWeatherAPIRoute();
|
|
$result = department_weather_fallback_invoke_private($route, 'fetchDepartmentForecastOrFallback', [null, 2]);
|
|
|
|
expect($result['fallback_reason'])->toBe('invalid_department_coordinates');
|
|
expect($result['forecast'])->toBeObject();
|
|
expect($result['forecast']->forecast->forecastday ?? null)->toBeArray();
|
|
expect($result['forecast']->forecast->forecastday ?? [1])->toHaveCount(0);
|
|
});
|
|
|
|
it('builds timeline entries with mostly_clear weather when forecast payload is empty', function (): void {
|
|
$route = new moduleWeatherAPIRoute();
|
|
$start = new DateTime('2026-03-24 08:00:00');
|
|
$end_exclusive = new DateTime('2026-03-24 11:00:00');
|
|
|
|
$timeline = department_weather_fallback_invoke_private($route, 'buildDepartmentWeatherTimeline', [
|
|
[],
|
|
[],
|
|
(object)['forecast' => (object)['forecastday' => []]],
|
|
[
|
|
'start' => $start,
|
|
'endExclusive' => $end_exclusive,
|
|
],
|
|
]);
|
|
|
|
expect($timeline)->toHaveCount(3);
|
|
expect($timeline[0]['date'])->toBe('2026-03-24');
|
|
expect($timeline[0]['time'])->toBe('08:00');
|
|
expect($timeline[0]['weather'])->toBe('mostly_clear');
|
|
expect($timeline[0]['washes'])->toBe(0);
|
|
expect($timeline[0]['hours'])->toBe(0.0);
|
|
expect($timeline[0]['status'])->toBe('unknown');
|
|
expect($timeline[1]['weather'])->toBe('mostly_clear');
|
|
expect($timeline[2]['weather'])->toBe('mostly_clear');
|
|
});
|
|
|
|
it('wires departments weather route through the forecast fallback path', function (): void {
|
|
$routeFile = app_path('routes/moduleWeatherAPIRoute.php');
|
|
$content = (string)file_get_contents($routeFile);
|
|
|
|
expect($content)->toContain('fetchDepartmentForecastOrFallback');
|
|
expect($content)->toContain('DEPARTMENTS_WEATHER_FALLBACK');
|
|
expect($content)->not->toContain('Selected department(s) do not have GPS coordinates configured');
|
|
});
|