feat: ForecastMapper and file Cache
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Weerscherm\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Weerscherm\Cache;
|
||||
|
||||
final class CacheTest extends TestCase
|
||||
{
|
||||
private string $dir;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->dir = sys_get_temp_dir() . '/weerscherm-cache-' . uniqid();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
array_map('unlink', glob($this->dir . '/*') ?: []);
|
||||
@rmdir($this->dir);
|
||||
}
|
||||
|
||||
public function testWriteThenReadRoundTrip(): void
|
||||
{
|
||||
$cache = new Cache($this->dir);
|
||||
$cache->write('stations', ['a' => 1.5, 'b' => null], 1000);
|
||||
|
||||
self::assertSame(['data' => ['a' => 1.5, 'b' => null], 'stored_at' => 1000], $cache->read('stations'));
|
||||
}
|
||||
|
||||
public function testMissingKeyReadsNull(): void
|
||||
{
|
||||
self::assertNull((new Cache($this->dir))->read('nope'));
|
||||
}
|
||||
|
||||
public function testCorruptFileReadsNull(): void
|
||||
{
|
||||
mkdir($this->dir, 0700, true);
|
||||
file_put_contents($this->dir . '/broken.json', '{not json');
|
||||
|
||||
self::assertNull((new Cache($this->dir))->read('broken'));
|
||||
}
|
||||
|
||||
public function testFreshness(): void
|
||||
{
|
||||
$cache = new Cache($this->dir);
|
||||
$cache->write('k', [], 1000);
|
||||
|
||||
self::assertTrue($cache->isFresh('k', 300, 1299));
|
||||
self::assertFalse($cache->isFresh('k', 300, 1300));
|
||||
self::assertFalse($cache->isFresh('missing', 300, 1000));
|
||||
}
|
||||
|
||||
public function testKeyIsSanitised(): void
|
||||
{
|
||||
$cache = new Cache($this->dir);
|
||||
$cache->write('../Evil Key', ['x' => 1], 1);
|
||||
|
||||
self::assertFileExists($this->dir . '/evil_key.json');
|
||||
self::assertSame(['x' => 1], $cache->read('../Evil Key')['data']);
|
||||
}
|
||||
|
||||
public function testWriteCreatesDirectory(): void
|
||||
{
|
||||
(new Cache($this->dir . '/nested'))->write('k', [], 1);
|
||||
self::assertDirectoryExists($this->dir . '/nested');
|
||||
unlink($this->dir . '/nested/k.json');
|
||||
rmdir($this->dir . '/nested');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Weerscherm\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Weerscherm\ForecastMapper;
|
||||
|
||||
final class ForecastMapperTest extends TestCase
|
||||
{
|
||||
/** @return array<string, mixed> */
|
||||
private function daily(): array
|
||||
{
|
||||
return json_decode((string) file_get_contents(__DIR__ . '/fixtures/forecast.json'), true)['daily'];
|
||||
}
|
||||
|
||||
public function testSkipsTodayAndReturnsFiveDays(): void
|
||||
{
|
||||
$f = ForecastMapper::map($this->daily());
|
||||
|
||||
self::assertCount(5, $f);
|
||||
self::assertSame([
|
||||
'date' => '2026-09-12', 'weather_code' => 2, 'temp_max' => 18.3, 'temp_min' => 10.1,
|
||||
'precip_mm' => 0.4, 'precip_prob' => 20,
|
||||
], $f[0]);
|
||||
self::assertSame('2026-09-16', $f[4]['date']);
|
||||
}
|
||||
|
||||
public function testNullValuesStayNull(): void
|
||||
{
|
||||
$daily = $this->daily();
|
||||
$daily['weather_code'][3] = null;
|
||||
$daily['precipitation_probability_max'][3] = null;
|
||||
|
||||
$f = ForecastMapper::map($daily);
|
||||
|
||||
self::assertNull($f[2]['weather_code']);
|
||||
self::assertNull($f[2]['precip_prob']);
|
||||
self::assertSame(17.4, $f[2]['temp_max']);
|
||||
}
|
||||
|
||||
public function testShorterResponseGivesFewerDays(): void
|
||||
{
|
||||
$daily = $this->daily();
|
||||
foreach ($daily as $key => $values) {
|
||||
$daily[$key] = array_slice($values, 0, 3);
|
||||
}
|
||||
|
||||
self::assertCount(2, ForecastMapper::map($daily));
|
||||
}
|
||||
|
||||
public function testMissingTimeArrayGivesEmptyList(): void
|
||||
{
|
||||
self::assertSame([], ForecastMapper::map([]));
|
||||
}
|
||||
}
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"latitude": 51.82,
|
||||
"longitude": 6.56,
|
||||
"generationtime_ms": 0.2,
|
||||
"utc_offset_seconds": 7200,
|
||||
"timezone": "Europe/Berlin",
|
||||
"timezone_abbreviation": "CEST",
|
||||
"elevation": 20.0,
|
||||
"daily_units": {
|
||||
"time": "iso8601",
|
||||
"weather_code": "wmo code",
|
||||
"temperature_2m_max": "°C",
|
||||
"temperature_2m_min": "°C",
|
||||
"precipitation_sum": "mm",
|
||||
"precipitation_probability_max": "%"
|
||||
},
|
||||
"daily": {
|
||||
"time": ["2026-09-11", "2026-09-12", "2026-09-13", "2026-09-14", "2026-09-15", "2026-09-16"],
|
||||
"weather_code": [3, 2, 0, 61, 80, 2],
|
||||
"temperature_2m_max": [17.9, 18.3, 20.1, 17.4, 15.2, 16.0],
|
||||
"temperature_2m_min": [11.0, 10.1, 11.3, 12.0, 9.4, 8.1],
|
||||
"precipitation_sum": [0.0, 0.4, 0.0, 8.2, 3.1, 0.2],
|
||||
"precipitation_probability_max": [10, 20, 5, 80, 60, 15]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user