From d23f407ef19afb43f76f5090da1fea02d3bc93fd Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 22 May 2024 17:06:06 +0400 Subject: [PATCH 01/52] move model tests to models folder --- tests/ClientTest.php | 145 --------------------- tests/api/ProductsTest.php | 70 +++++++++++ tests/{ => models}/AdministratorTest.php | 5 +- tests/models/ClientTest.php | 153 +++++++++++++++++++++++ tests/{ => models}/CommentTest.php | 32 +++-- tests/{ => models}/FuzzyTest.php | 4 +- tests/{ => models}/OrderProductTest.php | 14 ++- tests/{ => models}/OrderTest.php | 14 ++- tests/{ => models}/ProductTest.php | 41 ++++-- tests/{ => models}/ReviewTest.php | 16 ++- tests/{ => models}/StoreTest.php | 31 ++++- 11 files changed, 335 insertions(+), 190 deletions(-) delete mode 100644 tests/ClientTest.php create mode 100644 tests/api/ProductsTest.php rename tests/{ => models}/AdministratorTest.php (99%) create mode 100644 tests/models/ClientTest.php rename tests/{ => models}/CommentTest.php (88%) rename tests/{ => models}/FuzzyTest.php (95%) rename tests/{ => models}/OrderProductTest.php (98%) rename tests/{ => models}/OrderTest.php (98%) rename tests/{ => models}/ProductTest.php (80%) rename tests/{ => models}/ReviewTest.php (95%) rename tests/{ => models}/StoreTest.php (82%) diff --git a/tests/ClientTest.php b/tests/ClientTest.php deleted file mode 100644 index 61865f9a..00000000 --- a/tests/ClientTest.php +++ /dev/null @@ -1,145 +0,0 @@ -dummy_client = new Client( - "john_u@gmail.com", "john", "johhny", "abcd", - "13213431", $address); - - $success = $this->dummy_client->save(); - if (!$success) { - throw new Exception('Unable to save client'); - } - } - -public function tearDown(): void -{ - $this->dummy_client = null; - - // Clear all data from client and user tables - self::query('DELETE FROM client; DELETE FROM user;'); -} - -public function testConstructor(): void -{ - // check if fields were correctly set - self::assertEquals("john_u@gmail.com", $this->dummy_client->getEmail()); - self::assertEquals("john", $this->dummy_client->getFirstName()); - self::assertEquals("johhny", $this->dummy_client->getLastName()); - self::assertEquals("13213431", $this->dummy_client->getPhoneNo()); - self::assertEquals("Royal Road, Curepipe, Moka", $this->dummy_client->getAddress()->getFormattedAddress()); -} - -public function testToArray(): void -{ - $result = $this->dummy_client->toArray(); - - // check if all required keys are present - $this->assertArrayHasKey('user_id', $result); - $this->assertArrayHasKey('email', $result); - $this->assertArrayHasKey('first_name', $result); - $this->assertArrayHasKey('last_name', $result); - $this->assertArrayHasKey('phone_no', $result); - $this->assertArrayHasKey('district_id', $result); - $this->assertArrayHasKey('street', $result); - $this->assertArrayHasKey('city', $result); - $this->assertArrayHasKey('password', $result); - - // check if actual values are correct - self::assertEquals("john_u@gmail.com", $result['email']); - self::assertEquals("john", $result['first_name']); - self::assertEquals("johhny", $result['last_name']); - self::assertEquals("13213431", $result['phone_no']); - self::assertEquals("Royal Road", $result['street']); - self::assertEquals("Curepipe", $result['city']); - self::assertEquals(1, $result['district_id']); -} - -public function testValidate(): void -{ - $client = new Client( - "", "", "", "abcd", - "", new Location(), // pass an empty Location object for testing - ); - - // Test if existence checks work - self::assertEquals([ - 'email' => 'Invalid email format', - 'first_name' => 'First name must be at least 3 characters long', - 'last_name' => 'Last name must be at least 3 characters long', - 'phone_no' => 'Phone number must be at least 7 characters long', - 'district' => 'District does not exist' - ], $client->validate()); - - // Test for range checks - $client = new Client( - "a@a.com", "Jo", "Doe", "1234567", - "123456", new Location(), // pass an empty Location object for testing - ); - - self::assertEquals([ - 'first_name' => 'First name must be at least 3 characters long', - 'phone_no' => 'Phone number must be at least 7 characters long', - 'district' => 'District does not exist' - ], $client->validate()); -} - -public function testVerifyPassword(): void -{ - // verify true password - self::assertTrue($this->dummy_client->verifyPassword("abcd")); - - // reject empty string - self::assertFalse($this->dummy_client->verifyPassword("")); - - // reject any other string - self::assertFalse($this->dummy_client->verifyPassword("abcde")); - self::assertFalse($this->dummy_client->verifyPassword("abcd ")); - self::assertFalse($this->dummy_client->verifyPassword(" abcd")); -} - -public function testGetByEmail(): void -{ - // Test for valid email - // Save the dummy record to the database - $this->dummy_client->save(); - // Fetch the client by email - $fetched_client = Client::getByEmail($this->dummy_client->getEmail()); - // Assert that the fetched client is not null - self::assertNotNull($fetched_client); - - // Assert the attributes of the fetched client - self::assertEquals("john_u@gmail.com", $fetched_client->getEmail()); - self::assertEquals("john", $fetched_client->getFirstName()); - self::assertEquals("johhny", $fetched_client->getLastName()); - self::assertEquals("13213431", $fetched_client->getPhoneNo()); - self::assertEquals("Royal Road, Curepipe, Moka", $fetched_client->getAddress()->getFormattedAddress()); - - // Delete the dummy record - $fetched_client->deleteUser(); - - // Add a small delay to ensure the deletion operation is completed - usleep(500000); // 500 milliseconds = 0.5 seconds - - // Fetch the client by email again - $fetched_client = Client::getByEmail($this->dummy_client->getEmail()); - - // Test for invalid email - // Assert that the fetched client is null or false - self::assertNull($fetched_client); -} -} diff --git a/tests/api/ProductsTest.php b/tests/api/ProductsTest.php new file mode 100644 index 00000000..a525ea1a --- /dev/null +++ b/tests/api/ProductsTest.php @@ -0,0 +1,70 @@ +client = new GuzzleClient([ + 'base_uri' => $_ENV['API_BASE_URI'] + ]); + + // Create a dummy product for testing + $this->dummy_product = new Product( + "Velvet Bean", + 70, + "Velvet.jpeg", + "Velvet Bean Image", + "Velvet", + 6.50, + "Each bottle contains 90% Pure Coffee powder and 10% Velvet bean Powder", + new DateTime() + ); + + $success = $this->dummy_product->save(); + if (!$success) { + throw new Exception('Unable to save product'); + } + } + + public function tearDown(): void + { + $this->client = null; + self::query('DELETE FROM product;'); + } + + /** + * @throws GuzzleException + */ + public function testGetEndpoint() + { + $response = $this->client->get('products'); + $this->assertEquals(200, $response->getStatusCode()); + + $body = $response->getBody(); + $json = json_decode((string)$body, true); + echo json_encode($json, JSON_PRETTY_PRINT) . "\n"; + +// $this->assertArrayHasKey('key', $data); +// $this->assertEquals('expected_value', $data['key']); + } + +} \ No newline at end of file diff --git a/tests/AdministratorTest.php b/tests/models/AdministratorTest.php similarity index 99% rename from tests/AdministratorTest.php rename to tests/models/AdministratorTest.php index dde5673a..4a4d8e33 100644 --- a/tests/AdministratorTest.php +++ b/tests/models/AdministratorTest.php @@ -2,9 +2,12 @@ declare(strict_types=1); +namespace models; + +use Exception; use PHPUnit\Framework\TestCase; -use Steamy\Model\Administrator; use Steamy\Core\Database; +use Steamy\Model\Administrator; final class AdministratorTest extends TestCase { diff --git a/tests/models/ClientTest.php b/tests/models/ClientTest.php new file mode 100644 index 00000000..a6168119 --- /dev/null +++ b/tests/models/ClientTest.php @@ -0,0 +1,153 @@ +dummy_client = new Client( + "john_u@gmail.com", "john", "johhny", "abcd", + "13213431", $address + ); + + $success = $this->dummy_client->save(); + if (!$success) { + throw new Exception('Unable to save client'); + } + } + + public function tearDown(): void + { + $this->dummy_client = null; + + // Clear all data from client and user tables + self::query('DELETE FROM client; DELETE FROM user;'); + } + + public function testConstructor(): void + { + // check if fields were correctly set + self::assertEquals("john_u@gmail.com", $this->dummy_client->getEmail()); + self::assertEquals("john", $this->dummy_client->getFirstName()); + self::assertEquals("johhny", $this->dummy_client->getLastName()); + self::assertEquals("13213431", $this->dummy_client->getPhoneNo()); + self::assertEquals("Royal Road, Curepipe, Moka", $this->dummy_client->getAddress()->getFormattedAddress()); + } + + public function testToArray(): void + { + $result = $this->dummy_client->toArray(); + + // check if all required keys are present + $this->assertArrayHasKey('user_id', $result); + $this->assertArrayHasKey('email', $result); + $this->assertArrayHasKey('first_name', $result); + $this->assertArrayHasKey('last_name', $result); + $this->assertArrayHasKey('phone_no', $result); + $this->assertArrayHasKey('district_id', $result); + $this->assertArrayHasKey('street', $result); + $this->assertArrayHasKey('city', $result); + $this->assertArrayHasKey('password', $result); + + // check if actual values are correct + self::assertEquals("john_u@gmail.com", $result['email']); + self::assertEquals("john", $result['first_name']); + self::assertEquals("johhny", $result['last_name']); + self::assertEquals("13213431", $result['phone_no']); + self::assertEquals("Royal Road", $result['street']); + self::assertEquals("Curepipe", $result['city']); + self::assertEquals(1, $result['district_id']); + } + + public function testValidate(): void + { + $client = new Client( + "", "", "", "abcd", + "", new Location(), // pass an empty Location object for testing + ); + + // Test if existence checks work + self::assertEquals([ + 'email' => 'Invalid email format', + 'first_name' => 'First name must be at least 3 characters long', + 'last_name' => 'Last name must be at least 3 characters long', + 'phone_no' => 'Phone number must be at least 7 characters long', + 'district' => 'District does not exist' + ], $client->validate()); + + // Test for range checks + $client = new Client( + "a@a.com", "Jo", "Doe", "1234567", + "123456", new Location(), // pass an empty Location object for testing + ); + + self::assertEquals([ + 'first_name' => 'First name must be at least 3 characters long', + 'phone_no' => 'Phone number must be at least 7 characters long', + 'district' => 'District does not exist' + ], $client->validate()); + } + + public function testVerifyPassword(): void + { + // verify true password + self::assertTrue($this->dummy_client->verifyPassword("abcd")); + + // reject empty string + self::assertFalse($this->dummy_client->verifyPassword("")); + + // reject any other string + self::assertFalse($this->dummy_client->verifyPassword("abcde")); + self::assertFalse($this->dummy_client->verifyPassword("abcd ")); + self::assertFalse($this->dummy_client->verifyPassword(" abcd")); + } + + public function testGetByEmail(): void + { + // Test for valid email + // Save the dummy record to the database + $this->dummy_client->save(); + // Fetch the client by email + $fetched_client = Client::getByEmail($this->dummy_client->getEmail()); + // Assert that the fetched client is not null + self::assertNotNull($fetched_client); + + // Assert the attributes of the fetched client + self::assertEquals("john_u@gmail.com", $fetched_client->getEmail()); + self::assertEquals("john", $fetched_client->getFirstName()); + self::assertEquals("johhny", $fetched_client->getLastName()); + self::assertEquals("13213431", $fetched_client->getPhoneNo()); + self::assertEquals("Royal Road, Curepipe, Moka", $fetched_client->getAddress()->getFormattedAddress()); + + // Delete the dummy record + $fetched_client->deleteUser(); + + // Add a small delay to ensure the deletion operation is completed + usleep(500000); // 500 milliseconds = 0.5 seconds + + // Fetch the client by email again + $fetched_client = Client::getByEmail($this->dummy_client->getEmail()); + + // Test for invalid email + // Assert that the fetched client is null or false + self::assertNull($fetched_client); + } +} diff --git a/tests/CommentTest.php b/tests/models/CommentTest.php similarity index 88% rename from tests/CommentTest.php rename to tests/models/CommentTest.php index cdbba2d7..2d7054df 100644 --- a/tests/CommentTest.php +++ b/tests/models/CommentTest.php @@ -2,22 +2,30 @@ declare(strict_types=1); +namespace models; + +use DateTime; +use Exception; use PHPUnit\Framework\TestCase; +use Steamy\Core\Database; +use Steamy\Model\Client; use Steamy\Model\Comment; -use Steamy\Model\Review; use Steamy\Model\Location; -use Steamy\Model\Client; -use Steamy\Core\Database; use Steamy\Model\Product; +use Steamy\Model\Review; -Class CommentTest extends TestCase +class CommentTest extends TestCase { use Database; + private ?Comment $dummy_comment; private ?Review $dummy_review; private ?Client $reviewer; private ?Product $dummy_product; + /** + * @throws Exception + */ public function setUp(): void { // Create a dummy product for testing @@ -31,7 +39,7 @@ public function setUp(): void "Each bottle contains 90% Pure Coffee powder and 10% Velvet bean Powder", new DateTime() ); - + $success = $this->dummy_product->save(); if (!$success) { throw new Exception('Unable to save product'); @@ -84,16 +92,18 @@ public function tearDown(): void $this->dummy_product = null; // clear all data from review and client tables - self::query('DELETE FROM comment; DELETE FROM review; DELETE FROM client; DELETE FROM user; DELETE FROM store_product; DELETE FROM product;'); + self::query( + 'DELETE FROM comment; DELETE FROM review; DELETE FROM client; DELETE FROM user; DELETE FROM store_product; DELETE FROM product;' + ); } public function testConstructor(): void { self::assertEquals('This is a test comment.', $this->dummy_comment->getText()); - self::assertNotNull($this->dummy_comment->getUserID()); - self::assertNotNull($this->dummy_comment->getReviewID()); - self::assertNull($this->dummy_comment->getParentCommentID()); - self::assertInstanceOf(DateTime::class, $this->dummy_comment->getCreatedDate()); + self::assertNotNull($this->dummy_comment->getUserID()); + self::assertNotNull($this->dummy_comment->getReviewID()); + self::assertNull($this->dummy_comment->getParentCommentID()); + self::assertInstanceOf(DateTime::class, $this->dummy_comment->getCreatedDate()); } public function testValidate(): void @@ -146,7 +156,7 @@ public function testSave(): void self::assertFalse($saved); } - public function testGetByID(): void + public function testGetById(): void { // Test fetching an existing comment $comment_id = $this->dummy_comment->getCommentID(); diff --git a/tests/FuzzyTest.php b/tests/models/FuzzyTest.php similarity index 95% rename from tests/FuzzyTest.php rename to tests/models/FuzzyTest.php index 96bb5fe9..e76bc6bc 100644 --- a/tests/FuzzyTest.php +++ b/tests/models/FuzzyTest.php @@ -2,6 +2,8 @@ declare(strict_types=1); +namespace models; + use PHPUnit\Framework\TestCase; use Steamy\Core\Utility; @@ -24,7 +26,7 @@ public static function fuzzySearchDataProvider(): array ['Espreso', $strings, 1, ['Espresso']], // Missing 's' ['Espressso', $strings, 1, ['Espresso']], // Extra 's' ['', $strings, 1, []], // Empty search term - [(string) 123, $strings, 1, []], // Non-string search term (integer) + ["123", $strings, 1, []], // Non-string search term (integer) ['Latte!', $strings, 1, ['Latte']], // Search term containing special characters ['eSPRESSO', $strings, 1, ['Espresso']], // Case sensitivity test ]; diff --git a/tests/OrderProductTest.php b/tests/models/OrderProductTest.php similarity index 98% rename from tests/OrderProductTest.php rename to tests/models/OrderProductTest.php index 28508968..f157c5ab 100644 --- a/tests/OrderProductTest.php +++ b/tests/models/OrderProductTest.php @@ -2,14 +2,18 @@ declare(strict_types=1); +namespace models; + +use DateTime; +use Exception; use PHPUnit\Framework\TestCase; +use Steamy\Core\Database; +use Steamy\Model\Client; +use Steamy\Model\Location; use Steamy\Model\Order; use Steamy\Model\OrderProduct; -use Steamy\Model\Store; -use Steamy\Model\Client; use Steamy\Model\Product; -use Steamy\Core\Database; -use Steamy\Model\Location; +use Steamy\Model\Store; class OrderProductTest extends TestCase { @@ -141,7 +145,7 @@ public function testValidate(): void $this->assertArrayHasKey('unit_price', $errors); } - public function testGetByID(): void + public function testGetById(): void { // Assuming getByID is a method that retrieves an OrderProduct by order ID and product ID $retrievedOrderProduct = OrderProduct::getByID( diff --git a/tests/OrderTest.php b/tests/models/OrderTest.php similarity index 98% rename from tests/OrderTest.php rename to tests/models/OrderTest.php index 0bbda2a0..69c5fb7a 100644 --- a/tests/OrderTest.php +++ b/tests/models/OrderTest.php @@ -2,15 +2,19 @@ declare(strict_types=1); +namespace models; + +use DateTime; +use Exception; use PHPUnit\Framework\TestCase; +use Steamy\Core\Database; +use Steamy\Model\Client; +use Steamy\Model\Location; use Steamy\Model\Order; use Steamy\Model\OrderProduct; use Steamy\Model\OrderStatus; -use Steamy\Model\Store; -use Steamy\Model\Client; -use Steamy\Core\Database; -use Steamy\Model\Location; use Steamy\Model\Product; +use Steamy\Model\Store; class OrderTest extends TestCase { @@ -202,7 +206,7 @@ public function testAddLineItem(): void /** * @throws Exception */ - public function testGetByID(): void + public function testGetById(): void { $this->dummy_order->save(); $order_id = $this->dummy_order->getOrderID(); diff --git a/tests/ProductTest.php b/tests/models/ProductTest.php similarity index 80% rename from tests/ProductTest.php rename to tests/models/ProductTest.php index f821e8c5..1748b9b8 100644 --- a/tests/ProductTest.php +++ b/tests/models/ProductTest.php @@ -2,17 +2,24 @@ declare(strict_types=1); +namespace models; + +use DateTime; +use Exception; use PHPUnit\Framework\TestCase; -use Steamy\Model\Product; -use Steamy\Model\Review; use Steamy\Core\Database; +use Steamy\Model\Product; final class ProductTest extends TestCase { use Database; + private ?Product $dummy_product; + /** + * @throws Exception + */ public function setUp(): void { // Create a dummy product for testing @@ -26,7 +33,7 @@ public function setUp(): void "Each bottle contains 90% Pure Coffee powder and 10% Velvet bean Powder", new DateTime() ); - + $success = $this->dummy_product->save(); if (!$success) { throw new Exception('Unable to save product'); @@ -50,14 +57,20 @@ public function testConstructor(): void self::assertEquals("Velvet Bean Image", $this->dummy_product->getImgAltText()); self::assertEquals("Velvet", $this->dummy_product->getCategory()); self::assertEquals(6.50, $this->dummy_product->getPrice()); - self::assertEquals("Each bottle contains 90% Pure Coffee powder and 10% Velvet bean Powder", $this->dummy_product->getDescription()); - self::assertInstanceOf(DateTime::class, $this->dummy_product->getCreatedDate()); // Check if created_date is an instance of DateTime + self::assertEquals( + "Each bottle contains 90% Pure Coffee powder and 10% Velvet bean Powder", + $this->dummy_product->getDescription() + ); + self::assertInstanceOf( + DateTime::class, + $this->dummy_product->getCreatedDate() + ); // Check if created_date is an instance of DateTime } public function testToArray(): void { $result = $this->dummy_product->toArray(); - + // Check if all required keys are present $this->assertArrayHasKey('product_id', $result); $this->assertArrayHasKey('name', $result); @@ -68,7 +81,7 @@ public function testToArray(): void $this->assertArrayHasKey('price', $result); $this->assertArrayHasKey('description', $result); $this->assertArrayHasKey('created_date', $result); // Ensure created_date is included in toArray result - + // Check if the actual values are correct self::assertEquals("Velvet Bean", $result['name']); self::assertEquals(70, $result['calories']); @@ -76,20 +89,26 @@ public function testToArray(): void self::assertEquals("Velvet Bean Image", $result['img_alt_text']); self::assertEquals("Velvet", $result['category']); self::assertEquals(6.50, $result['price']); - self::assertEquals("Each bottle contains 90% Pure Coffee powder and 10% Velvet bean Powder", $result['description']); - self::assertInstanceOf(DateTime::class, $result['created_date']); // Check if created_date is an instance of DateTime + self::assertEquals( + "Each bottle contains 90% Pure Coffee powder and 10% Velvet bean Powder", + $result['description'] + ); + self::assertInstanceOf( + DateTime::class, + $result['created_date'] + ); // Check if created_date is an instance of DateTime } public function testSave(): void { // Save the dummy product $result = $this->dummy_product->save(); - + // Check if the product was saved successfully self::assertTrue($result); // Assert that save() returns true upon successful save self::assertNotNull($this->dummy_product->getProductID()); } - + public function testValidate(): void { // Validate the dummy product diff --git a/tests/ReviewTest.php b/tests/models/ReviewTest.php similarity index 95% rename from tests/ReviewTest.php rename to tests/models/ReviewTest.php index 16ce6d49..a6bb4ba0 100644 --- a/tests/ReviewTest.php +++ b/tests/models/ReviewTest.php @@ -2,12 +2,16 @@ declare(strict_types=1); +namespace models; + +use DateTime; +use Exception; use PHPUnit\Framework\TestCase; use Steamy\Core\Database; use Steamy\Model\Client; use Steamy\Model\Location; -use Steamy\Model\Review; use Steamy\Model\Product; +use Steamy\Model\Review; final class ReviewTest extends TestCase { @@ -37,12 +41,12 @@ public function setUp(): void "Each bottle contains 90% Pure Coffee powder and 10% Velvet bean Powder", new DateTime() ); - + $success = $this->dummy_product->save(); if (!$success) { throw new Exception('Unable to save product'); } - + // create a client object and save to database $this->reviewer = new Client( "john_u@gmail.com", "john", "johhny", "User0", @@ -80,7 +84,9 @@ public function tearDown(): void $this->dummy_product = null; // clear all data from review and client tables - self::query('DELETE FROM comment; DELETE FROM review; DELETE FROM client; DELETE FROM user; DELETE FROM store_product; DELETE FROM product;'); + self::query( + 'DELETE FROM comment; DELETE FROM review; DELETE FROM client; DELETE FROM user; DELETE FROM store_product; DELETE FROM product;' + ); } public function testConstructor(): void @@ -154,7 +160,7 @@ public function testValidate(): void $this->assertEquals('Rating must be between 1 and 5', $errors['rating']); // Assert specific message } - public function testGetByID(): void + public function testGetById(): void { $fetched_review = Review::getByID($this->dummy_review->getReviewID()); diff --git a/tests/StoreTest.php b/tests/models/StoreTest.php similarity index 82% rename from tests/StoreTest.php rename to tests/models/StoreTest.php index a560c7d9..785238fe 100644 --- a/tests/StoreTest.php +++ b/tests/models/StoreTest.php @@ -2,10 +2,13 @@ declare(strict_types=1); +namespace models; + +use Exception; use PHPUnit\Framework\TestCase; -use Steamy\Model\Store; -use Steamy\Model\Location; use Steamy\Core\Database; +use Steamy\Model\Location; +use Steamy\Model\Store; class StoreTest extends TestCase { @@ -114,13 +117,29 @@ public static function validateDataProvider(): array // Valid phone number, valid address (no errors) ["1234567890", new Location("Royal", "Curepipe", 1, 50, 50), []], // Invalid phone number (less than 7 characters) - ["123456", new Location("Royal", "Curepipe", 1, 50, 50), ["phone_no" => "Phone number must be at least 7 characters long"]], + [ + "123456", + new Location("Royal", "Curepipe", 1, 50, 50), + ["phone_no" => "Phone number must be at least 7 characters long"] + ], // Empty phone number - ["", new Location("Royal", "Curepipe", 1, 50, 50), ["phone_no" => "Phone number must be at least 7 characters long"]], + [ + "", + new Location("Royal", "Curepipe", 1, 50, 50), + ["phone_no" => "Phone number must be at least 7 characters long"] + ], // Invalid characters in phone number - ["123abc", new Location("Royal", "Curepipe", 1, 50, 50), ["phone_no" => "Phone number must be at least 7 characters long"]], + [ + "123abc", + new Location("Royal", "Curepipe", 1, 50, 50), + ["phone_no" => "Phone number must be at least 7 characters long"] + ], // Invalid address with invalid latitude/longitude - ["1234567890", new Location("Royal", "Curepipe", 1, -100, 50), ["coordinates" => "Invalid latitude or longitude."]], + [ + "1234567890", + new Location("Royal", "Curepipe", 1, -100, 50), + ["coordinates" => "Invalid latitude or longitude."] + ], ]; } } From c6b26d89825d32be908e3a5b93f5e11dcb3858fb Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 22 May 2024 17:06:30 +0400 Subject: [PATCH 02/52] install guzzle --- composer.json | 3 +- composer.lock | 710 +++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 621 insertions(+), 92 deletions(-) diff --git a/composer.json b/composer.json index 54a49b78..a68fb0ca 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ } }, "require-dev": { - "phpunit/phpunit": "^10.5" + "phpunit/phpunit": "^10.5", + "guzzlehttp/guzzle": "^7.0" } } diff --git a/composer.lock b/composer.lock index 6240fdec..84378e99 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4b9294620a906d7c357a1575b7938828", + "content-hash": "da0cc170d488ab4eb8ade85e7261d00c", "packages": [ { "name": "carbonphp/carbon-doctrine-types", @@ -780,16 +780,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", "shasum": "" }, "require": { @@ -803,9 +803,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -842,7 +839,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" }, "funding": [ { @@ -858,20 +855,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "42292d99c55abe617799667f454222c54c60e229" + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", - "reference": "42292d99c55abe617799667f454222c54c60e229", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", "shasum": "" }, "require": { @@ -885,9 +882,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -925,7 +919,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" }, "funding": [ { @@ -941,20 +935,20 @@ "type": "tidelift" } ], - "time": "2023-07-28T09:04:16+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", "shasum": "" }, "require": { @@ -962,9 +956,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -1008,7 +999,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" }, "funding": [ { @@ -1024,7 +1015,7 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-php83", @@ -1362,6 +1353,331 @@ } ], "packages-dev": [ + { + "name": "guzzlehttp/guzzle", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:35:24+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:19:20+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.6.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:05:35+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.11.1", @@ -1423,16 +1739,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.0.0", + "version": "v5.0.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc" + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4a21235f7e56e713259a6f76bf4b5ea08502b9dc", - "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", "shasum": "" }, "require": { @@ -1475,26 +1791,27 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" }, - "time": "2024-01-07T17:17:35+00:00" + "time": "2024-03-05T20:51:40+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -1535,9 +1852,15 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -1592,16 +1915,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "10.1.11", + "version": "10.1.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "78c3b7625965c2513ee96569a4dbb62601784145" + "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/78c3b7625965c2513ee96569a4dbb62601784145", - "reference": "78c3b7625965c2513ee96569a4dbb62601784145", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", + "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", "shasum": "" }, "require": { @@ -1658,7 +1981,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.11" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.14" }, "funding": [ { @@ -1666,7 +1989,7 @@ "type": "github" } ], - "time": "2023-12-21T15:38:30+00:00" + "time": "2024-03-12T15:33:41+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1913,16 +2236,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.7", + "version": "10.5.20", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e5c5b397a95cb0db013270a985726fcae93e61b8" + "reference": "547d314dc24ec1e177720d45c6263fb226cc2ae3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e5c5b397a95cb0db013270a985726fcae93e61b8", - "reference": "e5c5b397a95cb0db013270a985726fcae93e61b8", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/547d314dc24ec1e177720d45c6263fb226cc2ae3", + "reference": "547d314dc24ec1e177720d45c6263fb226cc2ae3", "shasum": "" }, "require": { @@ -1994,7 +2317,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.7" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.20" }, "funding": [ { @@ -2010,20 +2333,224 @@ "type": "tidelift" } ], - "time": "2024-01-14T16:40:30+00:00" + "time": "2024-04-24T06:32:35+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" }, { "name": "sebastian/cli-parser", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae" + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/efdc130dbbbb8ef0b545a994fd811725c5282cae", - "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", "shasum": "" }, "require": { @@ -2058,7 +2585,8 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.0" + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" }, "funding": [ { @@ -2066,7 +2594,7 @@ "type": "github" } ], - "time": "2023-02-03T06:58:15+00:00" + "time": "2024-03-02T07:12:49+00:00" }, { "name": "sebastian/code-unit", @@ -2316,16 +2844,16 @@ }, { "name": "sebastian/diff", - "version": "5.1.0", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f" + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/fbf413a49e54f6b9b17e12d900ac7f6101591b7f", - "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", "shasum": "" }, "require": { @@ -2333,7 +2861,7 @@ }, "require-dev": { "phpunit/phpunit": "^10.0", - "symfony/process": "^4.2 || ^5" + "symfony/process": "^6.4" }, "type": "library", "extra": { @@ -2371,7 +2899,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/5.1.0" + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" }, "funding": [ { @@ -2379,20 +2907,20 @@ "type": "github" } ], - "time": "2023-12-22T10:55:06+00:00" + "time": "2024-03-02T07:15:17+00:00" }, { "name": "sebastian/environment", - "version": "6.0.1", + "version": "6.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951" + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/43c751b41d74f96cbbd4e07b7aec9675651e2951", - "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", "shasum": "" }, "require": { @@ -2407,7 +2935,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "6.1-dev" } }, "autoload": { @@ -2435,7 +2963,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" }, "funding": [ { @@ -2443,20 +2971,20 @@ "type": "github" } ], - "time": "2023-04-11T05:39:26+00:00" + "time": "2024-03-23T08:47:14+00:00" }, { "name": "sebastian/exporter", - "version": "5.1.1", + "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc" + "reference": "955288482d97c19a372d3f31006ab3f37da47adf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/64f51654862e0f5e318db7e9dcc2292c63cdbddc", - "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf", "shasum": "" }, "require": { @@ -2513,7 +3041,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.1" + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" }, "funding": [ { @@ -2521,20 +3049,20 @@ "type": "github" } ], - "time": "2023-09-24T13:22:09+00:00" + "time": "2024-03-02T07:17:12+00:00" }, { "name": "sebastian/global-state", - "version": "6.0.1", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "7ea9ead78f6d380d2a667864c132c2f7b83055e4" + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/7ea9ead78f6d380d2a667864c132c2f7b83055e4", - "reference": "7ea9ead78f6d380d2a667864c132c2f7b83055e4", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", "shasum": "" }, "require": { @@ -2568,14 +3096,14 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" }, "funding": [ { @@ -2583,7 +3111,7 @@ "type": "github" } ], - "time": "2023-07-19T07:19:23+00:00" + "time": "2024-03-02T07:19:19+00:00" }, { "name": "sebastian/lines-of-code", @@ -2929,16 +3457,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.2", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -2967,7 +3495,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.2" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -2975,7 +3503,7 @@ "type": "github" } ], - "time": "2023-11-20T00:12:19+00:00" + "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], From 9fd5f7139fca02a3c81f71cfd1d6f0299430aa24 Mon Sep 17 00:00:00 2001 From: divyesh000 Date: Mon, 20 May 2024 21:14:41 +0400 Subject: [PATCH 03/52] add Review model table property, getAll, getAllReviewsForProduct, updateReview, and deleteReview methods --- src/controllers/api/Reviews.php | 236 ++++++++++++++++++++++++++++++++ src/models/Review.php | 70 +++++++++- 2 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 src/controllers/api/Reviews.php diff --git a/src/controllers/api/Reviews.php b/src/controllers/api/Reviews.php new file mode 100644 index 00000000..015c0aa4 --- /dev/null +++ b/src/controllers/api/Reviews.php @@ -0,0 +1,236 @@ +toArray(); + } + + // Return JSON response + echo json_encode($result); + } + + /** + * Get all reviews for a particular product by its ID. + */ + private function getAllReviewsForProduct(): void + { + $productId = (int)Utility::splitURL()[4]; + + // Retrieve all reviews for the specified product from the database + $reviews = Review::getAllReviewsForProduct($productId); + + // Check if Review exists + if ($reviews === null) { + // Review not found, return 404 + http_response_code(404); + echo json_encode(['error' => 'Review not found']); + return; + } + + // Return JSON response + echo json_encode($reviews); + } + + /** + * Create a new review for a product. + */ + private 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)) { + http_response_code(400); + echo json_encode(['error' => "Missing fields: " . implode(', ', $requiredFields)]); + 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'] + ); + + // Save the new review to the database + if ($newReview->save()) { + // Review created successfully, return 201 Created + http_response_code(201); + echo json_encode(['message' => 'Review created successfully', 'review_id' => $newReview->getReviewID()]); + } else { + // Failed to create review, return 500 Internal Server Error + http_response_code(500); + echo json_encode(['error' => 'Failed to create review']); + } + } + + /** + * Update the details of a review with the specified ID. + */ + private function updateReview(): void + { + $reviewId = (int)Utility::splitURL()[3]; + + // Retrieve PUT request data + $putData = 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']); + return; + } + + // Retrieve existing review + $review = Review::getByID($reviewId); + + // Check if review exists + if ($review === null) { + // Review not found + http_response_code(404); // Not Found + echo json_encode(['error' => 'Review not found']); + return; + } + + // Update review in the database + $success = $review->updateReview($putData); + + if ($success) { + // Review updated successfully + http_response_code(200); // OK + echo json_encode(['message' => 'Review updated successfully']); + } else { + // Failed to update review + http_response_code(500); // Internal Server Error + echo json_encode(['error' => 'Failed to update review']); + } + } + + /** + * Delete a review with the specified ID. + */ + private function deleteReview(): void + { + $reviewId = (int)Utility::splitURL()[3]; + + // Retrieve the review by ID + $review = Review::getByID($reviewId); + + // Check if review exists + if ($review === null) { + // Review not found, return 404 + http_response_code(404); + echo json_encode(['error' => 'Review not found']); + return; + } + + // Attempt to delete the review + if ($review->deleteReview($reviewId)) { + // Review successfully deleted + http_response_code(204); // No Content + } else { + // Failed to delete the review + http_response_code(500); // Internal Server Error + echo json_encode(['error' => 'Failed to delete review']); + } + } + + private function getHandler($routes): ?string + { + foreach ($routes[$_SERVER['REQUEST_METHOD']] as $route => $handler) { + $pattern = str_replace('/', '\/', $route); // Convert to regex pattern + $pattern = preg_replace( + '/\{([a-zA-Z0-9_]+)\}/', + '(?P<$1>[^\/]+)', + $pattern + ); // Replace placeholders with regex capture groups + $pattern = '/^' . $pattern . '$/'; + + if (preg_match($pattern, '/' . Utility::getURL(), $matches)) { + return $handler; + } + } + return null; + } + + /** + * Main entry point for the Reviews API. + */ + public function index(): void + { + $routes = [ + 'GET' => [ + '/api/v1/reviews' => 'getAllReviews', + '/api/v1/products/{id}/reviews' => 'getAllReviewsForProduct', + ], + 'POST' => [ + '/api/v1/reviews' => 'createReview', + ], + 'PUT' => [ + '/api/v1/reviews/{id}' => 'updateReview', + ], + 'DELETE' => [ + '/api/v1/reviews/{id}' => 'deleteReview', + ] + ]; + + // Handle the request + $handler = $this->getHandler($routes); + + if ($handler !== null) { + $functionName = $handler; + if (method_exists($this, $functionName)) { + call_user_func([$this, $functionName]); + } else { + // Handle function not found + http_response_code(404); + echo "Function Not Found"; + die(); + } + } else { + // Handle route not found + http_response_code(404); + echo "Route Not Found"; + die(); + } + } +} diff --git a/src/models/Review.php b/src/models/Review.php index fb74c640..0be20ff9 100644 --- a/src/models/Review.php +++ b/src/models/Review.php @@ -12,7 +12,7 @@ class Review { use Model; - + protected string $table = 'review'; private int $review_id; private int $product_id; @@ -88,6 +88,74 @@ public static function getByID(int $review_id): ?Review ); } + /** + * Retrieve all reviews from the database. + * + * @return array Array of Review objects representing all reviews in the database. + */ + public static function getAll(): array + { + // Prepare and execute SQL query to retrieve all reviews + $query = "SELECT * FROM review"; + $results = self::query($query); + + // Fetch all reviews as Review objects + $reviews = []; + foreach ($results as $result) { + $obj = new Review( + product_id: $result->product_id, + client_id: $result->client_id, + text: $result->text, + rating: $result->rating, + created_date: Utility::stringToDate($result->created_date) + ); + $obj->setReviewID($result->review_id); + $reviews[] = $obj; + } + + return $reviews; + } + + /** + * Retrieves all reviews for a particular product from the database. + * + * @param int $productId The ID of the product. + * @return array An array containing all reviews for the specified product. + */ + public static function getAllReviewsForProduct(int $productId): array + { + $query = "SELECT * FROM review WHERE product_id = :product_id"; + $params = ['product_id' => $productId]; + return self::query($query, $params); + } + + /** + * Updates review record in database but does not update the object itself. + * @param array $newReviewData Associative array indexed by attribute name. + * The values are the new review data. + * @return bool Success or not + */ + public function updateReview (array $newReviewData): bool + { + // remove review_id (if present) from user data + unset($newReviewData['review_id']); + + 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 + { + $query = "DELETE FROM review WHERE review_id = :review_id"; + $params = ['review_id' => $reviewId]; + return self::query($query, $params); + } + public function getReviewID(): int { return $this->review_id; From 88d623726625768026f41b14b76558b7be81c392 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Tue, 21 May 2024 13:11:08 +0400 Subject: [PATCH 04/52] rewrite logic for the API to make errors clearer and to reduce code duplication (e.g. `getHandler` was being duplicated in all api controllers) --- src/controllers/API.php | 77 ++++++++++++++++++---- src/controllers/api/Districts.php | 62 +++--------------- src/controllers/api/Products.php | 90 +++++++------------------- src/controllers/api/Reviews.php | 103 +++++++++--------------------- src/controllers/api/Sessions.php | 21 +++--- 5 files changed, 136 insertions(+), 217 deletions(-) diff --git a/src/controllers/API.php b/src/controllers/API.php index d5ea393e..84f387f9 100644 --- a/src/controllers/API.php +++ b/src/controllers/API.php @@ -21,8 +21,12 @@ class API public function __construct() { + // Set the Content-Type header to application/json header("Content-Type:application/json"); + // Allow access from any origin (CORS) + header('Access-Control-Allow-Origin: *'); + $this->resource = Utility::splitURL()[2] ?? ""; } @@ -32,32 +36,83 @@ public function __construct() */ private function validateURLFormat(): bool { - return preg_match("/^api\/v1/", $_GET["url"]) > 0; + return preg_match("/^api\/v1/", Utility::getURL()) > 0; + } + + + /** + * Returns the name of function responsible for handling the current request, as defined by the $routes variable. + * @param string $controllerName class name of controller + * @return string|null + */ + private function getHandler(string $controllerName): ?string + { + $all_routes = $controllerName::$routes; + + // check if there are handlers defined for current request method + $my_routes = $all_routes[$_SERVER['REQUEST_METHOD']] ?? ""; + if (empty($my_routes)) { + return null; + } + + foreach ($my_routes as $route => $handler) { + $pattern = str_replace('/', '\/', $route); // Convert to regex pattern + $pattern = preg_replace( + '/\{([a-zA-Z0-9_]+)\}/', + '(?P<$1>[^\/]+)', + $pattern + ); // Replace placeholders with regex capture groups + $pattern = '/^' . $pattern . '$/'; + + if (preg_match($pattern, '/' . Utility::getURL(), $matches)) { + return $handler; + } + } + return null; } public function index(): void { if (!$this->validateURLFormat()) { http_response_code(400); - die(); + return; } - // call appropriate controller to handle resource + // check if there is a controller to handle resource $controllerClassName = 'Steamy\\Controller\\API\\' . ucfirst($this->resource); + if (!class_exists($controllerClassName)) { + // no controller available + http_response_code(404); + echo 'Invalid resource: ' . $this->resource; // comment this line for production + return; + } + + // determine which function to call in the controller to handle route + $functionName = $this->getHandler($controllerClassName); + if ($functionName === null) { + // Controller does not have any method defined for route + http_response_code(404); + echo "Request has not been defined in \$routes for " . $controllerClassName; + return; + } + + $controller = new $controllerClassName(); + + if (!method_exists($controller, $functionName)) { + // handle function not found in controller + http_response_code(500); + echo $controllerClassName . ' does not have a public method ' . $functionName; + return; + } + + // call function in controller for handling request try { - if (class_exists($controllerClassName)) { - (new $controllerClassName())->index(); - } else { - http_response_code(404); - die(); - } + call_user_func(array($controller, $functionName)); } catch (Exception $e) { http_response_code(500); // Uncomment line below only when testing API echo $e->getMessage(); - - die(); } } } diff --git a/src/controllers/api/Districts.php b/src/controllers/api/Districts.php index 7d3ed3a4..ea0f6105 100644 --- a/src/controllers/api/Districts.php +++ b/src/controllers/api/Districts.php @@ -12,10 +12,17 @@ class Districts { use Model; + public static array $routes = [ + 'GET' => [ + '/api/v1/districts' => 'getAllDistricts', + '/api/v1/districts/{id}' => 'getDistrictById', + ] + ]; + /** * Get the list of all districts available. */ - private function getAllDistricts(): void + public function getAllDistricts(): void { // Retrieve all districts from the database $allDistricts = District::getAll(); @@ -36,7 +43,7 @@ private function getAllDistricts(): void /** * Get the details of a specific district by its ID. */ - private function getDistrictById(): void + public function getDistrictById(): void { $districtId = (int)Utility::splitURL()[3]; @@ -57,55 +64,4 @@ private function getDistrictById(): void 'name' => $district->getName() ]); } - - private function getHandler($routes): ?string - { - foreach ($routes[$_SERVER['REQUEST_METHOD']] as $route => $handler) { - $pattern = str_replace('/', '\/', $route); // Convert to regex pattern - $pattern = preg_replace( - '/\{([a-zA-Z0-9_]+)\}/', - '(?P<$1>[^\/]+)', - $pattern - ); // Replace placeholders with regex capture groups - $pattern = '/^' . $pattern . '$/'; - - if (preg_match($pattern, '/' . Utility::getURL(), $matches)) { - return $handler; - } - } - return null; - } - - /** - * Main entry point for the Districts API. - */ - public function index(): void - { - $routes = [ - 'GET' => [ - '/api/v1/districts' => 'getAllDistricts', - '/api/v1/districts/{id}' => 'getDistrictById', - ] - ]; - - // Handle the request - $handler = $this->getHandler($routes); - - if ($handler !== null) { - $functionName = $handler; - if (method_exists($this, $functionName)) { - call_user_func(array($this, $functionName)); - } else { - // Handle function not found - http_response_code(404); - echo "Function Not Found"; - die(); - } - } else { - // Handle route not found - http_response_code(404); - echo "Route Not Found"; - die(); - } - } } diff --git a/src/controllers/api/Products.php b/src/controllers/api/Products.php index 978005d5..dc87b101 100644 --- a/src/controllers/api/Products.php +++ b/src/controllers/api/Products.php @@ -12,10 +12,27 @@ class Products { use Model; + public static array $routes = [ + 'GET' => [ + '/api/v1/products' => 'getAllProducts', + '/api/v1/products/categories' => 'getProductCategories', + '/api/v1/products/{id}' => 'getProductById', + ], + 'POST' => [ + '/api/v1/products' => 'createProduct', + ], + 'PUT' => [ + '/api/v1/products/{id}' => 'updateProduct', + ], + 'DELETE' => [ + '/api/v1/products/{id}' => 'deleteProduct', + ] + ]; + /** * Get the list of all products available in the store. */ - private function getAllProducts(): void + public function getAllProducts(): void { // Retrieve all products from the database $allProducts = Product::getAll(); @@ -33,7 +50,7 @@ private function getAllProducts(): void /** * Get the details of a specific product by its ID. */ - private function getProductById(): void + public function getProductById(): void { $productId = (int)Utility::splitURL()[3]; @@ -55,7 +72,7 @@ private function getProductById(): void /** * Get the list of product categories. */ - private function getProductCategories(): void + public function getProductCategories(): void { // Retrieve all product categories from the database $categories = Product::getCategories(); @@ -67,7 +84,7 @@ private function getProductCategories(): void /** * Create a new product entry in the database. */ - private function createProduct(): void + public function createProduct(): void { // Retrieve POST data $postData = $_POST; @@ -126,7 +143,7 @@ private function createProduct(): void /** * Delete a product with the specified ID. */ - private function deleteProduct(): void + public function deleteProduct(): void { $productId = (int)Utility::splitURL()[3]; @@ -155,7 +172,7 @@ private function deleteProduct(): void /** * Update the details of a product with the specified ID. */ - private function updateProduct(): void + public function updateProduct(): void { $productId = (int)Utility::splitURL()[3]; @@ -194,65 +211,4 @@ private function updateProduct(): void echo json_encode(['error' => 'Failed to update product']); } } - - private function getHandler($routes): ?string - { - foreach ($routes[$_SERVER['REQUEST_METHOD']] as $route => $handler) { - $pattern = str_replace('/', '\/', $route); // Convert to regex pattern - $pattern = preg_replace( - '/\{([a-zA-Z0-9_]+)\}/', - '(?P<$1>[^\/]+)', - $pattern - ); // Replace placeholders with regex capture groups - $pattern = '/^' . $pattern . '$/'; - - if (preg_match($pattern, '/' . Utility::getURL(), $matches)) { - return $handler; - } - } - return null; - } - - /** - * Main entry point for the Products API. - */ - public function index(): void - { - $routes = [ - 'GET' => [ - '/api/v1/products' => 'getAllProducts', - '/api/v1/products/categories' => 'getProductCategories', - '/api/v1/products/{id}' => 'getProductById', - ], - 'POST' => [ - '/api/v1/products' => 'createProduct', - ], - 'PUT' => [ - '/api/v1/products/{id}' => 'updateProduct', - ], - 'DELETE' => [ - '/api/v1/products/{id}' => 'deleteProduct', - ] - ]; - - // Handle the request - $handler = $this->getHandler($routes); - - if ($handler !== null) { - $functionName = $handler; - if (method_exists($this, $functionName)) { - call_user_func(array($this, $functionName)); - } else { - // Handle function not found - http_response_code(404); - echo "Function Not Found"; - die(); - } - } else { - // Handle route not found - http_response_code(404); - echo "Route Not Found"; - die(); - } - } } diff --git a/src/controllers/api/Reviews.php b/src/controllers/api/Reviews.php index 015c0aa4..bfbcc9f3 100644 --- a/src/controllers/api/Reviews.php +++ b/src/controllers/api/Reviews.php @@ -6,14 +6,31 @@ use Steamy\Core\Utility; use Steamy\Model\Review; +use \Steamy\Model\Product as ProductModel; class Reviews { + public static array $routes = [ + 'GET' => [ + '/api/v1/reviews' => 'getAllReviews', + '/api/v1/products/{id}/reviews' => 'getAllReviewsForProduct', + ], + 'POST' => [ + '/api/v1/reviews' => 'createReview', + ], + 'PUT' => [ + '/api/v1/reviews/{id}' => 'updateReview', + ], + 'DELETE' => [ + '/api/v1/reviews/{id}' => 'deleteReview', + ] + ]; + /** * Get the list of all reviews available. */ - private function getAllReviews(): void + public function getAllReviews(): void { // Retrieve all reviews from the database $allReviews = Review::getAll(); @@ -31,21 +48,21 @@ private function getAllReviews(): void /** * Get all reviews for a particular product by its ID. */ - private function getAllReviewsForProduct(): void + public function getAllReviewsForProduct(): void { $productId = (int)Utility::splitURL()[4]; - // Retrieve all reviews for the specified product from the database - $reviews = Review::getAllReviewsForProduct($productId); - - // Check if Review exists - if ($reviews === null) { - // Review not found, return 404 + // Check if product exists + if (ProductModel::getById($productId) === null) { + // product not found, return 404 http_response_code(404); - echo json_encode(['error' => 'Review not found']); + echo json_encode(['error' => 'Product not found']); return; } + // Retrieve all reviews for the specified product from the database + $reviews = Review::getAllReviewsForProduct($productId); + // Return JSON response echo json_encode($reviews); } @@ -53,13 +70,13 @@ private function getAllReviewsForProduct(): void /** * Create a new review for a product. */ - private function createReview(): 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 + // Check if required fields are present $requiredFields = [ 'product_id', 'client_id', @@ -105,7 +122,7 @@ private function createReview(): void /** * Update the details of a review with the specified ID. */ - private function updateReview(): void + public function updateReview(): void { $reviewId = (int)Utility::splitURL()[3]; @@ -148,7 +165,7 @@ private function updateReview(): void /** * Delete a review with the specified ID. */ - private function deleteReview(): void + public function deleteReview(): void { $reviewId = (int)Utility::splitURL()[3]; @@ -173,64 +190,4 @@ private function deleteReview(): void echo json_encode(['error' => 'Failed to delete review']); } } - - private function getHandler($routes): ?string - { - foreach ($routes[$_SERVER['REQUEST_METHOD']] as $route => $handler) { - $pattern = str_replace('/', '\/', $route); // Convert to regex pattern - $pattern = preg_replace( - '/\{([a-zA-Z0-9_]+)\}/', - '(?P<$1>[^\/]+)', - $pattern - ); // Replace placeholders with regex capture groups - $pattern = '/^' . $pattern . '$/'; - - if (preg_match($pattern, '/' . Utility::getURL(), $matches)) { - return $handler; - } - } - return null; - } - - /** - * Main entry point for the Reviews API. - */ - public function index(): void - { - $routes = [ - 'GET' => [ - '/api/v1/reviews' => 'getAllReviews', - '/api/v1/products/{id}/reviews' => 'getAllReviewsForProduct', - ], - 'POST' => [ - '/api/v1/reviews' => 'createReview', - ], - 'PUT' => [ - '/api/v1/reviews/{id}' => 'updateReview', - ], - 'DELETE' => [ - '/api/v1/reviews/{id}' => 'deleteReview', - ] - ]; - - // Handle the request - $handler = $this->getHandler($routes); - - if ($handler !== null) { - $functionName = $handler; - if (method_exists($this, $functionName)) { - call_user_func([$this, $functionName]); - } else { - // Handle function not found - http_response_code(404); - echo "Function Not Found"; - die(); - } - } else { - // Handle route not found - http_response_code(404); - echo "Route Not Found"; - die(); - } - } } diff --git a/src/controllers/api/Sessions.php b/src/controllers/api/Sessions.php index 48d559c4..537c7c34 100644 --- a/src/controllers/api/Sessions.php +++ b/src/controllers/api/Sessions.php @@ -11,7 +11,14 @@ */ class Sessions { - private function handleLogin(): void + + public static array $routes = [ + 'POST' => [ + '/api/v1/products' => 'handleLogin', + ] + ]; + + public function handleLogin(): void { $email = trim($_POST['email'] ?? ""); $password = trim($_POST['password'] ?? ""); @@ -39,16 +46,4 @@ private function handleLogin(): void $_SESSION['admin_email'] = $email; session_regenerate_id(); } - - public function index(): void - { - switch ($_SERVER['REQUEST_METHOD']) { - case 'POST': - $this->handleLogin(); - break; - default: - http_response_code(400); - die(); - } - } } \ No newline at end of file From 4f404fc2946c24e9412a557062b985aa190fb5bb Mon Sep 17 00:00:00 2001 From: divyesh000 Date: Tue, 21 May 2024 19:42:49 +0400 Subject: [PATCH 05/52] add new API endpoint to retrieve all reviews for a product by ID and implemente corresponding controller logic in Products and Reviews controllers --- src/controllers/api/Products.php | 20 ++++++++++++++++++++ src/controllers/api/Reviews.php | 3 +-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/Products.php b/src/controllers/api/Products.php index dc87b101..c973359f 100644 --- a/src/controllers/api/Products.php +++ b/src/controllers/api/Products.php @@ -17,6 +17,7 @@ class Products '/api/v1/products' => 'getAllProducts', '/api/v1/products/categories' => 'getProductCategories', '/api/v1/products/{id}' => 'getProductById', + '/api/v1/products/{id}/reviews' => 'getAllReviewsForProduct', ], 'POST' => [ '/api/v1/products' => 'createProduct', @@ -211,4 +212,23 @@ public function updateProduct(): void echo json_encode(['error' => 'Failed to update product']); } } + + /** + * Get all reviews for a particular product by its ID. + */ + public function getAllReviewsForProduct(): void + { + // Get product ID from URL + $productId = (int)Utility::splitURL()[3]; + + // Instantiate the Reviews controller + $reviewsController = new Reviews(); + + // Call the method to get all reviews for the specified product + // Since the Reviews controller method expects the ID to be in the URL, we'll set it directly + $_SERVER['REQUEST_URI'] = "/api/v1/products/$productId/reviews"; + + // Call the method from the Reviews controller + $reviewsController->getAllReviewsForProduct(); + } } diff --git a/src/controllers/api/Reviews.php b/src/controllers/api/Reviews.php index bfbcc9f3..3b1608d6 100644 --- a/src/controllers/api/Reviews.php +++ b/src/controllers/api/Reviews.php @@ -50,8 +50,7 @@ public function getAllReviews(): void */ public function getAllReviewsForProduct(): void { - $productId = (int)Utility::splitURL()[4]; - + $productId = (int)Utility::splitURL()[3]; // Check if product exists if (ProductModel::getById($productId) === null) { // product not found, return 404 From c2cccaadfd326f3ef04b95b46295fb3e2327393d Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 22 May 2024 20:57:01 +0400 Subject: [PATCH 06/52] ensure that getAll() and getAllReviewsForProduct() always return array --- src/models/Review.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/models/Review.php b/src/models/Review.php index 0be20ff9..f45d3e9b 100644 --- a/src/models/Review.php +++ b/src/models/Review.php @@ -12,6 +12,7 @@ class Review { use Model; + protected string $table = 'review'; private int $review_id; private int $product_id; @@ -99,6 +100,10 @@ public static function getAll(): array $query = "SELECT * FROM review"; $results = self::query($query); + if (!$results) { + return []; + } + // Fetch all reviews as Review objects $reviews = []; foreach ($results as $result) { @@ -126,7 +131,8 @@ public static function getAllReviewsForProduct(int $productId): array { $query = "SELECT * FROM review WHERE product_id = :product_id"; $params = ['product_id' => $productId]; - return self::query($query, $params); + $result = self::query($query, $params); + return $result ?: []; } /** @@ -135,7 +141,7 @@ public static function getAllReviewsForProduct(int $productId): array * The values are the new review data. * @return bool Success or not */ - public function updateReview (array $newReviewData): bool + public function updateReview(array $newReviewData): bool { // remove review_id (if present) from user data unset($newReviewData['review_id']); @@ -340,7 +346,7 @@ public function getNestedComments(): array // Order the children array of each comment by created_date foreach ($commentMap as $comment) { - usort($comment->children, function($a, $b) { + usort($comment->children, function ($a, $b) { return strtotime($a->created_date) - strtotime($b->created_date); }); } From ee4e1117d39e42b7560125aa629d9d36e4fa650f Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 22 May 2024 21:01:37 +0400 Subject: [PATCH 07/52] add getReviewByID, send appropriate errors when review attributes are invalid in createReview --- src/controllers/api/Reviews.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/controllers/api/Reviews.php b/src/controllers/api/Reviews.php index 3b1608d6..892735fb 100644 --- a/src/controllers/api/Reviews.php +++ b/src/controllers/api/Reviews.php @@ -14,6 +14,7 @@ class Reviews public static array $routes = [ 'GET' => [ '/api/v1/reviews' => 'getAllReviews', + '/api/v1/reviews/{id}' => 'getReviewByID', '/api/v1/products/{id}/reviews' => 'getAllReviewsForProduct', ], 'POST' => [ @@ -45,6 +46,26 @@ public function getAllReviews(): void echo json_encode($result); } + public function getReviewByID(): void + { + $id = (int)Utility::splitURL()[3]; + + // Retrieve all reviews from the database + $review = Review::getByID($id); + + // Check if product exists + if ($review === null) { + // review not found, return 404 + http_response_code(404); + echo json_encode(['error' => 'Review not found']); + return; + } + + // Return JSON response + echo json_encode($review->toArray()); + } + + /** * Get all reviews for a particular product by its ID. */ @@ -106,6 +127,14 @@ public function createReview(): void (int)$postData['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 From 619030cc449c90977a4e911cdae163127730def2 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 22 May 2024 21:20:15 +0400 Subject: [PATCH 08/52] make $routes relative to API_BASE_URI make $routes relative to API_BASE_URI add static variable $API_BASE_URI --- src/controllers/API.php | 3 +++ src/controllers/api/Districts.php | 4 ++-- src/controllers/api/Products.php | 14 +++++++------- src/controllers/api/Reviews.php | 12 ++++++------ src/controllers/api/Sessions.php | 2 +- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/controllers/API.php b/src/controllers/API.php index 84f387f9..a8afffdd 100644 --- a/src/controllers/API.php +++ b/src/controllers/API.php @@ -17,6 +17,8 @@ class API { use Controller; + public static string $API_BASE_URI = '/api/v1'; // root-relative + private string $resource; public function __construct() @@ -56,6 +58,7 @@ private function getHandler(string $controllerName): ?string } foreach ($my_routes as $route => $handler) { + $route = API::$API_BASE_URI . $route; $pattern = str_replace('/', '\/', $route); // Convert to regex pattern $pattern = preg_replace( '/\{([a-zA-Z0-9_]+)\}/', diff --git a/src/controllers/api/Districts.php b/src/controllers/api/Districts.php index ea0f6105..37db780c 100644 --- a/src/controllers/api/Districts.php +++ b/src/controllers/api/Districts.php @@ -14,8 +14,8 @@ class Districts public static array $routes = [ 'GET' => [ - '/api/v1/districts' => 'getAllDistricts', - '/api/v1/districts/{id}' => 'getDistrictById', + '/districts' => 'getAllDistricts', + '/districts/{id}' => 'getDistrictById', ] ]; diff --git a/src/controllers/api/Products.php b/src/controllers/api/Products.php index c973359f..aeecc4b2 100644 --- a/src/controllers/api/Products.php +++ b/src/controllers/api/Products.php @@ -14,19 +14,19 @@ class Products public static array $routes = [ 'GET' => [ - '/api/v1/products' => 'getAllProducts', - '/api/v1/products/categories' => 'getProductCategories', - '/api/v1/products/{id}' => 'getProductById', - '/api/v1/products/{id}/reviews' => 'getAllReviewsForProduct', + '/products' => 'getAllProducts', + '/products/categories' => 'getProductCategories', + '/products/{id}' => 'getProductById', + '/products/{id}/reviews' => 'getAllReviewsForProduct', ], 'POST' => [ - '/api/v1/products' => 'createProduct', + '/products' => 'createProduct', ], 'PUT' => [ - '/api/v1/products/{id}' => 'updateProduct', + '/products/{id}' => 'updateProduct', ], 'DELETE' => [ - '/api/v1/products/{id}' => 'deleteProduct', + '/products/{id}' => 'deleteProduct', ] ]; diff --git a/src/controllers/api/Reviews.php b/src/controllers/api/Reviews.php index 892735fb..b9c680d5 100644 --- a/src/controllers/api/Reviews.php +++ b/src/controllers/api/Reviews.php @@ -13,18 +13,18 @@ class Reviews public static array $routes = [ 'GET' => [ - '/api/v1/reviews' => 'getAllReviews', - '/api/v1/reviews/{id}' => 'getReviewByID', - '/api/v1/products/{id}/reviews' => 'getAllReviewsForProduct', + '/reviews' => 'getAllReviews', + '/reviews/{id}' => 'getReviewByID', + '/products/{id}/reviews' => 'getAllReviewsForProduct', ], 'POST' => [ - '/api/v1/reviews' => 'createReview', + '/reviews' => 'createReview', ], 'PUT' => [ - '/api/v1/reviews/{id}' => 'updateReview', + '/reviews/{id}' => 'updateReview', ], 'DELETE' => [ - '/api/v1/reviews/{id}' => 'deleteReview', + '/reviews/{id}' => 'deleteReview', ] ]; diff --git a/src/controllers/api/Sessions.php b/src/controllers/api/Sessions.php index 537c7c34..39a8101e 100644 --- a/src/controllers/api/Sessions.php +++ b/src/controllers/api/Sessions.php @@ -14,7 +14,7 @@ class Sessions public static array $routes = [ 'POST' => [ - '/api/v1/products' => 'handleLogin', + '/sessions' => 'handleLogin', ] ]; From c9c6dca9ea4413384b2d454d339343989044a23e Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Thu, 23 May 2024 08:11:23 +0400 Subject: [PATCH 09/52] use testing database when request contains X-Test-Env in header --- public/.htaccess | 5 +++++ src/core/config.php | 9 +++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/public/.htaccess b/public/.htaccess index 62c38266..8ed56dcb 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -6,3 +6,8 @@ RewriteCond %{REQUEST_FILENAME} !-d # send everything else to index page RewriteRule ^(.*)$ index.php?url=$1 [NC,L,QSA] + +# pass X-Test-Env header to PHP as $_SERVER['HTTP_X_TEST_ENV']. + + SetEnvIfNoCase X-Test-Env "testing" HTTP_X_TEST_ENV=testing + \ No newline at end of file diff --git a/src/core/config.php b/src/core/config.php index 8d8b887b..c0a9ec47 100644 --- a/src/core/config.php +++ b/src/core/config.php @@ -3,7 +3,7 @@ declare(strict_types=1); // load environment variables -$dotenv = Dotenv\Dotenv::createImmutable(__DIR__.'/../..'); +$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../..'); $dotenv->load(); // define database credentials @@ -11,7 +11,12 @@ define('DB_USERNAME', $_ENV['DB_USERNAME']); define('DB_PASSWORD', $_ENV['DB_PASSWORD']); -if (defined('PHPUNIT_STEAMY_TESTSUITE') && PHPUNIT_STEAMY_TESTSUITE) { + +// Check for a custom header to switch to the testing environment +if (isset($_SERVER['HTTP_X_TEST_ENV']) && $_SERVER['HTTP_X_TEST_ENV'] === 'testing') { + // a request is coming from the testing environment + define('DB_NAME', $_ENV['TEST_DB_NAME']); +} elseif (defined('PHPUNIT_STEAMY_TESTSUITE') && PHPUNIT_STEAMY_TESTSUITE) { // application is currently being tested with phpunit => use testing database define('DB_NAME', $_ENV['TEST_DB_NAME']); } else { From b6f7e1788f55d964d5f64f0f7d2b1528e462d8f7 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Fri, 24 May 2024 19:26:28 +0400 Subject: [PATCH 10/52] change namespace to Steamy\Tests\Model --- tests/models/AdministratorTest.php | 2 +- tests/models/ClientTest.php | 2 +- tests/models/CommentTest.php | 2 +- tests/models/FuzzyTest.php | 2 +- tests/models/OrderProductTest.php | 2 +- tests/models/OrderTest.php | 2 +- tests/models/ProductTest.php | 2 +- tests/models/ReviewTest.php | 2 +- tests/models/StoreTest.php | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/models/AdministratorTest.php b/tests/models/AdministratorTest.php index 4a4d8e33..2c38db26 100644 --- a/tests/models/AdministratorTest.php +++ b/tests/models/AdministratorTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace models; +namespace Steamy\Tests\Model; use Exception; use PHPUnit\Framework\TestCase; diff --git a/tests/models/ClientTest.php b/tests/models/ClientTest.php index a6168119..216b0f7d 100644 --- a/tests/models/ClientTest.php +++ b/tests/models/ClientTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace models; +namespace Steamy\Tests\Model; use Exception; use PHPUnit\Framework\TestCase; diff --git a/tests/models/CommentTest.php b/tests/models/CommentTest.php index 2d7054df..53faf9b5 100644 --- a/tests/models/CommentTest.php +++ b/tests/models/CommentTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace models; +namespace Steamy\Tests\Model; use DateTime; use Exception; diff --git a/tests/models/FuzzyTest.php b/tests/models/FuzzyTest.php index e76bc6bc..e5bc4b88 100644 --- a/tests/models/FuzzyTest.php +++ b/tests/models/FuzzyTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace models; +namespace Steamy\Tests\Model; use PHPUnit\Framework\TestCase; use Steamy\Core\Utility; diff --git a/tests/models/OrderProductTest.php b/tests/models/OrderProductTest.php index f157c5ab..7a4e55c9 100644 --- a/tests/models/OrderProductTest.php +++ b/tests/models/OrderProductTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace models; +namespace Steamy\Tests\Model; use DateTime; use Exception; diff --git a/tests/models/OrderTest.php b/tests/models/OrderTest.php index 69c5fb7a..177060b9 100644 --- a/tests/models/OrderTest.php +++ b/tests/models/OrderTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace models; +namespace Steamy\Tests\Model; use DateTime; use Exception; diff --git a/tests/models/ProductTest.php b/tests/models/ProductTest.php index 1748b9b8..5c66df3a 100644 --- a/tests/models/ProductTest.php +++ b/tests/models/ProductTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace models; +namespace Steamy\Tests\Model; use DateTime; use Exception; diff --git a/tests/models/ReviewTest.php b/tests/models/ReviewTest.php index a6bb4ba0..4011edad 100644 --- a/tests/models/ReviewTest.php +++ b/tests/models/ReviewTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace models; +namespace Steamy\Tests\Model; use DateTime; use Exception; diff --git a/tests/models/StoreTest.php b/tests/models/StoreTest.php index 785238fe..514888af 100644 --- a/tests/models/StoreTest.php +++ b/tests/models/StoreTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace models; +namespace Steamy\Tests\Model; use Exception; use PHPUnit\Framework\TestCase; From 1e570f3a2825a250cb6cd1bee1f969eb24a9f28a Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Fri, 24 May 2024 19:30:42 +0400 Subject: [PATCH 11/52] replace test suite with 2 new ones: models, api --- phpunit.xml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 24c274bd..db2b44ba 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -4,8 +4,11 @@ - - tests/ + + tests/models + + + tests/api \ No newline at end of file From 8b9df8ef158b706b0a35b0d7f683de52ccca5c6d Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Fri, 24 May 2024 19:31:03 +0400 Subject: [PATCH 12/52] add namespace for tests, add new scripts for testing testsuites --- composer.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index a68fb0ca..8db6a6c2 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,9 @@ "nesbot/carbon": "^3.3" }, "scripts": { - "test": "phpunit tests" + "test": "phpunit tests", + "modeltest": "phpunit --testsuite models", + "apitest": "phpunit --testsuite api" }, "autoload": { "psr-4": { @@ -33,7 +35,11 @@ "Steamy\\Core\\": "src/core/", "Steamy\\Controller\\": "src/controllers/", "Steamy\\Controller\\API\\": "src/controllers/api/", - "Steamy\\Model\\": "src/models/" + "Steamy\\Model\\": "src/models/", + "Steamy\\Tests\\": "tests/", + "Steamy\\Tests\\Model\\": "tests/models/", + "Steamy\\Tests\\Api\\": "tests/api/" + } }, "require-dev": { From 255912a5422027b0cb2d1f0d7b7717a04afb32e5 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Fri, 24 May 2024 19:32:20 +0400 Subject: [PATCH 13/52] add tests for product endpoint of api --- tests/api/ProductsTest.php | 124 ++++++++++++++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 7 deletions(-) diff --git a/tests/api/ProductsTest.php b/tests/api/ProductsTest.php index a525ea1a..98c54db4 100644 --- a/tests/api/ProductsTest.php +++ b/tests/api/ProductsTest.php @@ -2,11 +2,13 @@ declare(strict_types=1); -namespace api; +namespace Steamy\Tests\Api; use DateTime; use Exception; use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; use PHPUnit\Framework\TestCase; use GuzzleHttp\Client as GuzzleClient; use Steamy\Core\Database; @@ -17,14 +19,27 @@ final class ProductsTest extends TestCase use Database; private ?GuzzleClient $client; + private Product $dummy_product; /** * @throws Exception */ public function setUp(): void { + // Create a handler stack + $handlerStack = HandlerStack::create(); + + // Add middleware to the handler stack + $handlerStack->push(Middleware::mapRequest(function ($request) { + // Add custom header to each request + return $request->withHeader('X-Test-Env', 'testing'); + })); + $this->client = new GuzzleClient([ - 'base_uri' => $_ENV['API_BASE_URI'] + 'base_uri' => $_ENV['API_BASE_URI'], + 'http_errors' => false, // Optionally disable throwing exceptions for HTTP errors + 'handler' => $handlerStack, + ]); // Create a dummy product for testing @@ -54,17 +69,112 @@ public function tearDown(): void /** * @throws GuzzleException */ - public function testGetEndpoint() + public function testGetAllProducts() { $response = $this->client->get('products'); $this->assertEquals(200, $response->getStatusCode()); $body = $response->getBody(); - $json = json_decode((string)$body, true); - echo json_encode($json, JSON_PRETTY_PRINT) . "\n"; + $json = json_decode($body->getContents(), true); +// echo json_encode($json, JSON_PRETTY_PRINT) . "\n"; + + self::assertIsArray($json); + self::assertCount(1, $json); + + $data = $json[0]; + + $this->assertEquals($this->dummy_product->getName(), $data['name']); + $this->assertEquals($this->dummy_product->getCalories(), $data['calories']); + $this->assertEquals($this->dummy_product->getImgRelativePath(), $data['img_url']); + $this->assertEquals($this->dummy_product->getCategory(), $data['category']); + $this->assertEquals($this->dummy_product->getPrice(), $data['price']); + $this->assertEquals($this->dummy_product->getDescription(), $data['description']); + $this->assertEquals($this->dummy_product->getImgAltText(), $data['img_alt_text']); + + + // only check presence of the following keys but not the actual value + $this->assertArrayHasKey('product_id', $data); + $this->assertArrayHasKey('created_date', $data); + } + + + /** + * @throws GuzzleException + */ + public function testGetProductById() + { + // test valid product ID + $response = $this->client->get('products/' . $this->dummy_product->getProductID()); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode($response->getBody()->getContents(), true); + $this->assertIsArray($data); + $this->assertArrayHasKey('product_id', $data); + $this->assertEquals($this->dummy_product->getProductID(), $data['product_id']); + + // test invalid product ID + $response = $this->client->get('products/-1'); + $this->assertEquals(404, $response->getStatusCode()); + } + + /** + * @throws GuzzleException + */ + public function testGetProductCategories() + { + $response = $this->client->get('products/categories'); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode($response->getBody()->getContents(), true); + $this->assertIsArray($data); + self::assertCount(1, $data); + self::assertEquals($this->dummy_product->getCategory(), $data[0]); + } + + /** + * @throws GuzzleException + */ + public function testCreateProduct() + { + self::markTestIncomplete('Incomplete test'); + $response = $this->client->post('products', [ + 'json' => [ + 'name' => 'Test Product', + 'category' => 'Test Category', + 'price' => 99.99, + // Add more fields as needed + ] + ]); + $this->assertEquals(201, $response->getStatusCode()); + $data = json_decode($response->getBody()->getContents(), true); + $this->assertArrayHasKey('id', $data); + // Add more assertions as needed + } -// $this->assertArrayHasKey('key', $data); -// $this->assertEquals('expected_value', $data['key']); + public function testDeleteProductById() + { + self::markTestIncomplete('Incomplete test'); + $response = $this->client->delete('products/1'); + $this->assertEquals(204, $response->getStatusCode()); + // No content expected, so no further assertions needed } + /** + * @throws GuzzleException + */ + public function testUpdateProductById() + { + self::markTestIncomplete('Incomplete test'); + $response = $this->client->put('products/1', [ + 'json' => [ + 'name' => 'Updated Product', + 'category' => 'Updated Category', + 'price' => 199.99, + // Add more fields as needed + ] + ]); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode($response->getBody()->getContents(), true); + $this->assertArrayHasKey('id', $data); + $this->assertEquals(1, $data['id']); + // Add more assertions as needed + } } \ No newline at end of file From b9b0b0272fb6e1863bee992f14a27fad39770553 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Fri, 24 May 2024 19:35:23 +0400 Subject: [PATCH 14/52] remove unnecessary call to parent setUp in setUp since class has no parent --- tests/models/OrderProductTest.php | 2 -- tests/models/OrderTest.php | 2 -- tests/models/StoreTest.php | 2 -- 3 files changed, 6 deletions(-) diff --git a/tests/models/OrderProductTest.php b/tests/models/OrderProductTest.php index 7a4e55c9..7277e712 100644 --- a/tests/models/OrderProductTest.php +++ b/tests/models/OrderProductTest.php @@ -30,8 +30,6 @@ class OrderProductTest extends TestCase */ public function setUp(): void { - parent::setUp(); - // Initialize a dummy store object for testing $this->dummy_store = new Store( phone_no: "987654321", // Phone number diff --git a/tests/models/OrderTest.php b/tests/models/OrderTest.php index 177060b9..e8f26d26 100644 --- a/tests/models/OrderTest.php +++ b/tests/models/OrderTest.php @@ -30,8 +30,6 @@ class OrderTest extends TestCase */ public function setUp(): void { - parent::setUp(); - // Initialize a dummy store object for testing $this->dummy_store = new Store( phone_no: "987654321", // Phone number diff --git a/tests/models/StoreTest.php b/tests/models/StoreTest.php index 514888af..1c96a991 100644 --- a/tests/models/StoreTest.php +++ b/tests/models/StoreTest.php @@ -21,8 +21,6 @@ class StoreTest extends TestCase */ public function setUp(): void { - parent::setUp(); - // Initialize a dummy store object for testing $this->dummy_store = new Store( phone_no: "12345678", // Phone number From 33ecd02bd087d7339cf3bdaa5b82d84b709255f8 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Fri, 24 May 2024 19:49:29 +0400 Subject: [PATCH 15/52] use 2 separate env files for production and testing --- .gitignore | 4 +++- src/core/Database.php | 4 ++-- src/core/config.php | 19 +++++-------------- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index fed39897..13cd2e83 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ /vendor/ .vscode .idea -.env node_modules .phpunit.result.cache public/js + +.env +.env.testing diff --git a/src/core/Database.php b/src/core/Database.php index 0e32b126..636164b3 100644 --- a/src/core/Database.php +++ b/src/core/Database.php @@ -18,9 +18,9 @@ trait Database */ protected static function connect(): PDO { - $string = "mysql:hostname=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4"; + $string = "mysql:hostname=" . $_ENV['DB_HOST'] . ";dbname=" . $_ENV['DB_NAME'] . ";charset=utf8mb4"; try { - $conn = new PDO($string, DB_USERNAME, DB_PASSWORD); + $conn = new PDO($string, $_ENV['DB_USERNAME'], $_ENV['DB_PASSWORD']); $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); return $conn; } catch (PDOException $e) { diff --git a/src/core/config.php b/src/core/config.php index c0a9ec47..49b52168 100644 --- a/src/core/config.php +++ b/src/core/config.php @@ -2,25 +2,16 @@ declare(strict_types=1); -// load environment variables -$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../..'); -$dotenv->load(); - -// define database credentials -define('DB_HOST', $_ENV['DB_HOST']); -define('DB_USERNAME', $_ENV['DB_USERNAME']); -define('DB_PASSWORD', $_ENV['DB_PASSWORD']); - - // Check for a custom header to switch to the testing environment -if (isset($_SERVER['HTTP_X_TEST_ENV']) && $_SERVER['HTTP_X_TEST_ENV'] === 'testing') { +if ((isset($_SERVER['HTTP_X_TEST_ENV']) && $_SERVER['HTTP_X_TEST_ENV'] === 'testing')) { // a request is coming from the testing environment - define('DB_NAME', $_ENV['TEST_DB_NAME']); + $dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../..', '.env.testing'); } elseif (defined('PHPUNIT_STEAMY_TESTSUITE') && PHPUNIT_STEAMY_TESTSUITE) { // application is currently being tested with phpunit => use testing database - define('DB_NAME', $_ENV['TEST_DB_NAME']); + $dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../..', '.env.testing'); } else { // application is running normally => use production database - define('DB_NAME', $_ENV['PROD_DB_NAME']); + $dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../..'); } +$dotenv->load(); From 9020c00069ca766f73a01153682f0e2f2833f486 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Fri, 24 May 2024 21:10:46 +0400 Subject: [PATCH 16/52] update docs based on new tests structure --- docs/API.md | 3 +++ docs/INSTALLATION_GUIDE.md | 14 +++++++++++--- docs/USAGE_GUIDE.md | 18 +++++++++++++++++- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/API.md b/docs/API.md index d3a08ce4..c5c07cbc 100644 --- a/docs/API.md +++ b/docs/API.md @@ -13,6 +13,9 @@ The Steamy Sips API is a REST API. +Add `X-TEST-ENV` to the header of your request if you want to use the testing database. This is required when running +tests for API. + ## Endpoints There are two types of endpoints: diff --git a/docs/INSTALLATION_GUIDE.md b/docs/INSTALLATION_GUIDE.md index 062ea7f5..9cbbaf6e 100644 --- a/docs/INSTALLATION_GUIDE.md +++ b/docs/INSTALLATION_GUIDE.md @@ -60,9 +60,7 @@ In the root directory of the project, create a `.env` file with the following co DB_HOST="localhost" DB_USERNAME="root" DB_PASSWORD="" - -PROD_DB_NAME="cafe" -TEST_DB_NAME="cafe_test" +DB_NAME="cafe" BUSINESS_GMAIL="" BUSINESS_GMAIL_PASSWORD="" @@ -78,6 +76,16 @@ whenever a client places an order. > a [Gmail App password](https://knowledge.workspace.google.com/kb/how-to-create-app-passwords-000009237) > for `BUSINESS_GMAIL_PASSWORD` instead of your actual gmail account password. +If you want to run tests, create `.env.testing` file in the root directory: + +``` +DB_HOST="localhost" +DB_USERNAME="root" +DB_PASSWORD="" +DB_NAME="cafe_test" +API_BASE_URI="http://steamy.localhost/api/v1/" +``` + ## Database setup Start your MySQL server: diff --git a/docs/USAGE_GUIDE.md b/docs/USAGE_GUIDE.md index a0953dc5..cb8acf7a 100644 --- a/docs/USAGE_GUIDE.md +++ b/docs/USAGE_GUIDE.md @@ -28,12 +28,28 @@ Visit [`http://steamy.localhost/`](http://steamy.localhost/) in your browser to ## Run tests -Assuming that your MySQL database is running, in the root directory of the project run tests: +> [!IMPORTANT] +> You must start your MySQL database to be able to run tests in the `tests/models` folder. +> To run tests in the `tests/api` folder, you need to start both your Apache server and your MySQL database. + +To run all tests: ```bash composer test ``` +To run tests only in `tests/models`: + +```bash +composer modeltest +``` + +To run tests only in `tests/api`: + +```bash +composer apitest +``` + ## Export database To export only the schema of the `cafe` database: From c8a64b0c445b7310bd1807fbb60011e834845c73 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sat, 25 May 2024 12:07:51 +0400 Subject: [PATCH 17/52] rename folder to schemas --- resources/{schema => schemas}/Administrator.json | 0 resources/{schema => schemas}/Client.json | 0 resources/{schema => schemas}/Comment.json | 0 resources/{schema => schemas}/Product.json | 0 resources/{schema => schemas}/Review.json | 0 resources/{schema => schemas}/User.json | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename resources/{schema => schemas}/Administrator.json (100%) rename resources/{schema => schemas}/Client.json (100%) rename resources/{schema => schemas}/Comment.json (100%) rename resources/{schema => schemas}/Product.json (100%) rename resources/{schema => schemas}/Review.json (100%) rename resources/{schema => schemas}/User.json (100%) diff --git a/resources/schema/Administrator.json b/resources/schemas/Administrator.json similarity index 100% rename from resources/schema/Administrator.json rename to resources/schemas/Administrator.json diff --git a/resources/schema/Client.json b/resources/schemas/Client.json similarity index 100% rename from resources/schema/Client.json rename to resources/schemas/Client.json diff --git a/resources/schema/Comment.json b/resources/schemas/Comment.json similarity index 100% rename from resources/schema/Comment.json rename to resources/schemas/Comment.json diff --git a/resources/schema/Product.json b/resources/schemas/Product.json similarity index 100% rename from resources/schema/Product.json rename to resources/schemas/Product.json diff --git a/resources/schema/Review.json b/resources/schemas/Review.json similarity index 100% rename from resources/schema/Review.json rename to resources/schemas/Review.json diff --git a/resources/schema/User.json b/resources/schemas/User.json similarity index 100% rename from resources/schema/User.json rename to resources/schemas/User.json From dc4384808c5654b5b482b1765071432c300142c5 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sat, 25 May 2024 12:19:41 +0400 Subject: [PATCH 18/52] disallow additional properties --- resources/schemas/Client.json | 3 ++- resources/schemas/Comment.json | 3 ++- resources/schemas/Review.json | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/resources/schemas/Client.json b/resources/schemas/Client.json index 842b1652..67ce1edf 100644 --- a/resources/schemas/Client.json +++ b/resources/schemas/Client.json @@ -24,7 +24,8 @@ "street", "city", "district_id" - ] + ], + "additionalProperties": false } ] } diff --git a/resources/schemas/Comment.json b/resources/schemas/Comment.json index a6ed716d..156884b6 100644 --- a/resources/schemas/Comment.json +++ b/resources/schemas/Comment.json @@ -30,5 +30,6 @@ "text", "created_date", "user_id" - ] + ], + "additionalProperties": false } diff --git a/resources/schemas/Review.json b/resources/schemas/Review.json index 04657aa2..ec6e6e30 100644 --- a/resources/schemas/Review.json +++ b/resources/schemas/Review.json @@ -34,5 +34,6 @@ "text", "client_id", "product_id" - ] + ], + "additionalProperties": false } From 3074f9e34954b718577be64ba1c7c37e7e08fb96 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sat, 25 May 2024 14:44:07 +0400 Subject: [PATCH 19/52] use json validation in createProduct --- src/controllers/api/Products.php | 66 ++++++++++++++++---------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/src/controllers/api/Products.php b/src/controllers/api/Products.php index aeecc4b2..d1c20e5a 100644 --- a/src/controllers/api/Products.php +++ b/src/controllers/api/Products.php @@ -4,6 +4,7 @@ namespace Steamy\Controller\API; +use Opis\JsonSchema\{Helper, Validator, Errors\ErrorFormatter}; use Steamy\Core\Utility; use Steamy\Model\Product; use Steamy\Core\Model; @@ -87,45 +88,46 @@ public function getProductCategories(): void */ public function createProduct(): void { - // Retrieve POST data - $postData = $_POST; - - // TODO : Use json schema validation here - // Check if required fields are present - $requiredFields = [ - 'name', - 'calories', - 'img_url', - 'img_alt_text', - 'category', - 'price', - 'description' - ]; - - if (empty($postData)) { + $data = (object) json_decode(file_get_contents("php://input"), true); + + var_dump($data); + + $schemaPath = __DIR__ . '/../../../resources/schemas'; + $validator = new Validator(); + + $validator->resolver()->registerPrefix( + "https://example.com/", + $schemaPath, + ); + + $result = $validator->validate( + $data, + "https://example.com/products/create.json" + ); + + + + if (!($result->isValid())) { + $errors = (new ErrorFormatter())->format($result->error()); + $response = [ + 'errors' => $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; - } - } + return; // Create a new Product object $newProduct = new Product( - $postData['name'], - (int)$postData['calories'], - $postData['img_url'], - $postData['img_alt_text'], - $postData['category'], - (float)$postData['price'], - $postData['description'] + $data['name'], + (int)$data['calories'], + $data['img_url'], + $data['img_alt_text'], + $data['category'], + (float)$data['price'], + $data['description'] ); // Save the new product to the database From 826bcb49677d30b99f777c16dc2d80170929aa01 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sat, 25 May 2024 14:44:23 +0400 Subject: [PATCH 20/52] new schema structure --- .../{Product.json => common/product.json} | 13 ++--------- resources/schemas/products/create.json | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 11 deletions(-) rename resources/schemas/{Product.json => common/product.json} (88%) create mode 100644 resources/schemas/products/create.json diff --git a/resources/schemas/Product.json b/resources/schemas/common/product.json similarity index 88% rename from resources/schemas/Product.json rename to resources/schemas/common/product.json index 3c6f3dff..9a8636ae 100644 --- a/resources/schemas/Product.json +++ b/resources/schemas/common/product.json @@ -1,7 +1,8 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/common/product.json", "title": "Product", - "description": "Schema for a product object", + "description": "A product object", "type": "object", "properties": { "name": { @@ -48,16 +49,6 @@ "description": "The date and time when the product was created" } }, - "required": [ - "name", - "calories", - "img_url", - "img_alt_text", - "category", - "price", - "description", - "created_date" - ], "additionalProperties": false, "patternProperties": { "img_url": { diff --git a/resources/schemas/products/create.json b/resources/schemas/products/create.json new file mode 100644 index 00000000..f1391379 --- /dev/null +++ b/resources/schemas/products/create.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/products/create.json", + "title": "Create Product", + "properties": { + "product": { + "$ref": "https://example.com/definitions/product.json" + } + }, + "required": [ + "product" + ], + "definitions": { + "product": { + "required": [ + "name", + "calories", + "img_url" + ] + } + } +} From b99a5566a02b56ac1d9f598f9288a87781ebebbb Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 2 Jun 2024 15:09:20 +0400 Subject: [PATCH 21/52] remove incorrectly merged file --- tests/ClientTest.php | 232 ------------------------------------------- 1 file changed, 232 deletions(-) delete mode 100644 tests/ClientTest.php diff --git a/tests/ClientTest.php b/tests/ClientTest.php deleted file mode 100644 index 6eb2a10b..00000000 --- a/tests/ClientTest.php +++ /dev/null @@ -1,232 +0,0 @@ -dummy_client = new Client( - "john_u@gmail.com", - "john", - "johhny", - "abcd", - "13213431", - $address - ); - - $success = $this->dummy_client->save(); - if (!$success) { - throw new Exception('Unable to save client'); - } - } - - public function tearDown(): void - { - $this->dummy_client = null; - - // Clear all data from client and user tables - self::query('DELETE FROM client; DELETE FROM user;'); - } - - public function testConstructor(): void - { - // check if fields were correctly set - self::assertEquals("john_u@gmail.com", $this->dummy_client->getEmail()); - self::assertEquals("john", $this->dummy_client->getFirstName()); - self::assertEquals("johhny", $this->dummy_client->getLastName()); - self::assertEquals("13213431", $this->dummy_client->getPhoneNo()); - self::assertEquals("Royal Road, Curepipe, Moka", $this->dummy_client->getAddress()->getFormattedAddress()); - } - - public function testToArray(): void - { - $result = $this->dummy_client->toArray(); - - // check if all required keys are present - $this->assertArrayHasKey('user_id', $result); - $this->assertArrayHasKey('email', $result); - $this->assertArrayHasKey('first_name', $result); - $this->assertArrayHasKey('last_name', $result); - $this->assertArrayHasKey('phone_no', $result); - $this->assertArrayHasKey('district_id', $result); - $this->assertArrayHasKey('street', $result); - $this->assertArrayHasKey('city', $result); - $this->assertArrayHasKey('password', $result); - - // check if actual values are correct - self::assertEquals("john_u@gmail.com", $result['email']); - self::assertEquals("john", $result['first_name']); - self::assertEquals("johhny", $result['last_name']); - self::assertEquals("13213431", $result['phone_no']); - self::assertEquals("Royal Road", $result['street']); - self::assertEquals("Curepipe", $result['city']); - self::assertEquals(1, $result['district_id']); - } - - public function testValidate(): void - { - $client = new Client( - "", - "", - "", - "abcd", - "", - new Location(), // pass an empty Location object for testing - ); - - // Test if existence checks work - self::assertEquals([ - 'email' => 'Invalid email format', - 'first_name' => 'First name must be at least 3 characters long', - 'last_name' => 'Last name must be at least 3 characters long', - 'phone_no' => 'Phone number must be at least 7 characters long', - 'district' => 'District does not exist' - ], $client->validate()); - - // Test for range checks - $client = new Client( - "a@a.com", - "Jo", - "Doe", - "1234567", - "123456", - new Location(), // pass an empty Location object for testing - ); - - self::assertEquals([ - 'first_name' => 'First name must be at least 3 characters long', - 'phone_no' => 'Phone number must be at least 7 characters long', - 'district' => 'District does not exist' - ], $client->validate()); - } - - public function testVerifyPassword(): void - { - // verify true password - self::assertTrue($this->dummy_client->verifyPassword("abcd")); - - // reject empty string - self::assertFalse($this->dummy_client->verifyPassword("")); - - // reject any other string - self::assertFalse($this->dummy_client->verifyPassword("abcde")); - self::assertFalse($this->dummy_client->verifyPassword("abcd ")); - self::assertFalse($this->dummy_client->verifyPassword(" abcd")); - } - - /** - * @dataProvider getByIDProvider - */ - public static function testGetByID(int $userID, ?string $expectedEmail): void - { - $client = Client::getByID($userID); - if ($expectedEmail !== null) { - self::assertNotNull($client); - self::assertEquals($expectedEmail, $client->getEmail()); - } else { - self::assertNull($client); - } - } - - public static function getByIDProvider(): array - { - return [ - [999, null], // Non-existing user - [-1, null], // Negative ID - ]; - } - - /** - * @dataProvider getByEmailProvider - */ - public static function testGetByEmail(string $email, ?string $expectedEmail): void - { - $client = Client::getByEmail($email); - if ($expectedEmail !== null) { - self::assertNotNull($client); - self::assertEquals($expectedEmail, $client->getEmail()); - } else { - self::assertNull($client); - } - } - - public static function getByEmailProvider(): array - { - return [ - ['john_u@gmail.com', 'john_u@gmail.com'], // Existing email - ['nonexistent@gmail.com', null], // Non-existing email - ['invalidemail', null], // Invalid email format - ]; - } - - /** - * @dataProvider updateUserProvider - */ - public static function testUpdateUser(bool $updatePassword, bool $success): void - { - // Create a client with a known ID - $client = Client::getByEmail('john_u@gmail.com'); - if ($client === null) { - self::fail('Failed to fetch client'); - } - - // Update user and check if successful - $client->setFirstName('UpdatedName'); - $client->setLastName('UpdatedLastName'); - $client->getAddress()->setCity('UpdatedCity'); - - if ($updatePassword) { - $client->setPassword('newPassword'); - } - - $result = $client->updateUser($updatePassword); - self::assertEquals($success, $result); - - // Check if data was actually updated in the database - $updatedClient = Client::getByID($client->getUserID()); - if ($updatedClient === null) { - self::fail('Failed to fetch updated client'); - } - - self::assertEquals('UpdatedName', $updatedClient->getFirstName()); - self::assertEquals('UpdatedLastName', $updatedClient->getLastName()); - self::assertEquals('UpdatedCity', $updatedClient->getAddress()->getCity()); - } - - public static function updateUserProvider(): array - { - return [ - [false, true], // Update without password change - [true, true], // Update with password change - ]; - } - - public function testDeleteUser(): void - { - // Fetch the client by email to get its ID - $client = Client::getByEmail('john_u@gmail.com'); - if ($client === null) { - self::fail('Failed to fetch client'); - } - - // Delete the user - $client->deleteUser(); - - // Attempt to fetch the user again - $deletedClient = Client::getByID($client->getUserID()); - - // Ensure the user does not exist anymore - self::assertNull($deletedClient); - } -} From 310b6c6ce2a5fd8f2fcf948f43eacfe03554de7c Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 2 Jun 2024 16:03:35 +0400 Subject: [PATCH 22/52] fix empty array bug in getAll foreach() argument must be of type array|object --- src/models/Product.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/models/Product.php b/src/models/Product.php index 1035c4b9..531a2eda 100644 --- a/src/models/Product.php +++ b/src/models/Product.php @@ -114,6 +114,10 @@ public static function getAll(): array $query = "SELECT * FROM product"; $results = self::query($query); + if (empty($results)) { + return []; + } + // convert results to an array of Product $products = []; foreach ($results as $result) { From d61d650f0c2bf104a5625bf2fb787386a38cd18a Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 2 Jun 2024 16:04:17 +0400 Subject: [PATCH 23/52] move faker to dev dependency --- composer.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index b7b08818..088ff18f 100644 --- a/composer.json +++ b/composer.json @@ -22,8 +22,7 @@ "vlucas/phpdotenv": "^5.6", "phpmailer/phpmailer": "^6.9", "opis/json-schema": "^2.3", - "nesbot/carbon": "^3.3", - "fakerphp/faker": "^1.23" + "nesbot/carbon": "^3.3" }, "scripts": { "test": "phpunit tests", @@ -40,11 +39,11 @@ "Steamy\\Tests\\": "tests/", "Steamy\\Tests\\Model\\": "tests/models/", "Steamy\\Tests\\Api\\": "tests/api/" - } }, "require-dev": { "phpunit/phpunit": "^10.5", - "guzzlehttp/guzzle": "^7.0" + "guzzlehttp/guzzle": "^7.0", + "fakerphp/faker": "^1.23" } } From 6469c91a357e8035350a618b3df87bfeb72d25fc Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 2 Jun 2024 16:04:33 +0400 Subject: [PATCH 24/52] update versions so that php 8.1 is enough --- composer.lock | 893 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 711 insertions(+), 182 deletions(-) diff --git a/composer.lock b/composer.lock index 5d44cac9..66ee9fde 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "806bf0c7a9b1ff411f27ccfcab5af00c", + "content-hash": "232edb4d48e7f32ade38be3e08861d6e", "packages": [ { "name": "carbonphp/carbon-doctrine-types", @@ -75,69 +75,6 @@ ], "time": "2024-02-09T16:56:22+00:00" }, - { - "name": "fakerphp/faker", - "version": "v1.23.1", - "source": { - "type": "git", - "url": "https://github.com/FakerPHP/Faker.git", - "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/bfb4fe148adbf78eff521199619b93a52ae3554b", - "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0", - "psr/container": "^1.0 || ^2.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" - }, - "conflict": { - "fzaninotto/faker": "*" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", - "doctrine/persistence": "^1.3 || ^2.0", - "ext-intl": "*", - "phpunit/phpunit": "^9.5.26", - "symfony/phpunit-bridge": "^5.4.16" - }, - "suggest": { - "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", - "ext-curl": "Required by Faker\\Provider\\Image to download images.", - "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", - "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", - "ext-mbstring": "Required for multibyte Unicode string functionality." - }, - "type": "library", - "autoload": { - "psr-4": { - "Faker\\": "src/Faker/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "François Zaninotto" - } - ], - "description": "Faker is a PHP library that generates fake data for you.", - "keywords": [ - "data", - "faker", - "fixtures" - ], - "support": { - "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.23.1" - }, - "time": "2024-01-02T13:46:09+00:00" - }, { "name": "graham-campbell/result-type", "version": "v1.1.2", @@ -700,59 +637,6 @@ }, "time": "2022-11-25T14:36:26+00:00" }, - { - "name": "psr/container", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" - }, - "time": "2021-11-05T16:47:00+00:00" - }, { "name": "symfony/clock", "version": "v6.4.7", @@ -1479,140 +1363,528 @@ ], "packages-dev": [ { - "name": "myclabs/deep-copy", - "version": "1.11.1", + "name": "fakerphp/faker", + "version": "v1.23.1", "source": { "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/bfb4fe148adbf78eff521199619b93a52ae3554b", + "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" }, "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "fzaninotto/faker": "*" }, "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." }, "type": "library", "autoload": { - "files": [ - "src/DeepCopy/deep_copy.php" - ], "psr-4": { - "DeepCopy\\": "src/DeepCopy/" + "Faker\\": "src/Faker/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Create deep copies (clones) of your objects", + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" + "data", + "faker", + "fixtures" ], "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.23.1" }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-01-02T13:46:09+00:00" }, { - "name": "nikic/php-parser", - "version": "v5.0.0", + "name": "guzzlehttp/guzzle", + "version": "7.8.1", "source": { "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc" + "url": "https://github.com/guzzle/guzzle.git", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4a21235f7e56e713259a6f76bf4b5ea08502b9dc", - "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-json": "*", - "ext-tokenizer": "*", - "php": ">=7.4" + "guzzlehttp/promises": "^1.5.3 || ^2.0.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" }, "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" }, - "bin": [ - "bin/php-parse" - ], "type": "library", "extra": { - "branch-alias": { - "dev-master": "5.0-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { + "files": [ + "src/functions_include.php" + ], "psr-4": { - "PhpParser\\": "lib/PhpParser" + "GuzzleHttp\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Nikita Popov" + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" } ], - "description": "A PHP parser written in PHP", + "description": "Guzzle is a PHP HTTP client library", "keywords": [ - "parser", - "php" + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" ], "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.0" + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.8.1" }, - "time": "2024-01-07T17:17:35+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:35:24+00:00" }, { - "name": "phar-io/manifest", - "version": "2.0.3", + "name": "guzzlehttp/promises", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "url": "https://github.com/guzzle/promises.git", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:19:20+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.6.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:05:35+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.0.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4a21235f7e56e713259a6f76bf4b5ea08502b9dc", + "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.0" + }, + "time": "2024-01-07T17:17:35+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", "php": "^7.2 || ^8.0" }, @@ -2128,6 +2400,263 @@ ], "time": "2024-01-14T16:40:30+00:00" }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, { "name": "sebastian/cli-parser", "version": "2.0.0", From c98a3416e75d24df3405e3d7ce563ac5c7c51e30 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 2 Jun 2024 17:47:29 +0400 Subject: [PATCH 25/52] create a test helper trait --- tests/helpers/TestHelper.php | 172 +++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 tests/helpers/TestHelper.php diff --git a/tests/helpers/TestHelper.php b/tests/helpers/TestHelper.php new file mode 100644 index 00000000..3c0a8f34 --- /dev/null +++ b/tests/helpers/TestHelper.php @@ -0,0 +1,172 @@ +beginTransaction(); + + // Order of deletion is important to prevent foreign key violation + $query = <<< SQL + DELETE FROM password_change_request; + + DELETE FROM `order_product`; + DELETE FROM `order`; + + DELETE FROM `comment`; + DELETE FROM `review`; + + DELETE FROM `administrator`; + DELETE FROM `client`; + DELETE FROM `user`; + + DELETE FROM `store_product`; + DELETE FROM `store`; + DELETE FROM `product`; + SQL; + + $conn->exec($query); + $conn->commit(); + $conn = null; + } + + /** + * Creates a client and saves it to database + * @return Client + * @throws Exception + */ + public static function createClient(): Client + { + $client = new Client( + self::$faker->email(), + self::$faker->firstName(), + self::$faker->lastName(), + self::$faker->password(), + self::$faker->phoneNumber(), + new Location(self::$faker->streetAddress(), self::$faker->city(), self::$faker->numberBetween(1, 9)) + ); + + $success = $client->save(); + if (!$success) { + throw new Exception('Unable to save a unique client to database'); + } + return $client; + } + + /** + * Creates a product and saves it to database. + * @return Product + * @throws Exception + */ + public static function createProduct(): Product + { + $product = new Product( + self::$faker->company(), + 70, + "Velvet.jpeg", + self::$faker->sentence(), + self::$faker->word(), + 6.50, + self::$faker->sentence(), + new DateTime() + ); + + $success = $product->save(); + + if (!$success) { + throw new Exception('Unable to save product to database'); + } + + return $product; + } + + /** + * 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 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 + { + if ($verified) { + // place an order for client and product + + // create store + $store = new Store( + phone_no: "13213431", + address: new Location( + street: "Royal", + city: "Curepipe", + district_id: 1, + latitude: 50, + longitude: 50 + ) + ); + $success = $store->save(); + if (!$success) { + throw new Exception('Unable to create store'); + } + + // Add stock to the store for the product to be bought + $store->addProductStock($product->getProductID(), 10); + + $order = new Order($store->getStoreID(), $client->getUserID(), [ + new OrderProduct($product->getProductID(), 'small', 'oat', 1) + ]); + + $success = $order->save(); + if (!$success) { + throw new Exception('Unable to save order'); + } + } + + $review = new Review( + product_id: $product->getProductID(), + client_id: $client->getUserID(), + text: "This is a test review.", + rating: 5 + ); + + $success = $review->save(); + + if (!$success) { + throw new Exception('Unable to save review'); + } + + return $review; + } + + public static function logAction($action) + { + // Implementation of logging an action + } +} \ No newline at end of file From 702c0d833cdc67ef835b5ba7f6d38a496744aebb Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 2 Jun 2024 17:48:16 +0400 Subject: [PATCH 26/52] use new test helper --- tests/models/AdministratorTest.php | 8 +- tests/models/ClientTest.php | 5 +- tests/models/CommentTest.php | 8 +- tests/models/OrderProductTest.php | 8 +- tests/models/OrderTest.php | 8 +- tests/models/ProductTest.php | 129 ++++++++++++------------ tests/models/ReviewTest.php | 151 +++++------------------------ tests/models/StoreTest.php | 6 +- 8 files changed, 111 insertions(+), 212 deletions(-) diff --git a/tests/models/AdministratorTest.php b/tests/models/AdministratorTest.php index 2c38db26..1bf0ce2d 100644 --- a/tests/models/AdministratorTest.php +++ b/tests/models/AdministratorTest.php @@ -6,12 +6,12 @@ use Exception; use PHPUnit\Framework\TestCase; -use Steamy\Core\Database; use Steamy\Model\Administrator; +use Steamy\Tests\helpers\TestHelper; final class AdministratorTest extends TestCase { - use Database; + use TestHelper; private ?Administrator $dummy_admin; @@ -36,9 +36,7 @@ public function tearDown(): void { // Clear the administrator object $this->dummy_admin = null; - - // Clear all data from administrator and user tables - self::query('DELETE FROM administrator; DELETE FROM user;'); + self::resetDatabase(); } public function testConstructor(): void diff --git a/tests/models/ClientTest.php b/tests/models/ClientTest.php index 216b0f7d..05cc66dd 100644 --- a/tests/models/ClientTest.php +++ b/tests/models/ClientTest.php @@ -9,11 +9,12 @@ use Steamy\Core\Database; use Steamy\Model\Client; use Steamy\Model\Location; +use Steamy\Tests\helpers\TestHelper; final class ClientTest extends TestCase { - use Database; + use TestHelper; private ?Client $dummy_client; @@ -39,7 +40,7 @@ public function tearDown(): void $this->dummy_client = null; // Clear all data from client and user tables - self::query('DELETE FROM client; DELETE FROM user;'); + self::resetDatabase(); } public function testConstructor(): void diff --git a/tests/models/CommentTest.php b/tests/models/CommentTest.php index 53faf9b5..bfe266ea 100644 --- a/tests/models/CommentTest.php +++ b/tests/models/CommentTest.php @@ -7,16 +7,16 @@ use DateTime; use Exception; use PHPUnit\Framework\TestCase; -use Steamy\Core\Database; use Steamy\Model\Client; use Steamy\Model\Comment; use Steamy\Model\Location; use Steamy\Model\Product; use Steamy\Model\Review; +use Steamy\Tests\helpers\TestHelper; class CommentTest extends TestCase { - use Database; + use TestHelper; private ?Comment $dummy_comment; private ?Review $dummy_review; @@ -92,9 +92,7 @@ public function tearDown(): void $this->dummy_product = null; // clear all data from review and client tables - self::query( - 'DELETE FROM comment; DELETE FROM review; DELETE FROM client; DELETE FROM user; DELETE FROM store_product; DELETE FROM product;' - ); + self::resetDatabase(); } public function testConstructor(): void diff --git a/tests/models/OrderProductTest.php b/tests/models/OrderProductTest.php index 7277e712..7a3e0b7c 100644 --- a/tests/models/OrderProductTest.php +++ b/tests/models/OrderProductTest.php @@ -7,17 +7,17 @@ use DateTime; use Exception; use PHPUnit\Framework\TestCase; -use Steamy\Core\Database; use Steamy\Model\Client; use Steamy\Model\Location; use Steamy\Model\Order; use Steamy\Model\OrderProduct; use Steamy\Model\Product; use Steamy\Model\Store; +use Steamy\Tests\helpers\TestHelper; class OrderProductTest extends TestCase { - use Database; + use TestHelper; private ?Order $dummy_order; private ?Client $client; @@ -119,9 +119,7 @@ public function tearDown(): void $this->line_items = []; // Clear all data from relevant tables - self::query( - 'DELETE FROM order_product; DELETE FROM `order`; DELETE FROM client; DELETE FROM user; DELETE FROM store_product; DELETE FROM product; DELETE FROM store;' - ); + self::resetDatabase(); } public function testValidate(): void diff --git a/tests/models/OrderTest.php b/tests/models/OrderTest.php index e8f26d26..5d855fad 100644 --- a/tests/models/OrderTest.php +++ b/tests/models/OrderTest.php @@ -7,7 +7,6 @@ use DateTime; use Exception; use PHPUnit\Framework\TestCase; -use Steamy\Core\Database; use Steamy\Model\Client; use Steamy\Model\Location; use Steamy\Model\Order; @@ -15,10 +14,11 @@ use Steamy\Model\OrderStatus; use Steamy\Model\Product; use Steamy\Model\Store; +use Steamy\Tests\helpers\TestHelper; class OrderTest extends TestCase { - use Database; + use TestHelper; private ?Order $dummy_order = null; private ?Client $client = null; @@ -126,9 +126,7 @@ public function tearDown(): void $this->line_items = []; // Clear all data from relevant tables - self::query( - 'DELETE FROM order_product; DELETE FROM `order`; DELETE FROM client; DELETE FROM user; DELETE FROM store_product; DELETE FROM product; DELETE FROM store;' - ); + self::resetDatabase(); } public function testConstructor(): void diff --git a/tests/models/ProductTest.php b/tests/models/ProductTest.php index e849e78c..351150a1 100644 --- a/tests/models/ProductTest.php +++ b/tests/models/ProductTest.php @@ -2,72 +2,67 @@ declare(strict_types=1); +namespace Steamy\Tests\Model; + +use DateTime; +use Exception; +use Faker\Factory; use PHPUnit\Framework\TestCase; use Steamy\Model\Product; use Steamy\Model\Review; -use Steamy\Core\Database; -use Steamy\Model\Location; use Steamy\Model\Client; +use Steamy\Tests\helpers\TestHelper; +use Throwable; final class ProductTest extends TestCase { - use Database; + use TestHelper; private ?Product $dummy_product; private ?Client $dummy_client; private ?Review $dummy_review; + + public static function setUpBeforeClass(): void + { + self::$faker = Factory::create(); + self::$seed = mt_rand(); + self::$faker->seed(self::$seed); + } + + public static function tearDownAfterClass(): void + { + self::$faker = null; + } + + public function onNotSuccessfulTest(Throwable $t): never + { + $seed = self::$seed; + + $error_message = <<< EOL + + ------------ Faker seed ------------ + Faker seed for failed test: $seed + ------------------------------------ + EOL; + + error_log($error_message); + parent::onNotSuccessfulTest($t); + } + /** * @throws Exception */ public function setUp(): void { - $address = new Location("Royal Road", "Curepipe", 1); - $this->dummy_client = new Client( - "jo@gmail.com", - "john", - "johnny", - "abcd", - "13213431", - $address - ); - - $success = $this->dummy_client->save(); - if (!$success) { - throw new Exception('Unable to save client'); - } + $this->dummy_client = self::createClient(); // Create a dummy product for testing - $this->dummy_product = new Product( - "Velvet Bean", - 70, - "Velvet.jpeg", - "Velvet Bean Image", - "Velvet", - 6.50, - "Each bottle contains 90% Pure Coffee powder and 10% Velvet bean Powder", - new DateTime() - ); - - $success = $this->dummy_product->save(); - if (!$success) { - throw new Exception('Unable to save product'); - } + $this->dummy_product = self::createProduct(); // Create a review object and save to the database - $this->dummy_review = new Review( - product_id: $this->dummy_product->getProductID(), - client_id: $this->dummy_client->getUserID(), - text: "This is a test review.", - rating: 5, - created_date: new DateTime() - ); - $success = $this->dummy_review->save(); - - if (!$success) { - throw new Exception('Unable to save review'); - } + $this->dummy_review = self::createReview($this->dummy_product, $this->dummy_client); } public function tearDown(): void @@ -76,26 +71,38 @@ public function tearDown(): void $this->dummy_client = null; $this->dummy_review = null; - // Clear all data from product, review, and client tables - self::query('DELETE FROM review; DELETE FROM product; DELETE FROM client; DELETE FROM user;'); + self::resetDatabase(); } public function testConstructor(): void { + // Do not use dummy_product to test constructor as dummy_product attributes may change + + $product = new Product( + "Velvet Bean", + 70, + "Velvet.jpeg", + "Velvet Bean Image", + "Velvet", + 6.50, + "Each bottle contains 90% Pure Coffee powder and 10% Velvet bean Powder", + new DateTime() + ); + // Check if product attributes are correctly set - self::assertEquals("Velvet Bean", $this->dummy_product->getName()); - self::assertEquals(70, $this->dummy_product->getCalories()); - self::assertEquals("Velvet.jpeg", $this->dummy_product->getImgRelativePath()); - self::assertEquals("Velvet Bean Image", $this->dummy_product->getImgAltText()); - self::assertEquals("Velvet", $this->dummy_product->getCategory()); - self::assertEquals(6.50, $this->dummy_product->getPrice()); + self::assertEquals("Velvet Bean", $product->getName()); + self::assertEquals(70, $product->getCalories()); + self::assertEquals("Velvet.jpeg", $product->getImgRelativePath()); + self::assertEquals("Velvet Bean Image", $product->getImgAltText()); + self::assertEquals("Velvet", $product->getCategory()); + self::assertEquals(6.50, $product->getPrice()); self::assertEquals( "Each bottle contains 90% Pure Coffee powder and 10% Velvet bean Powder", - $this->dummy_product->getDescription() + $product->getDescription() ); self::assertInstanceOf( DateTime::class, - $this->dummy_product->getCreatedDate() + $product->getCreatedDate() ); // Check if created_date is an instance of DateTime } @@ -115,14 +122,14 @@ public function testToArray(): void $this->assertArrayHasKey('created_date', $result); // Ensure created_date is included in toArray result // Check if the actual values are correct - self::assertEquals("Velvet Bean", $result['name']); - self::assertEquals(70, $result['calories']); - self::assertEquals("Velvet.jpeg", $result['img_url']); - self::assertEquals("Velvet Bean Image", $result['img_alt_text']); - self::assertEquals("Velvet", $result['category']); - self::assertEquals(6.50, $result['price']); + self::assertEquals($this->dummy_product->getName(), $result['name']); + self::assertEquals($this->dummy_product->getCalories(), $result['calories']); + self::assertEquals($this->dummy_product->getImgRelativePath(), $result['img_url']); + self::assertEquals($this->dummy_product->getImgAltText(), $result['img_alt_text']); + self::assertEquals($this->dummy_product->getCategory(), $result['category']); + self::assertEquals($this->dummy_product->getPrice(), $result['price']); self::assertEquals( - "Each bottle contains 90% Pure Coffee powder and 10% Velvet bean Powder", + $this->dummy_product->getDescription(), $result['description'] ); self::assertInstanceOf( diff --git a/tests/models/ReviewTest.php b/tests/models/ReviewTest.php index fee94c0b..fb2ce54e 100644 --- a/tests/models/ReviewTest.php +++ b/tests/models/ReviewTest.php @@ -2,23 +2,23 @@ declare(strict_types=1); +namespace Steamy\Tests\Model; + +use DateTime; +use Exception; use PHPUnit\Framework\TestCase; -use Steamy\Core\Database; use Steamy\Model\Client; use Steamy\Model\Location; -use Steamy\Model\Order; -use Steamy\Model\OrderProduct; use Steamy\Model\Review; use Steamy\Model\Product; -use Steamy\Model\Store; use Faker\Factory; -use Faker\Generator; +use Steamy\Tests\helpers\TestHelper; +use Throwable; final class ReviewTest extends TestCase { - use Database; + use TestHelper; - private static ?Generator $faker; private ?Review $dummy_review; private ?Client $reviewer; private ?Product $dummy_product; @@ -26,6 +26,8 @@ final class ReviewTest extends TestCase public static function setUpBeforeClass(): void { self::$faker = Factory::create(); + self::$seed = mt_rand(); + self::$faker->seed(self::$seed); } public static function tearDownAfterClass(): void @@ -33,6 +35,21 @@ public static function tearDownAfterClass(): void self::$faker = null; } + public function onNotSuccessfulTest(Throwable $t): never + { + $seed = self::$seed; + + $error_message = <<< EOL + + ------------ Faker seed ------------ + Faker seed for failed test: $seed + ------------------------------------ + EOL; + + error_log($error_message); + parent::onNotSuccessfulTest($t); + } + /** * Clears previously inserted data in database. * @return void @@ -43,125 +60,7 @@ public function tearDown(): void $this->reviewer = null; $this->dummy_product = null; - - self::query( - "DELETE FROM order_product; - DELETE FROM `order`; - DELETE FROM comment; - DELETE FROM review; - DELETE FROM client; - DELETE FROM user; - DELETE FROM store_product; - DELETE FROM store; - DELETE FROM product; - " - ); - } - - /** - * Creates a client and saves it to database - * @return Client - * @throws Exception - */ - public static function createClient(): Client - { - $client = new Client( - self::$faker->email(), - self::$faker->name(), - self::$faker->name(), - "User0", - "13213431", - new Location("Royal Road", "Curepipe", 1) - ); - - $success = $client->save(); - if (!$success) { - throw new Exception('Unable to save client'); - } - return $client; - } - - /** - * Creates a product and saves it to database. - * @return Product - * @throws Exception - */ - public static function createProduct(): Product - { - $product = new Product( - "Velvet Bean", - 70, - "Velvet.jpeg", - "Velvet Bean Image", - "Velvet", - 6.50, - "Each bottle contains 90% Pure Coffee powder and 10% Velvet bean Powder", - new DateTime() - ); - - $success = $product->save(); - if (!$success) { - throw new Exception('Unable to save product'); - } - return $product; - } - - /** - * 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 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 - { - if ($verified) { - // place an order for client and product - - // create store - $store = new Store( - phone_no: "13213431", - address: new Location( - street: "Royal", - city: "Curepipe", - district_id: 1, - latitude: 50, - longitude: 50 - ) - ); - $success = $store->save(); - if (!$success) { - throw new Exception('Unable to create store'); - } - - // Add stock to the store for the product to be bought - $store->addProductStock($product->getProductID(), 10); - - $order = new Order($store->getStoreID(), $client->getUserID(), [ - new OrderProduct($product->getProductID(), 'small', 'oat', 1) - ]); - - $success = $order->save(); - if (!$success) { - throw new Exception('Unable to save order'); - } - } - - $review = new Review( - product_id: $product->getProductID(), - client_id: $client->getUserID(), - text: "This is a test review.", - rating: 5 - ); - - $success = $review->save(); - - if (!$success) { - throw new Exception('Unable to save review'); - } - - return $review; + self::resetDatabase(); } /** diff --git a/tests/models/StoreTest.php b/tests/models/StoreTest.php index 1c96a991..723ce17a 100644 --- a/tests/models/StoreTest.php +++ b/tests/models/StoreTest.php @@ -6,13 +6,13 @@ use Exception; use PHPUnit\Framework\TestCase; -use Steamy\Core\Database; use Steamy\Model\Location; use Steamy\Model\Store; +use Steamy\Tests\helpers\TestHelper; class StoreTest extends TestCase { - use Database; + use TestHelper; private ?Store $dummy_store; @@ -55,7 +55,7 @@ public function tearDown(): void } // clear all data from store tables - self::query('DELETE FROM store;'); + self::resetDatabase(); } /** From 53097642c63eb14ae4a735a527ef06728f93638d Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 2 Jun 2024 18:54:49 +0400 Subject: [PATCH 27/52] add new methods and make tests more random - ensure that client email is unique - add initFaker, printFakerSeed, createStore --- tests/helpers/TestHelper.php | 109 +++++++++++++++++++++++++---------- tests/models/ProductTest.php | 39 +++++-------- tests/models/ReviewTest.php | 16 +---- 3 files changed, 97 insertions(+), 67 deletions(-) diff --git a/tests/helpers/TestHelper.php b/tests/helpers/TestHelper.php index 3c0a8f34..f9ca821b 100644 --- a/tests/helpers/TestHelper.php +++ b/tests/helpers/TestHelper.php @@ -4,8 +4,8 @@ namespace Steamy\Tests\helpers; -use DateTime; use Exception; +use Faker\Factory; use Steamy\Core\Database; use Steamy\Model\Client; use Steamy\Model\Location; @@ -23,6 +23,35 @@ trait TestHelper private static ?Generator $faker; private static int $seed; + /** + * Initializes faker generator with a random seed + * @return void + */ + public static function initFaker(): void + { + self::$faker = Factory::create(); + self::$seed = mt_rand(); + self::$faker->seed(self::$seed); + } + + /** + * Prints current faker seed to terminal + * @return void + */ + public static function printFakerSeed(): void + { + $seed = self::$seed; + + $error_message = <<< EOL + + ------------ Faker seed ------------ + Faker seed for failed test: $seed + ------------------------------------ + + EOL; + + error_log($error_message); + } /** * Clears data from all tables except district table. @@ -58,14 +87,15 @@ public static function resetDatabase(): void } /** - * Creates a client and saves it to database + * Creates a random client and saves it to database. + * Client email is guaranteed to be unique. * @return Client * @throws Exception */ public static function createClient(): Client { $client = new Client( - self::$faker->email(), + self::$faker->unique()->email(), self::$faker->firstName(), self::$faker->lastName(), self::$faker->password(), @@ -81,32 +111,66 @@ public static function createClient(): Client } /** - * Creates a product and saves it to database. + * Creates a random product and saves it to database. * @return Product * @throws Exception */ public static function createProduct(): Product { $product = new Product( - self::$faker->company(), - 70, - "Velvet.jpeg", - self::$faker->sentence(), - self::$faker->word(), - 6.50, - self::$faker->sentence(), - new DateTime() + name: self::$faker->company(), + calories: self::$faker->numberBetween(1, 500), + img_url: "Velvet.jpeg", + img_alt_text: self::$faker->sentence(), + category: self::$faker->lexify(), + price: 6.50, + description: self::$faker->sentence() ); $success = $product->save(); if (!$success) { - throw new Exception('Unable to save product to database'); + $json = json_encode($product->toArray()); + $errors = json_encode($product->validate()); + + $msg = <<< EOL + Unable to save product to database: + $json + + Attribute errors: + $errors + EOL; + + throw new Exception($msg); } return $product; } + /** + * @throws Exception + */ + public static function createStore(): Store + { + $store = new Store( + phone_no: self::$faker->phoneNumber(), + address: new Location( + street: self::$faker->streetAddress(), + city: self::$faker->city(), + district_id: self::$faker->numberBetween(1, 9), + latitude: self::$faker->numberBetween(-90, 90), + longitude: self::$faker->numberBetween(-180, 180) + ) + ); + + $success = $store->save(); + + if (!$success) { + throw new Exception('Unable to save store to database'); + } + return $store; + } + /** * Create a review and saves it to database. * @param Product $product A valid product already present in database @@ -121,20 +185,7 @@ public static function createReview(Product $product, Client $client, bool $veri // place an order for client and product // create store - $store = new Store( - phone_no: "13213431", - address: new Location( - street: "Royal", - city: "Curepipe", - district_id: 1, - latitude: 50, - longitude: 50 - ) - ); - $success = $store->save(); - if (!$success) { - throw new Exception('Unable to create store'); - } + $store = self::createStore(); // Add stock to the store for the product to be bought $store->addProductStock($product->getProductID(), 10); @@ -152,8 +203,8 @@ public static function createReview(Product $product, Client $client, bool $veri $review = new Review( product_id: $product->getProductID(), client_id: $client->getUserID(), - text: "This is a test review.", - rating: 5 + text: self::$faker->sentence(10), + rating: self::$faker->numberBetween(1, 5) ); $success = $review->save(); diff --git a/tests/models/ProductTest.php b/tests/models/ProductTest.php index 351150a1..652b58ea 100644 --- a/tests/models/ProductTest.php +++ b/tests/models/ProductTest.php @@ -21,14 +21,16 @@ final class ProductTest extends TestCase private ?Product $dummy_product; private ?Client $dummy_client; + + /** + * @var Review|null A review written by $dummy_client for $dummy_product + */ private ?Review $dummy_review; public static function setUpBeforeClass(): void { - self::$faker = Factory::create(); - self::$seed = mt_rand(); - self::$faker->seed(self::$seed); + self::initFaker(); } public static function tearDownAfterClass(): void @@ -38,16 +40,7 @@ public static function tearDownAfterClass(): void public function onNotSuccessfulTest(Throwable $t): never { - $seed = self::$seed; - - $error_message = <<< EOL - - ------------ Faker seed ------------ - Faker seed for failed test: $seed - ------------------------------------ - EOL; - - error_log($error_message); + self::printFakerSeed(); parent::onNotSuccessfulTest($t); } @@ -140,12 +133,9 @@ public function testToArray(): void public function testSave(): void { - // Save the dummy product - $result = $this->dummy_product->save(); - - // Check if the product was saved successfully - self::assertTrue($result); // Assert that save() returns true upon successful save - self::assertNotNull($this->dummy_product->getProductID()); + $this->markTestIncomplete( + 'Use data providers here for at least 3 test cases, ...', + ); } public function testValidate(): void @@ -166,11 +156,12 @@ public function testGetRatingDistribution(): void $distribution = $this->dummy_product->getRatingDistribution(); // Check if the distribution contains the expected keys and values - $this->assertArrayHasKey(5, $distribution); - $this->assertEquals(100.0, $distribution[5]); // 1 out of 1 reviews is 5 stars + // Here dummy product contains a single review: + $this->assertArrayHasKey($this->dummy_review->getRating(), $distribution); + $this->assertEquals(100.0, $distribution[$this->dummy_review->getRating()]); $this->markTestIncomplete( - 'This test lacks test cases, ...', + 'This test lacks test cases. This test might fail when getRatingDistribution excludes unverified reviews.', ); } @@ -239,7 +230,7 @@ public function testGetReviews(): void // Check if the reviews contain the expected values $this->assertCount(1, $reviews); - $this->assertEquals('This is a test review.', $reviews[0]->getText()); - $this->assertEquals(5, $reviews[0]->getRating()); + $this->assertEquals($this->dummy_review->getText(), $reviews[0]->getText()); + $this->assertEquals($this->dummy_review->getRating(), $reviews[0]->getRating()); } } diff --git a/tests/models/ReviewTest.php b/tests/models/ReviewTest.php index fb2ce54e..690d7981 100644 --- a/tests/models/ReviewTest.php +++ b/tests/models/ReviewTest.php @@ -11,7 +11,6 @@ use Steamy\Model\Location; use Steamy\Model\Review; use Steamy\Model\Product; -use Faker\Factory; use Steamy\Tests\helpers\TestHelper; use Throwable; @@ -25,9 +24,7 @@ final class ReviewTest extends TestCase public static function setUpBeforeClass(): void { - self::$faker = Factory::create(); - self::$seed = mt_rand(); - self::$faker->seed(self::$seed); + self::initFaker(); } public static function tearDownAfterClass(): void @@ -37,16 +34,7 @@ public static function tearDownAfterClass(): void public function onNotSuccessfulTest(Throwable $t): never { - $seed = self::$seed; - - $error_message = <<< EOL - - ------------ Faker seed ------------ - Faker seed for failed test: $seed - ------------------------------------ - EOL; - - error_log($error_message); + self::printFakerSeed(); parent::onNotSuccessfulTest($t); } From ff2b58da943841a0fa2a5af1c73209f3905e2d4f Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 2 Jun 2024 19:04:15 +0400 Subject: [PATCH 28/52] add note on modifying css/js files --- docs/INSTALLATION_GUIDE.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/INSTALLATION_GUIDE.md b/docs/INSTALLATION_GUIDE.md index 9cbbaf6e..4e2ab37d 100644 --- a/docs/INSTALLATION_GUIDE.md +++ b/docs/INSTALLATION_GUIDE.md @@ -167,4 +167,15 @@ You can use `php --ini` to find the location of your `php.ini` file. ## Autoload setup -Whenever changes are made to the autoload settings in `composer.json`, you must run `composer dump-autoload`. \ No newline at end of file +Whenever changes are made to the autoload settings in `composer.json`, you must run `composer dump-autoload`. + +## Modifying CSS/JS Files + +If you need to make changes to the CSS or JavaScript files located in the `public` folder, you need to run the following +command in your terminal: + +```bash +npm run build +``` + +This command will compile your changes and update the necessary files for your application. \ No newline at end of file From e41bf2721c55c9857dd3c00d26c437730ae65507 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 2 Jun 2024 19:43:02 +0400 Subject: [PATCH 29/52] use helper functions from TestHelper --- tests/models/OrderProductTest.php | 77 ++++++++++--------------------- 1 file changed, 24 insertions(+), 53 deletions(-) diff --git a/tests/models/OrderProductTest.php b/tests/models/OrderProductTest.php index 7a3e0b7c..8e1ab734 100644 --- a/tests/models/OrderProductTest.php +++ b/tests/models/OrderProductTest.php @@ -4,16 +4,15 @@ namespace Steamy\Tests\Model; -use DateTime; use Exception; use PHPUnit\Framework\TestCase; use Steamy\Model\Client; -use Steamy\Model\Location; use Steamy\Model\Order; use Steamy\Model\OrderProduct; use Steamy\Model\Product; use Steamy\Model\Store; use Steamy\Tests\helpers\TestHelper; +use Throwable; class OrderProductTest extends TestCase { @@ -25,72 +24,44 @@ class OrderProductTest extends TestCase private ?Product $dummy_product; private array $line_items = []; + public static function setUpBeforeClass(): void + { + self::initFaker(); + } + + public static function tearDownAfterClass(): void + { + self::$faker = null; + } + + public function onNotSuccessfulTest(Throwable $t): never + { + self::printFakerSeed(); + parent::onNotSuccessfulTest($t); + } + /** * @throws Exception */ public function setUp(): void { // Initialize a dummy store object for testing - $this->dummy_store = new Store( - phone_no: "987654321", // Phone number - address: new Location( - street: "Augus", - city: "Flacq", - district_id: 2, - latitude: 60, - longitude: 60 - ) - ); - - $success = $this->dummy_store->save(); - if (!$success) { - $errors = $this->dummy_store->validate(); - $error_msg = "Unable to save store to database. "; - if (!empty($errors)) { - $error_msg .= "Errors: " . implode(',', $errors); - } else { - $error_msg .= "Attributes seem to be ok as per validate()."; - } - - throw new Exception($error_msg); - } + $this->dummy_store = self::createStore(); // Create a dummy client - $this->client = new Client( - "john@example.com", - "John", - "Doe", - "john_doe", - "password", - new Location("Royal", "Curepipe", 1, 50, 50) - ); - $success = $this->client->save(); - if (!$success) { - throw new Exception('Unable to save client'); - } + $this->client = self::createClient(); // Create a dummy product - $this->dummy_product = new Product( - "Latte", - 50, - "latte.jpeg", - "A delicious latte", - "Beverage", - 5.0, - "A cup of latte", - new DateTime() - ); - $success = $this->dummy_product->save(); - if (!$success) { - throw new Exception('Unable to save product'); - } + $this->dummy_product = self::createProduct(); // Update stock level for the product $this->dummy_store->addProductStock($this->dummy_product->getProductID(), 10); // Create dummy order line items $this->line_items = [ - new OrderProduct($this->dummy_product->getProductID(), "medium", "oat", 2, 5.0) + new OrderProduct( + $this->dummy_product->getProductID(), "medium", "oat", 2 + ) ]; // Create a dummy order @@ -155,6 +126,6 @@ public function testGetById(): void $this->assertEquals("medium", $retrievedOrderProduct->getCupSize()); $this->assertEquals("oat", $retrievedOrderProduct->getMilkType()); $this->assertEquals(2, $retrievedOrderProduct->getQuantity()); - $this->assertEquals(5.0, $retrievedOrderProduct->getUnitPrice()); + $this->assertEquals($this->dummy_product->getPrice(), $retrievedOrderProduct->getUnitPrice()); } } From ed5e71c66f4acd66692e61f6fbc1b3605910ed50 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 2 Jun 2024 20:41:42 +0400 Subject: [PATCH 30/52] remove transaction from resetDatabase --- tests/helpers/TestHelper.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/helpers/TestHelper.php b/tests/helpers/TestHelper.php index f9ca821b..3688213b 100644 --- a/tests/helpers/TestHelper.php +++ b/tests/helpers/TestHelper.php @@ -60,7 +60,6 @@ public static function printFakerSeed(): void public static function resetDatabase(): void { $conn = self::connect(); - $conn->beginTransaction(); // Order of deletion is important to prevent foreign key violation $query = <<< SQL @@ -82,7 +81,6 @@ public static function resetDatabase(): void SQL; $conn->exec($query); - $conn->commit(); $conn = null; } From 97ebdd8090df301430f362c5b5a6e500af461620 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 2 Jun 2024 21:02:52 +0400 Subject: [PATCH 31/52] create an APIHelper trait --- tests/helpers/APIHelper.php | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/helpers/APIHelper.php diff --git a/tests/helpers/APIHelper.php b/tests/helpers/APIHelper.php new file mode 100644 index 00000000..0100afb7 --- /dev/null +++ b/tests/helpers/APIHelper.php @@ -0,0 +1,33 @@ +push(Middleware::mapRequest(function ($request) { + // Add custom header to each request + return $request->withHeader('X-Test-Env', 'testing'); + })); + + self::$guzzle = new GuzzleClient([ + 'base_uri' => $_ENV['API_BASE_URI'], + 'http_errors' => false, // Optionally disable throwing exceptions for HTTP errors + 'handler' => $handlerStack, + + ]); + } +} \ No newline at end of file From 45b1acc0e006d8c65173699c15987cc8d4fb84bf Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 2 Jun 2024 21:03:52 +0400 Subject: [PATCH 32/52] use test helpers --- tests/api/ProductsTest.php | 81 +++++++++++++++----------------------- 1 file changed, 32 insertions(+), 49 deletions(-) diff --git a/tests/api/ProductsTest.php b/tests/api/ProductsTest.php index 98c54db4..ac8da5e7 100644 --- a/tests/api/ProductsTest.php +++ b/tests/api/ProductsTest.php @@ -4,66 +4,50 @@ namespace Steamy\Tests\Api; -use DateTime; use Exception; use GuzzleHttp\Exception\GuzzleException; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Middleware; use PHPUnit\Framework\TestCase; -use GuzzleHttp\Client as GuzzleClient; -use Steamy\Core\Database; use Steamy\Model\Product; +use Steamy\Tests\helpers\APIHelper; +use Steamy\Tests\helpers\TestHelper; +use Throwable; final class ProductsTest extends TestCase { - use Database; + use TestHelper; + use APIHelper; - private ?GuzzleClient $client; private Product $dummy_product; + public static function setUpBeforeClass(): void + { + self::initFaker(); + self::initGuzzle(); + } + + public static function tearDownAfterClass(): void + { + self::$faker = null; + self::$guzzle = null; + } + + public function onNotSuccessfulTest(Throwable $t): never + { + self::printFakerSeed(); + parent::onNotSuccessfulTest($t); + } + /** * @throws Exception */ public function setUp(): void { - // Create a handler stack - $handlerStack = HandlerStack::create(); - - // Add middleware to the handler stack - $handlerStack->push(Middleware::mapRequest(function ($request) { - // Add custom header to each request - return $request->withHeader('X-Test-Env', 'testing'); - })); - - $this->client = new GuzzleClient([ - 'base_uri' => $_ENV['API_BASE_URI'], - 'http_errors' => false, // Optionally disable throwing exceptions for HTTP errors - 'handler' => $handlerStack, - - ]); - - // Create a dummy product for testing - $this->dummy_product = new Product( - "Velvet Bean", - 70, - "Velvet.jpeg", - "Velvet Bean Image", - "Velvet", - 6.50, - "Each bottle contains 90% Pure Coffee powder and 10% Velvet bean Powder", - new DateTime() - ); - - $success = $this->dummy_product->save(); - if (!$success) { - throw new Exception('Unable to save product'); - } + $this->dummy_product = self::createProduct(); } public function tearDown(): void { - $this->client = null; - self::query('DELETE FROM product;'); + self::resetDatabase(); } /** @@ -71,7 +55,7 @@ public function tearDown(): void */ public function testGetAllProducts() { - $response = $this->client->get('products'); + $response = self::$guzzle->get('products'); $this->assertEquals(200, $response->getStatusCode()); $body = $response->getBody(); @@ -91,7 +75,6 @@ public function testGetAllProducts() $this->assertEquals($this->dummy_product->getDescription(), $data['description']); $this->assertEquals($this->dummy_product->getImgAltText(), $data['img_alt_text']); - // only check presence of the following keys but not the actual value $this->assertArrayHasKey('product_id', $data); $this->assertArrayHasKey('created_date', $data); @@ -104,7 +87,7 @@ public function testGetAllProducts() public function testGetProductById() { // test valid product ID - $response = $this->client->get('products/' . $this->dummy_product->getProductID()); + $response = self::$guzzle->get('products/' . $this->dummy_product->getProductID()); $this->assertEquals(200, $response->getStatusCode()); $data = json_decode($response->getBody()->getContents(), true); $this->assertIsArray($data); @@ -112,7 +95,7 @@ public function testGetProductById() $this->assertEquals($this->dummy_product->getProductID(), $data['product_id']); // test invalid product ID - $response = $this->client->get('products/-1'); + $response = self::$guzzle->get('products/-1'); $this->assertEquals(404, $response->getStatusCode()); } @@ -121,7 +104,7 @@ public function testGetProductById() */ public function testGetProductCategories() { - $response = $this->client->get('products/categories'); + $response = self::$guzzle->get('products/categories'); $this->assertEquals(200, $response->getStatusCode()); $data = json_decode($response->getBody()->getContents(), true); $this->assertIsArray($data); @@ -135,7 +118,7 @@ public function testGetProductCategories() public function testCreateProduct() { self::markTestIncomplete('Incomplete test'); - $response = $this->client->post('products', [ + $response = self::$guzzle->post('products', [ 'json' => [ 'name' => 'Test Product', 'category' => 'Test Category', @@ -152,7 +135,7 @@ public function testCreateProduct() public function testDeleteProductById() { self::markTestIncomplete('Incomplete test'); - $response = $this->client->delete('products/1'); + $response = self::$guzzle->delete('products/1'); $this->assertEquals(204, $response->getStatusCode()); // No content expected, so no further assertions needed } @@ -163,7 +146,7 @@ public function testDeleteProductById() public function testUpdateProductById() { self::markTestIncomplete('Incomplete test'); - $response = $this->client->put('products/1', [ + $response = self::$guzzle->put('products/1', [ 'json' => [ 'name' => 'Updated Product', 'category' => 'Updated Category', From 365651789e46a5bfb173d62ef71febc830b8c3c4 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 2 Jun 2024 21:08:42 +0400 Subject: [PATCH 33/52] remove getAllReviewsForProduct since it is already present in products controller api --- src/controllers/api/Reviews.php | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/controllers/api/Reviews.php b/src/controllers/api/Reviews.php index b9c680d5..eeaec323 100644 --- a/src/controllers/api/Reviews.php +++ b/src/controllers/api/Reviews.php @@ -15,7 +15,6 @@ class Reviews 'GET' => [ '/reviews' => 'getAllReviews', '/reviews/{id}' => 'getReviewByID', - '/products/{id}/reviews' => 'getAllReviewsForProduct', ], 'POST' => [ '/reviews' => 'createReview', @@ -65,28 +64,6 @@ public function getReviewByID(): void echo json_encode($review->toArray()); } - - /** - * Get all reviews for a particular product by its ID. - */ - public function getAllReviewsForProduct(): void - { - $productId = (int)Utility::splitURL()[3]; - // Check if product exists - if (ProductModel::getById($productId) === null) { - // product not found, return 404 - http_response_code(404); - echo json_encode(['error' => 'Product not found']); - return; - } - - // Retrieve all reviews for the specified product from the database - $reviews = Review::getAllReviewsForProduct($productId); - - // Return JSON response - echo json_encode($reviews); - } - /** * Create a new review for a product. */ From c2fd70a23ebff49f1df72b4bc505266320f63800 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 2 Jun 2024 21:53:10 +0400 Subject: [PATCH 34/52] remove irrelevant todo --- README.md | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/README.md b/README.md index 88d179cb..93fd2058 100644 --- a/README.md +++ b/README.md @@ -59,18 +59,4 @@ Attribution-ShareAlike - https://youtu.be/q0JhJBYi4sw?si=cTdEzzGijlG41ix8 - https://github.com/kevinisaac/php-mvc 4. The filesystem was inspired by https://github.com/php-pds/sklseleton -5. Additional references are included within the code itself. - -# Todo - -Add `X-TEST-ENV` to the header of your request and set its value to `testing` if you want to use the testing database. -This is required when running tests for API. Without this key-value pair, the production database will be used. - -USE DOCKER PHP - -- read guzzle documentation. read base_uri -- test database is being used -- add variable to .env - -line 18 in Reviews API is redundant -use correct namespace \ No newline at end of file +5. Additional references are included within the code itself. \ No newline at end of file From bb3266500b3c4b76e84b7a3118129a40a62f4c2a Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 5 Jun 2024 14:22:46 +0400 Subject: [PATCH 35/52] fix issues - use random image extension in createProduct - improve error message in createClient, createReview, createStore --- tests/helpers/TestHelper.php | 72 ++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 8 deletions(-) diff --git a/tests/helpers/TestHelper.php b/tests/helpers/TestHelper.php index 3688213b..d2e04a3a 100644 --- a/tests/helpers/TestHelper.php +++ b/tests/helpers/TestHelper.php @@ -92,10 +92,21 @@ public static function resetDatabase(): void */ public static function createClient(): Client { + $first_name = self::$faker->firstName(); + $last_name = self::$faker->lastName(); + + // ensure that length is correct + if (strlen($first_name) < 3) { + $first_name .= "aaa"; + } + if (strlen($last_name) < 3) { + $last_name .= "aaa"; + } + $client = new Client( self::$faker->unique()->email(), - self::$faker->firstName(), - self::$faker->lastName(), + $first_name, + $last_name, self::$faker->password(), self::$faker->phoneNumber(), new Location(self::$faker->streetAddress(), self::$faker->city(), self::$faker->numberBetween(1, 9)) @@ -103,7 +114,18 @@ public static function createClient(): Client $success = $client->save(); if (!$success) { - throw new Exception('Unable to save a unique client to database'); + $json = json_encode($client->toArray()); + $errors = json_encode($client->validate()); + + $msg = <<< EOL + Unable to save client to database: + $json + + Attribute errors: + $errors + EOL; + + throw new Exception($msg); } return $client; } @@ -115,10 +137,13 @@ public static function createClient(): Client */ public static function createProduct(): Product { + $img_ext = self::$faker->randomElement(['png', 'jpeg', 'avif', 'jpg', 'webp']); + $product_name = self::$faker->words(2, true); + $product = new Product( - name: self::$faker->company(), + name: $product_name, calories: self::$faker->numberBetween(1, 500), - img_url: "Velvet.jpeg", + img_url: $product_name . "." . $img_ext, img_alt_text: self::$faker->sentence(), category: self::$faker->lexify(), price: 6.50, @@ -164,7 +189,18 @@ public static function createStore(): Store $success = $store->save(); if (!$success) { - throw new Exception('Unable to save store to database'); + $json = json_encode($store->toArray()); + $errors = json_encode($store->validate()); + + $msg = <<< EOL + Unable to save store to database: + $json + + Attribute errors: + $errors + EOL; + + throw new Exception($msg); } return $store; } @@ -194,7 +230,17 @@ public static function createReview(Product $product, Client $client, bool $veri $success = $order->save(); if (!$success) { - throw new Exception('Unable to save order'); + $json = json_encode($order->toArray()); + $errors = json_encode($order->validate()); + + $msg = <<< EOL + Unable to save order to database: + $json + + Attribute errors: + $errors + EOL; + throw new Exception($msg); } } @@ -208,7 +254,17 @@ public static function createReview(Product $product, Client $client, bool $veri $success = $review->save(); if (!$success) { - throw new Exception('Unable to save review'); + $json = json_encode($review->toArray()); + $errors = json_encode($review->validate()); + + $msg = <<< EOL + Unable to save review to database: + $json + + Attribute errors: + $errors + EOL; + throw new Exception($msg); } return $review; From 563371775794e49e9d5c723581859de93ced62af Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 5 Jun 2024 14:23:06 +0400 Subject: [PATCH 36/52] update acceptable img extensions in validate --- src/models/Product.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/Product.php b/src/models/Product.php index 531a2eda..1c687bc9 100644 --- a/src/models/Product.php +++ b/src/models/Product.php @@ -314,7 +314,7 @@ public function validate(): array } // Validate img_url - if (!preg_match('/\.(png|jpeg|avif)$/', $this->img_url)) { + if (!preg_match('/\.(png|jpeg|avif|jpg|webp)$/', $this->img_url)) { $errors['img_url'] = "Image URL must end with .png, .jpeg, or .avif"; } From b75f50e841a58303709f6c19fd39bb4e38eb03a7 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:03:53 +0400 Subject: [PATCH 37/52] add log_json --- tests/helpers/APIHelper.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/helpers/APIHelper.php b/tests/helpers/APIHelper.php index 0100afb7..66bdbfdc 100644 --- a/tests/helpers/APIHelper.php +++ b/tests/helpers/APIHelper.php @@ -12,6 +12,10 @@ trait APIHelper { private static ?GuzzleClient $guzzle; + /** + * Initializes static variable $guzzle so that API requests can be made. + * @return void + */ public static function initGuzzle(): void { // Create a handler stack @@ -25,9 +29,18 @@ public static function initGuzzle(): void self::$guzzle = new GuzzleClient([ 'base_uri' => $_ENV['API_BASE_URI'], - 'http_errors' => false, // Optionally disable throwing exceptions for HTTP errors + 'http_errors' => false, // disable throwing exceptions for HTTP errors 'handler' => $handlerStack, - ]); } + + /** + * Logs data in JSON format in terminal. Use for debugging only. + * @param $data + * @return void + */ + public static function log_json($data): void + { + error_log(json_encode($data, JSON_PRETTY_PRINT)); + } } \ No newline at end of file From 6691f619fcdc25a406ab9e29a0098ed4f7e5e34f Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:04:47 +0400 Subject: [PATCH 38/52] remove unused import --- tests/models/ProductTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/models/ProductTest.php b/tests/models/ProductTest.php index 652b58ea..a3384cd8 100644 --- a/tests/models/ProductTest.php +++ b/tests/models/ProductTest.php @@ -6,7 +6,6 @@ use DateTime; use Exception; -use Faker\Factory; use PHPUnit\Framework\TestCase; use Steamy\Model\Product; use Steamy\Model\Review; From 91fcb419ed00b167d9f7a065d11a7026b7523cbb Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:46:18 +0400 Subject: [PATCH 39/52] fix bug in createProduct --- resources/schemas/common/product.json | 14 ++++----- resources/schemas/products/create.json | 40 ++++++++++++++++++-------- src/controllers/api/Products.php | 24 ++++++---------- 3 files changed, 43 insertions(+), 35 deletions(-) diff --git a/resources/schemas/common/product.json b/resources/schemas/common/product.json index 9a8636ae..024d7387 100644 --- a/resources/schemas/common/product.json +++ b/resources/schemas/common/product.json @@ -5,6 +5,10 @@ "description": "A product object", "type": "object", "properties": { + "product_id": { + "type": "integer", + "description": "Unique identifier for product" + }, "name": { "type": "string", "description": "The name of the product", @@ -18,7 +22,7 @@ }, "img_url": { "type": "string", - "format": "uri", + "pattern": "^.+\\.(png|jpeg|avif|jpg|webp)$", "description": "The URL of the product image" }, "img_alt_text": { @@ -49,11 +53,5 @@ "description": "The date and time when the product was created" } }, - "additionalProperties": false, - "patternProperties": { - "img_url": { - "pattern": "^.+\\.(png|jpeg|avif|jpg|webp)$", - "description": "The URL should end with one of the supported image formats: png, jpeg, avif, jpg, webp" - } - } + "additionalProperties": false } diff --git a/resources/schemas/products/create.json b/resources/schemas/products/create.json index f1391379..23e23164 100644 --- a/resources/schemas/products/create.json +++ b/resources/schemas/products/create.json @@ -3,20 +3,36 @@ "$id": "https://example.com/products/create.json", "title": "Create Product", "properties": { - "product": { - "$ref": "https://example.com/definitions/product.json" + "name": { + "$ref": "https://example.com/common/product.json#/properties/name" + }, + "calories": { + "$ref": "https://example.com/common/product.json#/properties/calories" + }, + "img_url": { + "$ref": "https://example.com/common/product.json#/properties/img_url" + }, + "img_alt_text": { + "$ref": "https://example.com/common/product.json#/properties/img_alt_text" + }, + "category": { + "$ref": "https://example.com/common/product.json#/properties/category" + }, + "price": { + "$ref": "https://example.com/common/product.json#/properties/price" + }, + "description": { + "$ref": "https://example.com/common/product.json#/properties/description" } }, "required": [ - "product" + "name", + "calories", + "img_url", + "img_alt_text", + "category", + "price", + "description" ], - "definitions": { - "product": { - "required": [ - "name", - "calories", - "img_url" - ] - } - } + "additionalProperties": false } diff --git a/src/controllers/api/Products.php b/src/controllers/api/Products.php index d1c20e5a..8964d42c 100644 --- a/src/controllers/api/Products.php +++ b/src/controllers/api/Products.php @@ -88,9 +88,7 @@ public function getProductCategories(): void */ public function createProduct(): void { - $data = (object) json_decode(file_get_contents("php://input"), true); - - var_dump($data); + $data = (object)json_decode(file_get_contents("php://input"), true); $schemaPath = __DIR__ . '/../../../resources/schemas'; $validator = new Validator(); @@ -105,29 +103,25 @@ public function createProduct(): void "https://example.com/products/create.json" ); - - if (!($result->isValid())) { $errors = (new ErrorFormatter())->format($result->error()); $response = [ - 'errors' => $errors + 'error' => $errors ]; http_response_code(400); echo json_encode($response); return; } - return; - // Create a new Product object $newProduct = new Product( - $data['name'], - (int)$data['calories'], - $data['img_url'], - $data['img_alt_text'], - $data['category'], - (float)$data['price'], - $data['description'] + $data->name, + $data->calories, + $data->img_url, + $data->img_alt_text, + $data->category, + (float)$data->price, + $data->description ); // Save the new product to the database From 5239abdadec532822461a1c2a6f4e05f51ae5e7c Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:19:54 +0400 Subject: [PATCH 40/52] complete testCreateValidProduct and testDeleteProductById --- tests/api/ProductsTest.php | 49 +++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/tests/api/ProductsTest.php b/tests/api/ProductsTest.php index ac8da5e7..4cb9c204 100644 --- a/tests/api/ProductsTest.php +++ b/tests/api/ProductsTest.php @@ -114,30 +114,47 @@ public function testGetProductCategories() /** * @throws GuzzleException + * @throws Exception Expected product could not be created */ - public function testCreateProduct() + public function testCreateValidProduct() { - self::markTestIncomplete('Incomplete test'); - $response = self::$guzzle->post('products', [ - 'json' => [ - 'name' => 'Test Product', - 'category' => 'Test Category', - 'price' => 99.99, - // Add more fields as needed - ] - ]); + $expected_product = self::createProduct(false); + + $data_to_send = $expected_product->toArray(); + unset($data_to_send['product_id']); + unset($data_to_send['created_date']); + +// self::log_json($data_to_send); + + $response = self::$guzzle->post( + 'products', + ['json' => $data_to_send] + ); + + $data_received = json_decode($response->getBody()->getContents(), true); +// self::log_json($data_received); + $this->assertEquals(201, $response->getStatusCode()); - $data = json_decode($response->getBody()->getContents(), true); - $this->assertArrayHasKey('id', $data); - // Add more assertions as needed + + $this->assertArrayHasKey('product_id', $data_received); + self::assertTrue($data_received['product_id'] > 0); } + /** + * @throws GuzzleException + * @throws Exception + */ public function testDeleteProductById() { - self::markTestIncomplete('Incomplete test'); - $response = self::$guzzle->delete('products/1'); + // delete a non-existent product + $response = self::$guzzle->delete('products/0'); + $this->assertEquals(404, $response->getStatusCode()); + + // delete a valid product + $product = self::createProduct(); + $response = self::$guzzle->delete('products/' . $product->getProductID()); $this->assertEquals(204, $response->getStatusCode()); - // No content expected, so no further assertions needed + self::assertNull(Product::getByID($product->getProductID())); } /** From 93a15b2c01ee3b5fcd6f4653f2a52676d9bc341e Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:29:49 +0400 Subject: [PATCH 41/52] create and use a helper method to validate data against schema --- src/controllers/api/Products.php | 13 +------------ src/core/Utility.php | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/controllers/api/Products.php b/src/controllers/api/Products.php index 8964d42c..f1b06787 100644 --- a/src/controllers/api/Products.php +++ b/src/controllers/api/Products.php @@ -90,18 +90,7 @@ public function createProduct(): void { $data = (object)json_decode(file_get_contents("php://input"), true); - $schemaPath = __DIR__ . '/../../../resources/schemas'; - $validator = new Validator(); - - $validator->resolver()->registerPrefix( - "https://example.com/", - $schemaPath, - ); - - $result = $validator->validate( - $data, - "https://example.com/products/create.json" - ); + $result = Utility::validateAgainstSchema($data, "products/create.json"); if (!($result->isValid())) { $errors = (new ErrorFormatter())->format($result->error()); diff --git a/src/core/Utility.php b/src/core/Utility.php index 75a41703..1bf14753 100644 --- a/src/core/Utility.php +++ b/src/core/Utility.php @@ -6,6 +6,8 @@ use DateTime; use Exception; +use Opis\JsonSchema\{ValidationResult, Validator}; + /** * Utility class containing various helper functions. @@ -157,4 +159,28 @@ public static function stringToDate(string $date): ?DateTime return null; } } + + /** + * @param object $data Data to be validated + * @param string $schemaPath Relative path (starting from schema folder) to schema file. + * Example: `products/create.json` + * @return ValidationResult + */ + public static function validateAgainstSchema(object $data, string $schemaPath): ValidationResult + { + $schemaDirPath = __DIR__ . '/../../../resources/schemas'; + $schemaPrefix = "https://example.com/"; + + $validator = new Validator(); + + $validator->resolver()->registerPrefix( + $schemaPrefix, + $schemaDirPath, + ); + + return $validator->validate( + $data, + $schemaPrefix . $schemaPath + ); + } } From 0ad60270f3ae4c6b7d5c4e137567df038795ebea Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 5 Jun 2024 18:54:22 +0400 Subject: [PATCH 42/52] complete updateProduct and getAllReviewsForProduct --- src/controllers/api/Products.php | 54 ++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/src/controllers/api/Products.php b/src/controllers/api/Products.php index f1b06787..98327222 100644 --- a/src/controllers/api/Products.php +++ b/src/controllers/api/Products.php @@ -4,10 +4,12 @@ namespace Steamy\Controller\API; -use Opis\JsonSchema\{Helper, Validator, Errors\ErrorFormatter}; +use Opis\JsonSchema\{Errors\ErrorFormatter}; use Steamy\Core\Utility; use Steamy\Model\Product; use Steamy\Core\Model; +use Steamy\Model\Product as ProductModel; +use Steamy\Model\Review; class Products { @@ -162,30 +164,32 @@ public function updateProduct(): void { $productId = (int)Utility::splitURL()[3]; - // Retrieve PUT request data - $putData = 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']); - return; - } - - // Retrieve existing product + // Retrieve the product by ID $product = Product::getByID($productId); // Check if product exists if ($product === null) { - // Product not found - http_response_code(404); // Not Found + // Product not found, return 404 + http_response_code(404); echo json_encode(['error' => 'Product not found']); return; } + // Retrieve PUT request data + $data = (object)json_decode(file_get_contents("php://input"), true); + $result = Utility::validateAgainstSchema($data, "products/update.json"); + + if (!($result->isValid())) { + $errors = (new ErrorFormatter())->format($result->error()); + $response = [ + 'error' => $errors + ]; + http_response_code(400); + echo json_encode($response); + return; + } // Update product in the database - $success = $product->updateProduct($putData); + $success = $product->updateProduct((array)$data); if ($success) { // Product updated successfully @@ -206,14 +210,18 @@ public function getAllReviewsForProduct(): void // Get product ID from URL $productId = (int)Utility::splitURL()[3]; - // Instantiate the Reviews controller - $reviewsController = new Reviews(); + // Check if product exists + if (ProductModel::getById($productId) === null) { + // product not found, return 404 + http_response_code(404); + echo json_encode(['error' => 'Product not found']); + return; + } - // Call the method to get all reviews for the specified product - // Since the Reviews controller method expects the ID to be in the URL, we'll set it directly - $_SERVER['REQUEST_URI'] = "/api/v1/products/$productId/reviews"; + // Retrieve all reviews for the specified product from the database + $reviews = Review::getAllReviewsForProduct($productId); - // Call the method from the Reviews controller - $reviewsController->getAllReviewsForProduct(); + // Return JSON response + echo json_encode($reviews); } } From 2c8974db2e92f7694d82dc025ecb48e4b7add584 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 5 Jun 2024 18:54:37 +0400 Subject: [PATCH 43/52] fix file path bug in validateAgainstSchema --- src/core/Utility.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Utility.php b/src/core/Utility.php index 1bf14753..d19f5d7a 100644 --- a/src/core/Utility.php +++ b/src/core/Utility.php @@ -168,7 +168,7 @@ public static function stringToDate(string $date): ?DateTime */ public static function validateAgainstSchema(object $data, string $schemaPath): ValidationResult { - $schemaDirPath = __DIR__ . '/../../../resources/schemas'; + $schemaDirPath = __DIR__ . '/../../resources/schemas'; $schemaPrefix = "https://example.com/"; $validator = new Validator(); From c01adc1b995a795c72895a812110c833647cc5e2 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 5 Jun 2024 18:54:49 +0400 Subject: [PATCH 44/52] add schema for updating products --- resources/schemas/products/update.json | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 resources/schemas/products/update.json diff --git a/resources/schemas/products/update.json b/resources/schemas/products/update.json new file mode 100644 index 00000000..f957dfd1 --- /dev/null +++ b/resources/schemas/products/update.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/products/update.json", + "title": "Create Product", + "properties": { + "name": { + "$ref": "https://example.com/common/product.json#/properties/name" + }, + "calories": { + "$ref": "https://example.com/common/product.json#/properties/calories" + }, + "img_url": { + "$ref": "https://example.com/common/product.json#/properties/img_url" + }, + "img_alt_text": { + "$ref": "https://example.com/common/product.json#/properties/img_alt_text" + }, + "category": { + "$ref": "https://example.com/common/product.json#/properties/category" + }, + "price": { + "$ref": "https://example.com/common/product.json#/properties/price" + }, + "description": { + "$ref": "https://example.com/common/product.json#/properties/description" + } + }, + "additionalProperties": false +} From 063ddfff096024d958e4d7af59dd716038ecde8e Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 5 Jun 2024 19:02:19 +0400 Subject: [PATCH 45/52] reset database both before and after test suite --- tests/api/ProductsTest.php | 1 + tests/models/AdministratorTest.php | 5 +++++ tests/models/ClientTest.php | 5 +++++ tests/models/CommentTest.php | 5 +++++ tests/models/OrderProductTest.php | 1 + tests/models/OrderTest.php | 6 ++++++ tests/models/ProductTest.php | 1 + tests/models/ReviewTest.php | 1 + tests/models/StoreTest.php | 5 +++++ 9 files changed, 30 insertions(+) diff --git a/tests/api/ProductsTest.php b/tests/api/ProductsTest.php index 4cb9c204..a38a8a80 100644 --- a/tests/api/ProductsTest.php +++ b/tests/api/ProductsTest.php @@ -23,6 +23,7 @@ public static function setUpBeforeClass(): void { self::initFaker(); self::initGuzzle(); + self::resetDatabase(); } public static function tearDownAfterClass(): void diff --git a/tests/models/AdministratorTest.php b/tests/models/AdministratorTest.php index 1bf0ce2d..1a4b6f66 100644 --- a/tests/models/AdministratorTest.php +++ b/tests/models/AdministratorTest.php @@ -15,6 +15,11 @@ final class AdministratorTest extends TestCase private ?Administrator $dummy_admin; + public static function setUpBeforeClass(): void + { + self::resetDatabase(); + } + /** * @throws Exception */ diff --git a/tests/models/ClientTest.php b/tests/models/ClientTest.php index 05cc66dd..74f4949a 100644 --- a/tests/models/ClientTest.php +++ b/tests/models/ClientTest.php @@ -18,6 +18,11 @@ final class ClientTest extends TestCase private ?Client $dummy_client; + public static function setUpBeforeClass(): void + { + self::resetDatabase(); + } + /** * @throws Exception */ diff --git a/tests/models/CommentTest.php b/tests/models/CommentTest.php index bfe266ea..70d122f3 100644 --- a/tests/models/CommentTest.php +++ b/tests/models/CommentTest.php @@ -23,6 +23,11 @@ class CommentTest extends TestCase private ?Client $reviewer; private ?Product $dummy_product; + public static function setUpBeforeClass(): void + { + self::resetDatabase(); + } + /** * @throws Exception */ diff --git a/tests/models/OrderProductTest.php b/tests/models/OrderProductTest.php index 8e1ab734..69563b6b 100644 --- a/tests/models/OrderProductTest.php +++ b/tests/models/OrderProductTest.php @@ -27,6 +27,7 @@ class OrderProductTest extends TestCase public static function setUpBeforeClass(): void { self::initFaker(); + self::resetDatabase(); } public static function tearDownAfterClass(): void diff --git a/tests/models/OrderTest.php b/tests/models/OrderTest.php index 5d855fad..b7a91735 100644 --- a/tests/models/OrderTest.php +++ b/tests/models/OrderTest.php @@ -25,6 +25,12 @@ class OrderTest extends TestCase private ?Store $dummy_store = null; private array $line_items = []; + public static function setUpBeforeClass(): void + { + self::initFaker(); + self::resetDatabase(); + } + /** * @throws Exception */ diff --git a/tests/models/ProductTest.php b/tests/models/ProductTest.php index a3384cd8..26ba5685 100644 --- a/tests/models/ProductTest.php +++ b/tests/models/ProductTest.php @@ -29,6 +29,7 @@ final class ProductTest extends TestCase public static function setUpBeforeClass(): void { + self::resetDatabase(); self::initFaker(); } diff --git a/tests/models/ReviewTest.php b/tests/models/ReviewTest.php index 690d7981..6d7c8ceb 100644 --- a/tests/models/ReviewTest.php +++ b/tests/models/ReviewTest.php @@ -25,6 +25,7 @@ final class ReviewTest extends TestCase public static function setUpBeforeClass(): void { self::initFaker(); + self::resetDatabase(); } public static function tearDownAfterClass(): void diff --git a/tests/models/StoreTest.php b/tests/models/StoreTest.php index 723ce17a..70a8de3a 100644 --- a/tests/models/StoreTest.php +++ b/tests/models/StoreTest.php @@ -16,6 +16,11 @@ class StoreTest extends TestCase private ?Store $dummy_store; + public static function setUpBeforeClass(): void + { + self::resetDatabase(); + } + /** * @throws Exception */ From 6e2e57ebc3d7fa618507e26b483e51df38d0a2a2 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 5 Jun 2024 19:13:10 +0400 Subject: [PATCH 46/52] complete testUpdateProductById --- tests/api/ProductsTest.php | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/api/ProductsTest.php b/tests/api/ProductsTest.php index a38a8a80..f1463aa4 100644 --- a/tests/api/ProductsTest.php +++ b/tests/api/ProductsTest.php @@ -160,22 +160,27 @@ public function testDeleteProductById() /** * @throws GuzzleException + * @throws Exception */ public function testUpdateProductById() { - self::markTestIncomplete('Incomplete test'); - $response = self::$guzzle->put('products/1', [ - 'json' => [ - 'name' => 'Updated Product', - 'category' => 'Updated Category', - 'price' => 199.99, - // Add more fields as needed - ] - ]); + // update a non-existent product + $response = self::$guzzle->put('products/0'); + $this->assertEquals(404, $response->getStatusCode()); + + // save a valid product to database + $old_product = self::createProduct(); + + // update the name of a valid product + $new_name = 'dashkdla'; + + $response = self::$guzzle->put( + 'products/' . $old_product->getProductID(), + ['json' => ['name' => $new_name]] + ); + $this->assertEquals(200, $response->getStatusCode()); - $data = json_decode($response->getBody()->getContents(), true); - $this->assertArrayHasKey('id', $data); - $this->assertEquals(1, $data['id']); - // Add more assertions as needed + $new_product = Product::getByID($old_product->getProductID()); + $this->assertEquals($new_name, $new_product->getName()); } } \ No newline at end of file From ef51a851c12c627f2aa4334d235a35ab23886aaf Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 5 Jun 2024 19:23:15 +0400 Subject: [PATCH 47/52] add option to not save fake product and client to database --- tests/helpers/TestHelper.php | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/tests/helpers/TestHelper.php b/tests/helpers/TestHelper.php index d2e04a3a..3d97a675 100644 --- a/tests/helpers/TestHelper.php +++ b/tests/helpers/TestHelper.php @@ -85,12 +85,13 @@ public static function resetDatabase(): void } /** - * Creates a random client and saves it to database. + * Creates a random valid client and may save it to database. * Client email is guaranteed to be unique. + * @param bool $saveToDatabase Defaults to true. * @return Client * @throws Exception */ - public static function createClient(): Client + public static function createClient(bool $saveToDatabase = true): Client { $first_name = self::$faker->firstName(); $last_name = self::$faker->lastName(); @@ -112,6 +113,10 @@ public static function createClient(): Client new Location(self::$faker->streetAddress(), self::$faker->city(), self::$faker->numberBetween(1, 9)) ); + if (!$saveToDatabase) { + return $client; + } + $success = $client->save(); if (!$success) { $json = json_encode($client->toArray()); @@ -131,11 +136,12 @@ public static function createClient(): Client } /** - * Creates a random product and saves it to database. + * Creates a random valid product and may save it to database. + * @param bool $saveToDatabase Defaults to True. * @return Product * @throws Exception */ - public static function createProduct(): Product + public static function createProduct(bool $saveToDatabase = true): Product { $img_ext = self::$faker->randomElement(['png', 'jpeg', 'avif', 'jpg', 'webp']); $product_name = self::$faker->words(2, true); @@ -150,6 +156,10 @@ public static function createProduct(): Product description: self::$faker->sentence() ); + if (!$saveToDatabase) { + return $product; + } + $success = $product->save(); if (!$success) { @@ -173,7 +183,7 @@ public static function createProduct(): Product /** * @throws Exception */ - public static function createStore(): Store + public static function createStore(bool $saveToDatabase = true): Store { $store = new Store( phone_no: self::$faker->phoneNumber(), @@ -186,6 +196,10 @@ public static function createStore(): Store ) ); + if (!$saveToDatabase) { + return $store; + } + $success = $store->save(); if (!$success) { @@ -269,9 +283,4 @@ public static function createReview(Product $product, Client $client, bool $veri return $review; } - - public static function logAction($action) - { - // Implementation of logging an action - } } \ No newline at end of file From c1ae17c85a29e24aa7a96538beaf3cd1355cf5b0 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 5 Jun 2024 21:47:15 +0400 Subject: [PATCH 48/52] rework testGetById - split into 2 separate test functions - compare all attributes in testGetByIDForValidId --- tests/models/ReviewTest.php | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/models/ReviewTest.php b/tests/models/ReviewTest.php index 6d7c8ceb..4a34579f 100644 --- a/tests/models/ReviewTest.php +++ b/tests/models/ReviewTest.php @@ -68,8 +68,7 @@ public function setUp(): void "Velvet Bean Image", "Velvet", 6.50, - "Each bottle contains 90% Pure Coffee powder and 10% Velvet bean Powder", - new DateTime() + "Each bottle contains 90% Pure Coffee powder and 10% Velvet bean Powder" ); $success = $this->dummy_product->save(); @@ -224,22 +223,32 @@ public function testValidate(string $text, int $rating, DateTime $created_date, $this->assertEquals($expectedErrors, $review->validate()); } - public function testGetByID(): void + public function testGetByIDForValidId(): void { $fetched_review = Review::getByID($this->dummy_review->getReviewID()); $this->assertNotNull($fetched_review); - // Assert that the properties of the returned Review object match the data - $this->assertEquals($this->dummy_review->getText(), $fetched_review->getText()); - $this->assertEquals($this->dummy_review->getRating(), $fetched_review->getRating()); + $expected_data = $this->dummy_review->toArray(); + $fetched_data = $fetched_review->toArray(); + + // ignore creation dates because the date for expected review + // was set by php while the date for fetched_data was set by mysql + unset($expected_data['created_date']); + unset($fetched_data['created_date']); + + // compare all attributes except created_date + $this->assertEquals($expected_data, $fetched_data); // Compare dates by formatting $this->assertEquals( $this->dummy_review->getCreatedDate()->format('Y-m-d'), $fetched_review->getCreatedDate()->format('Y-m-d') ); + } + public function testGetByIDForInvalidId(): void + { // Test getByID with invalid ID $invalid_ids = [0, -1, 999, -111]; foreach ($invalid_ids as $id) { From 608735625c7a0b15b49542fe9c5aa61441266ad9 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Thu, 6 Jun 2024 08:16:40 +0400 Subject: [PATCH 49/52] add better tests for updateProductById --- tests/api/ProductsTest.php | 91 +++++++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/tests/api/ProductsTest.php b/tests/api/ProductsTest.php index f1463aa4..b59f6997 100644 --- a/tests/api/ProductsTest.php +++ b/tests/api/ProductsTest.php @@ -12,6 +12,8 @@ use Steamy\Tests\helpers\TestHelper; use Throwable; +use function PHPUnit\Framework\assertEquals; + final class ProductsTest extends TestCase { use TestHelper; @@ -162,25 +164,100 @@ public function testDeleteProductById() * @throws GuzzleException * @throws Exception */ - public function testUpdateProductById() + public function testUpdateProductByIdForInvalidProduct() { - // update a non-existent product $response = self::$guzzle->put('products/0'); $this->assertEquals(404, $response->getStatusCode()); + $response = self::$guzzle->put('products/-43'); + $this->assertEquals(404, $response->getStatusCode()); + } + + + public static function provideNewProductData(): array + { + return [ + 'new name' => [ + 'new_data' => [ + 'name' => 'dsajd' + ], + 'changed_data' => [ + 'name' => 'dsajd' + ] + ], + 'new product id' => [ + 'new_data' => [ + 'product_id' => 444 + ], + 'changed_data' => [ + 'product_id' => null + ] + ], + 'new name and description' => [ + 'new_data' => [ + 'name' => 'my new name', + 'description' => 'new description' + ], + 'changed_data' => [ + 'name' => 'my new name', + 'description' => 'new description' + ] + ] + ]; + } + + /** + * @throws GuzzleException + * @throws Exception + * @dataProvider provideNewProductData + */ + public function testUpdateProductByIdForValidProduct(array $new_data, array $expected_data) + { // save a valid product to database $old_product = self::createProduct(); + $old_data = $old_product->toArray(); // update the name of a valid product - $new_name = 'dashkdla'; - $response = self::$guzzle->put( 'products/' . $old_product->getProductID(), - ['json' => ['name' => $new_name]] + ['json' => $new_data] ); + if (array_key_exists('product_id', $new_data)) { + // if request attempts to modify product ID, request should be rejected + $this->assertEquals(400, $response->getStatusCode()); + + // ensure that original product was not modified + $fetched_product = Product::getByID($old_product->getProductID()); + self::assertNotNull($fetched_product); + + $fetched_data = $fetched_product->toArray(); + + unset($old_data['created_date']); + unset($fetched_data['created_date']); + + assertEquals($old_data, $fetched_data); + + return; + } + + // else request is valid + $this->assertEquals(200, $response->getStatusCode()); - $new_product = Product::getByID($old_product->getProductID()); - $this->assertEquals($new_name, $new_product->getName()); + + // fetch same product directly from database + $fetched_product = Product::getByID($old_product->getProductID()); + self::assertNotNull($fetched_product); + $fetched_data = $fetched_product->toArray(); + + foreach (array_keys($expected_data) as $key) { + if ($expected_data[$key] === null) { + // data corresponding to key must not change + $this->assertEquals($old_data[$key], $fetched_data[$key]); + } else { + // data corresponding to key must change + $this->assertEquals($expected_data[$key], $fetched_data[$key]); + } + } } } \ No newline at end of file From 4fb5fcd86bc1664104317de3b5abd9ae27736892 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:53:00 +0400 Subject: [PATCH 50/52] fix bug where latitude=0 or longitude=0 caused error --- src/models/Store.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/Store.php b/src/models/Store.php index 7bb82bef..d9fc9855 100644 --- a/src/models/Store.php +++ b/src/models/Store.php @@ -137,7 +137,7 @@ public function validate(): array $latitude = $this->address->getLatitude(); $longitude = $this->address->getLongitude(); - if ($latitude == null || $longitude == null || + if ($latitude === null || $longitude === null || ($latitude < -90 || $latitude > 90 || $longitude < -180 || $longitude > 180)) { $errors['coordinates'] = "Invalid latitude or longitude."; From 61fcf48c443bf9237b24a483d34139321c627f19 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:53:21 +0400 Subject: [PATCH 51/52] run only model test suite --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab307727..8317372e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -66,5 +66,5 @@ jobs: - name: Install Composer dependencies run: composer install --prefer-dist --no-progress - - name: Run test suite - run: composer test + - name: Run model test suite + run: composer modeltest From bbebed964ac1cdb39630835f13c24bd938ab58d4 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:56:12 +0400 Subject: [PATCH 52/52] create .env.testing file --- .github/workflows/test.yml | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8317372e..4c41df8c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,19 +37,17 @@ jobs: mysql -u$DB_USER -p$DB_PASSWORD -hlocalhost -P3306 -Dcafe_test < "resources/database/cafe_test_data.sql" mysql -e "USE cafe_test; SHOW TABLES;" -u$DB_USER -p$DB_PASSWORD - - name: Create .env file + - name: Create .env.testing file env: - ENV: | - PUBLIC_ROOT="http://localhost/steamy-sips/public" - DB_HOST="localhost" - DB_USERNAME="root" - DB_PASSWORD="root" - TEST_DB_NAME="cafe_test" - BUSINESS_GMAIL="" - BUSINESS_GMAIL_PASSWORD="" + ENV: | + DB_HOST="localhost" + DB_USERNAME="root" + DB_PASSWORD="root" + DB_NAME="cafe_test" + API_BASE_URI="http://steamy.localhost/api/v1/" run: | - echo "$ENV" > .env - cat .env + echo "$ENV" > .env.testing + cat .env.testing - name: Validate composer.json and composer.lock run: composer validate --strict