diff --git a/docs/api/v1/users/create.md b/docs/api/v1/users/create.md new file mode 100644 index 00000000..a5a43197 --- /dev/null +++ b/docs/api/v1/users/create.md @@ -0,0 +1,172 @@ +# Create User + +Create a new user. + +**URL** : `/api/v1/users/create` + +**Method** : `POST` + +**Auth required** : YES + +**Request Body constraints** + +```json +{ + "name": "[string: username, alphanumeric, spaces, underscores and hyphens, max 64 chars]", + "display_name": "[string: max 255 chars]", + "email": "[string: valid email, max 255 chars]", + "password": "[string: max 255 chars]", + "is_admin": "[string or boolean: 'true', 'false', true, false, default false]" +} +``` + +**Body example** + +```json +{ + "name": "user", + "display_name": "New User", + "email": "user@user.user", + "password": "password", + "is_admin": "false" +} +``` + +## Success Response + +**Code** : `200 OK` + +**Content examples** + +```json +{ + "status": "success", + "data": { + "user_id": 5, + "user_name": "user" + }, + "timestamp": "2026-09-23T15:01:33+01:00" +} +``` + +## Error Response + +**Condition** : If 'X-Davis-API-Token' is not present or mismatched in headers. + +**Code** : `401 UNAUTHORIZED` + +**Content** : + +```json +{ + "message": "No API token provided", + "timestamp": "2026-09-23T15:01:33+01:00" +} +``` + +or + +```json +{ + "message": "Invalid API token", + "timestamp": "2026-09-23T15:01:33+01:00" +} +``` + +**Condition** : If request body contains invalid JSON. + +**Code** : `400 BAD REQUEST` + +**Content** : + +```json +{ + "status": "error", + "message": "Invalid JSON", + "timestamp": "2026-09-23T15:01:33+01:00" +} +``` + +**Condition** : If 'name' parameter is invalid (not matching the regex or exceeds length). + +**Code** : `400 BAD REQUEST` + +**Content** : + +```json +{ + "status": "error", + "message": "Invalid Userame", + "timestamp": "2026-09-23T15:01:33+01:00" +} +``` + +**Condition** : If 'display_name' parameter is invalid (null or empty string). + +**Code** : `400 BAD REQUEST` + +**Content** : + +```json +{ + "status": "error", + "message": "Invalid Display Name", + "timestamp": "2026-09-23T15:01:33+01:00" +} +``` + +**Condition** : If 'email' parameter is invalid (null, empty string or invalid email). + +**Code** : `400 BAD REQUEST` + +**Content** : + +```json +{ + "status": "error", + "message": "Invalid Email", + "timestamp": "2026-09-23T15:01:33+01:00" +} +``` + +**Condition** : If 'password' parameter is invalid (null or empty string). + +**Code** : `400 BAD REQUEST` + +**Content** : + +```json +{ + "status": "error", + "message": "Invalid Password", + "timestamp": "2026-09-23T15:01:33+01:00" +} +``` + +**Condition** : If 'is_admin' parameter is invalid (not in [true, false, 'true', 'false']). + +**Code** : `400 BAD REQUEST` + +**Content** : + +```json +{ + "status": "error", + "message": "Invalid Is Admin", + "timestamp": "2026-09-23T15:01:33+01:00" +} +``` + +**Condition** : If user with specified username already exists. + +**Code** : `400 BAD REQUEST` + +**Content** : + +```json +{ + "status": "error", + "message": "Usrname Already Exists", + "timestamp": "2026-09-23T15:01:33+01:00" +} +``` diff --git a/docs/api/v1/users/delete.md b/docs/api/v1/users/delete.md new file mode 100644 index 00000000..28500151 --- /dev/null +++ b/docs/api/v1/users/delete.md @@ -0,0 +1,86 @@ +# Delete User + +Deletes a specific user. + +**URL** : `/api/v1/users/:user_id` + +**Method** : `DELETE` + +**Auth required** : YES + +**Params constraints** + +``` +:user_id -> "[user id as an int]", +``` + +**URL example** + +``` +/api/v1/users/1 +``` + +## Success Response + +**Code** : `200 OK` + +**Content examples** + +```json +{ + "status": "success", + "timestamp": "2026-09-23T15:01:33+01:00" +} +``` + +## Error Response + +**Condition** : If 'X-Davis-API-Token' is not present or mismatched in headers. + +**Code** : `401 UNAUTHORIZED` + +**Content** : + +```json +{ + "message": "No API token provided", + "timestamp": "2026-09-23T15:01:33+01:00" +} +``` + +or + +```json +{ + "message": "Invalid API token", + "timestamp": "2026-09-23T15:01:33+01:00" +} +``` + +**Condition** : If user is not found. + +**Code** : `404 NOT FOUND` + +**Content** : + +```json +{ + "status": "error", + "message": "User Not Found", + "timestamp": "2026-09-23T15:01:33+01:00" +} +``` + +**Condition** : If any error arises while deleting, like if a principal was deleted without removing the corresponding user. + +**Code** : `500 INTERNAL SERVER ERROR` + +**Content** : + +```json +{ + "status": "error", + "message": "Failed to Delete User", + "timestamp": "2026-09-23T15:01:33+01:00" +} +``` diff --git a/src/Controller/Admin/UserController.php b/src/Controller/Admin/UserController.php index 4943bfca..7d94d191 100644 --- a/src/Controller/Admin/UserController.php +++ b/src/Controller/Admin/UserController.php @@ -2,12 +2,7 @@ namespace App\Controller\Admin; -use App\Entity\AddressBook; -use App\Entity\Calendar; -use App\Entity\CalendarInstance; -use App\Entity\CalendarSubscription; use App\Entity\Principal; -use App\Entity\SchedulingObject; use App\Entity\User; use App\Form\UserType; use App\Services\Utils; @@ -37,23 +32,28 @@ public function users(ManagerRegistry $doctrine): Response #[Route('/edit/{userId}', name: 'edit')] public function userCreate(ManagerRegistry $doctrine, Utils $utils, Request $request, ?int $userId, TranslatorInterface $trans): Response { - if ($userId) { + $new_user = is_null($userId) ? false : true; + if ($new_user) { $user = $doctrine->getRepository(User::class)->findOneById($userId); if (!$user) { throw $this->createNotFoundException('User not found'); } $oldHash = $user->getPassword(); $principal = $doctrine->getRepository(Principal::class)->findOneByUri($user->getPrincipalUri()); + if (!$principal) { + throw $this->createNotFoundException('Principal not found'); + } } else { $user = new User(); - $principal = new Principal(); } - $form = $this->createForm(UserType::class, $user, ['new' => !$userId]); + $form = $this->createForm(UserType::class, $user, ['new' => !$new_user]); - $form->get('displayName')->setData($principal->getDisplayName()); - $form->get('email')->setData($principal->getEmail()); - $form->get('isAdmin')->setData($principal->getIsAdmin()); + if ($new_user) { + $form->get('displayName')->setData($principal->getDisplayName()); + $form->get('email')->setData($principal->getEmail()); + $form->get('isAdmin')->setData($principal->getIsAdmin()); + } $form->handleRequest($request); @@ -63,7 +63,7 @@ public function userCreate(ManagerRegistry $doctrine, Utils $utils, Request $req $isAdmin = $form->get('isAdmin')->getData(); // Create password for user - if ($userId && is_null($user->getPassword())) { + if ($new_user && is_null($user->getPassword())) { // The user is not new and does not want to change its password $user->setPassword($oldHash); } else { @@ -71,45 +71,18 @@ public function userCreate(ManagerRegistry $doctrine, Utils $utils, Request $req $user->setPassword($hash); } - $entityManager = $doctrine->getManager(); - - // If it's a new user, create default calendar and address book, and principal - if (null === $user->getId()) { - $principal->setUri($user->getPrincipalUri()); - - $calendarInstance = new CalendarInstance(); - $calendar = new Calendar(); - $calendarInstance->setPrincipalUri($user->getPrincipalUri()) - ->setUri('default') // No risk of collision since unicity is guaranteed by the new user principal - ->setDisplayName($trans->trans('default.calendar.title')) - ->setDescription($trans->trans('default.calendar.description', ['user' => $displayName])) - ->setCalendar($calendar); - - // Enable delegation by default - $principalProxyRead = new Principal(); - $principalProxyRead->setUri($principal->getUri().Principal::READ_PROXY_SUFFIX) - ->setIsMain(false); - $entityManager->persist($principalProxyRead); - - $principalProxyWrite = new Principal(); - $principalProxyWrite->setUri($principal->getUri().Principal::WRITE_PROXY_SUFFIX) - ->setIsMain(false); - $entityManager->persist($principalProxyWrite); - - $addressbook = new AddressBook(); - $addressbook->setPrincipalUri($user->getPrincipalUri()) - ->setUri('default') // No risk of collision since unicity is guaranteed by the new user principal - ->setDisplayName($trans->trans('default.addressbook.title')) - ->setDescription($trans->trans('default.addressbook.description', ['user' => $displayName])); - $entityManager->persist($calendarInstance); - $entityManager->persist($addressbook); - $entityManager->persist($principal); + // If it's a new user, create default objects, otherwise set the new values + if (!$new_user) { + $username = $form->get('username')->getData(); + $user->setUsername($username); + $utils->createDefaultObjectsForUser($user, $displayName, $email, $isAdmin); + } else { + $principal->setDisplayName($displayName) + ->setEmail($email) + ->setIsAdmin($isAdmin); } - $principal->setDisplayName($displayName) - ->setEmail($email) - ->setIsAdmin($isAdmin); - + $entityManager = $doctrine->getManager(); $entityManager->persist($user); $entityManager->flush(); @@ -126,75 +99,21 @@ public function userCreate(ManagerRegistry $doctrine, Utils $utils, Request $req } #[Route('/delete/{userId}', name: 'delete', methods: ['POST'])] - public function userDelete(ManagerRegistry $doctrine, Request $request, #[MapEntity(id: 'userId')] User $user, int $userId, TranslatorInterface $trans): Response + public function userDelete(ManagerRegistry $doctrine, Request $request, #[MapEntity(id: 'userId')] User $user, int $userId, TranslatorInterface $trans, Utils $utils): Response { if (!$this->isCsrfTokenValid('admin_action', $request->getPayload()->getString('_token'))) { throw $this->createAccessDeniedException('Invalid CSRF token.'); } $entityManager = $doctrine->getManager(); - $entityManager->remove($user); - - $principal = $doctrine->getRepository(Principal::class)->findOneByUri($user->getPrincipalUri()); - $principalProxyRead = $doctrine->getRepository(Principal::class)->findOneByUri($principal->getUri().Principal::READ_PROXY_SUFFIX); - $principalProxyWrite = $doctrine->getRepository(Principal::class)->findOneByUri($principal->getUri().Principal::WRITE_PROXY_SUFFIX); - - $entityManager->remove($principal); - - if ($principalProxyRead) { - $entityManager->remove($principalProxyRead); - } - - if ($principalProxyWrite) { - $entityManager->remove($principalProxyWrite); - } - - $principalUri = $user->getPrincipalUri(); - - // Remove calendars and addressbooks - $calendars = $doctrine->getRepository(CalendarInstance::class)->findByPrincipalUriWithCalendars($principalUri); - foreach ($calendars ?? [] as $instance) { - // We're only removing the calendar objects / changes / and calendar if the deleted user is an owner, - // which means that the underlying calendar instance should not have another principal as owner. - $hasDifferentOwner = $doctrine->getRepository(CalendarInstance::class)->hasDifferentOwner($instance->getCalendar()->getId(), $principalUri); - if (!$hasDifferentOwner) { - foreach ($instance->getCalendar()->getObjects() ?? [] as $object) { - $entityManager->remove($object); - } - foreach ($instance->getCalendar()->getChanges() ?? [] as $change) { - $entityManager->remove($change); - } - // We need to remove the shared versions of this calendar, too - foreach ($instance->getCalendar()->getInstances() ?? [] as $instances) { - $entityManager->remove($instances); - } - $entityManager->remove($instance->getCalendar()); - } - $entityManager->remove($instance); - } - $calendarsSubscriptions = $doctrine->getRepository(CalendarSubscription::class)->findByPrincipalUri($principalUri); - foreach ($calendarsSubscriptions ?? [] as $subscription) { - $entityManager->remove($subscription); - } - $schedulingObjects = $doctrine->getRepository(SchedulingObject::class)->findByPrincipalUri($principalUri); - foreach ($schedulingObjects ?? [] as $object) { - $entityManager->remove($object); - } - - $addressbooks = $doctrine->getRepository(AddressBook::class)->findByPrincipalUri($principalUri); - foreach ($addressbooks ?? [] as $addressbook) { - foreach ($addressbook->getCards() ?? [] as $card) { - $entityManager->remove($card); - } - foreach ($addressbook->getChanges() ?? [] as $change) { - $entityManager->remove($change); - } - $entityManager->remove($addressbook); + try { + $utils->deleteUser($user); + $entityManager->flush(); + $this->addFlash('success', $trans->trans('user.deleted')); + } catch (\Exception $e) { + $this->addFlash('error', $trans->trans('user.deleted.error')); } - $entityManager->flush(); - $this->addFlash('success', $trans->trans('user.deleted')); - return $this->redirectToRoute('user_index'); } diff --git a/src/Controller/Api/ApiController.php b/src/Controller/Api/ApiController.php index 597b08a1..5e84d117 100644 --- a/src/Controller/Api/ApiController.php +++ b/src/Controller/Api/ApiController.php @@ -14,6 +14,9 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; #[Route('/api/v1', name: 'api_v1_')] class ApiController extends AbstractController @@ -146,6 +149,93 @@ public function getUserDetails(Request $request, ManagerRegistry $doctrine, int return $this->json($response, 200); } + /** + * Creates a new user. + * + * @param Request $request The HTTP POST request + * + * @return JsonResponse A JSON response containing the user details if successfull + */ + #[Route('/users/create', name: 'user_create', methods: ['POST'])] + public function createUser(Request $request, ManagerRegistry $doctrine, TranslatorInterface $trans, ValidatorInterface $validator, Utils $utils): JsonResponse + { + // Parse JSON body + $data = json_decode($request->getContent(), true); + if (JSON_ERROR_NONE !== json_last_error()) { + return $this->json(['status' => 'error', 'message' => 'Invalid JSON', 'timestamp' => $this->getTimestamp()], 400); + } + + $username = $data['name'] ?? null; + if (is_null($username) || !$this->validateUsername($username)) { + return $this->json(['status' => 'error', 'message' => 'Invalid Username', 'timestamp' => $this->getTimestamp()], 400); + } + $display_name = $data['display_name'] ?? null; + if (empty($display_name)) { + return $this->json(['status' => 'error', 'message' => 'Invalid Display Name', 'timestamp' => $this->getTimestamp()], 400); + } + $email = $data['email'] ?? null; + if (empty($email) || count($validator->validate($email, new Assert\Email())) > 0) { + return $this->json(['status' => 'error', 'message' => 'Invalid Email', 'timestamp' => $this->getTimestamp()], 400); + } + $password = $data['password'] ?? null; + if (is_null($password)) { + return $this->json(['status' => 'error', 'message' => 'Invalid Password', 'timestamp' => $this->getTimestamp()], 400); + } + $isAdmin = $data['is_admin'] ?? false; + if (!in_array($isAdmin, [true, false, 'true', 'false'], true)) { + return $this->json(['status' => 'error', 'message' => 'Invalid Is Admin', 'timestamp' => $this->getTimestamp()], 400); + } + + $existingUsername = $doctrine->getRepository(User::class)->findOneByUsername($username); + if ($existingUsername) { + return $this->json(['status' => 'error', 'message' => 'Username Already Exists', 'timestamp' => $this->getTimestamp()], 400); + } + + $hashed_password = password_hash($password, PASSWORD_DEFAULT); + $user = $utils->createUserWithDefaultObjects($username, $display_name, $email, $hashed_password, (true === $isAdmin || 'true' === $isAdmin) ? true : false); + + $entityManager = $doctrine->getManager(); + $entityManager->flush(); + + $response = [ + 'status' => 'success', + 'data' => [ + 'user_id' => $user->getId(), + 'user_name' => $user->getUsername(), + ], + 'timestamp' => $this->getTimestamp(), + ]; + + return $this->json($response, 201); + } + + /** + * Deletes a specific user. + * + * @param int $userId The ID of the user to delete + * + * @return JsonResponse A JSON response indicating the success or failure of the operation + */ + #[Route('/users/{userId}', name: 'user_delete', methods: ['DELETE'], requirements: ['userId' => '\d+'])] + public function deleteUser(int $userId, ManagerRegistry $doctrine, Utils $utils): JsonResponse + { + $user = $this->resolveUser($doctrine, $userId); + if (!$user) { + return $this->json(['status' => 'error', 'message' => 'User Not Found', 'timestamp' => $this->getTimestamp()], 404); + } + + try { + $utils->deleteUser($user); + } catch (\Exception $e) { + return $this->json(['status' => 'error', 'message' => 'Error while Deleting User', 'timestamp' => $this->getTimestamp()], 500); + } + + $entityManager = $doctrine->getManager(); + $entityManager->flush(); + + return $this->json(['status' => 'success', 'timestamp' => $this->getTimestamp()], 200); + } + /** * Retrieves a list of calendars for a specific user, including user calendars, shared calendars, and subscriptions. * diff --git a/src/Services/Utils.php b/src/Services/Utils.php index 38aca4bd..90a52a1c 100644 --- a/src/Services/Utils.php +++ b/src/Services/Utils.php @@ -5,7 +5,9 @@ use App\Entity\AddressBook; use App\Entity\Calendar; use App\Entity\CalendarInstance; +use App\Entity\CalendarSubscription; use App\Entity\Principal; +use App\Entity\SchedulingObject; use App\Entity\User; use Doctrine\Persistence\ManagerRegistry; use Symfony\Contracts\Translation\TranslatorInterface; @@ -58,6 +60,18 @@ public static function isValidUsername(?string $username): bool } public function createPasswordlessUserWithDefaultObjects(string $username, string $displayName, string $email) + { + // Set the password to a random string (but hashed beforehand) + $password = substr(bin2hex(random_bytes(256)), 0, 48); + $hash = password_hash($password, PASSWORD_DEFAULT); + + return $this->createUserWithDefaultObjects($username, $displayName, $email, $hash, false); + } + + /** + * Return the new user, persisted in the database. + */ + public function createUserWithDefaultObjects(string $username, string $displayName, string $email, string $hashed_password, bool $isAdmin) { if (!self::isValidUsername($username)) { throw new \InvalidArgumentException(sprintf('Refusing to create the user "%s": a username may only contain letters, digits and the characters _ . @ + \' -', $username)); @@ -65,18 +79,29 @@ public function createPasswordlessUserWithDefaultObjects(string $username, strin $user = new User(); $user->setUsername($username); + $user->setPassword($hashed_password); - // Set the password to a random string (but hashed beforehand) - $randomBytes = substr(bin2hex(random_bytes(256)), 0, 48); - $hash = password_hash($randomBytes, PASSWORD_DEFAULT); - $user->setPassword($hash); + $this->createDefaultObjectsForUser($user, $displayName, $email, $isAdmin); + + $em = $this->doctrine->getManager(); + $em->persist($user); - // Create principal, default calendar and addressbook + return $user; + } + + /** + * Create the default objects for a new user: principal, default calendar + * and addressbook, all persisted in the database. + * + * Return the new principal + */ + public function createDefaultObjectsForUser(User $user, string $displayName, string $email, bool $isAdmin) + { $principal = new Principal(); $principal->setUri($user->getPrincipalUri()) ->setDisplayName($displayName) ->setEmail($email) - ->setIsAdmin(false); + ->setIsAdmin($isAdmin); $calendarInstance = new CalendarInstance(); $calendar = new Calendar(); @@ -108,6 +133,74 @@ public function createPasswordlessUserWithDefaultObjects(string $username, strin $em->persist($calendarInstance); $em->persist($addressbook); $em->persist($principal); - $em->persist($user); + + return $principal; + } + + public function deleteUser(User $user) + { + $entityManager = $this->doctrine->getManager(); + $entityManager->remove($user); + + $principal = $this->doctrine->getRepository(Principal::class)->findOneByUri($user->getPrincipalUri()); + if (!$principal) { + throw new \Exception('Principal is null'); + } + + $principalProxyRead = $this->doctrine->getRepository(Principal::class)->findOneByUri($principal->getUri().Principal::READ_PROXY_SUFFIX); + $principalProxyWrite = $this->doctrine->getRepository(Principal::class)->findOneByUri($principal->getUri().Principal::WRITE_PROXY_SUFFIX); + + $entityManager->remove($principal); + + if ($principalProxyRead) { + $entityManager->remove($principalProxyRead); + } + + if ($principalProxyWrite) { + $entityManager->remove($principalProxyWrite); + } + + $principalUri = $user->getPrincipalUri(); + + // Remove calendars and addressbooks + $calendars = $this->doctrine->getRepository(CalendarInstance::class)->findByPrincipalUriWithCalendars($principalUri); + foreach ($calendars ?? [] as $instance) { + // We're only removing the calendar objects / changes / and calendar if the deleted user is an owner, + // which means that the underlying calendar instance should not have another principal as owner. + $hasDifferentOwner = $this->doctrine->getRepository(CalendarInstance::class)->hasDifferentOwner($instance->getCalendar()->getId(), $principalUri); + if (!$hasDifferentOwner) { + foreach ($instance->getCalendar()->getObjects() ?? [] as $object) { + $entityManager->remove($object); + } + foreach ($instance->getCalendar()->getChanges() ?? [] as $change) { + $entityManager->remove($change); + } + // We need to remove the shared versions of this calendar, too + foreach ($instance->getCalendar()->getInstances() ?? [] as $instances) { + $entityManager->remove($instances); + } + $entityManager->remove($instance->getCalendar()); + } + $entityManager->remove($instance); + } + $calendarsSubscriptions = $this->doctrine->getRepository(CalendarSubscription::class)->findByPrincipalUri($principalUri); + foreach ($calendarsSubscriptions ?? [] as $subscription) { + $entityManager->remove($subscription); + } + $schedulingObjects = $this->doctrine->getRepository(SchedulingObject::class)->findByPrincipalUri($principalUri); + foreach ($schedulingObjects ?? [] as $object) { + $entityManager->remove($object); + } + + $addressbooks = $this->doctrine->getRepository(AddressBook::class)->findByPrincipalUri($principalUri); + foreach ($addressbooks ?? [] as $addressbook) { + foreach ($addressbook->getCards() ?? [] as $card) { + $entityManager->remove($card); + } + foreach ($addressbook->getChanges() ?? [] as $change) { + $entityManager->remove($change); + } + $entityManager->remove($addressbook); + } } } diff --git a/tests/Functional/Controllers/ApiControllerTest.php b/tests/Functional/Controllers/ApiControllerTest.php index dc559646..03161834 100644 --- a/tests/Functional/Controllers/ApiControllerTest.php +++ b/tests/Functional/Controllers/ApiControllerTest.php @@ -2,6 +2,7 @@ namespace App\Tests\Functional; +use App\Entity\User; use App\Entity\Calendar; use App\Entity\CalendarInstance; use App\Entity\CalendarObject; @@ -210,6 +211,290 @@ public function testUserDetails(): void $this->assertStringEqualsStringIgnoringLineEndings($username, $data['data']['username']); } + /* + * Test the user creation endpoint + */ + public function testUserCreate(): void + { + $client = static::createClient(); + $em = static::getContainer()->get('doctrine.orm.entity_manager'); + + // Create user API request with JSON body + $payload = [ + 'name' => 'user', + 'display_name' => 'user display name', + 'email' => 'user@email.com', + 'password' => 'password', + 'is_admin' => false, + ]; + + $client->request('POST', '/api/v1/users/create', [], [], [ + 'HTTP_ACCEPT' => 'application/json', + 'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'], + 'CONTENT_TYPE' => 'application/json', + ], json_encode($payload)); + $this->assertResponseIsSuccessful(); + $this->assertResponseHeaderSame('Content-Type', 'application/json'); + + $data = json_decode($client->getResponse()->getContent(), true); + + // Check if the user was created + $user = $em->getRepository(User::class)->findOneByUsername('user'); + $this->assertNotNull($user, 'The user was not created'); + + // Check if user details are correct + $this->assertArrayHasKey('user_id', $data['data']); + $this->assertEquals($user->getId(), $data['data']['user_id']); + $this->assertArrayHasKey('user_name', $data['data']); + $this->assertEquals($user->getUsername(), $data['data']['user_name']); + $this->assertTrue(password_verify('password', $user->getPassword())); + + // Check if the principal was created + $principal = $em->getRepository(Principal::class)->findOneByUri($user->getPrincipalUri()); + $this->assertNotNull($principal, 'The principal was not created'); + + // Check if principal details are correct + $this->assertEquals('user display name', $principal->getDisplayName()); + $this->assertEquals('user@email.com', $principal->getEmail()); + $this->assertFalse($principal->getIsAdmin()); + } + + /* + * Test the user creation endpoint if is_admin is passed as string + */ + public function testUserCreateIfIsAdminIsString(): void + { + $client = static::createClient(); + $em = static::getContainer()->get('doctrine.orm.entity_manager'); + + // Create user API request with JSON body + $payload = [ + 'name' => 'user', + 'display_name' => 'user display name', + 'email' => 'user@email.com', + 'password' => 'password', + 'is_admin' => "true", + ]; + + $client->request('POST', '/api/v1/users/create', [], [], [ + 'HTTP_ACCEPT' => 'application/json', + 'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'], + 'CONTENT_TYPE' => 'application/json', + ], json_encode($payload)); + $this->assertResponseIsSuccessful(); + $this->assertResponseHeaderSame('Content-Type', 'application/json'); + + $data = json_decode($client->getResponse()->getContent(), true); + + // Check if the user was created + $user = $em->getRepository(User::class)->findOneByUsername('user'); + $this->assertNotNull($user, 'The user was not created'); + + // Check if user details are correct + $this->assertArrayHasKey('user_id', $data['data']); + $this->assertEquals($user->getId(), $data['data']['user_id']); + $this->assertArrayHasKey('user_name', $data['data']); + $this->assertEquals($user->getUsername(), $data['data']['user_name']); + $this->assertTrue(password_verify('password', $user->getPassword())); + + // Check if the principal was created + $principal = $em->getRepository(Principal::class)->findOneByUri($user->getPrincipalUri()); + $this->assertNotNull($principal, 'The principal was not created'); + + // Check if principal details are correct + $this->assertEquals('user display name', $principal->getDisplayName()); + $this->assertEquals('user@email.com', $principal->getEmail()); + $this->assertTrue($principal->getIsAdmin()); + } + + /* + * Test that the user creation endpoint fails when passing invalid email + */ + public function testUserCreateFailInvalidEmail(): void + { + $client = static::createClient(); + $em = static::getContainer()->get('doctrine.orm.entity_manager'); + + // Create user API request with JSON body + $payload = [ + 'name' => 'user', + 'display_name' => 'user display name', + 'email' => 'email', + 'password' => 'password', + 'is_admin' => false, + ]; + + $client->request('POST', '/api/v1/users/create', [], [], [ + 'HTTP_ACCEPT' => 'application/json', + 'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'], + 'CONTENT_TYPE' => 'application/json', + ], json_encode($payload)); + + $this->assertResponseStatusCodeSame(400); + $this->assertResponseHeaderSame('Content-Type', 'application/json'); + + $data = json_decode($client->getResponse()->getContent(), true); + $this->assertEquals('error', $data['status']); + $this->assertStringContainsString('Invalid Email', $data['message']); + } + + /* + * Test that the user creation endpoint fails when passing invalid is_admin + */ + public function testUserCreateFailInvalidIsAdmin(): void + { + $client = static::createClient(); + $em = static::getContainer()->get('doctrine.orm.entity_manager'); + + // Create user API request with JSON body + $payload = [ + 'name' => 'user', + 'display_name' => 'user display name', + 'email' => 'user@email.com', + 'password' => 'password', + 'is_admin' => "notfalse", + ]; + + $client->request('POST', '/api/v1/users/create', [], [], [ + 'HTTP_ACCEPT' => 'application/json', + 'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'], + 'CONTENT_TYPE' => 'application/json', + ], json_encode($payload)); + + $this->assertResponseStatusCodeSame(400); + $this->assertResponseHeaderSame('Content-Type', 'application/json'); + + $data = json_decode($client->getResponse()->getContent(), true); + $this->assertEquals('error', $data['status']); + $this->assertStringContainsString('Invalid Is Admin', $data['message']); + } + + /* + * Test that the user creation endpoint fails when the user already exists + */ + public function testUserCreateFailWhenUserAlreadyExists(): void + { + $client = static::createClient(); + $em = static::getContainer()->get('doctrine.orm.entity_manager'); + + // Get username from existing user lists + $username = $this->getUserUsername($client, 0); + + // Create user API request with JSON body + $payload = [ + 'name' => $username, + 'display_name' => 'user display name', + 'email' => 'user@email.com', + 'password' => 'password', + 'is_admin' => false, + ]; + + $client->request('POST', '/api/v1/users/create', [], [], [ + 'HTTP_ACCEPT' => 'application/json', + 'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'], + 'CONTENT_TYPE' => 'application/json', + ], json_encode($payload)); + + $this->assertResponseStatusCodeSame(400); + $this->assertResponseHeaderSame('Content-Type', 'application/json'); + + $data = json_decode($client->getResponse()->getContent(), true); + $this->assertEquals('error', $data['status']); + $this->assertStringContainsString('Username Already Exists', $data['message']); + } + + /* + * Test the user deletion endpoint + */ + public function testUserDelete(): void + { + $client = static::createClient(); + $em = static::getContainer()->get('doctrine.orm.entity_manager'); + + // Get userId, username and principal from existing user + $userId = $this->getUserId($client, 0); + $username = $this->getUserUsername($client, 0); + $user = $em->getRepository(User::class)->findOneByUsername($username); + $principal = $em->getRepository(Principal::class)->findOneByUri($user->getPrincipalUri()); + + $client->request('DELETE', '/api/v1/users/'.$userId, [], [], [ + 'HTTP_ACCEPT' => 'application/json', + 'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'], + 'CONTENT_TYPE' => 'application/json', + ]); + $this->assertResponseIsSuccessful(); + $this->assertResponseHeaderSame('Content-Type', 'application/json'); + + $data = json_decode($client->getResponse()->getContent(), true); + + // Check if the user was deleted + $user = $em->getRepository(User::class)->findOneByUsername($username); + $this->assertNull($user, 'The user was not deleted'); + + // Check if the principal was deleted + $principal = $em->getRepository(Principal::class)->findOneByUri($principal->getUri()); + $this->assertNull($principal, 'The principal was not deleted'); + } + + /* + * Test that the user deletion endpoint fails when the user does not exist + */ + public function testUserDeleteFailUserDoesNotExists(): void + { + $client = static::createClient(); + $em = static::getContainer()->get('doctrine.orm.entity_manager'); + + // Get userId, username and principal from existing user + $userId = $this->getUserId($client, 0); + $username = $this->getUserUsername($client, 0); + $user = $em->getRepository(User::class)->findOneByUsername($username); + $em->remove($user); + $em->flush(); + + $client->request('DELETE', '/api/v1/users/'.$userId, [], [], [ + 'HTTP_ACCEPT' => 'application/json', + 'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'], + 'CONTENT_TYPE' => 'application/json', + ]); + + $this->assertResponseStatusCodeSame(404); + $this->assertResponseHeaderSame('Content-Type', 'application/json'); + + $data = json_decode($client->getResponse()->getContent(), true); + $this->assertEquals('error', $data['status']); + $this->assertStringContainsString('User Not Found', $data['message']); + } + + /* + * Test that the user deletion endpoint fails when the user does not have a principal + */ + public function testUserDeleteFailIfNoPrincipal(): void + { + $client = static::createClient(); + $em = static::getContainer()->get('doctrine.orm.entity_manager'); + + // Get userId, username and principal from existing user + $userId = $this->getUserId($client, 0); + $username = $this->getUserUsername($client, 0); + $user = $em->getRepository(User::class)->findOneByUsername($username); + $principal = $em->getRepository(Principal::class)->findOneByUri($user->getPrincipalUri()); + $em->remove($principal); + $em->flush(); + + $client->request('DELETE', '/api/v1/users/'.$userId, [], [], [ + 'HTTP_ACCEPT' => 'application/json', + 'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'], + 'CONTENT_TYPE' => 'application/json', + ]); + + $this->assertResponseStatusCodeSame(500); + $this->assertResponseHeaderSame('Content-Type', 'application/json'); + + $data = json_decode($client->getResponse()->getContent(), true); + $this->assertEquals('error', $data['status']); + $this->assertStringContainsString('Error while Deleting User', $data['message']); + } + /* * Test the user calendars list endpoint */ diff --git a/translations/messages+intl-icu.de.xlf b/translations/messages+intl-icu.de.xlf index 3f9a28c7..99e4b126 100644 --- a/translations/messages+intl-icu.de.xlf +++ b/translations/messages+intl-icu.de.xlf @@ -261,6 +261,10 @@ user.deleted Benutzer erfolgreich gelöscht + + user.deleted.error + Fehler beim Löschen dieses Benutzers + calendar.saved Kalender erfolgreich gespeichert @@ -585,7 +589,7 @@ calendar.share_access.3 lesen / schreiben - + calendar.public öffentlich diff --git a/translations/messages+intl-icu.en.xlf b/translations/messages+intl-icu.en.xlf index 4c6e1cd4..8c693210 100644 --- a/translations/messages+intl-icu.en.xlf +++ b/translations/messages+intl-icu.en.xlf @@ -261,6 +261,10 @@ user.deleted User deleted successfully + + user.deleted.error + Error while deleting this user + calendar.saved Calendar saved successfully @@ -585,7 +589,7 @@ calendar.share_access.3 read / write - + calendar.public public diff --git a/translations/messages+intl-icu.fr.xliff b/translations/messages+intl-icu.fr.xliff index fad1aede..753a4e6d 100644 --- a/translations/messages+intl-icu.fr.xliff +++ b/translations/messages+intl-icu.fr.xliff @@ -261,6 +261,10 @@ user.deleted Utilisateur supprimé avec succès + + user.deleted.error + Erreur lors de la suppression de cet utilisateur + calendar.saved Calendrier enregistré avec succès @@ -585,7 +589,7 @@ calendar.share_access.3 lecture / écriture - + calendar.share_access.10 public