diff --git a/app/app.controller.php b/app/app.controller.php index 629caf1..d1612a3 100644 --- a/app/app.controller.php +++ b/app/app.controller.php @@ -5,6 +5,7 @@ @include_once "./utils/request.php"; @include_once "./users/users.controller.php"; @include_once "./db/db.controller.php"; + @include_once "./chats/chats.controller.php"; @include_once "./auth/auth.controller.php"; @include_once "./guards/jwtAuthGuard.php"; @@ -20,6 +21,7 @@ class AppController # Controllers private $dbController; private $usersController; + private $chatsController; private $authController; # Guards private $jwtAuthGuard; @@ -36,6 +38,7 @@ function __construct($dbConfig) # Initialize controllers $this->usersController = new UsersController($this->conn); $this->authController = new AuthController($this->conn); + $this->chatsController = new ChatsController($this->conn); # Initialize guards $this->jwtAuthGuard = new JwtAuthGuard(new UsersService($this->conn)); @@ -55,22 +58,49 @@ private function setHeaders() private function router() { - switch ($this->req['method']) { + switch ($this->req['method']) + { case 'GET': - if ($this->req['resource'] === '/api/users/me') { + if ($this->req['resource'] === '/api/users/me') + { + // When we require authorization and want to get user in controller $this->_req->useGuard($this->jwtAuthGuard); + // We need to call getRequest() method in original request method $this->usersController->getMe($this->_req->getRequest()); return; } - // /users/:userId - if (strpos($this->req['resource'], '/api/users/') === 0) { + # /users/me/chats + if ($this->req['resource'] === '/api/users/me/chats') + { + $this->_req->useGuard($this->jwtAuthGuard); + $this->usersController->getUserChats($this->_req->getRequest()); + return; + } + # /users/:userId + if (strpos($this->req['resource'], '/api/users/') === 0) + { $this->usersController->getUserById($this->req); return; } - if ($this->req['resource'] === '/api/users') { + if ($this->req['resource'] === '/api/users') + { $this->usersController->getUsers(); return; } + # /chats/:chatId/users + if (preg_match("/\/api\/chats\/(?'chatId'[a-z0-9]+)\/users/", $this->req['resource'])) + { + $this->_req->useGuard($this->jwtAuthGuard); + $this->chatsController->getChatParticipants($this->_req->getRequest()); + return; + } + # /chats/:chatId + if (strpos($this->req['resource'], '/api/chats/') === 0) + { + $this->_req->useGuard($this->jwtAuthGuard); + $this->chatsController->getChatById($this->_req->getRequest()); + return; + } httpException("Route not found " . $this->req['resource'], 404)['end'](); logMessage("Route not found $this->req"); @@ -79,16 +109,26 @@ private function router() case 'POST': $reqBody = $this->_req->parseBody(); - - if ($this->req['resource'] === '/api/users') { - $this->usersController->createUser($reqBody["data"]); + + if (preg_match("/\/api\/chats\/(?'chatId'[a-z0-9]+)\/users/", $this->req['resource'])) + { + $this->_req->useGuard($this->jwtAuthGuard); + $this->chatsController->addUserToChat($this->_req->getRequest(), $reqBody["data"]); + return; + } + if ($this->req['resource'] === '/api/chats') + { + $this->_req->useGuard($this->jwtAuthGuard); + $this->chatsController->createChat($this->_req->getRequest(), $reqBody["data"]); return; } - if ($this->req['resource'] === '/api/auth/sign-up') { + if ($this->req['resource'] === '/api/auth/sign-up') + { $this->authController->signUp($reqBody["data"]); return; } - if ($this->req['resource'] === '/api/auth/sign-in') { + if ($this->req['resource'] === '/api/auth/sign-in') + { $this->authController->signIn($reqBody["data"]); return; } @@ -96,6 +136,24 @@ private function router() httpException("Route not found", 404)['end'](); break; + + case 'DELETE': + $reqBody = $this->_req->parseBody(); + + if (preg_match("/\/api\/chats\/(?'chatId'[a-z0-9]+)\/users\/(?'userId'[a-z0-9]+)/", $this->req['resource'])) + { + $this->_req->useGuard($this->jwtAuthGuard); + $this->chatsController->deleteChatParticipant($this->_req->getRequest()); + return; + } + if (strpos($this->req['resource'], '/api/chats/') === 0) + { + $this->_req->useGuard($this->jwtAuthGuard); + $this->chatsController->deleteChat($this->_req->getRequest()); + return; + } + + break; default: httpException("Method not supported", 404)['end'](); diff --git a/auth/auth.controller.php b/auth/auth.controller.php index 76835d8..e0cd89c 100644 --- a/auth/auth.controller.php +++ b/auth/auth.controller.php @@ -2,18 +2,16 @@ @include_once __DIR__ . "/../utils/httpException.php"; @include_once __DIR__ . "/../utils/jsonResponse.php"; - @include_once __DIR__ . "/auth.service.php"; @include_once __DIR__ . "/../utils/jwt.php"; @include_once __DIR__ . "/../locale/en/messages.php"; + @include_once __DIR__ . "/../utils/randomId.php"; class AuthController { - private $authService; private $usersService; function __construct($conn) { - $this->authService = new AuthService($conn); $this->usersService = new UsersService($conn); } @@ -30,7 +28,7 @@ function signUp($registerUserDto): array httpException($messages["username_taken"])['end'](); } - $id = random_int(0, 9999999); + $id = randomId(); try { $this->usersService->createUser($id, $registerUserDto["username"], $registerUserDto["password"]); diff --git a/auth/auth.service.php b/auth/auth.service.php deleted file mode 100644 index 1b1ee70..0000000 --- a/auth/auth.service.php +++ /dev/null @@ -1,11 +0,0 @@ -conn = $conn; - } - } diff --git a/chats/chats.controller.php b/chats/chats.controller.php new file mode 100644 index 0000000..13123d6 --- /dev/null +++ b/chats/chats.controller.php @@ -0,0 +1,223 @@ +chatsService = new ChatsService($conn); + $this->usersService = new UsersService($conn); + } + + function getChatById($req) + { + global $messages; + + # Parse chat id from url + $chatId = substr($req['resource'], strlen('/api/chats/')); + + $chat = $this->chatsService->findById($chatId); + + if (is_null($chat)) { + httpException($messages["chat_not_found"], 404)['end'](); + } + + if ($chat["isPrivate"] && !$this->chatsService->isUserChatParticipant($req["user"]["id"], $chatId)) { + httpException($messages["not_enough_permission"], 403)['end'](); + } + + $response = [ + "chat" => $this->chatsService->createChatRO($chat) + ]; + + jsonResponse($response)['end'](); + } + + function createChat($req, $chatDto) + { + global $messages; + + try { + $chat = $chatDto; + + $chat["id"] = randomId(); + $chat["inviteLink"] = randomId(); + + $this->chatsService->createChat($chat["id"], $chatDto["name"], $chatDto["isPrivate"], $chat["inviteLink"]); + + $this->chatsService->addParticipantToChat($req["user"]["id"], $chat["id"], 2); + + $response = [ + "message" => $messages["chat_created"], + "chat" => $chat + ]; + + jsonResponse($response, 201)['end'](); + } + catch (PDOException $ex) + { + httpException($messages["failed_to_create_chat"])['end'](); + } + } + + function deleteChat($req) + { + global $messages; + + try { + $chatId = substr($req['resource'], strlen('/api/chats/')); + + $chat = $this->chatsService->findById($chatId); + + if (is_null($chat)) { + httpException($messages["chat_not_found"], 404)['end'](); + } + + $chatParticipant = $this->chatsService->getChatParticipantByUserId($req["user"]["id"], $chatId); + + if (is_null($chatParticipant) || intval($chatParticipant["permission"]) !== 2) + { + httpException($messages["not_enough_permission"], 403)['end'](); + } + + $this->chatsService->deleteChatById($chatId); + + $response = [ + "message" => $messages["chat_deleted"] + ]; + + jsonResponse($response)['end'](); + } + catch (PDOException $ex) + { + httpException($messages["failed_to_delete_chat"])['end'](); + } + } + + function deleteChatParticipant($req) { + global $messages; + + try { + preg_match("/\/api\/chats\/(?'chatId'[a-z0-9]+)\/users\/(?'userId'[a-z0-9]+)/", $req['resource'], $parsedUrl); + + $chat = $this->chatsService->findById($parsedUrl["chatId"]); + + if (is_null($chat)) { + httpException($messages["chat_not_found"], 404)['end'](); + } + + $initiatorParticipant = $this->chatsService->getChatParticipantByUserId($req["user"]["id"], $chat["id"]); + + if (is_null($initiatorParticipant) || intval($initiatorParticipant["permission"]) !== 2) + { + httpException($messages["not_enough_permission"], 403)['end'](); + } + + $chatParticipantToDelete = $this->chatsService->getChatParticipantByUserId($parsedUrl["userId"], $chat["id"]); + + if (is_null($chatParticipantToDelete)) + { + httpException($messages["participant_not_found"], 404)['end'](); + } + + $this->chatsService->deleteChatParticipant($parsedUrl["userId"], $chat["id"]); + + $response = [ + "message" => $messages["participant_deleted"] + ]; + + jsonResponse($response)['end'](); + } + catch (PDOException $ex) + { + httpException($messages["failed_to_delete_chat_participant"])['end'](); + } + } + + function addUserToChat($req, $addUserToChatDto) + { + global $messages; + + try { + preg_match("/\/api\/chats\/(?'chatId'[a-z0-9]+)\/users/", $req['resource'], $parsedUrl); + + $chat = $this->chatsService->findById($parsedUrl["chatId"]); + + if (is_null($chat)) { + httpException($messages["chat_not_found"], 404)['end'](); + } + + $initiatorParticipant = $this->chatsService->getChatParticipantByUserId($req["user"]["id"], $chat["id"]); + + if (is_null($addUserToChatDto["permission"])) + { + $addUserToChatDto["permission"] = 0; + } + + if (is_null($initiatorParticipant) || (intval($initiatorParticipant["permission"]) !== 2 && $addUserToChatDto["permission"] >= 1)) + { + httpException($messages["not_enough_permission"], 403)['end'](); + } + + $userToAdd = $this->usersService->getUserById($addUserToChatDto["userId"]); + + if (is_null($userToAdd)) + { + httpException($messages["user_not_found"], 404)['end'](); + } + + $this->chatsService->addParticipantToChat($addUserToChatDto["userId"], $chat["id"], $addUserToChatDto["permission"]); + + $response = [ + "message" => $messages["participant_added"] + ]; + + jsonResponse($response, 201)['end'](); + } + catch (PDOException $ex) + { + httpException($messages["failed_to_add_chat_participant"])['end'](); + } + } + + function getChatParticipants($req) + { + global $messages; + + try { + preg_match("/\/api\/chats\/(?'chatId'[a-z0-9]+)\/users/", $req['resource'], $parsedUrl); + + $chat = $this->chatsService->findById($parsedUrl["chatId"]); + + if (is_null($chat)) { + httpException($messages["chat_not_found"], 404)['end'](); + } + + $initiatorParticipant = $this->chatsService->getChatParticipantByUserId($req["user"]["id"], $chat["id"]); + + if (is_null($initiatorParticipant) && $chat["isPrivate"]) + { + httpException($messages["not_enough_permission"], 403)['end'](); + } + + $chatParticipants = $this->chatsService->getChatParticipants($chat["id"]); + + $response = [ + "participants" => $chatParticipants + ]; + + jsonResponse($response)['end'](); + } + catch (PDOException $ex) { + httpException($messages["failed_to_get_chat_participants"])['end'](); + } + } + } diff --git a/chats/chats.service.php b/chats/chats.service.php new file mode 100644 index 0000000..2715833 --- /dev/null +++ b/chats/chats.service.php @@ -0,0 +1,140 @@ +conn = $conn; + } + + function getChats(): array + { + $sql = "SELECT * FROM Chats"; + $stmt = $this->conn->prepare($sql); + $stmt->execute(); + + if ($stmt->rowCount() > 0) { + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } else { + return []; + } + } + + function createChat($id, $name, $isPrivate, $inviteLink) + { + $sql = "INSERT INTO Chats (id, name, isPrivate, inviteLink) VALUES (:id, :name, :isPrivate, :inviteLink)"; + $stmt = $this->conn->prepare($sql); + $stmt->bindValue(":id", $id); + $stmt->bindValue(":name", $name); + $stmt->bindValue(":isPrivate", $isPrivate); + $stmt->bindValue(":inviteLink", $inviteLink); + $stmt->execute(); + } + + function findById($id) + { + $sql = "SELECT * FROM Chats WHERE id=:id AND isDeleted=FALSE"; + $stmt = $this->conn->prepare($sql); + $stmt->bindValue(":id", $id); + $stmt->execute(); + + if ($stmt->rowCount() > 0) { + return $stmt->fetch(PDO::FETCH_ASSOC); + } else { + return null; + } + } + + function isUserChatParticipant($userId, $chatId): bool + { + $sql = "SELECT * FROM ChatParticipants WHERE userId=:userId AND chatId=:chatId"; + $stmt = $this->conn->prepare($sql); + $stmt->bindValue(":userId", $userId); + $stmt->bindValue(":chatId", $chatId); + $stmt->execute(); + + return $stmt->rowCount() > 0; + } + + function getChatParticipantByUserId($userId, $chatId) + { + $sql = "SELECT * FROM ChatParticipants WHERE userId=:userId AND chatId=:chatId"; + $stmt = $this->conn->prepare($sql); + $stmt->bindValue(":userId", $userId); + $stmt->bindValue(":chatId", $chatId); + $stmt->execute(); + + if ($stmt->rowCount() > 0) { + return $stmt->fetch(PDO::FETCH_ASSOC); + } else { + return null; + } + } + + function createChatRO($chat) + { + $chatRO = $chat; + + unset($chatRO["isPrivate"]); + unset($chatRO["inviteLink"]); + + return $chatRO; + } + + function getUserChats($userId): array + { + $sql = "SELECT C.id AS id, C.image AS image, C.name AS name FROM Chats AS C, ChatParticipants AS CP WHERE CP.userId=:userId AND CP.chatId=C.id AND C.isDeleted=FALSE"; + $stmt = $this->conn->prepare($sql); + $stmt->bindValue(":userId", $userId); + $stmt->execute(); + + if ($stmt->rowCount() > 0) { + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } else { + return []; + } + } + + function addParticipantToChat($userId, $chatId, $permission = 0) + { + $sql = "INSERT INTO ChatParticipants (userId, chatId, permission) VALUES (:userId, :chatId, :permission)"; + $stmt = $this->conn->prepare($sql); + $stmt->bindValue(":userId", $userId); + $stmt->bindValue(":chatId", $chatId); + $stmt->bindValue(":permission", $permission); + $stmt->execute(); + } + + function deleteChatParticipant($userId, $chatId) + { + $sql = "DELETE FROM ChatParticipants WHERE userId=:userId AND chatId=:chatId"; + $stmt = $this->conn->prepare($sql); + $stmt->bindValue(":userId", $userId); + $stmt->bindValue(":chatId", $chatId); + $stmt->execute(); + } + + function deleteChatById($chatId) + { + $sql = "UPDATE Chats SET isDeleted=TRUE WHERE id=:chatId"; + $stmt = $this->conn->prepare($sql); + $stmt->bindValue(":chatId", $chatId); + $stmt->execute(); + } + + function getChatParticipants($chatId) + { + $sql = "SELECT U.id AS id, U.username AS username, U.fullname AS fullname, U.profileImage as profileImage, U.description as description FROM Users AS U, ChatParticipants AS CP WHERE CP.chatId=:chatId AND U.id=CP.userId"; + $stmt = $this->conn->prepare($sql); + $stmt->bindValue(":chatId", $chatId); + $stmt->execute(); + + if ($stmt->rowCount() > 0) { + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } else { + return []; + } + } + } diff --git a/db/db.controller.php b/db/db.controller.php index e470765..29e33d8 100644 --- a/db/db.controller.php +++ b/db/db.controller.php @@ -1,6 +1,8 @@ connectToDb($dbConfig); - $this->drop(['Users']); + // TODO: Don't run this if we are in production mode + $this->dropTables(['ChatParticipants', 'Users', 'Chats']); $this->initialize(); - # Include $usersFixtures from global scope here - global $usersFixtures; - $this->seed('Users', ["id", "username", "password"], $usersFixtures); - } - - public function getConnection() - { - return $this->conn; + # Include fixtures from global scope here + global $usersFixtures, $chatsFixtures, $chatParticipantsFixtures; + $this->seed('Users', $usersFixtures); + $this->seed('Chats', $chatsFixtures); + $this->seed('ChatParticipants', $chatParticipantsFixtures); } private function connectToDb(array $dbConfig) @@ -38,35 +38,11 @@ private function connectToDb(array $dbConfig) } } - /** - * Seeds data from array in specified table and columns - * @param {string} $table Table name where we want to seed data - * @param {string[]} $columns Array of fields names we want to insert - * @param {object[]} $fixtures Array of fixtures with fields from $fields in same order - */ - public function seed(string $table, array $columns, array $fixtures) - { - try { - $columnsString = implode(",", $columns); - - foreach ($fixtures as $fixture) { - $fixtureKeys = array_keys($fixture); - $fixtureKeysString = ":" . implode(", :", $fixtureKeys); - - $sql = "INSERT INTO $table ($columnsString) VALUES ($fixtureKeysString)"; - - $this->conn->prepare($sql)->execute($fixture); - } - } catch (Exception $ex) { - httpException("Failed to seed db, table '$table'", 500)['end'](); - } - } - /** * Drop specified table in DB * @param {string[]} $tables Array of tables names */ - private function drop($tables) + private function dropTables($tables) { try { foreach ($tables as $table) { @@ -74,7 +50,7 @@ private function drop($tables) $this->conn->exec($sql); } } catch (Exception $ex) { - httpException("Failed to drop db", 500); + httpException("Failed to drop db tables", 500); } } @@ -87,17 +63,65 @@ private function initialize() # Create Users table $users = <<conn->exec($users); } catch (Exception $ex) { httpException("Failed to initialize db", 500); } } + + /** + * Seeds data from array in specified table and columns + * @param {string} $table Table name where we want to seed data + * @param {string[]} $columns Array of fields names we want to insert + * @param {object[]} $fixtures Array of fixtures with fields from $fields in same order + */ + public function seed(string $table, array $fixtures) + { + try { + foreach ($fixtures as $fixture) { + $fixtureKeys = array_keys($fixture); + $columnsString = implode(",", $fixtureKeys); + $fixtureKeysString = ":" . implode(", :", $fixtureKeys); + + $sql = "INSERT INTO $table ($columnsString) VALUES ($fixtureKeysString)"; + + $this->conn->prepare($sql)->execute($fixture); + } + } catch (Exception $ex) { + httpException("Failed to seed db, table '$table'", 500)['end'](); + } + } + + public function getConnection() + { + return $this->conn; + } } diff --git a/docker-compose.yml b/docker-compose.yml index 986c006..c4dd25b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,8 @@ services: environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: web_chat + cap_add: + - SYS_NICE adminer: container_name: web_chat_adminer diff --git a/fixtures/chatParticipants.php b/fixtures/chatParticipants.php new file mode 100644 index 0000000..1f28bc3 --- /dev/null +++ b/fixtures/chatParticipants.php @@ -0,0 +1,44 @@ + $MaxAndIlyaChat["id"], + "userId" => $MaxDmitriev["id"], + "permission" => 2 + ]; + + $IlyaInChatWithMax = [ + "chatId" => $MaxAndIlyaChat["id"], + "userId" => $IlyaMehof["id"], + "permission" => 2 + ]; + + $MaxInDeletedChatWithMatvey = [ + "chatId" => $DeletedChatWithMaxAndMatvey["id"], + "userId" => $MaxDmitriev["id"], + "permission" => 2 + ]; + + $MatveyInDeletedChatWithMax = [ + "chatId" => $DeletedChatWithMaxAndMatvey["id"], + "userId" => $MatveyGorelik["id"], + "permission" => 2 + ]; + + $MaxInGymChat = [ + "chatId" => $GymPartyPublicChat["id"], + "userId" => $MaxDmitriev["id"], + "permission" => 2 + ]; + + $IlyaInGymChat = [ + "chatId" => $GymPartyPublicChat["id"], + "userId" => $IlyaMehof["id"], + "permission" => 0 + ]; + + $chatParticipantsFixtures = [$MaxInChatWithIlya, $IlyaInChatWithMax, $MaxInDeletedChatWithMatvey, $MatveyInDeletedChatWithMax, $IlyaInGymChat, $MaxInGymChat]; \ No newline at end of file diff --git a/fixtures/chats.php b/fixtures/chats.php new file mode 100644 index 0000000..5883167 --- /dev/null +++ b/fixtures/chats.php @@ -0,0 +1,26 @@ + "chat000000000001", + "name" => "First private chat", + "isPrivate" => 1, + "inviteLink" => "invitelink000001" + ]; + + $GymPartyPublicChat = [ + "id" => "chat000000000002", + "name" => "Welcome to the club buddy", + "isPrivate" => 0, + "inviteLink" => "invitelink000002" + ]; + + $DeletedChatWithMaxAndMatvey = [ + "id" => "chat000000000003", + "name" => "No one should see this chat", + "isPrivate" => 1, + "isDeleted" => 1, + "inviteLink" => "invitelink000003" + ]; + + $chatsFixtures = [$MaxAndIlyaChat, $GymPartyPublicChat, $DeletedChatWithMaxAndMatvey]; + \ No newline at end of file diff --git a/fixtures/users.php b/fixtures/users.php index c23fd90..d0dfddf 100644 --- a/fixtures/users.php +++ b/fixtures/users.php @@ -3,14 +3,21 @@ $plainPassword = "qwerty"; $MaxDmitriev = [ - "id" => 1, + "id" => "user000000000001", "username" => "dmitriev", "password" => $plainPassword ]; + $IlyaMehof = [ - "id" => 2, + "id" => "user000000000002", "username" => "mehoff", "password" => $plainPassword ]; - $usersFixtures = [$MaxDmitriev, $IlyaMehof]; + $MatveyGorelik = [ + "id" => "user000000000003", + "username" => "offiza", + "password" => $plainPassword + ]; + + $usersFixtures = [$MaxDmitriev, $IlyaMehof, $MatveyGorelik]; diff --git a/locale/en/messages.php b/locale/en/messages.php index 3bbb0a3..24c9711 100644 --- a/locale/en/messages.php +++ b/locale/en/messages.php @@ -2,10 +2,27 @@ $messages = [ "username_taken" => "Username is already taken", + "user_not_found" => "User not found", + "chat_not_found" => "Chat not found", + "participant_not_found" => "Participant not found", + "user_created" => "User created", + "chat_created" => "Chat created", + "participant_added" => "Participant added", + + "chat_deleted" => "Chat deleted", + "participant_deleted" => "Participant deleted", + "failed_to_create_user" => "Failed to create user", + "failed_to_create_chat" => "Failed to create chat", + "failed_to_delete_chat" => "Failed to delete chat", + "failed_to_delete_chat_participant" => "Failed to delete chat participant", + "failed_to_add_chat_participant" => "Failed to add chat participant", + "failed_to_get_chat_participants" => "Failed to get chat participants", "failed_to_sign_in" => "Failed to sign in", "failed_to_sign_up" => "Failed to sign up", + "not_authenticated" => "Not authenticated", + "not_enough_permission" => "You do not have enough permission to do this" ]; \ No newline at end of file diff --git a/tests/chats-e2e.php b/tests/chats-e2e.php new file mode 100644 index 0000000..aec4d4c --- /dev/null +++ b/tests/chats-e2e.php @@ -0,0 +1,409 @@ +

Chats e2e

+
+ ["Authorization: Bearer $jwt"]]);
+      
+      $json = json_decode($response['data']);
+      $chatData = $json->data->chat;
+      
+      assertStrict($response['info']['http_code'], 200);
+      assertStrict($chatData->id, $MaxAndIlyaChat['id']);
+      assertStrict($chatData->name, $MaxAndIlyaChat['name']);
+      assertStrict(isset($chatData->isPrivate), false);
+      assertStrict(isset($chatData->inviteLink), false);
+    });
+  
+    it("should return error when trying to get private chat by id for NOT a chat participant", function () {
+      global $testsConfig, $messages, $MatveyGorelik, $MaxAndIlyaChat;
+    
+      $jwt = signJwtForUser($MatveyGorelik);
+    
+      $response = request("GET", $testsConfig["host"] . "/api/chats/" . $MaxAndIlyaChat["id"], ["headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+    
+      assertStrict($response['info']['http_code'], 403);
+      assertStrict($json->data->error, $messages["not_enough_permission"]);
+    });
+  
+    it("should get public chat by id", function () {
+      global $testsConfig, $MaxDmitriev, $GymPartyPublicChat;
+      
+      $jwt = signJwtForUser($MaxDmitriev);
+    
+      $response = request("GET", $testsConfig["host"] . "/api/chats/" . $GymPartyPublicChat["id"], ["headers" => ["Authorization: Bearer $jwt"]]);
+    
+      $json = json_decode($response['data']);
+      $chatData = $json->data->chat;
+    
+      assertStrict($response['info']['http_code'], 200);
+      assertStrict($chatData->id, $GymPartyPublicChat['id']);
+      assertStrict($chatData->name, $GymPartyPublicChat['name']);
+      assertStrict(isset($chatData->isPrivate), false);
+      assertStrict(isset($chatData->inviteLink), false);
+    });
+  
+    it("should return not authorized when trying to get chat by id without authorization", function () {
+      global $testsConfig, $messages, $MaxAndIlyaChat;
+    
+      $response = request("GET", $testsConfig["host"] . "/api/chats/" . $MaxAndIlyaChat['id']);
+      $json = json_decode($response['data']);
+    
+      assertStrict($response['info']['http_code'], 401);
+      assertStrict($json->data->error, $messages["not_authenticated"]);
+    });
+  
+    it("should return chat not found for deleted chat", function () {
+      global $testsConfig, $MaxDmitriev, $messages, $DeletedChatWithMaxAndMatvey;
+    
+      $jwt = signJwtForUser($MaxDmitriev);
+    
+      $response = request("GET", $testsConfig["host"] . "/api/chats/" . $DeletedChatWithMaxAndMatvey["id"], ["headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+    
+      assertStrict($response['info']['http_code'], 404);
+      assertStrict($json->data->error, $messages["chat_not_found"]);
+    });
+    
+    it("should return chat not found for random chat id", function () {
+      global $testsConfig, $MaxDmitriev, $messages;
+      
+      $jwt = signJwtForUser($MaxDmitriev);
+      
+      $response = request("GET", $testsConfig["host"] . "/api/chats/random_id", ["headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+      
+      assertStrict($response['info']['http_code'], 404);
+      assertStrict($json->data->error, $messages["chat_not_found"]);
+    });
+  });
+  
+  describe("[POST] /api/chats", function () {
+    it("should return not authorized error when trying to create chat without authorization", function () {
+      global $testsConfig, $messages;
+    
+      $body = [
+        "name" => "My private chat",
+        "isPrivate" => true
+      ];
+    
+      $response = request("POST", $testsConfig["host"] . "/api/chats", ["json" => $body]);
+      $json = json_decode($response['data']);
+    
+      assertStrict($response['info']['http_code'], 401);
+      assertStrict($json->data->error, $messages["not_authenticated"]);
+    });
+    
+    it("should be able to create new chat", function () {
+      global $testsConfig, $MaxDmitriev;
+  
+      $jwt = signJwtForUser($MaxDmitriev);
+      
+      $body = [
+        "name" => "My private chat",
+        "isPrivate" => true
+      ];
+      
+      $response = request("POST", $testsConfig["host"] . "/api/chats", ["json" => $body, "headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+      $createdChat = $json->data->chat;
+      
+      assertStrict($response['info']['http_code'], 201);
+      assertStrict($createdChat->name, $body["name"]);
+    });
+  });
+  
+  describe("[DELETE] /api/chats/:chatId", function () {
+    it("should delete chat by chat admin", function () {
+      global $testsConfig, $messages, $MaxDmitriev, $MaxAndIlyaChat;
+      
+      $jwt = signJwtForUser($MaxDmitriev);
+      
+      $response = request("DELETE", $testsConfig["host"] . "/api/chats/" . $MaxAndIlyaChat["id"], ["headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+      
+      assertStrict($response['info']['http_code'], 200);
+      assertStrict($json->data->message, $messages["chat_deleted"]);
+    });
+  
+    it("should return not enough permission when trying to delete chat by a member", function () {
+      global $testsConfig, $messages, $MatveyGorelik, $GymPartyPublicChat;
+    
+      $jwt = signJwtForUser($MatveyGorelik);
+    
+      $response = request("DELETE", $testsConfig["host"] . "/api/chats/" . $GymPartyPublicChat["id"], ["headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+    
+      assertStrict($response['info']['http_code'], 403);
+      assertStrict($json->data->error, $messages["not_enough_permission"]);
+    });
+  
+    it("should return chat not found error for random chat id", function () {
+      global $testsConfig, $messages, $MaxDmitriev;
+    
+      $jwt = signJwtForUser($MaxDmitriev);
+    
+      $response = request("DELETE", $testsConfig["host"] . "/api/chats/random_chat_id", ["headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+    
+      assertStrict($response['info']['http_code'], 404);
+      assertStrict($json->data->error, $messages["chat_not_found"]);
+    });
+  });
+  
+  describe("[DELETE] /api/chats/:chatId/users/:userId", function () {
+    it("should delete chat participant by chat admin", function () {
+      global $testsConfig, $messages, $MaxDmitriev, $MaxAndIlyaChat, $IlyaMehof;
+      
+      $jwt = signJwtForUser($MaxDmitriev);
+      
+      $response = request("DELETE", $testsConfig["host"] . "/api/chats/" . $MaxAndIlyaChat["id"] . "/users/" . $IlyaMehof["id"], ["headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+      
+      assertStrict($response['info']['http_code'], 200);
+      assertStrict($json->data->message, $messages["participant_deleted"]);
+    });
+    
+    it("should return not enough permission when trying to delete chat by a member", function () {
+      global $testsConfig, $messages, $MatveyGorelik, $GymPartyPublicChat, $IlyaMehof;
+      
+      $jwt = signJwtForUser($MatveyGorelik);
+      
+      $response = request("DELETE", $testsConfig["host"] . "/api/chats/" . $GymPartyPublicChat["id"] . "/users/" . $IlyaMehof["id"], ["headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+      
+      assertStrict($response['info']['http_code'], 403);
+      assertStrict($json->data->error, $messages["not_enough_permission"]);
+    });
+    
+    it("should return chat not found error for random chat id", function () {
+      global $testsConfig, $messages, $MaxDmitriev;
+      
+      $jwt = signJwtForUser($MaxDmitriev);
+      
+      $response = request("DELETE", $testsConfig["host"] . "/api/chats/randomchatid/users/" . $MaxDmitriev["id"], ["headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+      
+      assertStrict($response['info']['http_code'], 404);
+      assertStrict($json->data->error, $messages["chat_not_found"]);
+    });
+  
+    it("should return participant not found error for random user id", function () {
+      global $testsConfig, $messages, $MaxAndIlyaChat, $MaxDmitriev;
+    
+      $jwt = signJwtForUser($MaxDmitriev);
+    
+      $response = request("DELETE", $testsConfig["host"] . "/api/chats/" . $MaxAndIlyaChat["id"] . "/users/randomid", ["headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+    
+      assertStrict($response['info']['http_code'], 404);
+      assertStrict($json->data->error, $messages["participant_not_found"]);
+    });
+  });
+  
+  describe("[GET] /api/chats/:chatId/users", function () {
+    it("should get private chat participants for chat participant", function () {
+      global $testsConfig, $MaxDmitriev, $IlyaMehof, $MaxAndIlyaChat;
+      
+      $jwt = signJwtForUser($MaxDmitriev);
+      
+      $response = request("GET", $testsConfig["host"] . "/api/chats/" . $MaxAndIlyaChat["id"] . "/users", ["headers" => ["Authorization: Bearer $jwt"]]);
+      
+      $json = json_decode($response['data']);
+      $chatParticipants = $json->data->participants;
+      
+      assertStrict($response['info']['http_code'], 200);
+      assertStrict($chatParticipants[0]->username, $MaxDmitriev["username"]);
+      assertStrict($chatParticipants[1]->username, $IlyaMehof["username"]);
+    });
+    
+    it("should return error when trying to get private chat for NOT a chat participant", function () {
+      global $testsConfig, $messages, $MatveyGorelik, $MaxAndIlyaChat;
+      
+      $jwt = signJwtForUser($MatveyGorelik);
+      
+      $response = request("GET", $testsConfig["host"] . "/api/chats/" . $MaxAndIlyaChat["id"] . "/users", ["headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+      
+      assertStrict($response['info']['http_code'], 403);
+      assertStrict($json->data->error, $messages["not_enough_permission"]);
+    });
+    
+    it("should get public chat participants", function () {
+      global $testsConfig, $MaxDmitriev, $IlyaMehof, $GymPartyPublicChat;
+      
+      $jwt = signJwtForUser($MaxDmitriev);
+      
+      $response = request("GET", $testsConfig["host"] . "/api/chats/" . $GymPartyPublicChat["id"] . "/users", ["headers" => ["Authorization: Bearer $jwt"]]);
+      
+      $json = json_decode($response['data']);
+      $chatParticipants = $json->data->participants;
+      
+      assertStrict($response['info']['http_code'], 200);
+      assertStrict($chatParticipants[0]->username, $MaxDmitriev["username"]);
+      assertStrict($chatParticipants[1]->username, $IlyaMehof["username"]);
+    });
+  
+    it("should get public chat participants for not a chat participant", function () {
+      global $testsConfig, $MatveyGorelik, $MaxDmitriev, $IlyaMehof, $GymPartyPublicChat;
+    
+      $jwt = signJwtForUser($MatveyGorelik);
+    
+      $response = request("GET", $testsConfig["host"] . "/api/chats/" . $GymPartyPublicChat["id"] . "/users", ["headers" => ["Authorization: Bearer $jwt"]]);
+    
+      $json = json_decode($response['data']);
+      $chatParticipants = $json->data->participants;
+    
+      assertStrict($response['info']['http_code'], 200);
+      assertStrict($chatParticipants[0]->username, $MaxDmitriev["username"]);
+      assertStrict($chatParticipants[1]->username, $IlyaMehof["username"]);
+    });
+    
+    it("should return not authorized when trying to get chat participants without authorization", function () {
+      global $testsConfig, $messages, $MaxAndIlyaChat;
+      
+      $response = request("GET", $testsConfig["host"] . "/api/chats/" . $MaxAndIlyaChat['id'] . "/users");
+      $json = json_decode($response['data']);
+      
+      assertStrict($response['info']['http_code'], 401);
+      assertStrict($json->data->error, $messages["not_authenticated"]);
+    });
+    
+    it("should return chat not found for deleted chat", function () {
+      global $testsConfig, $MaxDmitriev, $messages, $DeletedChatWithMaxAndMatvey;
+      
+      $jwt = signJwtForUser($MaxDmitriev);
+      
+      $response = request("GET", $testsConfig["host"] . "/api/chats/" . $DeletedChatWithMaxAndMatvey["id"] . "/users", ["headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+      
+      assertStrict($response['info']['http_code'], 404);
+      assertStrict($json->data->error, $messages["chat_not_found"]);
+    });
+    
+    it("should return chat not found for random chat id", function () {
+      global $testsConfig, $MaxDmitriev, $messages;
+      
+      $jwt = signJwtForUser($MaxDmitriev);
+      
+      $response = request("GET", $testsConfig["host"] . "/api/chats/random_id/users", ["headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+      
+      assertStrict($response['info']['http_code'], 404);
+      assertStrict($json->data->error, $messages["chat_not_found"]);
+    });
+  });
+  
+  describe("[POST] /api/chats/:chatId/users", function () {
+    it("should return not authorized error when trying to add participant to chat without authorization", function () {
+      global $testsConfig, $messages, $MaxDmitriev, $GymPartyPublicChat;
+      
+      $body = [
+        "userId" => $MaxDmitriev["id"]
+      ];
+      
+      $response = request("POST", $testsConfig["host"] . "/api/chats/" . $GymPartyPublicChat["id"] . "/users", ["json" => $body]);
+      $json = json_decode($response['data']);
+      
+      assertStrict($response['info']['http_code'], 401);
+      assertStrict($json->data->error, $messages["not_authenticated"]);
+    });
+    
+    it("chat admin should be able to add chat participant", function () {
+      global $testsConfig, $messages, $MaxDmitriev, $MatveyGorelik, $GymPartyPublicChat;
+      
+      $jwt = signJwtForUser($MaxDmitriev);
+      
+      $body = [
+        "userId" => $MatveyGorelik["id"]
+      ];
+      
+      $response = request("POST", $testsConfig["host"] . "/api/chats/" . $GymPartyPublicChat["id"] . "/users", ["json" => $body, "headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+      
+      assertStrict($response['info']['http_code'], 201);
+      assertStrict($json->data->message, $messages["participant_added"]);
+    });
+  
+    it("chat member should be able to add chat participant", function () {
+      global $testsConfig, $messages, $IlyaMehof, $MatveyGorelik, $GymPartyPublicChat;
+    
+      $jwt = signJwtForUser($IlyaMehof);
+    
+      $body = [
+        "userId" => $MatveyGorelik["id"]
+      ];
+    
+      $response = request("POST", $testsConfig["host"] . "/api/chats/" . $GymPartyPublicChat["id"] . "/users", ["json" => $body, "headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+    
+      assertStrict($response['info']['http_code'], 201);
+      assertStrict($json->data->message, $messages["participant_added"]);
+    });
+  
+    it("chat member should not be able to add chat participant with higher privilege", function () {
+      global $testsConfig, $messages, $IlyaMehof, $MatveyGorelik, $GymPartyPublicChat;
+    
+      $jwt = signJwtForUser($IlyaMehof);
+    
+      $body = [
+        "userId" => $MatveyGorelik["id"],
+        "permission" => 2
+      ];
+    
+      $response = request("POST", $testsConfig["host"] . "/api/chats/" . $GymPartyPublicChat["id"] . "/users", ["json" => $body, "headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+    
+      assertStrict($response['info']['http_code'], 403);
+      assertStrict($json->data->error, $messages["not_enough_permission"]);
+    });
+  
+    it("should return error when not chat participant trying to add user to chat", function () {
+      global $testsConfig, $messages, $MaxDmitriev, $MatveyGorelik, $GymPartyPublicChat;
+    
+      $jwt = signJwtForUser($MatveyGorelik);
+    
+      $body = [
+        "userId" => $MaxDmitriev["id"]
+      ];
+    
+      $response = request("POST", $testsConfig["host"] . "/api/chats/" . $GymPartyPublicChat["id"] . "/users", ["json" => $body, "headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+    
+      assertStrict($response['info']['http_code'], 403);
+      assertStrict($json->data->error, $messages["not_enough_permission"]);
+    });
+  
+    it("should return error when trying to add not existing user", function () {
+      global $testsConfig, $messages, $MaxDmitriev, $GymPartyPublicChat;
+    
+      $jwt = signJwtForUser($MaxDmitriev);
+    
+      $body = [
+        "userId" => "randomid"
+      ];
+    
+      $response = request("POST", $testsConfig["host"] . "/api/chats/" . $GymPartyPublicChat["id"] . "/users", ["json" => $body, "headers" => ["Authorization: Bearer $jwt"]]);
+      $json = json_decode($response['data']);
+    
+      assertStrict($response['info']['http_code'], 404);
+      assertStrict($json->data->error, $messages["user_not_found"]);
+    });
+  });
+  
+?>
+
\ No newline at end of file diff --git a/tests/lib/index.php b/tests/lib/index.php index e091aec..fad3cc1 100644 --- a/tests/lib/index.php +++ b/tests/lib/index.php @@ -80,21 +80,24 @@ function request($method, $url, array $options = null): array curl_setopt($req, CURLOPT_RETURNTRANSFER, true); curl_setopt($req, CURLOPT_FRESH_CONNECT, true); + $headers = []; + if ($options["json"]) { $body = json_encode($options["json"], JSON_UNESCAPED_UNICODE); curl_setopt($req, CURLOPT_POSTFIELDS, $body); - curl_setopt($req, CURLOPT_HTTPHEADER, [ - 'Content-Type: application/json', - 'Content-Length: ' . strlen($body) - ] - ); + $headers[] = 'Content-Type: application/json'; + $headers[] = 'Content-Length: ' . strlen($body); } if ($options["headers"]) { - curl_setopt($req, CURLOPT_HTTPHEADER, $options["headers"]); + foreach ($options["headers"] as $header) { + $headers[] = $header; + } } - + + curl_setopt($req, CURLOPT_HTTPHEADER, $headers); + $response = curl_exec($req); $responseInfo = curl_getinfo($req); diff --git a/tests/users-e2e.php b/tests/users-e2e.php index 86432dd..bbd700d 100644 --- a/tests/users-e2e.php +++ b/tests/users-e2e.php @@ -9,18 +9,6 @@ @include_once __DIR__ . "/../vendor/autoload.php"; @include_once __DIR__ . "/../utils/jwt.php"; - describe("[GET] /api/users", function () { - it("should get users list", function () { - global $testsConfig, $usersFixtures; - - $response = request("GET", $testsConfig["host"] . "/api/users"); - $json = json_decode($response['data']); - - assertStrict($response['info']['http_code'], 200); - assertStrict(count($json->data->users), count($usersFixtures)); - }); - }); - describe("[GET] /api/users/:userId", function () { it("should get user by id", function () { global $testsConfig, $MaxDmitriev; @@ -31,7 +19,7 @@ $userData = $json->data->user; assertStrict($response['info']['http_code'], 200); - assertStrict(intval($userData->id), $MaxDmitriev["id"]); + assertStrict($userData->id, $MaxDmitriev["id"]); assertStrict($userData->username, $MaxDmitriev["username"]); assertStrict(isset($userData->password), false); }); @@ -58,7 +46,7 @@ $userData = $json->data->user; assertStrict($response['info']['http_code'], 200); - assertStrict(intval($userData->id), $MaxDmitriev["id"]); + assertStrict($userData->id, $MaxDmitriev["id"]); assertStrict($userData->username, $MaxDmitriev["username"]); assertStrict(isset($userData->password), false); }); @@ -83,6 +71,21 @@ assertStrict($json->data->error, $messages["not_authenticated"]); }); }); + + describe("[GET] /api/users/me/chats", function () { + it("should get current user chats", function () { + global $testsConfig, $MaxDmitriev; + + $jwt = signJwtForUser($MaxDmitriev); + + $response = request("GET", $testsConfig["host"] . "/api/users/me/chats", ["headers" => ["Authorization: Bearer $jwt"]]); + $json = json_decode($response['data']); + $chats = $json->data->chats; + + assertStrict($response['info']['http_code'], 200); + assertStrict(count($chats), 2); + }); + }); ?> diff --git a/users/users.controller.php b/users/users.controller.php index c869466..378836a 100644 --- a/users/users.controller.php +++ b/users/users.controller.php @@ -3,15 +3,18 @@ @include_once __DIR__ . "/../utils/httpException.php"; @include_once __DIR__ . "/../utils/jsonResponse.php"; @include_once __DIR__ . "/users.service.php"; + @include_once __DIR__ . "../chats/chats.service.php"; @include_once __DIR__ . "/../locale/en/messages.php"; class UsersController { private $usersService; + private $chatsService; function __construct($conn) { $this->usersService = new UsersService($conn); + $this->chatsService = new ChatsService($conn); } function getUsers() @@ -30,7 +33,7 @@ function getUserById($req) global $messages; # Parse user id from url - $userId = intval(substr($req['resource'], strlen('/api/users/'))); + $userId = substr($req['resource'], strlen('/api/users/')); $user = $this->usersService->getUserById($userId); @@ -45,25 +48,18 @@ function getUserById($req) jsonResponse($response)['end'](); } - function createUser($userDto) + function getMe($req) { - $result = $this->usersService->createUser($userDto); - - if (!$result) { - httpException("Failed to create user")['end'](); - } - - $response = [ - "message" => "User created", - "user" => $result - ]; + $response = ["user" => $this->usersService->createUserRO($req["user"])]; - jsonResponse($response)['end'](); + jsonResponse($response); } - function getMe($req) + function getUserChats($req) { - $response = ["user" => $this->usersService->createUserRO($req["user"])]; + $chats = $this->chatsService->getUserChats($req["user"]["id"]); + + $response = ["chats" => $chats]; jsonResponse($response); } diff --git a/utils/randomId.php b/utils/randomId.php new file mode 100644 index 0000000..1415bca --- /dev/null +++ b/utils/randomId.php @@ -0,0 +1,7 @@ +contentType == 'application/json') { + if ($this->contentType == 'application/json') + { $body = file_get_contents("php://input"); $data = json_decode($body, true); - if (json_last_error() !== JSON_ERROR_NONE) { + if (json_last_error() !== JSON_ERROR_NONE) + { httpException("Error parsing json")['end'](); } - } else if ($this->contentType == 'application/x-www-form-urlencoded') { - httpException("Form content type not supported yet")['end'](); - } else { - httpException("Unsupported Content-Type $this->contentType")['end'](); + } + else if ($this->contentType == 'application/x-www-form-urlencoded') + { + httpException("Form content type not supported")['end'](); } return ["body" => $body, "data" => $data];