client = static::createClient(); $this->em = static::getContainer()->get(EntityManagerInterface::class); $this->cleanup(); } protected function tearDown(): void { $this->cleanup(); parent::tearDown(); } private function cleanup(): void { $conn = $this->em->getConnection(); $conn->executeStatement("DELETE FROM goals WHERE user_id IN (SELECT id FROM users WHERE email LIKE '%@test.dudi')"); $conn->executeStatement("DELETE FROM invites WHERE created_by IN (SELECT id FROM users WHERE email LIKE '%@test.dudi')"); $conn->executeStatement("DELETE FROM users_resets WHERE user IN (SELECT id FROM users WHERE email LIKE '%@test.dudi')"); $conn->executeStatement("DELETE FROM users WHERE email LIKE '%@test.dudi'"); $this->em->clear(); } private function createUser(string $suffix = 'main', string $password = 'test1234'): User { $hasher = static::getContainer()->get(UserPasswordHasherInterface::class); $user = new User(); $user->setEmail($suffix . '@test.dudi') ->setUsername('Tester ' . $suffix) ->setVerified(true); $user->setPassword($hasher->hashPassword($user, $password)); $this->em->persist($user); $this->em->flush(); return $user; } private function authClient(User $user): KernelBrowser { $this->client->loginUser($user); return $this->client; } private function json(KernelBrowser $client, string $method, string $url, array $body = []): array { $client->request($method, $url, [], [], ['CONTENT_TYPE' => 'application/json'], $body ? json_encode($body) : null); return json_decode($client->getResponse()->getContent(), true) ?? []; } // ── Auth ───────────────────────────────────────────────────────────────── public function testLoginSuccess(): void { $this->createUser('login', 'geheim99'); $data = $this->json($this->client, 'POST', '/api/login', ['email' => 'login@test.dudi', 'password' => 'geheim99']); $this->assertTrue($data['ok']); $this->assertSame('login@test.dudi', $data['email']); $this->assertSame('Tester login', $data['name']); } public function testLoginWrongPassword(): void { $this->createUser('login2'); $data = $this->json($this->client, 'POST', '/api/login', ['email' => 'login2@test.dudi', 'password' => 'falsch']); $this->assertArrayHasKey('error', $data); $this->assertSame(401, $this->client->getResponse()->getStatusCode()); } public function testLoginUnknownEmail(): void { $data = $this->json($this->client, 'POST', '/api/login', ['email' => 'nobody@test.dudi', 'password' => 'test1234']); $this->assertArrayHasKey('error', $data); $this->assertSame(401, $this->client->getResponse()->getStatusCode()); } public function testMe(): void { $user = $this->createUser('me'); $client = $this->authClient($user); $data = $this->json($client, 'GET', '/api/me'); $this->assertTrue($data['ok']); $this->assertSame('me@test.dudi', $data['email']); $this->assertSame('Tester me', $data['name']); $this->assertIsInt($data['id']); } public function testMeUnauthenticated(): void { $this->json($this->client, 'GET', '/api/me'); $this->assertSame(401, $this->client->getResponse()->getStatusCode()); } public function testUpdateName(): void { $user = $this->createUser('name'); $client = $this->authClient($user); $data = $this->json($client, 'PATCH', '/api/me', ['name' => 'Neuer Name']); $this->assertTrue($data['ok']); $this->assertSame('Neuer Name', $data['name']); $this->em->refresh($user); $this->assertSame('Neuer Name', $user->getUsername()); } public function testUpdateNameEmpty(): void { $user = $this->createUser('nameempty'); $client = $this->authClient($user); $data = $this->json($client, 'PATCH', '/api/me', ['name' => '']); $this->assertArrayHasKey('error', $data); $this->assertSame(400, $this->client->getResponse()->getStatusCode()); } public function testChangePassword(): void { $user = $this->createUser('pw', 'altesPasswort1'); $client = $this->authClient($user); $data = $this->json($client, 'POST', '/api/change-password', [ 'old_password' => 'altesPasswort1', 'new_password' => 'neuesPasswort1', ]); $this->assertTrue($data['ok']); // verify new password works $hasher = static::getContainer()->get(UserPasswordHasherInterface::class); $this->em->refresh($user); $this->assertTrue($hasher->isPasswordValid($user, 'neuesPasswort1')); } public function testChangePasswordWrongOld(): void { $user = $this->createUser('pwwrong'); $client = $this->authClient($user); $data = $this->json($client, 'POST', '/api/change-password', [ 'old_password' => 'falsch1234', 'new_password' => 'neuesPasswort1', ]); $this->assertArrayHasKey('error', $data); $this->assertSame(401, $this->client->getResponse()->getStatusCode()); } public function testChangePasswordTooShort(): void { $user = $this->createUser('pwshort'); $client = $this->authClient($user); $data = $this->json($client, 'POST', '/api/change-password', [ 'old_password' => 'test1234', 'new_password' => 'kurz', ]); $this->assertArrayHasKey('error', $data); $this->assertSame(400, $this->client->getResponse()->getStatusCode()); } // ── Password reset ──────────────────────────────────────────────────────── public function testResetRequestAlwaysReturnsOk(): void { // unknown email — still returns ok (don't leak existence) $data = $this->json($this->client, 'POST', '/api/reset-request', ['email' => 'ghost@test.dudi']); $this->assertTrue($data['ok']); } public function testResetRequestWritesToken(): void { $user = $this->createUser('reset'); $this->json($this->client, 'POST', '/api/reset-request', ['email' => 'reset@test.dudi']); $row = $this->em->getConnection()->fetchAssociative( 'SELECT * FROM users_resets WHERE user = ?', [$user->getId()] ); $this->assertNotFalse($row); $this->assertGreaterThan(time(), $row['expires']); } public function testResetPassword(): void { $user = $this->createUser('resetpw'); $selector = bin2hex(random_bytes(12)); $token = bin2hex(random_bytes(32)); $hash = password_hash($token, PASSWORD_BCRYPT); $this->em->getConnection()->executeStatement( 'INSERT INTO users_resets (user, selector, token, expires) VALUES (?, ?, ?, ?)', [$user->getId(), $selector, $hash, time() + 3600] ); $data = $this->json($this->client, 'POST', '/api/reset-password', [ 'selector' => $selector, 'token' => $token, 'password' => 'neuesPasswort99', ]); $this->assertTrue($data['ok']); $hasher = static::getContainer()->get(UserPasswordHasherInterface::class); $this->em->refresh($user); $this->assertTrue($hasher->isPasswordValid($user, 'neuesPasswort99')); // token must be deleted after use $row = $this->em->getConnection()->fetchAssociative( 'SELECT * FROM users_resets WHERE selector = ?', [$selector] ); $this->assertFalse($row); } public function testResetPasswordExpiredToken(): void { $user = $this->createUser('resetexp'); $selector = bin2hex(random_bytes(12)); $token = bin2hex(random_bytes(32)); $this->em->getConnection()->executeStatement( 'INSERT INTO users_resets (user, selector, token, expires) VALUES (?, ?, ?, ?)', [$user->getId(), $selector, password_hash($token, PASSWORD_BCRYPT), time() - 1] ); $data = $this->json($this->client, 'POST', '/api/reset-password', [ 'selector' => $selector, 'token' => $token, 'password' => 'neuesPasswort99', ]); $this->assertArrayHasKey('error', $data); $this->assertSame(400, $this->client->getResponse()->getStatusCode()); } // ── Goals ───────────────────────────────────────────────────────────────── public function testGoalListEmpty(): void { $user = $this->createUser('goallist'); $client = $this->authClient($user); $data = $this->json($client, 'GET', '/api/goals'); $this->assertSame([], $data); } public function testGoalCreate(): void { $user = $this->createUser('goalcreate'); $client = $this->authClient($user); $data = $this->json($client, 'POST', '/api/goals', [ 'name' => 'Liegestütz', 'unit' => 'Stück', 'daily' => 50, 'days' => 30, ]); $this->assertSame('Liegestütz', $data['name']); $this->assertSame('Stück', $data['unit']); $this->assertEquals(50.0, $data['daily']); $this->assertSame(30, $data['days']); $this->assertSame([], $data['sets']); $this->assertIsString($data['id']); } public function testGoalCreateMissingName(): void { $user = $this->createUser('goalnoname'); $client = $this->authClient($user); $data = $this->json($client, 'POST', '/api/goals', ['unit' => 'Stück', 'daily' => 10, 'days' => 7]); $this->assertArrayHasKey('error', $data); $this->assertSame(400, $this->client->getResponse()->getStatusCode()); } public function testGoalDefaultUnit(): void { $user = $this->createUser('goalunit'); $client = $this->authClient($user); $data = $this->json($client, 'POST', '/api/goals', ['name' => 'Plank', 'daily' => 1, 'days' => 7]); $this->assertSame('Stück', $data['unit']); } public function testGoalListReturnsOwned(): void { $user = $this->createUser('goalowned'); $other = $this->createUser('goalother'); foreach ([$user, $other] as $u) { $goal = new Goal(); $goal->setUserId($u->getId())->setName('Ziel')->setUnit('Stück')->setDaily(1)->setDays(7)->setStart(new \DateTime())->setSets([]); $this->em->persist($goal); } $this->em->flush(); $client = $this->authClient($user); $data = $this->json($client, 'GET', '/api/goals'); $this->assertCount(1, $data); $this->assertSame('Ziel', $data[0]['name']); } public function testGoalUpdate(): void { $user = $this->createUser('goalupdate'); $goal = new Goal(); $goal->setUserId($user->getId())->setName('Alt')->setUnit('Stück')->setDaily(10)->setDays(7)->setStart(new \DateTime())->setSets([]); $this->em->persist($goal); $this->em->flush(); $client = $this->authClient($user); $data = $this->json($client, 'PATCH', '/api/goals/' . $goal->getId(), [ 'name' => 'Neu', 'unit' => 'Min', 'daily' => 20, ]); $this->assertTrue($data['ok']); $this->em->refresh($goal); $this->assertSame('Neu', $goal->getName()); $this->assertSame('Min', $goal->getUnit()); $this->assertSame(20.0, $goal->getDaily()); } public function testGoalUpdateSets(): void { $user = $this->createUser('goalsets'); $goal = new Goal(); $goal->setUserId($user->getId())->setName('Test')->setUnit('Stück')->setDaily(50)->setDays(7)->setStart(new \DateTime())->setSets([]); $this->em->persist($goal); $this->em->flush(); $today = date('Y-m-d'); $client = $this->authClient($user); $data = $this->json($client, 'PATCH', '/api/goals/' . $goal->getId(), [ 'sets' => [$today => [20, 30]], ]); $this->assertTrue($data['ok']); $this->em->refresh($goal); $this->assertSame([20, 30], $goal->getSets()[$today]); } public function testGoalUpdateNotFound(): void { $user = $this->createUser('goalnotfound'); $client = $this->authClient($user); $data = $this->json($client, 'PATCH', '/api/goals/999999', ['name' => 'X']); $this->assertArrayHasKey('error', $data); $this->assertSame(404, $client->getResponse()->getStatusCode()); } public function testGoalUpdateOwnership(): void { $owner = $this->createUser('goalown1'); $other = $this->createUser('goalown2'); $goal = new Goal(); $goal->setUserId($owner->getId())->setName('Privat')->setUnit('Stück')->setDaily(1)->setDays(7)->setStart(new \DateTime())->setSets([]); $this->em->persist($goal); $this->em->flush(); $client = $this->authClient($other); $data = $this->json($client, 'PATCH', '/api/goals/' . $goal->getId(), ['name' => 'Gehackt']); $this->assertSame(404, $client->getResponse()->getStatusCode()); $this->em->refresh($goal); $this->assertSame('Privat', $goal->getName()); } public function testGoalDelete(): void { $user = $this->createUser('goaldel'); $goal = new Goal(); $goal->setUserId($user->getId())->setName('Weg')->setUnit('Stück')->setDaily(1)->setDays(7)->setStart(new \DateTime())->setSets([]); $this->em->persist($goal); $this->em->flush(); $id = $goal->getId(); $client = $this->authClient($user); $data = $this->json($client, 'DELETE', '/api/goals/' . $id); $this->assertTrue($data['ok']); $this->assertNull($this->em->find(Goal::class, $id)); } public function testGoalDeleteOwnership(): void { $owner = $this->createUser('goaldel1'); $other = $this->createUser('goaldel2'); $goal = new Goal(); $goal->setUserId($owner->getId())->setName('Privat')->setUnit('Stück')->setDaily(1)->setDays(7)->setStart(new \DateTime())->setSets([]); $this->em->persist($goal); $this->em->flush(); $id = $goal->getId(); $client = $this->authClient($other); $this->json($client, 'DELETE', '/api/goals/' . $id); $this->assertNotNull($this->em->find(Goal::class, $id)); } public function testGoalUnauthenticated(): void { $this->json($this->client, 'GET', '/api/goals'); $this->assertSame(401, $this->client->getResponse()->getStatusCode()); } // ── Invites ─────────────────────────────────────────────────────────────── public function testInviteCreate(): void { $user = $this->createUser('invcreate'); $client = $this->authClient($user); $data = $this->json($client, 'POST', '/api/invite', ['note' => 'Für Max']); $this->assertStringContainsString('invite=', $data['url']); } public function testInviteList(): void { $user = $this->createUser('invlist'); $invite = new Invite(); $invite->setToken(bin2hex(random_bytes(32))) ->setNote('Testeinladung') ->setCreatedBy($user->getId()) ->setExpiresAt(new \DateTimeImmutable('+7 days')); $this->em->persist($invite); $this->em->flush(); $client = $this->authClient($user); $data = $this->json($client, 'GET', '/api/invites'); $this->assertCount(1, $data); $this->assertSame('pending', $data[0]['status']); $this->assertSame('Testeinladung', $data[0]['note']); $this->assertStringContainsString('invite=', $data[0]['url']); } public function testInviteListExpired(): void { $user = $this->createUser('invexp'); $invite = new Invite(); $invite->setToken(bin2hex(random_bytes(32))) ->setCreatedBy($user->getId()) ->setExpiresAt(new \DateTimeImmutable('-1 day')); $this->em->persist($invite); $this->em->flush(); $client = $this->authClient($user); $data = $this->json($client, 'GET', '/api/invites'); $this->assertSame('expired', $data[0]['status']); $this->assertNull($data[0]['url']); } public function testInviteListUsed(): void { $user = $this->createUser('invused1'); $newUser = $this->createUser('invused2'); $invite = new Invite(); $invite->setToken(bin2hex(random_bytes(32))) ->setCreatedBy($user->getId()) ->setExpiresAt(new \DateTimeImmutable('+7 days')) ->setUsedBy($newUser->getId()) ->setUsedAt(new \DateTimeImmutable()); $this->em->persist($invite); $this->em->flush(); $client = $this->authClient($user); $data = $this->json($client, 'GET', '/api/invites'); $this->assertSame('used', $data[0]['status']); $this->assertSame('invused2@test.dudi', $data[0]['used_by_email']); $this->assertNull($data[0]['url']); } // ── Register ────────────────────────────────────────────────────────────── public function testRegisterWithInvite(): void { $creator = $this->createUser('reginviter'); $invite = new Invite(); $invite->setToken(bin2hex(random_bytes(32))) ->setCreatedBy($creator->getId()) ->setExpiresAt(new \DateTimeImmutable('+7 days')); $this->em->persist($invite); $this->em->flush(); $data = $this->json($this->client, 'POST', '/api/register', [ 'email' => 'regnew@test.dudi', 'password' => 'passwort99', 'name' => 'Neuer User', 'token' => $invite->getToken(), ]); $this->assertTrue($data['ok']); $this->assertSame('regnew@test.dudi', $data['email']); $newUser = static::getContainer()->get(\App\Repository\UserRepository::class)->findOneBy(['email' => 'regnew@test.dudi']); $this->assertNotNull($newUser); $this->assertTrue($newUser->isVerified()); $this->em->refresh($invite); $this->assertSame($newUser->getId(), $invite->getUsedBy()); $this->assertNotNull($invite->getUsedAt()); } public function testRegisterInvalidToken(): void { $data = $this->json($this->client, 'POST', '/api/register', [ 'email' => 'regbad@test.dudi', 'password' => 'passwort99', 'token' => 'ungueltig', ]); $this->assertArrayHasKey('error', $data); $this->assertSame(400, $this->client->getResponse()->getStatusCode()); } public function testRegisterDuplicateEmail(): void { $existing = $this->createUser('regdup'); $creator = $this->createUser('regdupinviter'); $invite = new Invite(); $invite->setToken(bin2hex(random_bytes(32))) ->setCreatedBy($creator->getId()) ->setExpiresAt(new \DateTimeImmutable('+7 days')); $this->em->persist($invite); $this->em->flush(); $data = $this->json($this->client, 'POST', '/api/register', [ 'email' => 'regdup@test.dudi', 'password' => 'passwort99', 'token' => $invite->getToken(), ]); $this->assertArrayHasKey('error', $data); $this->assertSame(409, $this->client->getResponse()->getStatusCode()); } public function testRegisterPasswordTooShort(): void { $creator = $this->createUser('regshortinviter'); $invite = new Invite(); $invite->setToken(bin2hex(random_bytes(32))) ->setCreatedBy($creator->getId()) ->setExpiresAt(new \DateTimeImmutable('+7 days')); $this->em->persist($invite); $this->em->flush(); $data = $this->json($this->client, 'POST', '/api/register', [ 'email' => 'regshort@test.dudi', 'password' => 'kurz', 'token' => $invite->getToken(), ]); $this->assertArrayHasKey('error', $data); $this->assertSame(400, $this->client->getResponse()->getStatusCode()); } public function testRegisterAutoLogsIn(): void { $creator = $this->createUser('regautologin'); $invite = new Invite(); $invite->setToken(bin2hex(random_bytes(32))) ->setCreatedBy($creator->getId()) ->setExpiresAt(new \DateTimeImmutable('+7 days')); $this->em->persist($invite); $this->em->flush(); $this->json($this->client, 'POST', '/api/register', [ 'email' => 'regauto@test.dudi', 'password' => 'passwort99', 'name' => 'Auto User', 'token' => $invite->getToken(), ]); $me = $this->json($this->client, 'GET', '/api/me'); $this->assertTrue($me['ok']); $this->assertSame('regauto@test.dudi', $me['email']); } public function testRegisterUsedTokenRejected(): void { $creator = $this->createUser('regusedtkinviter'); $existing = $this->createUser('regusedtkuser'); $invite = new Invite(); $invite->setToken(bin2hex(random_bytes(32))) ->setCreatedBy($creator->getId()) ->setExpiresAt(new \DateTimeImmutable('+7 days')) ->setUsedBy($existing->getId()) ->setUsedAt(new \DateTimeImmutable()); $this->em->persist($invite); $this->em->flush(); $data = $this->json($this->client, 'POST', '/api/register', [ 'email' => 'regusedtk@test.dudi', 'password' => 'passwort99', 'token' => $invite->getToken(), ]); $this->assertArrayHasKey('error', $data); $this->assertSame(400, $this->client->getResponse()->getStatusCode()); } // ── Locale ──────────────────────────────────────────────────────────────── public function testUpdateLocale(): void { $user = $this->createUser('locale'); $client = $this->authClient($user); foreach (['en', 'pl', 'de'] as $locale) { $data = $this->json($client, 'PATCH', '/api/me', ['locale' => $locale]); $this->assertTrue($data['ok'], "ok missing for locale $locale"); $this->assertSame($locale, $data['locale']); } $this->em->clear(); $user = $this->em->find(User::class, $user->getId()); $this->assertSame('de', $user->getLocale()); } public function testUpdateLocaleInvalid(): void { $user = $this->createUser('localeinvalid'); $client = $this->authClient($user); $data = $this->json($client, 'PATCH', '/api/me', ['locale' => 'fr']); $this->assertArrayHasKey('error', $data); $this->assertSame(400, $this->client->getResponse()->getStatusCode()); } // ── Logout ──────────────────────────────────────────────────────────────── public function testLogout(): void { $user = $this->createUser('logout'); $client = $this->authClient($user); $me = $this->json($client, 'GET', '/api/me'); $this->assertTrue($me['ok']); $this->json($client, 'POST', '/api/logout'); $this->json($client, 'GET', '/api/me'); $this->assertSame(401, $client->getResponse()->getStatusCode()); } // ── Name validation ─────────────────────────────────────────────────────── public function testUpdateNameWhitespaceOnlyIsRejected(): void { $user = $this->createUser('namewhitespace'); $client = $this->authClient($user); $data = $this->json($client, 'PATCH', '/api/me', ['name' => ' ']); $this->assertArrayHasKey('error', $data); $this->assertSame(400, $this->client->getResponse()->getStatusCode()); } // ── Password reset (additional) ──────────────────────────────────────────── public function testResetPasswordWrongToken(): void { $user = $this->createUser('resetwrongtk'); $selector = bin2hex(random_bytes(12)); $token = bin2hex(random_bytes(32)); $this->em->getConnection()->executeStatement( 'INSERT INTO users_resets (user, selector, token, expires) VALUES (?, ?, ?, ?)', [$user->getId(), $selector, password_hash($token, PASSWORD_BCRYPT), time() + 3600] ); $data = $this->json($this->client, 'POST', '/api/reset-password', [ 'selector' => $selector, 'token' => 'falschertoken123', 'password' => 'neuesPasswort99', ]); $this->assertArrayHasKey('error', $data); $this->assertSame(400, $this->client->getResponse()->getStatusCode()); } // ── Invites (additional) ─────────────────────────────────────────────────── public function testInviteCreateWithoutNote(): void { $user = $this->createUser('invnonote'); $client = $this->authClient($user); $data = $this->json($client, 'POST', '/api/invite', []); $this->assertArrayHasKey('url', $data); $this->assertStringContainsString('invite=', $data['url']); $invites = $this->em->getRepository(Invite::class)->findBy(['createdBy' => $user->getId()]); $this->assertCount(1, $invites); $this->assertNull($invites[0]->getNote()); } public function testInviteListIsolatedFromOtherUsers(): void { $user1 = $this->createUser('inviso1'); $user2 = $this->createUser('inviso2'); $invite = new Invite(); $invite->setToken(bin2hex(random_bytes(32))) ->setCreatedBy($user1->getId()) ->setExpiresAt(new \DateTimeImmutable('+7 days')); $this->em->persist($invite); $this->em->flush(); $client = $this->authClient($user2); $data = $this->json($client, 'GET', '/api/invites'); $this->assertSame([], $data); } // ── Goals (additional) ───────────────────────────────────────────────────── public function testGoalPartialUpdateOnlyName(): void { $user = $this->createUser('goalpartial'); $goal = new Goal(); $goal->setUserId($user->getId())->setName('Alt')->setUnit('Min')->setDaily(10.0)->setDays(7)->setStart(new \DateTime())->setSets([]); $this->em->persist($goal); $this->em->flush(); $client = $this->authClient($user); $data = $this->json($client, 'PATCH', '/api/goals/' . $goal->getId(), ['name' => 'Neu']); $this->assertTrue($data['ok']); $this->em->refresh($goal); $this->assertSame('Neu', $goal->getName()); $this->assertSame('Min', $goal->getUnit()); $this->assertSame(10.0, $goal->getDaily()); } public function testGoalCreateEmptyUnitFallsBackToDefault(): void { $user = $this->createUser('goalemptyunit'); $client = $this->authClient($user); $data = $this->json($client, 'POST', '/api/goals', [ 'name' => 'Plank', 'unit' => '', 'daily' => 1, 'days' => 7, ]); $this->assertSame('Stück', $data['unit']); } // ── Admin ───────────────────────────────────────────────────────────────── public function testAdminUsersUnauthenticated(): void { $this->json($this->client, 'GET', '/api/admin/users'); $this->assertSame(401, $this->client->getResponse()->getStatusCode()); } public function testAdminUsersNonAdminForbidden(): void { $user = $this->createUser('adminblocked'); $client = $this->authClient($user); $data = $this->json($client, 'GET', '/api/admin/users'); $this->assertArrayHasKey('error', $data); $this->assertSame(403, $client->getResponse()->getStatusCode()); } public function testAdminUsersReturnsAllUsers(): void { $adminEmail = $_ENV['ADMIN_EMAIL'] ?? ''; if (!$adminEmail) { $this->markTestSkipped('ADMIN_EMAIL not configured'); } $admin = $this->em->getRepository(User::class)->findOneBy(['email' => $adminEmail]); if (!$admin) { $this->markTestSkipped("Admin user $adminEmail not found in DB"); } $this->createUser('adminlistother'); $client = $this->authClient($admin); $data = $this->json($client, 'GET', '/api/admin/users'); $this->assertSame(200, $client->getResponse()->getStatusCode()); $this->assertIsArray($data); $emails = array_column($data, 'email'); $this->assertContains($adminEmail, $emails); $this->assertContains('adminlistother@test.dudi', $emails); foreach ($data as $row) { $this->assertArrayHasKey('email', $row); $this->assertArrayHasKey('username', $row); $this->assertArrayHasKey('registered', $row); } } public function testGoalDeleteReturnsOkForNonExistentGoal(): void { $user = $this->createUser('goaldelmissing'); $client = $this->authClient($user); $data = $this->json($client, 'DELETE', '/api/goals/999999'); $this->assertTrue($data['ok']); $this->assertSame(200, $client->getResponse()->getStatusCode()); } }