diff --git a/resources/schemas/Review.json b/resources/schemas/Review.json deleted file mode 100644 index ec6e6e3..0000000 --- a/resources/schemas/Review.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Review", - "type": "object", - "properties": { - "review_id": { - "type": "integer" - }, - "rating": { - "type": "integer", - "minimum": 1, - "maximum": 5 - }, - "created_date": { - "type": "string", - "format": "date-time" - }, - "text": { - "type": "string", - "minLength": 2, - "maxLength": 2000 - }, - "client_id": { - "type": "integer" - }, - "product_id": { - "type": "integer" - } - }, - "required": [ - "review_id", - "rating", - "created_date", - "text", - "client_id", - "product_id" - ], - "additionalProperties": false -} diff --git a/resources/schemas/common/review.json b/resources/schemas/common/review.json new file mode 100644 index 0000000..1a04c93 --- /dev/null +++ b/resources/schemas/common/review.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/common/review.json", + "title": "Review", + "description": "A review object", + "type": "object", + "properties": { + "review_id": { + "type": "integer", + "description": "Unique identifier for review" + }, + "rating": { + "type": "integer", + "minimum": 1, + "maximum": 5, + "description": "Rating of the product" + }, + "created_date": { + "type": "string", + "format": "date-time", + "description": "The date and time when the review was created" + }, + "text": { + "type": "string", + "minLength": 2, + "maxLength": 2000, + "description": "The text of the review" + }, + "client_id": { + "type": "integer", + "description": "Identifier for the client who wrote the review" + }, + "product_id": { + "type": "integer", + "description": "Identifier for the product being reviewed" + } + }, + "additionalProperties": false +} diff --git a/resources/schemas/products/update.json b/resources/schemas/products/update.json index f957dfd..5e4cfc5 100644 --- a/resources/schemas/products/update.json +++ b/resources/schemas/products/update.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://example.com/products/update.json", - "title": "Create Product", + "title": "Update Product", "properties": { "name": { "$ref": "https://example.com/common/product.json#/properties/name" diff --git a/resources/schemas/reviews/create.json b/resources/schemas/reviews/create.json new file mode 100644 index 0000000..28a8fa4 --- /dev/null +++ b/resources/schemas/reviews/create.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/reviews/create.json", + "title": "Create Review", + "properties": { + "rating": { + "$ref": "https://example.com/common/review.json#/properties/rating" + }, + "text": { + "$ref": "https://example.com/common/review.json#/properties/text" + }, + "client_id": { + "$ref": "https://example.com/common/review.json#/properties/client_id" + }, + "product_id": { + "$ref": "https://example.com/common/review.json#/properties/product_id" + } + }, + "required": [ + "rating", + "text", + "client_id", + "product_id" + ], + "additionalProperties": false +} diff --git a/resources/schemas/reviews/update.json b/resources/schemas/reviews/update.json new file mode 100644 index 0000000..df904e2 --- /dev/null +++ b/resources/schemas/reviews/update.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/reviews/update.json", + "title": "Update Review", + "properties": { + "rating": { + "$ref": "https://example.com/common/review.json#/properties/rating" + }, + "text": { + "$ref": "https://example.com/common/review.json#/properties/text" + }, + "client_id": { + "$ref": "https://example.com/common/review.json#/properties/client_id" + }, + "product_id": { + "$ref": "https://example.com/common/review.json#/properties/product_id" + } + }, + "additionalProperties": false +} diff --git a/src/controllers/api/Reviews.php b/src/controllers/api/Reviews.php index eeaec32..c1a9ee7 100644 --- a/src/controllers/api/Reviews.php +++ b/src/controllers/api/Reviews.php @@ -4,9 +4,10 @@ namespace Steamy\Controller\API; +use Opis\JsonSchema\{Errors\ErrorFormatter}; +use Exception; use Steamy\Core\Utility; use Steamy\Model\Review; -use \Steamy\Model\Product as ProductModel; class Reviews { @@ -70,54 +71,38 @@ public function getReviewByID(): void public function createReview(): void { // Retrieve POST data - $postData = $_POST; - - // TODO: Implement validation for required fields and data types - // Check if required fields are present - $requiredFields = [ - 'product_id', - 'client_id', - 'text', - 'rating', - ]; - - if (empty($postData)) { + $data = (object)json_decode(file_get_contents("php://input"), true); + + // Validate against JSON schema + $result = Utility::validateAgainstSchema($data, "reviews/create.json"); + + if (!($result->isValid())) { + $errors = (new ErrorFormatter())->format($result->error()); + $response = [ + 'error' => $errors + ]; http_response_code(400); - echo json_encode(['error' => "Missing fields: " . implode(', ', $requiredFields)]); + echo json_encode($response); return; } - foreach ($requiredFields as $field) { - if (empty($postData[$field])) { - // Required field is missing, return 400 Bad Request - http_response_code(400); - echo json_encode(['error' => "Missing required field: $field"]); - return; - } - } // Create a new Review object $newReview = new Review( null, // review_id will be auto-generated - (int)$postData['product_id'], - (int)$postData['client_id'], - $postData['text'], - (int)$postData['rating'] + (int)$data->product_id, + (int)$data->client_id, + $data->text, + (int)$data->rating ); - $errors = $newReview->validate(); - - if (!empty($errors)) { - http_response_code(400); - echo json_encode(['error' => ($errors)]); - return; - } - // Save the new review to the database - if ($newReview->save()) { + try { + $newReview->save(); // Review created successfully, return 201 Created http_response_code(201); - echo json_encode(['message' => 'Review created successfully', 'review_id' => $newReview->getReviewID()]); - } else { + echo json_encode(['message' => 'Review created successfully', 'review_id' => $newReview->getReviewID()] + ); + } catch (Exception $e) { // Failed to create review, return 500 Internal Server Error http_response_code(500); echo json_encode(['error' => 'Failed to create review']); @@ -132,13 +117,18 @@ public function updateReview(): void $reviewId = (int)Utility::splitURL()[3]; // Retrieve PUT request data - $putData = json_decode(file_get_contents("php://input"), true); + $data = (object)json_decode(file_get_contents("php://input"), true); - // Check if PUT data is valid - if (empty($putData)) { - // Invalid JSON data - http_response_code(400); // Bad Request - echo json_encode(['error' => 'Invalid JSON data']); + // Validate against JSON schema + $result = Utility::validateAgainstSchema($data, "reviews/update.json"); + + if (!($result->isValid())) { + $errors = (new ErrorFormatter())->format($result->error()); + $response = [ + 'error' => $errors + ]; + http_response_code(400); + echo json_encode($response); return; } @@ -154,7 +144,7 @@ public function updateReview(): void } // Update review in the database - $success = $review->updateReview($putData); + $success = $review->updateReview((array)$data); if ($success) { // Review updated successfully @@ -186,7 +176,7 @@ public function deleteReview(): void } // Attempt to delete the review - if ($review->deleteReview($reviewId)) { + if ($review->deleteReview()) { // Review successfully deleted http_response_code(204); // No Content } else { diff --git a/src/models/Review.php b/src/models/Review.php index 9779565..331c6b1 100644 --- a/src/models/Review.php +++ b/src/models/Review.php @@ -149,17 +149,9 @@ public function updateReview(array $newReviewData): bool return $this->update($newReviewData, ['review_id' => $this->review_id], $this->table); } - /** - * Deletes a review with the specified ID from the database. - * - * @param int $reviewId The ID of the review to delete. - * @return bool True if the deletion was successful, false otherwise. - */ - public static function deleteReview(int $reviewId): bool + public function deleteReview(): bool { - $query = "DELETE FROM review WHERE review_id = :review_id"; - $params = ['review_id' => $reviewId]; - return self::query($query, $params); + return $this->delete($this->review_id, $this->table, 'review_id'); } public function getReviewID(): int @@ -225,34 +217,34 @@ public function setCreatedDate(DateTime $created_date): void /** * Saves review to database if attributes are valid. review_id and created_date attributes * are automatically set by database and any set values are ignored. + * The review_id of the current object is updated after a successful insertion. * @return bool + * @throws Exception */ public function save(): bool { // If attributes of the object are invalid, exit - if (count($this->validate()) > 0) { - return false; + $validation_errors = $this->validate(); + if (count($validation_errors) > 0) { + throw new Exception(json_encode($validation_errors)); } // Get data to be inserted into the review table $reviewData = $this->toArray(); - // Remove review_id as it is auto-incremented in database + // let database handle review_id and creation date unset($reviewData['review_id']); - - unset($reviewData['created_date']); // let database handle creation date + unset($reviewData['created_date']); // Perform insertion to the review table - try { - $inserted_id = $this->insert($reviewData, 'review'); - if ($inserted_id === null) { - return false; - } - $this->review_id = $inserted_id; - return true; - } catch (Exception) { - return false; + $inserted_id = $this->insert($reviewData, 'review'); + + if ($inserted_id === null) { + throw new Exception("Insertion failed for some unknown reason"); } + + $this->review_id = $inserted_id; + return true; } public function validate(): array diff --git a/tests/api/ReviewsTest.php b/tests/api/ReviewsTest.php new file mode 100644 index 0000000..3acb912 --- /dev/null +++ b/tests/api/ReviewsTest.php @@ -0,0 +1,100 @@ + 'Updated review text', + 'rating' => 4, + ]; + + // Send PUT request to update the review + $response = self::$guzzle->put('reviews/' . $review->getReviewID(), [ + 'json' => $newData, + ]); + + $this->assertEquals(200, $response->getStatusCode()); + + $body = $response->getBody(); + $json = json_decode($body->getContents(), true); + + // Check if the update was successful + $this->assertEquals('Review updated successfully', $json['message']); + + // Fetch the review from the database + $saved_review = Review::getByID($review->getReviewID()); + + // Verify the review was updated + $this->assertEquals($newData['text'], $saved_review->getText()); + $this->assertEquals($newData['rating'], $saved_review->getRating()); + + // ensure that all other attributes did not change + assertEquals($review->getReviewID(), $saved_review->getReviewID()); + assertEquals($review->getProductID(), $saved_review->getProductID()); + assertEquals($review->getClientID(), $saved_review->getClientID()); + + $this->assertEquals( + $review->getCreatedDate()->format('Y-m-d'), + $saved_review->getCreatedDate()->format('Y-m-d') + ); + } + + public function testDeleteReview() + { + self::markTestIncomplete('TODO'); + } +} \ No newline at end of file diff --git a/tests/models/ReviewTest.php b/tests/models/ReviewTest.php index 1a2683d..92559ff 100644 --- a/tests/models/ReviewTest.php +++ b/tests/models/ReviewTest.php @@ -270,11 +270,17 @@ public function testSave(string $text, int $rating, DateTime $created_date, arra created_date: $created_date ); - // Attempt to save the review - $success = $review->save(); - // If expectedErrors array is empty, the review should be saved successfully - $this->assertEquals(empty($expectedErrors), $success); + if (empty($expectedErrors)) { + $success = $review->save(); + self::assertTrue($success); + } else { + try { + $review->save(); + } catch (Exception $e) { + $this->assertEquals($e->getMessage(), json_encode($expectedErrors)); + } + } } public function testGetNestedComments(): void @@ -290,33 +296,33 @@ public function testGetNestedComments(): void // Create root level comment $comment1 = new Comment( - review_id: $review->getReviewID(), user_id: $this->reviewer->getUserID(), + review_id: $review->getReviewID(), text: "This is a root level comment." ); $comment1->save(); // Create nested comments $comment2 = new Comment( - review_id: $review->getReviewID(), user_id: $this->reviewer->getUserID(), - text: "This is a child comment.", - parent_comment_id: $comment1->getCommentID() + review_id: $review->getReviewID(), + parent_comment_id: $comment1->getCommentID(), + text: "This is a child comment." ); $comment2->save(); $comment3 = new Comment( - review_id: $review->getReviewID(), user_id: $this->reviewer->getUserID(), + review_id: $review->getReviewID(), text: "This is another root level comment." ); $comment3->save(); $comment4 = new Comment( - review_id: $review->getReviewID(), user_id: $this->reviewer->getUserID(), - text: "This is a child of a child comment.", - parent_comment_id: $comment2->getCommentID() + review_id: $review->getReviewID(), + parent_comment_id: $comment2->getCommentID(), + text: "This is a child of a child comment." ); $comment4->save();