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.', ); } }