From 84f461ee6205075bf5bbe530a827b1f6fc2d149a Mon Sep 17 00:00:00 2001 From: divyesh000 Date: Mon, 10 Jun 2024 19:54:25 +0400 Subject: [PATCH 1/7] remove Review.json schema, update product update schema title, and refactor Reviews.php controller to use JSON schema validation --- resources/schemas/Review.json | 39 -------------- resources/schemas/common/review.json | 39 ++++++++++++++ resources/schemas/products/update.json | 2 +- resources/schemas/reviews/create.json | 26 ++++++++++ resources/schemas/reviews/update.json | 20 ++++++++ src/controllers/api/Reviews.php | 70 +++++++++++--------------- 6 files changed, 115 insertions(+), 81 deletions(-) delete mode 100644 resources/schemas/Review.json create mode 100644 resources/schemas/common/review.json create mode 100644 resources/schemas/reviews/create.json create mode 100644 resources/schemas/reviews/update.json 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..ae7e445 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 Steamy\Core\Utility; use Steamy\Model\Review; -use \Steamy\Model\Product as ProductModel; +use Steamy\Model\Product as ProductModel; class Reviews { @@ -70,48 +71,30 @@ 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()) { // Review created successfully, return 201 Created @@ -132,13 +115,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 +142,7 @@ public function updateReview(): void } // Update review in the database - $success = $review->updateReview($putData); + $success = $review->updateReview((array)$data); if ($success) { // Review updated successfully From 133b28098eaccc459c6d4079d1b29836465f4a8a Mon Sep 17 00:00:00 2001 From: divyesh000 Date: Wed, 12 Jun 2024 16:59:39 +0400 Subject: [PATCH 2/7] refactor Review model's deleteReview method to instance method and remove reviewId parameter --- src/controllers/api/Reviews.php | 2 +- src/models/Review.php | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/controllers/api/Reviews.php b/src/controllers/api/Reviews.php index ae7e445..ec29b59 100644 --- a/src/controllers/api/Reviews.php +++ b/src/controllers/api/Reviews.php @@ -174,7 +174,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..114df5a 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 From 020af46d5f2c06bd259c211276360c73541f064a Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:10:05 +0400 Subject: [PATCH 3/7] throw exception in save() method instead of returning false --- src/models/Review.php | 28 ++++++++++++++-------------- tests/models/ReviewTest.php | 14 ++++++++++---- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/models/Review.php b/src/models/Review.php index 114df5a..331c6b1 100644 --- a/src/models/Review.php +++ b/src/models/Review.php @@ -217,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/models/ReviewTest.php b/tests/models/ReviewTest.php index 1a2683d..df417b8 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 From 9411c654298af7b260dbf1405fc8f88981394f08 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:14:06 +0400 Subject: [PATCH 4/7] catch exception when saving review in createReview --- src/controllers/api/Reviews.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/controllers/api/Reviews.php b/src/controllers/api/Reviews.php index ec29b59..c1a9ee7 100644 --- a/src/controllers/api/Reviews.php +++ b/src/controllers/api/Reviews.php @@ -5,9 +5,9 @@ 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 { @@ -96,11 +96,13 @@ public function createReview(): void ); // 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']); From 9cfb64a7b6d999b1034323a493f3dc2d6fb1a0e0 Mon Sep 17 00:00:00 2001 From: divyesh000 Date: Wed, 17 Jul 2024 19:48:28 +0400 Subject: [PATCH 5/7] write the tests in PHP instead of using Postman --- tests/api/ReviewsTest.php | 106 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/api/ReviewsTest.php diff --git a/tests/api/ReviewsTest.php b/tests/api/ReviewsTest.php new file mode 100644 index 0000000..aa44a6e --- /dev/null +++ b/tests/api/ReviewsTest.php @@ -0,0 +1,106 @@ +dummy_review = self::createReview(); + } + + public function tearDown(): void + { + self::resetDatabase(); + } + + /** + * @throws GuzzleException + */ + public function testUpdateReview() + { + // Create new review data + $newData = [ + 'text' => 'Updated review text', + 'rating' => 4, + 'client_id' => $this->dummy_review->getClientID(), + 'product_id' => $this->dummy_review->getProductID(), + ]; + + // Send PUT request to update the review + $response = self::$guzzle->put('reviews/' . $this->dummy_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 updated review + $response = self::$guzzle->get('reviews/' . $this->dummy_review->getReviewID()); + $this->assertEquals(200, $response->getStatusCode()); + + $updatedReview = json_decode($response->getBody()->getContents(), true); + + // Verify the review was updated + $this->assertEquals($newData['text'], $updatedReview['text']); + $this->assertEquals($newData['rating'], $updatedReview['rating']); + } + + // Helper function to create a review for testing + private static function createReview(): Review + { + $review = new Review( + null, + self::$faker->randomDigitNotNull, + self::$faker->randomDigitNotNull, + self::$faker->text(100), + self::$faker->numberBetween(1, 5) + ); + $review->save(); + return $review; + } +} \ No newline at end of file From 69880f8fe77d70d6563e14122dd6e38d8cf8b009 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 28 Jul 2024 13:18:30 +0400 Subject: [PATCH 6/7] sort arguments in comment constructor --- tests/models/ReviewTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/models/ReviewTest.php b/tests/models/ReviewTest.php index df417b8..92559ff 100644 --- a/tests/models/ReviewTest.php +++ b/tests/models/ReviewTest.php @@ -296,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(); From 3dcf91fa297817819a9deb7bdeb1753b47840ceb Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 28 Jul 2024 13:27:21 +0400 Subject: [PATCH 7/7] remove invalid createReview(), add new incomplete tests --- tests/api/ReviewsTest.php | 54 +++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/tests/api/ReviewsTest.php b/tests/api/ReviewsTest.php index aa44a6e..3acb912 100644 --- a/tests/api/ReviewsTest.php +++ b/tests/api/ReviewsTest.php @@ -19,8 +19,6 @@ final class ReviewsTest extends TestCase use TestHelper; use APIHelper; - private Review $dummy_review; - public static function setUpBeforeClass(): void { self::initFaker(); @@ -40,34 +38,32 @@ public function onNotSuccessfulTest(Throwable $t): never parent::onNotSuccessfulTest($t); } - /** - * @throws Exception - */ - public function setUp(): void + public function tearDown(): void { - $this->dummy_review = self::createReview(); + self::resetDatabase(); } - public function tearDown(): void + public function testCreateReview() { - self::resetDatabase(); + self::markTestIncomplete('TODO'); } /** * @throws GuzzleException + * @throws Exception */ public function testUpdateReview() { + $review = self::createReview(self::createProduct(), self::createClient(), 4); + // Create new review data $newData = [ 'text' => 'Updated review text', 'rating' => 4, - 'client_id' => $this->dummy_review->getClientID(), - 'product_id' => $this->dummy_review->getProductID(), ]; // Send PUT request to update the review - $response = self::$guzzle->put('reviews/' . $this->dummy_review->getReviewID(), [ + $response = self::$guzzle->put('reviews/' . $review->getReviewID(), [ 'json' => $newData, ]); @@ -79,28 +75,26 @@ public function testUpdateReview() // Check if the update was successful $this->assertEquals('Review updated successfully', $json['message']); - // Fetch the updated review - $response = self::$guzzle->get('reviews/' . $this->dummy_review->getReviewID()); - $this->assertEquals(200, $response->getStatusCode()); - - $updatedReview = json_decode($response->getBody()->getContents(), true); + // Fetch the review from the database + $saved_review = Review::getByID($review->getReviewID()); // Verify the review was updated - $this->assertEquals($newData['text'], $updatedReview['text']); - $this->assertEquals($newData['rating'], $updatedReview['rating']); + $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') + ); } - // Helper function to create a review for testing - private static function createReview(): Review + public function testDeleteReview() { - $review = new Review( - null, - self::$faker->randomDigitNotNull, - self::$faker->randomDigitNotNull, - self::$faker->text(100), - self::$faker->numberBetween(1, 5) - ); - $review->save(); - return $review; + self::markTestIncomplete('TODO'); } } \ No newline at end of file