buckets/tests/Functional/ScenarioOwnerApiTest.php

156 lines
5.5 KiB
PHP

<?php
namespace App\Tests\Functional;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
final class ScenarioOwnerApiTest extends WebTestCase
{
private const EMAIL = 'scenario-owner-api-test@example.com';
private const PASSWORD = 'correct-horse-battery-staple';
private const OTHER_EMAIL = 'scenario-owner-api-spoof-target@example.com';
private KernelBrowser $client;
private EntityManagerInterface $em;
private User $caller;
private User $otherUser;
protected function setUp(): void
{
$this->client = static::createClient();
/** @var EntityManagerInterface $em */
$em = static::getContainer()->get(EntityManagerInterface::class);
$this->em = $em;
$factory = new PasswordHasherFactory([
User::class => new NativePasswordHasher(cost: 4),
]);
$hasher = new UserPasswordHasher($factory);
$this->caller = new User();
$this->caller->setEmail(self::EMAIL);
$this->caller->setPassword($hasher->hashPassword($this->caller, self::PASSWORD));
$this->em->persist($this->caller);
$this->otherUser = new User();
$this->otherUser->setEmail(self::OTHER_EMAIL);
$this->otherUser->setPassword($hasher->hashPassword($this->otherUser, 'some-other-password'));
$this->em->persist($this->otherUser);
$this->em->flush();
}
private function login(): void
{
$this->client->jsonRequest('POST', '/api/login', [
'username' => self::EMAIL,
'password' => self::PASSWORD,
]);
self::assertLessThan(
300,
$this->client->getResponse()->getStatusCode(),
'Test setup precondition: login must succeed before exercising the Scenario API.',
);
}
public function testPostCreatesAScenarioOwnedByTheAuthenticatedUser(): void
{
$this->login();
$this->client->request(
'POST',
'/api/scenarios',
server: [
'CONTENT_TYPE' => 'application/ld+json',
'HTTP_ACCEPT' => 'application/ld+json',
],
content: json_encode(['name' => 'Holiday Fund']),
);
self::assertSame(
201,
$this->client->getResponse()->getStatusCode(),
'POST /api/scenarios with a valid body must return 201 Created.',
);
$this->em->clear();
$persisted = $this->em->getRepository(\App\Entity\Scenario::class)->findOneBy(['name' => 'Holiday Fund']);
self::assertNotNull($persisted, 'A successful POST /api/scenarios must persist the new Scenario.');
self::assertNotNull($persisted->getOwner(), 'The persisted Scenario must have an owner set.');
self::assertTrue(
$this->caller->getId()->equals($persisted->getOwner()->getId()),
'The persisted Scenario must be owned by the authenticated caller, not left null or assigned elsewhere.',
);
}
public function testPostIgnoresAClientSuppliedOwner(): void
{
$this->login();
$this->client->request(
'POST',
'/api/scenarios',
server: [
'CONTENT_TYPE' => 'application/ld+json',
'HTTP_ACCEPT' => 'application/ld+json',
],
content: json_encode([
'name' => 'Spoofed Owner Fund',
'owner' => '/api/users/'.$this->otherUser->getId(),
]),
);
self::assertSame(
201,
$this->client->getResponse()->getStatusCode(),
'POST /api/scenarios with a valid body must return 201 Created even when a client-supplied owner is present in the payload.',
);
$this->em->clear();
$persisted = $this->em->getRepository(\App\Entity\Scenario::class)->findOneBy(['name' => 'Spoofed Owner Fund']);
self::assertNotNull($persisted, 'A successful POST /api/scenarios must persist the new Scenario.');
self::assertNotNull($persisted->getOwner(), 'The persisted Scenario must have an owner set.');
self::assertTrue(
$this->caller->getId()->equals($persisted->getOwner()->getId()),
'A client-supplied owner in the request body must be ignored — the owner must always be the authenticated caller.',
);
self::assertFalse(
$this->otherUser->getId()->equals($persisted->getOwner()->getId()),
'The spoofed owner IRI in the request body must never be honoured.',
);
}
public function testPostWithMissingNameIsStillRejectedAsUnprocessable(): void
{
$this->login();
$this->client->request(
'POST',
'/api/scenarios',
server: [
'CONTENT_TYPE' => 'application/ld+json',
'HTTP_ACCEPT' => 'application/ld+json',
],
content: json_encode([]),
);
self::assertSame(
422,
$this->client->getResponse()->getStatusCode(),
'POST /api/scenarios without a name must still return 422 Unprocessable Entity — the owner-setting persist processor must not interfere with existing validation order.',
);
}
}