test: éliminer les PHPUnit Notices (createMock → createStub)
Remplace createMock() par createStub() pour tous les collaborateurs sans expectations configurées. Les mocks avec expects() restent des mocks locaux créés dans le test concerné. 0 notices sur 49 tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
180
backend/tests/Unit/Controller/FeedbackControllerTest.php
Normal file
180
backend/tests/Unit/Controller/FeedbackControllerTest.php
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Unit\Controller;
|
||||||
|
|
||||||
|
use App\Controller\FeedbackController;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\RateLimiter\RateLimiterFactory;
|
||||||
|
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;
|
||||||
|
|
||||||
|
class FeedbackControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
private EntityManagerInterface $em;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->em = $this->createStub(EntityManagerInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeController(bool $exhaust = false, ?EntityManagerInterface $em = null): FeedbackController
|
||||||
|
{
|
||||||
|
$storage = new InMemoryStorage();
|
||||||
|
$factory = new RateLimiterFactory([
|
||||||
|
'id' => 'feedback_test',
|
||||||
|
'policy' => 'token_bucket',
|
||||||
|
'limit' => 10,
|
||||||
|
'rate' => ['interval' => '1 minute'],
|
||||||
|
], $storage);
|
||||||
|
|
||||||
|
if ($exhaust) {
|
||||||
|
// Vider les tokens avec la même clé que le controller (l'IP du request)
|
||||||
|
$limiter = $factory->create('1.2.3.4');
|
||||||
|
for ($i = 0; $i < 11; $i++) {
|
||||||
|
$limiter->consume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$controller = new FeedbackController($em ?? $this->em, $factory);
|
||||||
|
|
||||||
|
$container = $this->createStub(ContainerInterface::class);
|
||||||
|
$container->method('has')->willReturn(false);
|
||||||
|
$controller->setContainer($container);
|
||||||
|
|
||||||
|
return $controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeRequest(string $body, string $ip = '1.2.3.4'): Request
|
||||||
|
{
|
||||||
|
$request = Request::create('/api/feedback', 'POST', [], [], [], [], $body);
|
||||||
|
$request->server->set('REMOTE_ADDR', $ip);
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Taille de payload ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function testBodyTooLargeReturns413(): void
|
||||||
|
{
|
||||||
|
$body = json_encode(['lat' => 14.65, 'lng' => -61.0, 'hasSeaweed' => true, 'pad' => str_repeat('x', 4100)]);
|
||||||
|
self::assertSame(413, $this->makeController()->create($this->makeRequest($body))->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Validation JSON ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function testInvalidJsonReturns400(): void
|
||||||
|
{
|
||||||
|
self::assertSame(400, $this->makeController()->create($this->makeRequest('not-json'))->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Champs obligatoires ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function testMissingLatReturns422(): void
|
||||||
|
{
|
||||||
|
self::assertSame(422, $this->makeController()->create(
|
||||||
|
$this->makeRequest(json_encode(['lng' => -61.0, 'hasSeaweed' => true]))
|
||||||
|
)->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMissingLngReturns422(): void
|
||||||
|
{
|
||||||
|
self::assertSame(422, $this->makeController()->create(
|
||||||
|
$this->makeRequest(json_encode(['lat' => 14.65, 'hasSeaweed' => true]))
|
||||||
|
)->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMissingHasSeaweedReturns422(): void
|
||||||
|
{
|
||||||
|
self::assertSame(422, $this->makeController()->create(
|
||||||
|
$this->makeRequest(json_encode(['lat' => 14.65, 'lng' => -61.0]))
|
||||||
|
)->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Validation lat/lng ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function testInvalidLatStringReturns422(): void
|
||||||
|
{
|
||||||
|
self::assertSame(422, $this->makeController()->create(
|
||||||
|
$this->makeRequest(json_encode(['lat' => 'abc', 'lng' => -61.0, 'hasSeaweed' => true]))
|
||||||
|
)->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLatAboveRangeReturns422(): void
|
||||||
|
{
|
||||||
|
self::assertSame(422, $this->makeController()->create(
|
||||||
|
$this->makeRequest(json_encode(['lat' => 91.0, 'lng' => -61.0, 'hasSeaweed' => true]))
|
||||||
|
)->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLatBelowRangeReturns422(): void
|
||||||
|
{
|
||||||
|
self::assertSame(422, $this->makeController()->create(
|
||||||
|
$this->makeRequest(json_encode(['lat' => -91.0, 'lng' => -61.0, 'hasSeaweed' => true]))
|
||||||
|
)->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLngAboveRangeReturns422(): void
|
||||||
|
{
|
||||||
|
self::assertSame(422, $this->makeController()->create(
|
||||||
|
$this->makeRequest(json_encode(['lat' => 14.65, 'lng' => 181.0, 'hasSeaweed' => true]))
|
||||||
|
)->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Frontières exactes ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function testExactBoundaryLatLngIsAccepted(): void
|
||||||
|
{
|
||||||
|
$em = $this->createMock(EntityManagerInterface::class);
|
||||||
|
$em->expects(self::once())->method('persist');
|
||||||
|
$em->expects(self::once())->method('flush');
|
||||||
|
|
||||||
|
self::assertSame(201, $this->makeController(em: $em)->create(
|
||||||
|
$this->makeRequest(json_encode(['lat' => 90.0, 'lng' => -180.0, 'hasSeaweed' => false]))
|
||||||
|
)->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Requête valide ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function testValidRequestReturns201(): void
|
||||||
|
{
|
||||||
|
$em = $this->createMock(EntityManagerInterface::class);
|
||||||
|
$em->expects(self::once())->method('persist');
|
||||||
|
$em->expects(self::once())->method('flush');
|
||||||
|
|
||||||
|
self::assertSame(201, $this->makeController(em: $em)->create(
|
||||||
|
$this->makeRequest(json_encode(['lat' => 14.65, 'lng' => -61.0, 'hasSeaweed' => true]))
|
||||||
|
)->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidDensityIsAccepted(): void
|
||||||
|
{
|
||||||
|
$em = $this->createMock(EntityManagerInterface::class);
|
||||||
|
$em->expects(self::once())->method('persist');
|
||||||
|
$em->expects(self::once())->method('flush');
|
||||||
|
|
||||||
|
self::assertSame(201, $this->makeController(em: $em)->create(
|
||||||
|
$this->makeRequest(json_encode(['lat' => 14.65, 'lng' => -61.0, 'hasSeaweed' => true, 'density' => 'high']))
|
||||||
|
)->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInvalidDensityIsIgnoredAndRequestSucceeds(): void
|
||||||
|
{
|
||||||
|
$em = $this->createMock(EntityManagerInterface::class);
|
||||||
|
$em->expects(self::once())->method('persist');
|
||||||
|
$em->expects(self::once())->method('flush');
|
||||||
|
|
||||||
|
self::assertSame(201, $this->makeController(em: $em)->create(
|
||||||
|
$this->makeRequest(json_encode(['lat' => 14.65, 'lng' => -61.0, 'hasSeaweed' => true, 'density' => 'extreme']))
|
||||||
|
)->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Rate limiting ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function testRateLimitExceededReturns429(): void
|
||||||
|
{
|
||||||
|
self::assertSame(429, $this->makeController(exhaust: true)->create(
|
||||||
|
$this->makeRequest(json_encode(['lat' => 14.65, 'lng' => -61.0, 'hasSeaweed' => true]))
|
||||||
|
)->getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
137
backend/tests/Unit/Controller/PushControllerTest.php
Normal file
137
backend/tests/Unit/Controller/PushControllerTest.php
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Unit\Controller;
|
||||||
|
|
||||||
|
use App\Controller\PushController;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\RateLimiter\RateLimiterFactory;
|
||||||
|
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;
|
||||||
|
|
||||||
|
class PushControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
private EntityManagerInterface $em;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->em = $this->createStub(EntityManagerInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeController(bool $exhaust = false, ?EntityManagerInterface $em = null): PushController
|
||||||
|
{
|
||||||
|
$storage = new InMemoryStorage();
|
||||||
|
$factory = new RateLimiterFactory([
|
||||||
|
'id' => 'push_test',
|
||||||
|
'policy' => 'token_bucket',
|
||||||
|
'limit' => 5,
|
||||||
|
'rate' => ['interval' => '1 minute'],
|
||||||
|
], $storage);
|
||||||
|
|
||||||
|
if ($exhaust) {
|
||||||
|
$limiter = $factory->create('1.2.3.4');
|
||||||
|
for ($i = 0; $i < 6; $i++) {
|
||||||
|
$limiter->consume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$controller = new PushController($em ?? $this->em, $factory, 'fake-vapid-public-key');
|
||||||
|
|
||||||
|
$container = $this->createStub(ContainerInterface::class);
|
||||||
|
$container->method('has')->willReturn(false);
|
||||||
|
$controller->setContainer($container);
|
||||||
|
|
||||||
|
return $controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeRequest(string $method, string $body, string $ip = '1.2.3.4'): Request
|
||||||
|
{
|
||||||
|
$request = Request::create('/api/push/subscribe', $method, [], [], [], [], $body);
|
||||||
|
$request->server->set('REMOTE_ADDR', $ip);
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── subscribe ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function testSubscribeBodyTooLargeReturns413(): void
|
||||||
|
{
|
||||||
|
$body = json_encode([
|
||||||
|
'endpoint' => 'https://push.example.com/sub',
|
||||||
|
'keys' => ['auth' => 'a', 'p256dh' => 'b'],
|
||||||
|
'pad' => str_repeat('x', 4100),
|
||||||
|
]);
|
||||||
|
self::assertSame(413, $this->controller()->subscribe($this->makeRequest('POST', $body))->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSubscribeMissingFieldsReturns422(): void
|
||||||
|
{
|
||||||
|
self::assertSame(422, $this->controller()->subscribe(
|
||||||
|
$this->makeRequest('POST', json_encode(['endpoint' => 'https://push.example.com']))
|
||||||
|
)->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSubscribeInvalidEndpointUrlReturns422(): void
|
||||||
|
{
|
||||||
|
$body = json_encode(['endpoint' => 'not-a-url', 'keys' => ['auth' => 'a', 'p256dh' => 'b']]);
|
||||||
|
self::assertSame(422, $this->controller()->subscribe($this->makeRequest('POST', $body))->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSubscribeValidRequestReturns201(): void
|
||||||
|
{
|
||||||
|
$em = $this->createMock(EntityManagerInterface::class);
|
||||||
|
$em->method('getRepository')->willReturn(
|
||||||
|
$this->createConfiguredStub(\Doctrine\ORM\EntityRepository::class, ['findOneBy' => null])
|
||||||
|
);
|
||||||
|
$em->expects(self::once())->method('persist');
|
||||||
|
$em->expects(self::once())->method('flush');
|
||||||
|
|
||||||
|
$body = json_encode([
|
||||||
|
'endpoint' => 'https://fcm.googleapis.com/fcm/send/abc123',
|
||||||
|
'keys' => ['auth' => 'authtoken', 'p256dh' => 'p256key'],
|
||||||
|
]);
|
||||||
|
self::assertSame(201, $this->makeController(em: $em)->subscribe($this->makeRequest('POST', $body))->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSubscribeRateLimitExceededReturns429(): void
|
||||||
|
{
|
||||||
|
self::assertSame(429, $this->makeController(exhaust: true)->subscribe(
|
||||||
|
$this->makeRequest('POST', json_encode([
|
||||||
|
'endpoint' => 'https://push.example.com/sub',
|
||||||
|
'keys' => ['auth' => 'a', 'p256dh' => 'b'],
|
||||||
|
]))
|
||||||
|
)->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── unsubscribe ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function testUnsubscribeMissingEndpointReturns422(): void
|
||||||
|
{
|
||||||
|
self::assertSame(422, $this->controller()->unsubscribe(
|
||||||
|
$this->makeRequest('DELETE', json_encode([]))
|
||||||
|
)->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnsubscribeUnknownEndpointReturns204(): void
|
||||||
|
{
|
||||||
|
$this->em->method('getRepository')->willReturn(
|
||||||
|
$this->createConfiguredStub(\Doctrine\ORM\EntityRepository::class, ['findOneBy' => null])
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(204, $this->controller()->unsubscribe(
|
||||||
|
$this->makeRequest('DELETE', json_encode(['endpoint' => 'https://push.example.com/sub']))
|
||||||
|
)->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnsubscribeRateLimitExceededReturns429(): void
|
||||||
|
{
|
||||||
|
self::assertSame(429, $this->makeController(exhaust: true)->unsubscribe(
|
||||||
|
$this->makeRequest('DELETE', json_encode(['endpoint' => 'https://push.example.com/sub']))
|
||||||
|
)->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function controller(): PushController
|
||||||
|
{
|
||||||
|
return $this->makeController();
|
||||||
|
}
|
||||||
|
}
|
||||||
121
backend/tests/Unit/Service/DriftSimulationServiceTest.php
Normal file
121
backend/tests/Unit/Service/DriftSimulationServiceTest.php
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Unit\Service;
|
||||||
|
|
||||||
|
use App\Service\Forecast\DriftSimulationService;
|
||||||
|
use App\Service\Ocean\OceanCurrentClient;
|
||||||
|
use App\Service\Weather\OpenMeteoClient;
|
||||||
|
use Doctrine\DBAL\Connection;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
|
||||||
|
class DriftSimulationServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private DriftSimulationService $service;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->service = new DriftSimulationService(
|
||||||
|
$this->createStub(OpenMeteoClient::class),
|
||||||
|
$this->createStub(OceanCurrentClient::class),
|
||||||
|
$this->createStub(Connection::class),
|
||||||
|
$this->createStub(EntityManagerInterface::class),
|
||||||
|
new NullLogger(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── computeConfidence ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[\PHPUnit\Framework\Attributes\DataProvider('confidenceProvider')]
|
||||||
|
public function testComputeConfidence(int $horizon, float $expected): void
|
||||||
|
{
|
||||||
|
$method = new \ReflectionMethod(DriftSimulationService::class, 'computeConfidence');
|
||||||
|
$result = $method->invoke($this->service, $horizon);
|
||||||
|
|
||||||
|
self::assertSame($expected, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array{int, float}> */
|
||||||
|
public static function confidenceProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'H+6 → 0.85' => [6, 0.85],
|
||||||
|
'H+12 → 0.75' => [12, 0.75],
|
||||||
|
'H+24 → 0.60' => [24, 0.60],
|
||||||
|
'H+48 → 0.45' => [48, 0.45],
|
||||||
|
'inconnu → 0.50' => [72, 0.50],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── avgDriftVector ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function testAvgDriftVectorWithNoWindNoOcean(): void
|
||||||
|
{
|
||||||
|
$method = new \ReflectionMethod(DriftSimulationService::class, 'avgDriftVector');
|
||||||
|
$result = $method->invoke($this->service, [], 0, 6, 0.0, 0.0);
|
||||||
|
|
||||||
|
self::assertSame(0.0, $result['lat']);
|
||||||
|
self::assertSame(0.0, $result['lng']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAvgDriftVectorWithPureEastwardOceanCurrent(): void
|
||||||
|
{
|
||||||
|
// Courant purement Est (u=1.0 m/s), vent nul, 6 heures
|
||||||
|
$method = new \ReflectionMethod(DriftSimulationService::class, 'avgDriftVector');
|
||||||
|
$result = $method->invoke($this->service, [], 0, 6, 1.0, 0.0);
|
||||||
|
|
||||||
|
self::assertSame(1.0, $result['lng']); // Est-Ouest = 1 m/s
|
||||||
|
self::assertSame(0.0, $result['lat']); // Nord-Sud = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAvgDriftVectorWithNorthwardOceanCurrent(): void
|
||||||
|
{
|
||||||
|
$method = new \ReflectionMethod(DriftSimulationService::class, 'avgDriftVector');
|
||||||
|
$result = $method->invoke($this->service, [], 0, 6, 0.0, 1.0);
|
||||||
|
|
||||||
|
self::assertSame(0.0, $result['lng']);
|
||||||
|
self::assertSame(1.0, $result['lat']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAvgDriftVectorWithNorthWind(): void
|
||||||
|
{
|
||||||
|
// Vent venant du Nord (direction=0°), vitesse=10 m/s, courant nul
|
||||||
|
// sin(0°)=0 (pas de composante E-O), cos(0°)=1 (composante N-S)
|
||||||
|
// Stokes = 3% → 10 * 0.03 * cos(0) = 0.3 m/s vers Nord
|
||||||
|
$windData = array_fill(0, 6, ['speed' => 10.0, 'direction' => 0.0]);
|
||||||
|
$method = new \ReflectionMethod(DriftSimulationService::class, 'avgDriftVector');
|
||||||
|
$result = $method->invoke($this->service, $windData, 0, 6, 0.0, 0.0);
|
||||||
|
|
||||||
|
self::assertEqualsWithDelta(0.0, $result['lng'], 1e-10);
|
||||||
|
self::assertEqualsWithDelta(0.3, $result['lat'], 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAvgDriftVectorWithEastWind(): void
|
||||||
|
{
|
||||||
|
// Vent venant de l'Est (direction=90°), vitesse=10 m/s
|
||||||
|
// sin(90°)=1 → composante Est = 10*0.03 = 0.3 m/s
|
||||||
|
// cos(90°)=0 → composante Nord = 0
|
||||||
|
$windData = array_fill(0, 6, ['speed' => 10.0, 'direction' => 90.0]);
|
||||||
|
$method = new \ReflectionMethod(DriftSimulationService::class, 'avgDriftVector');
|
||||||
|
$result = $method->invoke($this->service, $windData, 0, 6, 0.0, 0.0);
|
||||||
|
|
||||||
|
self::assertEqualsWithDelta(0.3, $result['lng'], 1e-10);
|
||||||
|
self::assertEqualsWithDelta(0.0, $result['lat'], 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAvgDriftVectorUsesOnlyRequestedHourRange(): void
|
||||||
|
{
|
||||||
|
// windData[0..5] = vent Nord 10 m/s, windData[6..11] = vent Est 10 m/s
|
||||||
|
// Si on demande fromHour=6, toHour=12 → seules les heures 6..11 comptent
|
||||||
|
$windData = array_merge(
|
||||||
|
array_fill(0, 6, ['speed' => 10.0, 'direction' => 0.0]), // heures 0-5 : Nord
|
||||||
|
array_fill(6, 6, ['speed' => 10.0, 'direction' => 90.0]), // heures 6-11 : Est
|
||||||
|
);
|
||||||
|
$method = new \ReflectionMethod(DriftSimulationService::class, 'avgDriftVector');
|
||||||
|
$result = $method->invoke($this->service, $windData, 6, 12, 0.0, 0.0);
|
||||||
|
|
||||||
|
self::assertEqualsWithDelta(0.3, $result['lng'], 1e-10); // Est
|
||||||
|
self::assertEqualsWithDelta(0.0, $result['lat'], 1e-10);
|
||||||
|
}
|
||||||
|
}
|
||||||
164
backend/tests/Unit/Service/IngestionServiceTest.php
Normal file
164
backend/tests/Unit/Service/IngestionServiceTest.php
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Unit\Service;
|
||||||
|
|
||||||
|
use App\Service\Ingestion\IngestionService;
|
||||||
|
use App\Service\SentinelHub\SentinelHubClient;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
|
||||||
|
class IngestionServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private IngestionService $service;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->service = new IngestionService(
|
||||||
|
$this->createStub(SentinelHubClient::class),
|
||||||
|
$this->createStub(EntityManagerInterface::class),
|
||||||
|
new NullLogger(),
|
||||||
|
sys_get_temp_dir(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── buildMultiPolygon ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function callBuildMultiPolygon(string $geojsonContent): ?array
|
||||||
|
{
|
||||||
|
$file = tempnam(sys_get_temp_dir(), 'phpunit_ingestion_') . '.geojson';
|
||||||
|
file_put_contents($file, $geojsonContent);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$method = new \ReflectionMethod(IngestionService::class, 'buildMultiPolygon');
|
||||||
|
return $method->invoke($this->service, $file);
|
||||||
|
} finally {
|
||||||
|
@unlink($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function makePolygon(int $points = 10, int $dn = 1): array
|
||||||
|
{
|
||||||
|
// Carré simple autour de 0,0 — répété $points fois pour avoir assez de coordonnées
|
||||||
|
$coords = [];
|
||||||
|
for ($i = 0; $i < $points; $i++) {
|
||||||
|
$coords[] = [$i * 0.01, $i * 0.01];
|
||||||
|
}
|
||||||
|
$coords[] = $coords[0]; // fermer l'anneau
|
||||||
|
|
||||||
|
return [
|
||||||
|
'type' => 'Feature',
|
||||||
|
'properties' => ['DN' => $dn],
|
||||||
|
'geometry' => [
|
||||||
|
'type' => 'Polygon',
|
||||||
|
'coordinates' => [$coords],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReturnsNullWhenFileDoesNotExist(): void
|
||||||
|
{
|
||||||
|
$method = new \ReflectionMethod(IngestionService::class, 'buildMultiPolygon');
|
||||||
|
$result = $method->invoke($this->service, '/tmp/nonexistent_phpunit.geojson');
|
||||||
|
|
||||||
|
self::assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReturnsNullForEmptyFeatureCollection(): void
|
||||||
|
{
|
||||||
|
$geojson = json_encode(['type' => 'FeatureCollection', 'features' => []]);
|
||||||
|
self::assertNull($this->callBuildMultiPolygon($geojson));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReturnsNullForInvalidJson(): void
|
||||||
|
{
|
||||||
|
self::assertNull($this->callBuildMultiPolygon('not-json'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReturnsNullWhenAllFeaturesHaveWrongDN(): void
|
||||||
|
{
|
||||||
|
$geojson = json_encode([
|
||||||
|
'type' => 'FeatureCollection',
|
||||||
|
'features' => [self::makePolygon(10, 0), self::makePolygon(10, 2)],
|
||||||
|
]);
|
||||||
|
self::assertNull($this->callBuildMultiPolygon($geojson));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReturnsNullWhenPolygonHasTooFewPoints(): void
|
||||||
|
{
|
||||||
|
// MIN_RING_POINTS = 8 → 5 points doit être ignoré
|
||||||
|
$geojson = json_encode([
|
||||||
|
'type' => 'FeatureCollection',
|
||||||
|
'features' => [self::makePolygon(5, 1)],
|
||||||
|
]);
|
||||||
|
self::assertNull($this->callBuildMultiPolygon($geojson));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReturnMultiPolygonForValidFeature(): void
|
||||||
|
{
|
||||||
|
$geojson = json_encode([
|
||||||
|
'type' => 'FeatureCollection',
|
||||||
|
'features' => [self::makePolygon(10, 1)],
|
||||||
|
]);
|
||||||
|
$result = $this->callBuildMultiPolygon($geojson);
|
||||||
|
|
||||||
|
self::assertNotNull($result);
|
||||||
|
self::assertSame('MultiPolygon', $result['type']);
|
||||||
|
self::assertCount(1, $result['coordinates']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandlesMultipleValidFeatures(): void
|
||||||
|
{
|
||||||
|
$geojson = json_encode([
|
||||||
|
'type' => 'FeatureCollection',
|
||||||
|
'features' => [
|
||||||
|
self::makePolygon(10, 1),
|
||||||
|
self::makePolygon(12, 1),
|
||||||
|
self::makePolygon(5, 1), // trop petit → ignoré
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$result = $this->callBuildMultiPolygon($geojson);
|
||||||
|
|
||||||
|
self::assertNotNull($result);
|
||||||
|
self::assertCount(2, $result['coordinates']); // 2 valides sur 3
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandlesMultiPolygonGeometryType(): void
|
||||||
|
{
|
||||||
|
$coords1 = array_fill(0, 11, [0.0, 0.0]);
|
||||||
|
$coords1[] = $coords1[0];
|
||||||
|
$coords2 = array_fill(0, 11, [1.0, 1.0]);
|
||||||
|
$coords2[] = $coords2[0];
|
||||||
|
|
||||||
|
$feature = [
|
||||||
|
'type' => 'Feature',
|
||||||
|
'properties' => ['DN' => 1],
|
||||||
|
'geometry' => [
|
||||||
|
'type' => 'MultiPolygon',
|
||||||
|
'coordinates' => [[$coords1], [$coords2]],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$geojson = json_encode(['type' => 'FeatureCollection', 'features' => [$feature]]);
|
||||||
|
$result = $this->callBuildMultiPolygon($geojson);
|
||||||
|
|
||||||
|
self::assertNotNull($result);
|
||||||
|
self::assertSame('MultiPolygon', $result['type']);
|
||||||
|
self::assertCount(2, $result['coordinates']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIgnoresFeaturesWithDNZeroAndKeepsOthers(): void
|
||||||
|
{
|
||||||
|
$geojson = json_encode([
|
||||||
|
'type' => 'FeatureCollection',
|
||||||
|
'features' => [
|
||||||
|
self::makePolygon(10, 0), // DN=0 → ignoré
|
||||||
|
self::makePolygon(10, 1), // valide
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$result = $this->callBuildMultiPolygon($geojson);
|
||||||
|
|
||||||
|
self::assertNotNull($result);
|
||||||
|
self::assertCount(1, $result['coordinates']);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user