diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ab307727..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
@@ -66,5 +64,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
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/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
diff --git a/composer.json b/composer.json
index 1bcc0b6d..088ff18f 100644
--- a/composer.json
+++ b/composer.json
@@ -22,11 +22,12 @@
"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"
+ "test": "phpunit tests",
+ "modeltest": "phpunit --testsuite models",
+ "apitest": "phpunit --testsuite api"
},
"autoload": {
"psr-4": {
@@ -34,10 +35,15 @@
"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": {
- "phpunit/phpunit": "^10.5"
+ "phpunit/phpunit": "^10.5",
+ "guzzlehttp/guzzle": "^7.0",
+ "fakerphp/faker": "^1.23"
}
}
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",
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..4e2ab37d 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:
@@ -159,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
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:
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
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/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 92%
rename from resources/schema/Client.json
rename to resources/schemas/Client.json
index 842b1652..67ce1edf 100644
--- a/resources/schema/Client.json
+++ b/resources/schemas/Client.json
@@ -24,7 +24,8 @@
"street",
"city",
"district_id"
- ]
+ ],
+ "additionalProperties": false
}
]
}
diff --git a/resources/schema/Comment.json b/resources/schemas/Comment.json
similarity index 94%
rename from resources/schema/Comment.json
rename to resources/schemas/Comment.json
index a6ed716d..156884b6 100644
--- a/resources/schema/Comment.json
+++ b/resources/schemas/Comment.json
@@ -30,5 +30,6 @@
"text",
"created_date",
"user_id"
- ]
+ ],
+ "additionalProperties": false
}
diff --git a/resources/schema/Review.json b/resources/schemas/Review.json
similarity index 94%
rename from resources/schema/Review.json
rename to resources/schemas/Review.json
index 04657aa2..ec6e6e30 100644
--- a/resources/schema/Review.json
+++ b/resources/schemas/Review.json
@@ -34,5 +34,6 @@
"text",
"client_id",
"product_id"
- ]
+ ],
+ "additionalProperties": false
}
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
diff --git a/resources/schema/Product.json b/resources/schemas/common/product.json
similarity index 74%
rename from resources/schema/Product.json
rename to resources/schemas/common/product.json
index 3c6f3dff..024d7387 100644
--- a/resources/schema/Product.json
+++ b/resources/schemas/common/product.json
@@ -1,9 +1,14 @@
{
"$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": {
+ "product_id": {
+ "type": "integer",
+ "description": "Unique identifier for product"
+ },
"name": {
"type": "string",
"description": "The name of the product",
@@ -17,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": {
@@ -48,21 +53,5 @@
"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": {
- "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
new file mode 100644
index 00000000..23e23164
--- /dev/null
+++ b/resources/schemas/products/create.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://example.com/products/create.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"
+ }
+ },
+ "required": [
+ "name",
+ "calories",
+ "img_url",
+ "img_alt_text",
+ "category",
+ "price",
+ "description"
+ ],
+ "additionalProperties": false
+}
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
+}
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..98327222 100644
--- a/src/controllers/api/Products.php
+++ b/src/controllers/api/Products.php
@@ -4,9 +4,12 @@
namespace Steamy\Controller\API;
+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
{
@@ -14,19 +17,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',
]
];
@@ -87,45 +90,29 @@ 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);
+
+ $result = Utility::validateAgainstSchema($data, "products/create.json");
+
+ if (!($result->isValid())) {
+ $errors = (new ErrorFormatter())->format($result->error());
+ $response = [
+ 'error' => $errors
+ ];
http_response_code(400);
- echo json_encode(['error' => "Missing fields: " . implode(', ', $requiredFields)]);
+ echo json_encode($response);
return;
}
- foreach ($requiredFields as $field) {
- if (empty($postData[$field])) {
- // Required field is missing, return 400 Bad Request
- http_response_code(400);
- echo json_encode(['error' => "Missing required field: $field"]);
- return;
- }
- }
-
// Create a new 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,
+ $data->calories,
+ $data->img_url,
+ $data->img_alt_text,
+ $data->category,
+ (float)$data->price,
+ $data->description
);
// Save the new product to the database
@@ -177,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
@@ -221,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);
}
}
diff --git a/src/controllers/api/Reviews.php b/src/controllers/api/Reviews.php
index 892735fb..eeaec323 100644
--- a/src/controllers/api/Reviews.php
+++ b/src/controllers/api/Reviews.php
@@ -13,18 +13,17 @@ 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',
],
'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',
]
];
@@ -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.
*/
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',
]
];
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/Utility.php b/src/core/Utility.php
index 75a41703..d19f5d7a 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
+ );
+ }
}
diff --git a/src/core/config.php b/src/core/config.php
index 8d8b887b..49b52168 100644
--- a/src/core/config.php
+++ b/src/core/config.php
@@ -2,20 +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']);
-
-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
+ $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();
diff --git a/src/models/Product.php b/src/models/Product.php
index 1035c4b9..1c687bc9 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) {
@@ -310,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";
}
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.";
diff --git a/tests/api/ProductsTest.php b/tests/api/ProductsTest.php
new file mode 100644
index 00000000..b59f6997
--- /dev/null
+++ b/tests/api/ProductsTest.php
@@ -0,0 +1,263 @@
+dummy_product = self::createProduct();
+ }
+
+ public function tearDown(): void
+ {
+ self::resetDatabase();
+ }
+
+ /**
+ * @throws GuzzleException
+ */
+ public function testGetAllProducts()
+ {
+ $response = self::$guzzle->get('products');
+ $this->assertEquals(200, $response->getStatusCode());
+
+ $body = $response->getBody();
+ $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 = self::$guzzle->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 = self::$guzzle->get('products/-1');
+ $this->assertEquals(404, $response->getStatusCode());
+ }
+
+ /**
+ * @throws GuzzleException
+ */
+ public function testGetProductCategories()
+ {
+ $response = self::$guzzle->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
+ * @throws Exception Expected product could not be created
+ */
+ public function testCreateValidProduct()
+ {
+ $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());
+
+ $this->assertArrayHasKey('product_id', $data_received);
+ self::assertTrue($data_received['product_id'] > 0);
+ }
+
+ /**
+ * @throws GuzzleException
+ * @throws Exception
+ */
+ public function testDeleteProductById()
+ {
+ // 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());
+ self::assertNull(Product::getByID($product->getProductID()));
+ }
+
+ /**
+ * @throws GuzzleException
+ * @throws Exception
+ */
+ public function testUpdateProductByIdForInvalidProduct()
+ {
+ $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
+ $response = self::$guzzle->put(
+ 'products/' . $old_product->getProductID(),
+ ['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());
+
+ // 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
diff --git a/tests/helpers/APIHelper.php b/tests/helpers/APIHelper.php
new file mode 100644
index 00000000..66bdbfdc
--- /dev/null
+++ b/tests/helpers/APIHelper.php
@@ -0,0 +1,46 @@
+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, // 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
diff --git a/tests/helpers/TestHelper.php b/tests/helpers/TestHelper.php
new file mode 100644
index 00000000..3d97a675
--- /dev/null
+++ b/tests/helpers/TestHelper.php
@@ -0,0 +1,286 @@
+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.
+ * @return void
+ */
+ public static function resetDatabase(): void
+ {
+ $conn = self::connect();
+
+ // 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 = null;
+ }
+
+ /**
+ * 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(bool $saveToDatabase = true): 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(),
+ $first_name,
+ $last_name,
+ self::$faker->password(),
+ self::$faker->phoneNumber(),
+ 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());
+ $errors = json_encode($client->validate());
+
+ $msg = <<< EOL
+ Unable to save client to database:
+ $json
+
+ Attribute errors:
+ $errors
+ EOL;
+
+ throw new Exception($msg);
+ }
+ return $client;
+ }
+
+ /**
+ * 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(bool $saveToDatabase = true): Product
+ {
+ $img_ext = self::$faker->randomElement(['png', 'jpeg', 'avif', 'jpg', 'webp']);
+ $product_name = self::$faker->words(2, true);
+
+ $product = new Product(
+ name: $product_name,
+ calories: self::$faker->numberBetween(1, 500),
+ img_url: $product_name . "." . $img_ext,
+ img_alt_text: self::$faker->sentence(),
+ category: self::$faker->lexify(),
+ price: 6.50,
+ description: self::$faker->sentence()
+ );
+
+ if (!$saveToDatabase) {
+ return $product;
+ }
+
+ $success = $product->save();
+
+ if (!$success) {
+ $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(bool $saveToDatabase = true): 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)
+ )
+ );
+
+ if (!$saveToDatabase) {
+ return $store;
+ }
+
+ $success = $store->save();
+
+ if (!$success) {
+ $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;
+ }
+
+ /**
+ * 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 = self::createStore();
+
+ // 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) {
+ $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);
+ }
+ }
+
+ $review = new Review(
+ product_id: $product->getProductID(),
+ client_id: $client->getUserID(),
+ text: self::$faker->sentence(10),
+ rating: self::$faker->numberBetween(1, 5)
+ );
+
+ $success = $review->save();
+
+ if (!$success) {
+ $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;
+ }
+}
\ No newline at end of file
diff --git a/tests/AdministratorTest.php b/tests/models/AdministratorTest.php
similarity index 95%
rename from tests/AdministratorTest.php
rename to tests/models/AdministratorTest.php
index dde5673a..1a4b6f66 100644
--- a/tests/AdministratorTest.php
+++ b/tests/models/AdministratorTest.php
@@ -2,16 +2,24 @@
declare(strict_types=1);
+namespace Steamy\Tests\Model;
+
+use Exception;
use PHPUnit\Framework\TestCase;
use Steamy\Model\Administrator;
-use Steamy\Core\Database;
+use Steamy\Tests\helpers\TestHelper;
final class AdministratorTest extends TestCase
{
- use Database;
+ use TestHelper;
private ?Administrator $dummy_admin;
+ public static function setUpBeforeClass(): void
+ {
+ self::resetDatabase();
+ }
+
/**
* @throws Exception
*/
@@ -33,9 +41,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/ClientTest.php b/tests/models/ClientTest.php
similarity index 50%
rename from tests/ClientTest.php
rename to tests/models/ClientTest.php
index 6eb2a10b..74f4949a 100644
--- a/tests/ClientTest.php
+++ b/tests/models/ClientTest.php
@@ -2,27 +2,36 @@
declare(strict_types=1);
+namespace Steamy\Tests\Model;
+
+use Exception;
use PHPUnit\Framework\TestCase;
+use Steamy\Core\Database;
use Steamy\Model\Client;
use Steamy\Model\Location;
-use Steamy\Core\Database;
+use Steamy\Tests\helpers\TestHelper;
final class ClientTest extends TestCase
{
- use Database;
+ use TestHelper;
+
private ?Client $dummy_client;
+ public static function setUpBeforeClass(): void
+ {
+ self::resetDatabase();
+ }
+
+ /**
+ * @throws Exception
+ */
public function setUp(): void
{
$address = new Location("Royal Road", "Curepipe", 1);
$this->dummy_client = new Client(
- "john_u@gmail.com",
- "john",
- "johhny",
- "abcd",
- "13213431",
- $address
+ "john_u@gmail.com", "john", "johhny", "abcd",
+ "13213431", $address
);
$success = $this->dummy_client->save();
@@ -36,7 +45,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
@@ -77,12 +86,8 @@ public function testToArray(): void
public function testValidate(): void
{
$client = new Client(
- "",
- "",
- "",
- "abcd",
- "",
- new Location(), // pass an empty Location object for testing
+ "", "", "", "abcd",
+ "", new Location(), // pass an empty Location object for testing
);
// Test if existence checks work
@@ -96,12 +101,8 @@ public function testValidate(): void
// Test for range checks
$client = new Client(
- "a@a.com",
- "Jo",
- "Doe",
- "1234567",
- "123456",
- new Location(), // pass an empty Location object for testing
+ "a@a.com", "Jo", "Doe", "1234567",
+ "123456", new Location(), // pass an empty Location object for testing
);
self::assertEquals([
@@ -125,108 +126,34 @@ public function testVerifyPassword(): void
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
+ public function testGetByEmail(): void
{
- 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);
+ // 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..70d122f3 100644
--- a/tests/CommentTest.php
+++ b/tests/models/CommentTest.php
@@ -2,22 +2,35 @@
declare(strict_types=1);
+namespace Steamy\Tests\Model;
+
+use DateTime;
+use Exception;
use PHPUnit\Framework\TestCase;
+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;
+use Steamy\Tests\helpers\TestHelper;
-Class CommentTest extends TestCase
+class CommentTest extends TestCase
{
- use Database;
+ use TestHelper;
+
private ?Comment $dummy_comment;
private ?Review $dummy_review;
private ?Client $reviewer;
private ?Product $dummy_product;
+ public static function setUpBeforeClass(): void
+ {
+ self::resetDatabase();
+ }
+
+ /**
+ * @throws Exception
+ */
public function setUp(): void
{
// Create a dummy product for testing
@@ -31,7 +44,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 +97,16 @@ 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
{
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 +159,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 94%
rename from tests/FuzzyTest.php
rename to tests/models/FuzzyTest.php
index 96bb5fe9..e5bc4b88 100644
--- a/tests/FuzzyTest.php
+++ b/tests/models/FuzzyTest.php
@@ -2,6 +2,8 @@
declare(strict_types=1);
+namespace Steamy\Tests\Model;
+
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 60%
rename from tests/OrderProductTest.php
rename to tests/models/OrderProductTest.php
index 28508968..69563b6b 100644
--- a/tests/OrderProductTest.php
+++ b/tests/models/OrderProductTest.php
@@ -2,18 +2,21 @@
declare(strict_types=1);
+namespace Steamy\Tests\Model;
+
+use Exception;
use PHPUnit\Framework\TestCase;
+use Steamy\Model\Client;
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;
+use Steamy\Tests\helpers\TestHelper;
+use Throwable;
class OrderProductTest extends TestCase
{
- use Database;
+ use TestHelper;
private ?Order $dummy_order;
private ?Client $client;
@@ -21,74 +24,45 @@ class OrderProductTest extends TestCase
private ?Product $dummy_product;
private array $line_items = [];
+ public static function setUpBeforeClass(): void
+ {
+ self::initFaker();
+ self::resetDatabase();
+ }
+
+ 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
{
- parent::setUp();
-
// 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
@@ -117,9 +91,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
@@ -141,7 +113,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(
@@ -155,6 +127,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());
}
}
diff --git a/tests/OrderTest.php b/tests/models/OrderTest.php
similarity index 95%
rename from tests/OrderTest.php
rename to tests/models/OrderTest.php
index 0bbda2a0..b7a91735 100644
--- a/tests/OrderTest.php
+++ b/tests/models/OrderTest.php
@@ -2,32 +2,40 @@
declare(strict_types=1);
+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\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;
+use Steamy\Tests\helpers\TestHelper;
class OrderTest extends TestCase
{
- use Database;
+ use TestHelper;
private ?Order $dummy_order = null;
private ?Client $client = null;
private ?Store $dummy_store = null;
private array $line_items = [];
+ public static function setUpBeforeClass(): void
+ {
+ self::initFaker();
+ self::resetDatabase();
+ }
+
/**
* @throws Exception
*/
public function setUp(): void
{
- parent::setUp();
-
// Initialize a dummy store object for testing
$this->dummy_store = new Store(
phone_no: "987654321", // Phone number
@@ -124,9 +132,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
@@ -202,7 +208,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 64%
rename from tests/ProductTest.php
rename to tests/models/ProductTest.php
index e849e78c..26ba5685 100644
--- a/tests/ProductTest.php
+++ b/tests/models/ProductTest.php
@@ -2,72 +2,60 @@
declare(strict_types=1);
+namespace Steamy\Tests\Model;
+
+use DateTime;
+use Exception;
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;
+
+ /**
+ * @var Review|null A review written by $dummy_client for $dummy_product
+ */
private ?Review $dummy_review;
+
+ public static function setUpBeforeClass(): void
+ {
+ self::resetDatabase();
+ 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
{
- $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 +64,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 +115,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(
@@ -133,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
@@ -159,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.',
);
}
@@ -232,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/ReviewTest.php b/tests/models/ReviewTest.php
similarity index 69%
rename from tests/ReviewTest.php
rename to tests/models/ReviewTest.php
index fee94c0b..4a34579f 100644
--- a/tests/ReviewTest.php
+++ b/tests/models/ReviewTest.php
@@ -2,30 +2,30 @@
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;
public static function setUpBeforeClass(): void
{
- self::$faker = Factory::create();
+ self::initFaker();
+ self::resetDatabase();
}
public static function tearDownAfterClass(): void
@@ -33,6 +33,12 @@ public static function tearDownAfterClass(): void
self::$faker = null;
}
+ public function onNotSuccessfulTest(Throwable $t): never
+ {
+ self::printFakerSeed();
+ parent::onNotSuccessfulTest($t);
+ }
+
/**
* Clears previously inserted data in database.
* @return void
@@ -43,125 +49,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();
}
/**
@@ -180,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();
@@ -336,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) {
diff --git a/tests/StoreTest.php b/tests/models/StoreTest.php
similarity index 78%
rename from tests/StoreTest.php
rename to tests/models/StoreTest.php
index a560c7d9..70a8de3a 100644
--- a/tests/StoreTest.php
+++ b/tests/models/StoreTest.php
@@ -2,24 +2,30 @@
declare(strict_types=1);
+namespace Steamy\Tests\Model;
+
+use Exception;
use PHPUnit\Framework\TestCase;
-use Steamy\Model\Store;
use Steamy\Model\Location;
-use Steamy\Core\Database;
+use Steamy\Model\Store;
+use Steamy\Tests\helpers\TestHelper;
class StoreTest extends TestCase
{
- use Database;
+ use TestHelper;
private ?Store $dummy_store;
+ public static function setUpBeforeClass(): void
+ {
+ self::resetDatabase();
+ }
+
/**
* @throws Exception
*/
public function setUp(): void
{
- parent::setUp();
-
// Initialize a dummy store object for testing
$this->dummy_store = new Store(
phone_no: "12345678", // Phone number
@@ -54,7 +60,7 @@ public function tearDown(): void
}
// clear all data from store tables
- self::query('DELETE FROM store;');
+ self::resetDatabase();
}
/**
@@ -114,13 +120,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."]
+ ],
];
}
}