diff --git a/src/models/Product.php b/src/models/Product.php index d33ed78..ec5120d 100644 --- a/src/models/Product.php +++ b/src/models/Product.php @@ -276,19 +276,17 @@ public function getAverageRating(): float -- get IDs of all clients who purchased current product SELECT DISTINCT o.client_id FROM `order` o - JOIN order_product op ON o.order_id = op.order_id - WHERE op.product_id = r.product_id + JOIN order_product op + ON o.order_id = op.order_id + AND op.product_id = r.product_id ) EOL; - $params = ['product_id' => $this->product_id]; - - $result = $this->query($query, $params); + $result = $this->query($query, ['product_id' => $this->product_id]); // Extract the average rating from the result array if (!empty($result)) { - $averageRating = $result[0]->average_rating; - return $averageRating !== null ? round((float)$averageRating, 2) : 0; // Round to two decimal places + return (float)$result[0]->average_rating; } return 0; // No reviews, return 0 as the average rating diff --git a/tests/helpers/TestHelper.php b/tests/helpers/TestHelper.php index 3d97a67..661c177 100644 --- a/tests/helpers/TestHelper.php +++ b/tests/helpers/TestHelper.php @@ -223,12 +223,17 @@ public static function createStore(bool $saveToDatabase = true): Store * Create a review and saves it to database. * @param Product $product A valid product already present in database * @param Client $client A valid client already present in database + * @param int|null $rating Rating for review * @param bool $verified Whether to create an order for client for given product. * @return Review * @throws Exception */ - public static function createReview(Product $product, Client $client, bool $verified = false): Review - { + public static function createReview( + Product $product, + Client $client, + int $rating = null, + bool $verified = false + ): Review { if ($verified) { // place an order for client and product @@ -262,7 +267,7 @@ public static function createReview(Product $product, Client $client, bool $veri product_id: $product->getProductID(), client_id: $client->getUserID(), text: self::$faker->sentence(10), - rating: self::$faker->numberBetween(1, 5) + rating: $rating ?? self::$faker->numberBetween(1, 5) ); $success = $review->save(); diff --git a/tests/models/ProductTest.php b/tests/models/ProductTest.php index 26ba568..8ab51ac 100644 --- a/tests/models/ProductTest.php +++ b/tests/models/ProductTest.php @@ -133,53 +133,141 @@ public function testToArray(): void public function testSave(): void { - $this->markTestIncomplete( - 'Use data providers here for at least 3 test cases, ...', + // Prepare test data + $newProductData = [ + 'name' => 'New Product', + 'calories' => 100, + 'img_url' => 'new_product.jpeg', + 'img_alt_text' => 'New Product Image', + 'category' => 'New Category', + 'price' => 10.00, + 'description' => 'New Product Description' + ]; + + // Create a new product object with the test data + $newProduct = new Product( + $newProductData['name'], + $newProductData['calories'], + $newProductData['img_url'], + $newProductData['img_alt_text'], + $newProductData['category'], + $newProductData['price'], + $newProductData['description'] ); + + // Save the product to the database + $result = $newProduct->save(); + + // Assert that the product was saved successfully + $this->assertTrue($result); + + // Fetch the saved product from the database + $savedProduct = Product::getByID($newProduct->getProductID()); + + // Assert that the saved product matches the test data + $this->assertEquals($newProductData['name'], $savedProduct->getName()); + $this->assertEquals($newProductData['calories'], $savedProduct->getCalories()); + $this->assertEquals($newProductData['img_url'], $savedProduct->getImgRelativePath()); + $this->assertEquals($newProductData['img_alt_text'], $savedProduct->getImgAltText()); + $this->assertEquals($newProductData['category'], $savedProduct->getCategory()); + $this->assertEquals($newProductData['price'], $savedProduct->getPrice()); + $this->assertEquals($newProductData['description'], $savedProduct->getDescription()); } + public function testValidate(): void { - // Validate the dummy product - $errors = $this->dummy_product->validate(); - - // Check if there are no validation errors - $this->assertEmpty($errors); + // Prepare test data with invalid values + $invalidProductData = [ + 'name' => '', // Empty name + 'calories' => -10, // Negative calories + 'img_url' => 'invalid_image.txt', // Invalid image extension + 'img_alt_text' => 'In', // Invalid image alt text length + 'category' => '', // Empty category + 'price' => 0, // Zero price + 'description' => '' // Empty description + ]; - $this->markTestIncomplete( - 'This test lacks test cases, ...', + // Create a new product object with the invalid test data + $invalidProduct = new Product( + $invalidProductData['name'], + $invalidProductData['calories'], + $invalidProductData['img_url'], + $invalidProductData['img_alt_text'], + $invalidProductData['category'], + $invalidProductData['price'], + $invalidProductData['description'] ); + + // Validate the product + $errors = $invalidProduct->validate(); + + // Assert that validation errors are present for each invalid field + $this->assertArrayHasKey('name', $errors); + $this->assertArrayHasKey('calories', $errors); + $this->assertArrayHasKey('img_url', $errors); + $this->assertArrayHasKey('img_alt_text', $errors); + $this->assertArrayHasKey('category', $errors); + $this->assertArrayHasKey('price', $errors); + $this->assertArrayHasKey('description', $errors); + + // Assert that there are exactly 7 validation errors + $this->assertCount(7, $errors); } + + /** + * @throws Exception + */ public function testGetRatingDistribution(): void { - $distribution = $this->dummy_product->getRatingDistribution(); + // reset data from setUp + self::resetDatabase(); - // Check if the distribution contains the expected keys and values - // Here dummy product contains a single review: - $this->assertArrayHasKey($this->dummy_review->getRating(), $distribution); - $this->assertEquals(100.0, $distribution[$this->dummy_review->getRating()]); + // Create a new product for testing + $product = self::createProduct(); + $this->dummy_client = self::createClient(); - $this->markTestIncomplete( - 'This test lacks test cases. This test might fail when getRatingDistribution excludes unverified reviews.', - ); + // Create mock review data with different ratings + $verifiedReviewRatings = [5, 4, 3, 2, 1, 5, 4, 3, 4, 5]; + // Insert mock review data into the database + foreach ($verifiedReviewRatings as $reviewData) { + self::createReview($product, $this->dummy_client, $reviewData, true); + } + + // Create a random number of unverified reviews with different ratings + for ($i = 0; $i < self::$faker->numberBetween(0, 10); $i++) { + $rating = self::$faker->numberBetween(1, 5); + self::createReview($product, self::createClient(), $rating); + } + + // Retrieve the rating distribution for the product + $ratingDistribution = $product->getRatingDistribution(); + + // Assert that the rating distribution is accurate + $expectedDistribution = [ + 1 => 10.0, // 1 star + 2 => 10.0, // 2 stars + 3 => 20.0, // 3 stars + 4 => 30.0, // 4 stars + 5 => 30.0, // 5 stars + ]; + $this->assertEquals($expectedDistribution, $ratingDistribution); } public function testDeleteProduct(): void { - $product_id = $this->dummy_product->getProductID(); - $result = $this->dummy_product->deleteProduct(); - - // Check if the product was deleted successfully - $this->assertTrue($result); - - // Check if the product no longer exists in the database - $product = Product::getByID($product_id); - $this->assertNull($product); - - $this->markTestIncomplete( - 'This test lacks test cases, ...', - ); + // Save the product to the database + $product = $this->dummy_product; + + // Delete the product from the database + $success = $product->deleteProduct(); + // Assert that the delete operation was successful + self::assertTrue($success); + // Try to retrieve the product by ID to check if it was deleted + $deletedProduct = Product::getByID($product->getProductID()); + // Assert that the product is no longer in the database + self::assertNull($deletedProduct); } public function testUpdateProduct(): void @@ -210,15 +298,41 @@ public function testUpdateProduct(): void $this->assertEquals('Updated description', $updatedProduct->getDescription()); } + /** + * @throws Exception + */ public function testGetAverageRating(): void { - $averageRating = $this->dummy_product->getAverageRating(); + // reset database as we don't want previously created reviews from setUp. + self::resetDatabase(); - $this->assertNotEquals(999.0, $averageRating); + $this->dummy_product = self::createProduct(); + $this->dummy_client = self::createClient(); - $this->markTestIncomplete( - 'This test lacks test cases, ...', - ); + // Create a random number of verified reviews with different ratings + $verifiedReviewRatings = []; + for ($i = 0; $i < self::$faker->numberBetween(0, 10); $i++) { + $rating = self::$faker->numberBetween(1, 5); + $verifiedReviewRatings[] = $rating; + self::createReview($this->dummy_product, $this->dummy_client, $rating, true); + } + + // Note: $this->dummy_client can be a verified reviewer do not write unverified reviews with it + + // Create a random number of unverified reviews with different ratings + for ($i = 0; $i < self::$faker->numberBetween(0, 10); $i++) { + $rating = self::$faker->numberBetween(1, 5); + self::createReview($this->dummy_product, self::createClient(), $rating); + } + + // Retrieve the average rating for the product + $averageRating = $this->dummy_product->getAverageRating(); + + // Assert that the average rating is accurate + $expectedAverageRating = count($verifiedReviewRatings) === 0 ? 0 : (float)array_sum( + $verifiedReviewRatings + ) / count($verifiedReviewRatings); + $this->assertEqualsWithDelta($expectedAverageRating, $averageRating, 0.0001); } public function testGetReviews(): void diff --git a/tests/models/ReviewTest.php b/tests/models/ReviewTest.php index 4a34579..1a2683d 100644 --- a/tests/models/ReviewTest.php +++ b/tests/models/ReviewTest.php @@ -12,6 +12,7 @@ use Steamy\Model\Review; use Steamy\Model\Product; use Steamy\Tests\helpers\TestHelper; +use Steamy\Model\Comment; use Throwable; final class ReviewTest extends TestCase @@ -278,31 +279,83 @@ public function testSave(string $text, int $rating, DateTime $created_date, arra public function testGetNestedComments(): void { - $this->markTestIncomplete( - 'This test lacks test cases, ...', + // Create a review + $review = new Review( + product_id: $this->dummy_product->getProductID(), + client_id: $this->reviewer->getUserID(), + text: "This is a test review for nested comments.", + rating: 4 ); + $review->save(); - $review = new Review(review_id: 1); - $comments = $review->getNestedComments(); - - $this->assertIsArray($comments); - foreach ($comments as $comment) { - $this->assertObjectHasAttribute('children', $comment); - if (!empty($comment->children)) { - foreach ($comment->children as $childComment) { - $this->assertObjectHasAttribute('children', $childComment); - } - } - } + // Create root level comment + $comment1 = new Comment( + review_id: $review->getReviewID(), + user_id: $this->reviewer->getUserID(), + 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() + ); + $comment2->save(); + + $comment3 = new Comment( + review_id: $review->getReviewID(), + user_id: $this->reviewer->getUserID(), + 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() + ); + $comment4->save(); + + // Fetch nested comments + $nestedComments = $review->getNestedComments(); + + // Check if the structure is correct + $this->assertIsArray($nestedComments); + $this->assertCount(2, $nestedComments); // Should have 2 root level comments + + // Verify the first root level comment + $this->assertEquals($comment1->getCommentID(), $nestedComments[0]->comment_id); + $this->assertCount(1, $nestedComments[0]->children); // Should have 1 child + + // Verify the child comment of the first root level comment + $this->assertEquals($comment2->getCommentID(), $nestedComments[0]->children[0]->comment_id); + $this->assertCount(1, $nestedComments[0]->children[0]->children); // Should have 1 child + + // Verify the child of the child comment + $this->assertEquals($comment4->getCommentID(), $nestedComments[0]->children[0]->children[0]->comment_id); + $this->assertCount(0, $nestedComments[0]->children[0]->children[0]->children); // Should have no children + + // Verify the second root level comment + $this->assertEquals($comment3->getCommentID(), $nestedComments[1]->comment_id); + $this->assertCount(0, $nestedComments[1]->children); // Should have no children } + /** * @throws Exception */ public function testIsVerified(): void { // note: do not use data provider here because $faker is static and causes a bug - $verified_review = self::createReview(self::createProduct(), self::createClient(), true); + $verified_review = self::createReview( + self::createProduct(), + self::createClient(), + verified: true + ); $unverified_review = self::createReview(self::createProduct(), self::createClient()); $fake_review = new Review(review_id: -321, product_id: -32);