diff --git a/src/Controller/AuthController.php b/src/Controller/AuthController.php index 1e67bbe..5043d2c 100644 --- a/src/Controller/AuthController.php +++ b/src/Controller/AuthController.php @@ -5,8 +5,10 @@ namespace App\Controller; use App\Entity\User; use App\Repository\InviteRepository; use App\Repository\UserRepository; +use App\Security\JsonLoginAuthenticator; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -80,6 +82,7 @@ class AuthController extends AbstractController Request $request, InviteRepository $invites, UserPasswordHasherInterface $hasher, + Security $security, ): JsonResponse { $data = json_decode($request->getContent(), true) ?? []; $email = trim($data['email'] ?? ''); @@ -108,12 +111,21 @@ class AuthController extends AbstractController ->setUsername($name ?: null) ->setVerified(true); - $this->em->persist($user); - $this->em->flush(); // flush first to get user ID + $conn = $this->em->getConnection(); + $conn->beginTransaction(); + try { + $this->em->persist($user); + $this->em->flush(); + $invite->setUsedBy($user->getId()); + $invite->setUsedAt(new \DateTimeImmutable()); + $this->em->flush(); + $conn->commit(); + } catch (\Throwable $e) { + $conn->rollBack(); + return new JsonResponse(['error' => 'Registrierung fehlgeschlagen'], Response::HTTP_INTERNAL_SERVER_ERROR); + } - $invite->setUsedBy($user->getId()); - $invite->setUsedAt(new \DateTimeImmutable()); - $this->em->flush(); + $security->login($user, JsonLoginAuthenticator::class, 'main'); return new JsonResponse(['ok' => true, 'email' => $email, 'name' => $name]); } diff --git a/tests/AppIntegrationTest.php b/tests/AppIntegrationTest.php index a40131e..2e57564 100644 --- a/tests/AppIntegrationTest.php +++ b/tests/AppIntegrationTest.php @@ -583,4 +583,206 @@ class AppIntegrationTest extends WebTestCase $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']); + } + + 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()); + } }