diff --git a/docs/API.md b/docs/API.md index c5c07cb..35ffb1c 100644 --- a/docs/API.md +++ b/docs/API.md @@ -7,6 +7,7 @@ - [Product](#product) - [Order](#order) - [Review](#review) + - [Comment](#comment) - [District](#district) - [Query string parameters](#query-string-parameters) - [References](#references) @@ -77,6 +78,17 @@ Note: | `PUT /api/v1/reviews/[id]` | Update the details of a review with the specified ID. | Yes | | `DELETE /api/v1/reviews/[id]` | Delete a review with the specified ID. | Yes | + +### Comment + +| Endpoint | Description | Protected | +|--------------------------------------|--------------------------------------------------------|-----------| +| `GET /api/v1/comments` | Get the list of all comments. | No | +| `GET /api/v1/comments/[id]` | Get the details of a specific comments by its ID. | No | +| `POST /api/v1/comments` | Create a new comment for a product. | Yes | +| `PUT /api/v1/comments/[id]` | Update the details of a comment with the specified ID. | Yes | +| `DELETE /api/v1/comments/[id]` | Delete a comment with the specified ID. | Yes | + ### District | Endpoint | Description | Protected | diff --git a/resources/schemas/comments/create.json b/resources/schemas/comments/create.json new file mode 100644 index 0000000..314e456 --- /dev/null +++ b/resources/schemas/comments/create.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/comments/create.json", + "title": "Create Comment", + "properties": { + "text": { + "$ref": "https://example.com/common/comment.json#/properties/text" + }, + "parent_comment_id": { + "$ref": "https://example.com/common/comment.json#/properties/parent_comment_id" + }, + "user_id": { + "$ref": "https://example.com/common/comment.json#/properties/user_id" + }, + "review_id": { + "$ref": "https://example.com/common/comment.json#/properties/review_id" + } + }, + "required": [ + "text", + "user_id", + "review_id" + ], + "additionalProperties": false +} diff --git a/resources/schemas/comments/update.json b/resources/schemas/comments/update.json new file mode 100644 index 0000000..8a9b1b5 --- /dev/null +++ b/resources/schemas/comments/update.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/comments/update.json", + "title": "Update Comment", + "properties": { + "text": { + "$ref": "https://example.com/common/comment.json#/properties/text" + }, + "parent_comment_id": { + "$ref": "https://example.com/common/comment.json#/properties/parent_comment_id" + }, + "user_id": { + "$ref": "https://example.com/common/comment.json#/properties/user_id" + }, + "review_id": { + "$ref": "https://example.com/common/comment.json#/properties/review_id" + } + }, + "additionalProperties": false +} diff --git a/resources/schemas/common/comment.json b/resources/schemas/common/comment.json new file mode 100644 index 0000000..b396a0f --- /dev/null +++ b/resources/schemas/common/comment.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/common/comment.json", + "title": "Comment", + "description": "A comment object", + "type": "object", + "properties": { + "comment_id": { + "type": "integer", + "description": "Unique identifier for the comment" + }, + "text": { + "type": "string", + "description": "The text content of the comment", + "minLength": 1, + "maxLength": 2000 + }, + "created_date": { + "type": "string", + "format": "date-time", + "description": "The date and time when the comment was created" + }, + "parent_comment_id": { + "type": ["integer", "null"], + "description": "The ID of the parent comment, if any" + }, + "user_id": { + "type": "integer", + "description": "The ID of the user who made the comment" + }, + "review_id": { + "type": "integer", + "description": "The ID of the review under which the comment is found" + } + }, + "additionalProperties": false +} diff --git a/src/controllers/api/Comments.php b/src/controllers/api/Comments.php new file mode 100644 index 0000000..619aea4 --- /dev/null +++ b/src/controllers/api/Comments.php @@ -0,0 +1,171 @@ + [ + '/comments' => 'getAllComments', + '/comments/{id}' => 'getCommentById', + ], + 'POST' => [ + '/comments' => 'createComment', + ], + 'PUT' => [ + '/comments/{id}' => 'updateComment', + ], + 'DELETE' => [ + '/comments/{id}' => 'deleteComment', + ] + ]; + + /** + * Get the list of all comments. + */ + public function getAllComments(): void + { + $allComments = Comment::getAll(); + + $result = []; + foreach ($allComments as $comment) { + $result[] = $comment->toArray(); + } + + echo json_encode($result); + } + + /** + * Get the details of a specific comment by its ID. + */ + public function getCommentById(): void + { + $commentId = (int)Utility::splitURL()[3]; + + $comment = Comment::getByID($commentId); + + if ($comment === null) { + http_response_code(404); + echo json_encode(['error' => 'Comment not found']); + return; + } + + echo json_encode($comment->toArray()); + } + + /** + * Create a new comment. + */ + public function createComment(): void + { + $data = (object)json_decode(file_get_contents("php://input"), true); + + // Validate input data against create.json schema + $result = Utility::validateAgainstSchema($data, "comments/create.json"); + + if (!($result->isValid())) { + $errors = (new ErrorFormatter())->format($result->error()); + $response = [ + 'error' => $errors + ]; + http_response_code(400); + echo json_encode($response); + return; + } + + // Create a new Comment object + $newComment = new Comment( + user_id: $data->user_id, + review_id: $data->review_id, + parent_comment_id: $data->parent_comment_id ?? null, + text: $data->text + ); + + // Save the new Comment to the database + if ($newComment->save()) { + // Comment created successfully, return 201 Created + http_response_code(201); + echo json_encode(['message' => 'Comment created successfully', 'comment_id' => $newComment->getCommentID()] + ); + } else { + // Failed to create comment, return 500 Internal Server Error + http_response_code(500); + echo json_encode(['error' => 'Failed to create comment']); + } + } + + /** + * Update the details of a comment with the specified ID. + */ + public function updateComment(): void + { + $commentId = (int)Utility::splitURL()[3]; + + $comment = Comment::getByID($commentId); + + if ($comment === null) { + http_response_code(404); + echo json_encode(['error' => 'Comment not found']); + return; + } + + $data = (object)json_decode(file_get_contents("php://input"), true); + + // Validate input data against update.json schema + $result = Utility::validateAgainstSchema($data, "comments/update.json"); + + if (!($result->isValid())) { + $errors = (new ErrorFormatter())->format($result->error()); + $response = [ + 'error' => $errors + ]; + http_response_code(400); + echo json_encode($response); + return; + } + + // Update comment in the database + $success = $comment->updateComment((array)$data); + + if ($success) { + http_response_code(200); // OK + echo json_encode(['message' => 'Comment updated successfully']); + } else { + http_response_code(500); // Internal Server Error + echo json_encode(['error' => 'Failed to update Comment']); + } + } + + /** + * Delete a comment with the specified ID. + */ + public function deleteComment(): void + { + $commentId = (int)Utility::splitURL()[3]; + + $comment = Comment::getByID($commentId); + + if ($comment === null) { + http_response_code(404); + echo json_encode(['error' => 'Comment not found']); + return; + } + + if ($comment->deleteComment()) { + http_response_code(204); // No Content + } else { + http_response_code(500); // Internal Server Error + echo json_encode(['error' => 'Failed to delete comment']); + } + + } +} diff --git a/src/models/Comment.php b/src/models/Comment.php index 09c23c0..fa3bb65 100644 --- a/src/models/Comment.php +++ b/src/models/Comment.php @@ -207,4 +207,42 @@ public function setCreatedDate(DateTime $created_date): void { $this->created_date = $created_date; } + + public function deleteComment(): bool + { + return $this->delete($this->comment_id, $this->table, 'comment_id'); + } + + /** + * Retrieve all comments. + * + * @return array An array of Comment objects. + */ + public static function getAll(): array + { + $query = "SELECT * FROM comment"; + $result = Comment::query($query); + + $comments = []; + foreach ($result as $row) { + $comments[] = new Comment( + user_id: $row->user_id, + review_id: $row->review_id, + comment_id: $row->comment_id, + parent_comment_id: $row->parent_comment_id, + text: $row->text, + created_date: Utility::stringToDate($row->created_date) + ); + } + + return $comments; + } + + public function updateComment(array $newCommentData): bool + { + // remove comment_id (if present) from user data + unset($newCommentData['comment_id']); + + return $this->update($newCommentData, ['comment_id' => $this->comment_id], $this->table); + } } \ No newline at end of file diff --git a/tests/api/CommentsTest.php b/tests/api/CommentsTest.php new file mode 100644 index 0000000..df271eb --- /dev/null +++ b/tests/api/CommentsTest.php @@ -0,0 +1,170 @@ +dummy_client = self::createClient(); + $this->dummy_product = self::createProduct(); + $this->dummy_review = self::createReview($this->dummy_product, $this->dummy_client); + $this->dummy_comment = new Comment( + user_id: $this->dummy_client->getUserID(), + review_id: $this->dummy_review->getReviewID(), + text: self::$faker->sentence() + ); + $this->dummy_comment->save(); + } + + public function tearDown(): void + { + self::resetDatabase(); + } + + /** + * @throws GuzzleException + */ + public function testGetAllComments() + { + $response = self::$guzzle->get('comments'); + $this->assertEquals(200, $response->getStatusCode()); + + $body = $response->getBody(); + $json = json_decode($body->getContents(), true); + + self::assertIsArray($json); + self::assertCount(1, $json); + + $data = $json[0]; + + $this->assertEquals($this->dummy_comment->getUserID(), $data['user_id']); + $this->assertEquals($this->dummy_comment->getReviewID(), $data['review_id']); + $this->assertEquals($this->dummy_comment->getText(), $data['text']); + + $this->assertArrayHasKey('comment_id', $data); + $this->assertArrayHasKey('created_date', $data); + } + + /** + * @throws GuzzleException + */ + public function testGetCommentById() + { + // test valid comment ID + $response = self::$guzzle->get('comments/' . $this->dummy_comment->getCommentID()); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode($response->getBody()->getContents(), true); + $this->assertIsArray($data); + $this->assertArrayHasKey('comment_id', $data); + $this->assertEquals($this->dummy_comment->getCommentID(), $data['comment_id']); + + // test invalid comment ID + $response = self::$guzzle->get('comments/-1'); + $this->assertEquals(404, $response->getStatusCode()); + } + + /** + * @throws GuzzleException + */ + public function testCreateValidComment() + { + $new_comment = new Comment( + user_id: $this->dummy_client->getUserID(), + review_id: $this->dummy_review->getReviewID(), + text: self::$faker->sentence() + ); + + $data_to_send = $new_comment->toArray(); + unset($data_to_send['comment_id']); + unset($data_to_send['created_date']); + + $response = self::$guzzle->post('comments', [ + 'json' => $data_to_send + ]); + + $this->assertEquals(201, $response->getStatusCode()); + $response_data = json_decode($response->getBody()->getContents(), true); + $this->assertArrayHasKey('comment_id', $response_data); + $this->assertEquals('Comment created successfully', $response_data['message']); + } + + /** + * @throws GuzzleException + */ + public function testUpdateValidComment() + { + $updated_text = 'Updated comment text'; + $data_to_send = [ + 'text' => $updated_text + ]; + + $response = self::$guzzle->put('comments/' . $this->dummy_comment->getCommentID(), [ + 'json' => $data_to_send + ]); + + $this->assertEquals(200, $response->getStatusCode()); + $response_data = json_decode($response->getBody()->getContents(), true); + $this->assertEquals('Comment updated successfully', $response_data['message']); + + $updated_comment = Comment::getByID($this->dummy_comment->getCommentID()); + $this->assertEquals($updated_text, $updated_comment->getText()); + } + + /** + * @throws GuzzleException + */ + public function testDeleteComment() + { + $response = self::$guzzle->delete('comments/' . $this->dummy_comment->getCommentID()); + $this->assertEquals(204, $response->getStatusCode()); + + $deleted_comment = Comment::getByID($this->dummy_comment->getCommentID()); + $this->assertNull($deleted_comment); + } +} \ No newline at end of file