diff --git a/AUTHORS b/AUTHORS index 6fb53b5277cf..cd9d36789702 100644 --- a/AUTHORS +++ b/AUTHORS @@ -2,3 +2,4 @@ Ezra Nobrega Justin Majetich +Albert Mwanza \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000000..b9bf900d0749 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,115 @@ + +## [Final version] + + + +## [v2.1.0] - 2025-02-12 +### Chores +- **setup_mysql_dev.sql:** Added MySQL setup file for development ([f34c4e0](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/f34c4e0f3c21d8c0d5996b87cae25969fe1323ea)) +- **setup_mysql_test.sql:** Added MySQL setup file for testing ([59eeb6e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/59eeb6eed7268f39d8ed6d3f66d8fcaab73e3dbd)) + +### Code Refactoring +- **console.py:** refactor to use comparison operators for equality tests ([efeb973](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/efeb973a43ac69267b3ed331fbcd1ea6b29c442a)) +- **console.py:** refactored precmd to use comparison operator for equality test ([b1d03c2](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/b1d03c2cb0b3963acd0a8721020768343e6fcd8d)) +- **console.py:** refactored do_count to call storage.all ([a5737f4](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/a5737f442be825cb2ce0ee932a20606940a8c547)) +- **console.py:** refactored do_all to call storage.all ([1083dd1](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/1083dd15342653537467d4105dd6f08ec432f812)) +- **console.py:** Refactored do_show to call storage.all ([8efeabd](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/8efeabd761daf018b276b3a04c684654baa90c28)) +- **console.py:** Refactored storage import from models ([8fcd9e7](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/8fcd9e75692ff88124b7e5fa148e98db35337d0f)) +- **console.py:** Refactored exit() and print() with return True in do_EOF method ([d534c1e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d534c1e7a511d17424e8fcbdbe4c5ea7985d30a6)) +- **console.py:** Refactored exit() with return True in do_quit method ([ad891f6](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/ad891f6bae73541cf1153c37646596dd0bcb6f94)) +- **console.py:** refactor to use comparison operators for equality tests ([efeb973](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/efeb973)) +- **console.py:** refactored precmd to use comparison operator for equality test ([b1d03c2](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/b1d03c2)) +- **console.py:** refactored do_count to call storage.all ([a5737f4](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/a5737f4)) +- **console.py:** refactored do_all to call storage.all ([1083dd1](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/1083dd1)) +- **console.py:** Refactored do_show to call storage.all ([8efeabd](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/8efeabd)) +- **tests/test_models/review.py:** Added instantiation of review to setUp method and refactored the test methods ([ab20561](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/ab20561)) +- **tests/test_models/place.py:** Added instantiation of place to setUp method and refactored the test methods ([f843c72](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/f843c72)) +- **models/base_model.py:** Refactored to_dict method to iterate over __dict__ attribute and ignore the _sa_instance_state attribute ([ce431f5](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/ce431f5)) +- **models/base_model.py:** Refactored save method to call storage.new method ([71c19ff](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/71c19ff)) +- **models/base_model.py:** Refactored __str__ to use f-strings for the return string and to removed the _sa_instance_state attribute from the string representation ([a38b99e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/a38b99e)) +- **models/base_model.py:** Refactored __init__ to iterate over kwargs and update attributes using setattr function. Removed storage import from __init__ and calling of the storage.new method ([d788011](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d788011)) +- **models/engine/file_storage.py:** Refactored reload method to use self to access FileStorage__file_path and to load data to FileStorage__objects ([62bc9ce](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/62bc9ce)) +- **models/engine/file_storage.py:** Refactored save method to use dictionary comprehension ([9303c93](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/9303c93)) +- **models/engine/file_storage.py:** Refactored save method to use f-strings for key and self to update FileStorage__objects ([6578f5a](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/6578f5a)) +- **console.py:** Refactored storage import from models ([8fcd9e7](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/8fcd9e7)) +- **console.py:** Refactored exit() and print() with return True in do_EOF method ([d534c1e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d534c1e)) +- **console.py:** Refactored exit() with return True in do_quit method ([ad891f6](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/ad891f6)) +- **tests/test_models/amenity.py:** Renamed class to Pascal naming style ([b4b5a96](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/b4b5a96)) + +### Documentation +- **AUTHORS:** Added 'Albert Mwanza to authors' ([eb1305e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/eb1305e1b5bbad2ceae12d7b92fd9f3f1dbc1345)) +- **CHANGELOG.md:** Updated commit links ([8f2a484](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/8f2a484529f40376cd4aaa6349ecd02156348ac0)) +- **CHANGELOG.md:** Updated to show commit links ([f2dd2a2](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/f2dd2a296a9efdba2e8759482e1ddc2fd1a9c9b1)) +- **CHANGELOG.md:** Updated to show commit links ([169356d](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/169356dbc9b8542c9b1591745f377f42afaaf176)) +- **CHANGELOG.md:** Added change log file ([d976a1d](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d976a1d761adba4d73b8c8e364e8fd3af6d65df6)) +- **tests/test_models/city.py:** Added docstrings ([de81e99](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/de81e99)) +- **tests/test_models/amenity.py:** Added docstrings ([d70a4eb](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d70a4eb)) +- **tests/test_models/base_model.py:** Added module and method docstrings ([1ed07ba](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/1ed07ba)) +- **CHANGELOG.md:** Added change log file ([d976a1d](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d976a1d)) +- **/CHANGELOG.md:** Added commit links ([2dbd158](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/2dbd158)) +- **/CHANGELOG.md:** Updated log to include commit links ([c97d457](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/c97d457)) +- **/CHANGELOG.md:** Added title ([067afbe](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/067afbe)) +- **/CHANGELOG.md:** Added title ([7081a34](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/7081a34)) +- **/CHANGELOG.md:** Updated changelog ([29d9569](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/29d9569)) +- **CHANGELOG.md:** Updated changelog ([ed51879](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/ed51879)) +- CHANGELOG.md added ([7d81b6f](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/7d81b6f)) +- Add Albert Mwanza to AUTHORS file ([e574683](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/e574683)) + + +### Features +- **console.py:** Updated the do_create method to handle the = parameter syntax for instantiation with kwargs ([52f2dbc](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/52f2dbc1e76e7ca27dff33574cedd5313c3f6410)) +- **models/user.py:** Updated class with attributes for SLQAlchemy table mapping ([10f26ef](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/10f26ef)) +- **models/state.py:** Update class with attributes for SQLAlchemy table mapping ([6725fe2](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/6725fe2)) +- **model/review.py:** Updated class with attributes for SQLAlchemy table mapping ([166e957](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/166e957)) +- **models/place.py:** Updated class with class attributes for ORM mapping. Added association table for many-to-many relationship mapping between places and amenities tables. Added property and setter methods for use with FileStorage. ([22baca0](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/22baca0)) +- **models/city.py:** Added class attributes for SQLAlchemy table mapping and relationship mapping ([a8c7707](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/a8c7707)) +- **models/amenity.py:** Updated class Amenity to Inherit from BaseModel and Base. Added class attributes for SQLAlchemy table mapping and Many to Many relationship mapping with Places ([764f169](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/764f169)) +- **models/base_model.py:** Added delete method to call storage.delete method to delete object from storage ([984e473](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/984e473)) +- **models/basem_model.py:** Added class attributes for SQLAlchemy table mapping ([a2cbcac](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/a2cbcac)) +- **models/engine/db_storage.py:** Added DBStorage module for managing database storage in MySQL ([50988b8](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/50988b8)) +- **models/__init__.py:** Updated to instantiate storage based on the environment variable HBNB_TYPE_STORAGE ([0244162](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/0244162)) +- **models/engine/file_storage.py:** Updated all method to allow filtering based on class ([c773e45](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/c773e45)) +- **models/engine/file_storage.py:** Added a new public instance method to delete objects from storage ([a295eb8](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/a295eb8)) + + +### Styles +- **console:** pycodestyle formatting ([0d3e2ef](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/0d3e2efdcc3173010d5f0501e2a3ace12447f785)) +- **console.py:** pycodestyle formatting ([9be3ffe](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/9be3ffe3266439542d23b96d9ed890562f44d530)) +- **console:** pycodestyle formatting ([0d3e2ef](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/0d3e2ef)) +- **tests/test_console.py:** pycodestyle formatting ([e92953c](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/e92953c)) +- **tests/test_models/test_engine/test_file_storage.py:** pycodestyle formatting ([e32e936](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/e32e936)) +- **console.py:** pycodestyle formatting ([9be3ffe](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/9be3ffe)) +- **tests/test_models/city.py:** Renamed imported class test_base_model to pascal case TestBaseModel ([fa5dc8e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/fa5dc8e)) +- **tests/test_models/amenity.py:** Renamed imported class test_base_model to pascal case TestBaseModel ([62ff0d2](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/62ff0d2)) +- **tests/test_models/base_model.py:** renamed class name to conform to PascalCase ([83d8a3d](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/83d8a3d)) +- **tests/test_models/test_engine/test_db_storage.py:** pycodestyle formatting ([c0ce84b](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/c0ce84b)) +- **models/base_model.py:** pycodestyle formatting ([96d660f](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/96d660f)) +- **models/engine/file_storage.py:** Pycodestyle Formatting ([fcedbb6](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/fcedbb6)) + +### Tests +- **tests/test_models/test_engine/test_file_storage.py:** Added test for the delete method and test classes for each model ([534c45c](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/534c45c)) +- **tests/test_models/user.py:** Added docstrings and additional assertions for each test method ([8a55601](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/8a55601)) +- **tests/test_models/state.py:** Added docstrings and additional assertions for test_name3 ([a731604](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/a731604)) +- **tests/test_models/test_engine/test_db_storage.py:** Add tests for DBStorage ([3dd588b](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/3dd588b)) +- **tests/test_console.py:** Add console tests for file and database storage ([9a8fc14](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/9a8fc14)) + +### Chores +- **models/state.py:** Added executable rights ([f75d27f](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/f75d27f)) +- **models/place.py:** Added executable rights ([7eae5aa](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/7eae5aa)) +- **models/city.py:** Added executable rights ([4d8948c](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/4d8948c)) +- **models/amenity.py:** Added executable rights ([2d497fb](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/2d497fb)) +- **models/engine/file_storage.py:** Added encoding declaration ([37d9454](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/37d9454)) +- **models/__init__.py:** Added executable rights ([38af25e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/38af25e)) +- **models/__init_.py:** Added encoding declaration ([b767157](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/b767157)) +- **models/engine/__init__.py:** Added executable rights ([c1467b9](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/c1467b9)) +- **models/engine/file_storage.py:** Added executable rights ([a313ed6](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/a313ed6)) +- **setup_mysql_test.sql:** Added MySQL setup file for testing ([59eeb6e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/59eeb6e)) +- **setup_mysql_dev.sql:** Added MySQL setup file for development ([f34c4e0](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/f34c4e0)) +- **setup_mysql_dev.sql:** Added MySQL setup file for development ([628f4a2](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/628f4a2)) +- **tests/__init__:** Added executable rights ([0775e3f](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/0775e3f)) + + +## v2.0.0 - 2025-02-11 + +[Unreleased]: https://github.com/mwanzaalbert/AirBnB_clone_v2/compare/v2.1.0...HEAD +[v2.1.0]: https://github.com/mwanzaalbert/AirBnB_clone_v2/compare/v2.0.0...v2.1.0 diff --git a/README.md b/README.md index 3ce462902d67..f27d6bf173ba 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,17 @@ This repository contains the initial stage of a student project to build a clone | Tasks | Files | Description | | ----- | ----- | ------ | -| 0: Authors/README File | [AUTHORS](https://github.com/justinmajetich/AirBnB_clone/blob/dev/AUTHORS) | Project authors | +| 0: Authors/README File | [AUTHORS](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/master/AUTHORS) | Project authors | | 1: Pep8 | N/A | All code is pep8 compliant| -| 2: Unit Testing | [/tests](https://github.com/justinmajetich/AirBnB_clone/tree/dev/tests) | All class-defining modules are unittested | -| 3. Make BaseModel | [/models/base_model.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/base_model.py) | Defines a parent class to be inherited by all model classes| -| 4. Update BaseModel w/ kwargs | [/models/base_model.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/base_model.py) | Add functionality to recreate an instance of a class from a dictionary representation| -| 5. Create FileStorage class | [/models/engine/file_storage.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/engine/file_storage.py) [/models/_ _init_ _.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/__init__.py) [/models/base_model.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/base_model.py) | Defines a class to manage persistent file storage system| -| 6. Console 0.0.1 | [console.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/console.py) | Add basic functionality to console program, allowing it to quit, handle empty lines and ^D | -| 7. Console 0.1 | [console.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/console.py) | Update the console with methods allowing the user to create, destroy, show, and update stored data | -| 8. Create User class | [console.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/console.py) [/models/engine/file_storage.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/engine/file_storage.py) [/models/user.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/user.py) | Dynamically implements a user class | -| 9. More Classes | [/models/user.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/user.py) [/models/place.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/place.py) [/models/city.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/city.py) [/models/amenity.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/amenity.py) [/models/state.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/state.py) [/models/review.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/review.py) | Dynamically implements more classes | -| 10. Console 1.0 | [console.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/console.py) [/models/engine/file_storage.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/engine/file_storage.py) | Update the console and file storage system to work dynamically with all classes update file storage | +| 2: Unit Testing | [/tests](https://github.com/mwanzaalbert/AirBnB_clone_v2/tree/dev/tests) | All class-defining modules are unittested | +| 3. Make BaseModel | [/models/base_model.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/base_model.py) | Defines a parent class to be inherited by all model classes| +| 4. Update BaseModel w/ kwargs | [/models/base_model.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/base_model.py) | Add functionality to recreate an instance of a class from a dictionary representation| +| 5. Create FileStorage class | [/models/engine/file_storage.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/engine/file_storage.py) [/models/_ _init_ _.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/__init__.py) [/models/base_model.py]https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/base_model.py) | Defines a class to manage persistent file storage system| +| 6. Console 0.0.1 | [console.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/console.py) | Add basic functionality to console program, allowing it to quit, handle empty lines and ^D | +| 7. Console 0.1 | [console.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/console.py) | Update the console with methods allowing the user to create, destroy, show, and update stored data | +| 8. Create User class | [console.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/console.py) [/models/engine/file_storage.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/engine/file_storage.py) [/models/user.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/user.py) | Dynamically implements a user class | +| 9. More Classes | [/models/user.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/user.py) [/models/place.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/place.py) [/models/city.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/city.py) [/models/amenity.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/amenity.py) [/models/state.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/state.py) [/models/review.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/review.py) | Dynamically implements more classes | +| 10. Console 1.0 | [console.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/console.py) [/models/engine/file_storage.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/engine/file_storage.py) | Update the console and file storage system to work dynamically with all classes update file storage |

General Use

@@ -139,4 +139,4 @@ Usage: .update(<_id>, ) (hbnb) User.all() (hbnb) ["[User] (98bea5de-9cb0-4d78-8a9d-c4de03521c30) {'updated_at': datetime.datetime(2020, 2, 19, 21, 47, 29, 134362), 'name': 'Fred the Frog', 'age': 9, 'id': '98bea5de-9cb0-4d78-8a9d-c4de03521c30', 'created_at': datetime.datetime(2020, 2, 19, 21, 47, 29, 134343)}"] ``` -
\ No newline at end of file +
diff --git a/console.py b/console.py index 13a8af68e930..1739f89f7639 100755 --- a/console.py +++ b/console.py @@ -2,14 +2,17 @@ """ Console Module """ import cmd import sys +import re +import json +import sqlalchemy from models.base_model import BaseModel -from models.__init__ import storage from models.user import User from models.place import Place from models.state import State from models.city import City from models.amenity import Amenity from models.review import Review +from models import storage class HBNBCommand(cmd.Cmd): @@ -19,16 +22,17 @@ class HBNBCommand(cmd.Cmd): prompt = '(hbnb) ' if sys.__stdin__.isatty() else '' classes = { - 'BaseModel': BaseModel, 'User': User, 'Place': Place, - 'State': State, 'City': City, 'Amenity': Amenity, - 'Review': Review - } + 'BaseModel': BaseModel, 'User': User, 'Place': Place, + 'State': State, 'City': City, 'Amenity': Amenity, + 'Review': Review + } dot_cmds = ['all', 'count', 'show', 'destroy', 'update'] + types = { - 'number_rooms': int, 'number_bathrooms': int, - 'max_guest': int, 'price_by_night': int, - 'latitude': float, 'longitude': float - } + 'number_rooms': int, 'number_bathrooms': int, + 'max_guest': int, 'price_by_night': int, + 'latitude': float, 'longitude': float + } def preloop(self): """Prints if isatty is false""" @@ -65,7 +69,7 @@ def precmd(self, line): pline = pline.partition(', ') # pline convert to tuple # isolate _id, stripping quotes - _id = pline[0].replace('\"', '') + _id = pline[0].strip('\'"') # possible bug here: # empty quotes register as empty _id when replaced @@ -73,7 +77,7 @@ def precmd(self, line): pline = pline[2].strip() # pline is now str if pline: # check for *args or **kwargs - if pline[0] is '{' and pline[-1] is'}'\ + if pline[0] == '{' and pline[-1] == '}'\ and type(eval(pline)) is dict: _args = pline else: @@ -88,13 +92,15 @@ def precmd(self, line): def postcmd(self, stop, line): """Prints if isatty is false""" - if not sys.__stdin__.isatty(): + if not sys.__stdin__.isatty() and line.strip() != "": + # 'and line.strip() != ""' added to Suppress Extra + # (hbnb) Prompt in postcmd print('(hbnb) ', end='') return stop def do_quit(self, command): """ Method to exit the HBNB console""" - exit() + return True def help_quit(self): """ Prints the help documentation for quit """ @@ -102,8 +108,7 @@ def help_quit(self): def do_EOF(self, arg): """ Handles EOF to exit program """ - print() - exit() + return True def help_EOF(self): """ Prints the help documentation for EOF """ @@ -115,21 +120,70 @@ def emptyline(self): def do_create(self, args): """ Create an object of any class""" - if not args: + cls, _, params = args.partition(" ") + + if not cls: print("** class name missing **") return - elif args not in HBNBCommand.classes: + + if cls not in HBNBCommand.classes: print("** class doesn't exist **") return - new_instance = HBNBCommand.classes[args]() - storage.save() - print(new_instance.id) - storage.save() + + new_instance = HBNBCommand.classes[cls]() + + if params: + # substitute single quote for double to handle json parsing + params = re.sub(r'[\']', '"', params) + + # patterns for the attributes' values in the param syntax + # = + strings = r"""["'](?P[A-Za-z0-9_,!@#$%^&*\.-]+)["']""" + floats = r"(?P(?:[-]?[0-9]+(?=\.))(?:\.[0-9]*(?![\.0-9-]+))" + ints = r"(?P[-]?[0-9]+(?=\s))" + + # pattern for param syntax: + # = [=...] + attr_pattern = rf""" + \b(?P[A-Za-z_]+)(?=\=) # Attribute name + (?:\=) + (?P{strings}|(?P{ints}|{floats}))) # Attr value + (?:\s*) + """ + + pattern = re.compile(attr_pattern, re.VERBOSE) + matches = pattern.findall(params) + if matches: + attr_dict = {} + for match in matches: + key, value, str_val, num_val, _, _ = match + try: + val = json.loads( + value) if str_val else json.loads(num_val) + + except (SyntaxError, json.JSONDecodeError) as e: + pass + + else: + if isinstance(val, str) and '_' in val: + translator = str.maketrans("_", " ") + attr_dict[key] = val.translate(translator) + else: + attr_dict[key] = val + new_instance = HBNBCommand.classes[cls](**attr_dict) + + try: + new_instance.save() + except sqlalchemy.exc.IntegrityError as e: + storage.rollback() + return + else: + print(new_instance.id) def help_create(self): """ Help information for the create method """ print("Creates a class of any type") - print("[Usage]: create \n") + print("[Usage]: create [=...]\n") def do_show(self, args): """ Method to show an individual object """ @@ -155,7 +209,7 @@ def do_show(self, args): key = c_name + "." + c_id try: - print(storage._FileStorage__objects[key]) + print(storage.all()[key]) except KeyError: print("** no instance found **") @@ -187,8 +241,7 @@ def do_destroy(self, args): key = c_name + "." + c_id try: - del(storage.all()[key]) - storage.save() + storage.delete(storage.all()[key]) except KeyError: print("** no instance found **") @@ -201,17 +254,19 @@ def do_all(self, args): """ Shows all objects, or all objects of a class""" print_list = [] + storage.reload() + if args: - args = args.split(' ')[0] # remove possible trailing args - if args not in HBNBCommand.classes: + cls = args.split(' ')[0] # remove possible trailing args + if cls not in HBNBCommand.classes: print("** class doesn't exist **") return - for k, v in storage._FileStorage__objects.items(): - if k.split('.')[0] == args: - print_list.append(str(v)) + for key, value in storage.all().items(): + if key.split('.')[0] == cls: + print_list.append(str(value)) else: - for k, v in storage._FileStorage__objects.items(): - print_list.append(str(v)) + for key, value in storage.all().items(): + print_list.append(str(value)) print(print_list) @@ -222,10 +277,23 @@ def help_all(self): def do_count(self, args): """Count current number of class instances""" + cls, _, _ = args.partition(" ") + + if not cls: + print("** class name missing **") + return + + if cls not in HBNBCommand.classes: + print("** class doesn't exist **") + return + + storage.reload() + count = 0 - for k, v in storage._FileStorage__objects.items(): - if args == k.split('.')[0]: + for k, v in storage.all().items(): + if cls == k.split('.')[0]: count += 1 + print(count) def help_count(self): @@ -234,6 +302,7 @@ def help_count(self): def do_update(self, args): """ Updates a certain object with new info """ + c_name = c_id = att_name = att_val = kwargs = '' # isolate cls from id/args, ex: (, delim, ) @@ -272,7 +341,7 @@ def do_update(self, args): args.append(v) else: # isolate args args = args[2] - if args and args[0] is '\"': # check for quoted arg + if args and args[0] == '\"': # check for quoted arg second_quote = args.find('\"', 1) att_name = args[1:second_quote] args = args[second_quote + 1:] @@ -280,10 +349,10 @@ def do_update(self, args): args = args.partition(' ') # if att_name was not quoted arg - if not att_name and args[0] is not ' ': + if not att_name and (not args[0].isspace()): att_name = args[0] # check for quoted val arg - if args[2] and args[2][0] is '\"': + if args[2] and args[2][0] == '\"': att_val = args[2][1:args[2].find('\"', 1)] # if att_val was not quoted arg @@ -320,5 +389,6 @@ def help_update(self): print("Updates an object with new information") print("Usage: update \n") + if __name__ == "__main__": HBNBCommand().cmdloop() diff --git a/models/__init__.py b/models/__init__.py old mode 100644 new mode 100755 index d3765c2bc603..ddadb75fa491 --- a/models/__init__.py +++ b/models/__init__.py @@ -1,7 +1,16 @@ #!/usr/bin/python3 -"""This module instantiates an object of class FileStorage""" -from models.engine.file_storage import FileStorage +# -*- coding: utf-8 -*- +"""This module instantiates an object of class FileStorage or DBStorage +based on the value of the evironment variable "HBNB_TYPE_STORAGE." +""" +import os +if os.getenv("HBNB_TYPE_STORAGE") == "db": + from models.engine.db_storage import DBStorage + storage = DBStorage() + storage.reload() -storage = FileStorage() -storage.reload() +else: + from models.engine.file_storage import FileStorage + storage = FileStorage() + storage.reload() diff --git a/models/amenity.py b/models/amenity.py old mode 100644 new mode 100755 index a181095e4170..a881057b087b --- a/models/amenity.py +++ b/models/amenity.py @@ -1,7 +1,39 @@ #!/usr/bin/python3 -""" State Module for HBNB project """ -from models.base_model import BaseModel +# -*- coding: utf-8 -*- +""" +Amenity module for the hbnb project. +Defines the `Amenity` class, which represents amenities available for places. +""" +import os +from models.base_model import BaseModel, Base +from sqlalchemy.orm import relationship +from sqlalchemy import Column, String -class Amenity(BaseModel): - name = "" + +class Amenity(BaseModel, Base): + """ + Represents an amenity in the hbnb project. + + Inherits from_: + - BaseModel: Provides common attributes like `id`, `created_at`, and + `updated_at`. + - Base: Enables SQLAlchemy ORM functionality. + + Attributes_: + __tablename__ (str): The name of the MySQL table. + name (Column or str): The name of the amenity. + place_amenities (relationship): A many-to-many relationship with + `Place` (only for DB storage). + """ + + __tablename__ = "amenities" + + name = Column(String(128), nullable=False) + + if os.getenv("HBNB_TYPE_STORAGE") == 'db': + place_amenities = relationship('Place', + secondary='place_amenity', + overlaps="place_amenities") + else: + name: str = "" diff --git a/models/base_model.py b/models/base_model.py old mode 100644 new mode 100755 index 4856e9de421f..d7489b72b61a --- a/models/base_model.py +++ b/models/base_model.py @@ -1,44 +1,87 @@ #!/usr/bin/python3 -"""This module defines a base class for all models in our hbnb clone""" +# -*- coding: utf-8 -*- +"""This module defines common attributes and methods for all hbnb models.""" import uuid from datetime import datetime +from sqlalchemy import Column, String, DateTime +from sqlalchemy.ext.declarative import declarative_base + + +Base = declarative_base() class BaseModel: - """A base class for all hbnb models""" + """ + A base class for all hbnb models. + + This class defines common attributes and methods for all models + that inherit from it. It is marked as an abstract class to prevent + direct table creation. + + Attributes_: + id (Column): A unique identifier for each instance. + created_at (Column): Timestamp when the instance was created. + updated_at (Column): Timestamp when the instance was last updated. + """ + + __abstract__ = True # Prevents table creation for BaseModel + + # Mark this class as an abstract to prevent its table creation + id = Column(String(60), nullable=False, primary_key=True, unique=True) + created_at = Column(DateTime, nullable=False, default=datetime.utcnow) + updated_at = Column(DateTime, nullable=False, default=datetime.utcnow) + def __init__(self, *args, **kwargs): - """Instatntiates a new model""" - if not kwargs: - from models import storage - self.id = str(uuid.uuid4()) - self.created_at = datetime.now() - self.updated_at = datetime.now() - storage.new(self) - else: - kwargs['updated_at'] = datetime.strptime(kwargs['updated_at'], - '%Y-%m-%dT%H:%M:%S.%f') - kwargs['created_at'] = datetime.strptime(kwargs['created_at'], - '%Y-%m-%dT%H:%M:%S.%f') - del kwargs['__class__'] - self.__dict__.update(kwargs) + """Instantiate a new model.""" + self.id = str(uuid.uuid4()) + self.created_at = datetime.now() + self.updated_at = datetime.now() + + if kwargs: + for key, value in kwargs.items(): + if key != "__class__": + if key in ("created_at", "updated_at"): + setattr(self, key, + datetime.strptime(kwargs[f'{key}'], + '%Y-%m-%dT%H:%M:%S.%f')) + else: + setattr(self, key, value) def __str__(self): - """Returns a string representation of the instance""" - cls = (str(type(self)).split('.')[-1]).split('\'')[0] - return '[{}] ({}) {}'.format(cls, self.id, self.__dict__) + """Return a string representation of the instance.""" + __dict__copy = self.__dict__.copy() + + try: + __dict__copy.pop('_sa_instance_state') + + except KeyError: + pass + + return f"[{type(self).__name__}] ({self.id}) {__dict__copy}" def save(self): - """Updates updated_at with current time when instance is changed""" + """Update updated_at with current time when instance is changed.""" from models import storage + self.updated_at = datetime.now() + storage.new(self) storage.save() def to_dict(self): - """Convert instance into dict format""" - dictionary = {} - dictionary.update(self.__dict__) - dictionary.update({'__class__': - (str(type(self)).split('.')[-1]).split('\'')[0]}) - dictionary['created_at'] = self.created_at.isoformat() - dictionary['updated_at'] = self.updated_at.isoformat() - return dictionary + """Convert instance into dict format.""" + return_dict = {"__class__": type(self).__name__} + + for key, value in self.__dict__.copy().items(): + if key != '_sa_instance_state': # updates + if key in ("created_at", "updated_at"): + return_dict[key] = value.isoformat() + else: + return_dict[key] = value + + return return_dict + + def delete(self): + """Delete instance from the storage.""" + from models import storage + + storage.delete(self) # updates diff --git a/models/city.py b/models/city.py old mode 100644 new mode 100755 index b9b4fe221502..3bffb54c215a --- a/models/city.py +++ b/models/city.py @@ -1,9 +1,45 @@ #!/usr/bin/python3 -""" City Module for HBNB project """ -from models.base_model import BaseModel +# -*- coding: utf-8 -*- +""" +City module for the HBNB project. +Defines the `City` class, which represents a city associated with a state. +""" +import os +from models.base_model import BaseModel, Base +from sqlalchemy.orm import relationship +from sqlalchemy import Column, String, ForeignKey -class City(BaseModel): - """ The city class, contains state ID and name """ - state_id = "" - name = "" + +class City(BaseModel, Base): + """ + Represents a city in the HBNB project. + + Inherits from_: + - BaseModel: Provides common attributes like `id`, `created_at`, and + `updated_at`. + - Base: Enables SQLAlchemy ORM functionality. + + Attributes_: + __tablename__ (str): The name of the MySQL table for cities. + name (Column or str): The name of the city (string, required). + state_id (Column or str): The ID of the associated state (foreign key, + required). + places (relationship): A one-to-many relationship with `Place` + (only for DB storage). + """ + + __tablename__ = "cities" + + name = Column(String(128), nullable=False) + + state_id = Column(String(60), ForeignKey("states.id"), + nullable=False) + + if os.getenv("HBNB_TYPE_STORAGE") == 'db': + places = relationship('Place', + backref='cities', + cascade='all, delete, delete-orphan') + else: + name: str = "" + state_id: str = "" diff --git a/models/engine/__init__.py b/models/engine/__init__.py old mode 100644 new mode 100755 diff --git a/models/engine/db_storage.py b/models/engine/db_storage.py new file mode 100755 index 000000000000..94d3cf712c1e --- /dev/null +++ b/models/engine/db_storage.py @@ -0,0 +1,203 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +DBStorage module for managing database storage in MySQL. + +This module defines the `DBStorage` class, which provides methods +to interact with a MySQL database using SQLAlchemy. It handles +database connections, session management, and CRUD operations +for various models. + +""" +import os +import importlib +from sqlalchemy import create_engine +from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.engine import URL +from sqlalchemy.exc import OperationalError +from models.base_model import Base +from models.amenity import Amenity +from models.city import City +from models.place import Place, place_amenity +from models.review import Review +from models.state import State +from models.user import User + +__author__ = "Albert Mwanza" +__license__ = "MIT" +__date__ = "2025-02-12" +__version__ = "2.1" + + +class DBStorage: + """ + Manages storage of hbnb models in a MySQL database. + + This class provides methods to interact with a MySQL database + using SQLAlchemy. It handles database connections, session + management, and CRUD operations for various models. + + Attributes_: + __engine (sqlalchemy.engine.Engine): The database engine. + __session (sqlalchemy.orm.scoped_session): The database session. + __objects (dict): A dictionary to store objects in memory. + """ + + DB_CLASSES = [User, State, City, Amenity, Place, Review] + + __engine: None + __session: None + + __objects = {} + + def __init__(self): + """ + Initialize the DBStorage instance and set up the database connection. + + The connection URL is constructed using environment variables: + - HBNB_MYSQL_USER: MySQL username + - HBNB_MYSQL_PWD: MySQL password + - HBNB_MYSQL_HOST: MySQL host + - HBNB_MYSQL_DB: MySQL database name + - HBNB_ENV: Application environment (e.g., 'test') + + If `HBNB_ENV` is set to 'test', all tables are dropped. + """ + # Construct the database connection URL + url_object = URL.create( + "mysql+mysqldb", # dialect+driver + username=os.getenv("HBNB_MYSQL_USER"), + password=os.getenv("HBNB_MYSQL_PWD"), + host=os.getenv("HBNB_MYSQL_HOST"), + database=os.getenv("HBNB_MYSQL_DB"),) + + # Create the database engine + self.__engine = create_engine(url_object, pool_pre_ping=True) + + # Drop all tables if in test environment + if os.getenv("HBNB_ENV") == "test": + try: + # Bind the engine to the Base's metadata + Base.metadata.bind = self.__engine + + # Drop all tables defined in the Base + Base.metadata.drop_all(self.__engine) + + except OperationalError as e: + message = getattr(e, '_message') + print(message().partition(" ") + [-1].strip('()').split(',')[-1]) + + def all(self, cls=None): + """ + Return a dictionary of models currently in storage. + + Args_: + cls (str or class, optional): The class or class name to filter + results. + If None, returns all objects. + + Returns_: + dict: A dictionary of objects, where the key is in the format + `.` and the value is the object + itself. + """ + + if cls is not None: + classes = ('BaseModel', + 'User', + 'Place', + 'State', + 'City', + 'Amenity', + 'Review') + + if isinstance(cls, str): + if cls not in classes: + return {} + + try: + # Dynamically import the class + # Convert class name to module name + module_path = f"models.{cls.lower()}" + module = importlib.import_module(module_path) + # Get the class from the module + cls = getattr(module, cls) + + except (ImportError, AttributeError): + print("** class doesn't exist **") + + if cls in self.DB_CLASSES: + for obj in self.__session.query(cls).all(): + key = f"{type(obj).__name__}.{obj.id}" + self.__objects[key] = obj + else: + return {} + else: + for model in self.DB_CLASSES: + for obj in self.__session.query(model).all(): + key = f"{type(obj).__name__}.{obj.id}" + self.__objects[key] = obj + + return self.__objects + + def new(self, obj): + """ + Add a new object to the storage dictionary. + + Args_: + obj: The object to add to the session. + """ + self.__session.add(obj) + + def save(self): + """Commit the current session to the database.""" + self.__session.commit() + + def reload(self): + """ + Reload the storage dictionary from the database. + + This method creates all tables if they don't exist and initializes + a new session. + """ + # Bind the engine to the Base's metadata + Base.metadata.bind = self.__engine + + # Create all tables defined in the Base + Base.metadata.create_all(self.__engine) + + # Create a session factory + session_factory = sessionmaker(bind=self.__engine, + expire_on_commit=False) + + # Create a scoped session + Session = scoped_session(session_factory) + + self.__session = Session() + + def delete(self, obj=None): + """ + Delete an object from the storage. + + Args_: + obj: The object to delete from the session. + """ + if obj is not None: + self.__session.delete(obj) + + try: + del self.__objects[type(obj).__name__ + '.' + obj.id] + + except KeyError as e: + pass + + self.save() + + def close(self): + """Close the current SQLAlchemy session.""" + self.__session.close() + + def rollback(self): + """Roll back the session to clear the invalid state.""" + self.__session.rollback() diff --git a/models/engine/file_storage.py b/models/engine/file_storage.py old mode 100644 new mode 100755 index 6f5d7f8d4680..7eb2f0397689 --- a/models/engine/file_storage.py +++ b/models/engine/file_storage.py @@ -1,32 +1,64 @@ #!/usr/bin/python3 -"""This module defines a class to manage file storage for hbnb clone""" +# -*- coding: utf-8 -*- +"""This module defines a class to manage file storage for hbnb clone.""" import json +import importlib class FileStorage: - """This class manages storage of hbnb models in JSON format""" + """This class manages storage of hbnb models in JSON format.""" + __file_path = 'file.json' __objects = {} - def all(self): - """Returns a dictionary of models currently in storage""" - return FileStorage.__objects + def all(self, cls=None): + """Return a dictionary of models currently in storage.""" + classes = {'BaseModel', + 'User', 'Place', + 'State', 'City', 'Amenity', + 'Review' + } + + if cls is not None: + if isinstance(cls, str): + if cls not in classes: + return {} + try: + # Dynamically import the class and + # Convert class name to module name + module_path = f"models.{cls.lower()}" + module = importlib.import_module(module_path) + cls = getattr(module, cls) # Get the class from the module + + except (ImportError, AttributeError): + print("** class doesn't exist **") + + # Filter objects based on the class + return { + key: value + for key, value in self.__objects.items() + if isinstance(value, cls) + } + + return self.__objects def new(self, obj): - """Adds new object to storage dictionary""" - self.all().update({obj.to_dict()['__class__'] + '.' + obj.id: obj}) + """Add new object to storage dictionary.""" + key = f"{obj.__class__.__name__}.{obj.id}" + self.__objects[key] = obj def save(self): - """Saves storage dictionary to file""" - with open(FileStorage.__file_path, 'w') as f: - temp = {} - temp.update(FileStorage.__objects) - for key, val in temp.items(): - temp[key] = val.to_dict() - json.dump(temp, f) + """Save storage dictionary to file.""" + with open(self.__file_path, 'w', encoding='utf-8') as outfile: + temp = { + key: value.to_dict() + for key, value in self.__objects.items() + } + + json.dump(temp, outfile) def reload(self): - """Loads storage dictionary from file""" + """Load storage dictionary from file.""" from models.base_model import BaseModel from models.user import User from models.place import Place @@ -35,16 +67,26 @@ def reload(self): from models.amenity import Amenity from models.review import Review - classes = { - 'BaseModel': BaseModel, 'User': User, 'Place': Place, - 'State': State, 'City': City, 'Amenity': Amenity, - 'Review': Review - } + classes = {'BaseModel': BaseModel, + 'User': User, 'Place': Place, + 'State': State, 'City': City, 'Amenity': Amenity, + 'Review': Review + } try: - temp = {} - with open(FileStorage.__file_path, 'r') as f: - temp = json.load(f) - for key, val in temp.items(): - self.all()[key] = classes[val['__class__']](**val) + with open(self.__file_path, 'r', encoding='utf-8') as infile: + file_data = json.load(infile) + + for key, value in file_data.items(): + cls_name = value['__class__'] + self.__objects[key] = classes[cls_name](**value) + except FileNotFoundError: pass + + def delete(self, obj=None): + """Delete object from storage.""" + if obj is not None: + key = f"{obj.__class__.__name__}.{obj.id}" + if key in self.__objects: + del self.__objects[key] + self.save() diff --git a/models/place.py b/models/place.py old mode 100644 new mode 100755 index 5221e8210d17..f3de499a076c --- a/models/place.py +++ b/models/place.py @@ -1,18 +1,119 @@ #!/usr/bin/python3 -""" Place Module for HBNB project """ -from models.base_model import BaseModel - - -class Place(BaseModel): - """ A place to stay """ - city_id = "" - user_id = "" - name = "" - description = "" - number_rooms = 0 - number_bathrooms = 0 - max_guest = 0 - price_by_night = 0 - latitude = 0.0 - longitude = 0.0 - amenity_ids = [] +# -*- coding: utf-8 -*- +""" +Place Module for HBNB project. + +Defines the `Place` class, which represents accommodations available in the +system. + +Handles relationships with `City`, `User`, `Review`, and `Amenity`. +""" +import os +from models.base_model import BaseModel, Base +from sqlalchemy.orm import relationship +from sqlalchemy import Integer, Column, String, ForeignKey, Float, Table +from typing import List + + +# association table for the many-to-many relationship +place_amenity = Table( + "place_amenity", + Base.metadata, + Column("place_id", String(60), ForeignKey("places.id"), + nullable=False, primary_key=True), + Column("amenity_id", String(60), ForeignKey("amenities.id"), + nullable=False, primary_key=True), +) + + +class Place(BaseModel, Base): + """ + Represents a place (accommodation) in the HBNB project. + + Inherits from: + - BaseModel: Provides common attributes like `id`, `created_at`, and + `updated_at`. + - Base: Enables SQLAlchemy ORM functionality. + + Attributes_: + __tablename__ (str): The name of the MySQL table for places. + city_id (Column or str): The ID of the associated city (foreign key, + required). + user_id (Column or str): The ID of the owner/user (foreign key, + required). + name (Column or str): The name of the place (string, required). + description (Column or str): A detailed description of the place. + number_rooms (Column or int): The number of rooms in the place. + number_bathrooms (Column or int): The number of bathrooms in the place. + max_guest (Column or int): The maximum number of guests allowed. + price_by_night (Column or int): The cost per night. + latitude (Column or float): The latitude coordinate of the place. + longitude (Column or float): The longitude coordinate of the place. + reviews (relationship or property): A one-to-many relationship with + `Review` (DB storage). + amenities (relationship or property): A many-to-many relationship with + `Amenity` (DB storage). + """ + + __tablename__ = "places" + + city_id = Column(String(60), ForeignKey('cities.id'), nullable=False) + user_id = Column(String(60), ForeignKey('users.id'), nullable=False) + name = Column(String(128), nullable=False) + description = Column(String(1024), nullable=True) + number_rooms = Column(Integer, nullable=False, default=0) + number_bathrooms = Column(Integer, nullable=False, default=0) + max_guest = Column(Integer, nullable=False, default=0) + price_by_night = Column(Integer, nullable=False, default=0) + latitude = Column(Float, nullable=True) + longitude = Column(Float, nullable=True) + + if os.getenv("HBNB_TYPE_STORAGE") == 'db': + reviews = relationship('Review', + backref='place', + cascade='all, delete, delete-orphan') + + amenities = relationship('Amenity', + secondary='place_amenity', + overlaps="place_amenities", + viewonly=False) + + else: + city_id: str = "" + user_id: str = "" + name: str = "" + description: str = "" + number_rooms: int = 0 + number_bathrooms: int = 0 + max_guest: int = 0 + price_by_night: int = 0 + latitude: float = 0.0 + longitude: float = 0.0 + amenity_ids: List[str] = [] + + @property + def reviews(self): + """Returns a list of `Review` instances related to this Place.""" + from models import storage + from models.review import Review # Avoid circular import issues + + return [review for review in storage.all( + Review).values() if review.place_id == self.id] + + @property + def amenities(self): + """Returns a list of `Amenity` instances linked to this Place.""" + from models import storage + from models.amenity import Amenity # Avoid circular import issues + + return [amenity for amenity in storage.all( + Amenity).values() if amenity.id in self.amenity_ids] + + @amenities.setter + def amenities(self, obj): + """Add an Amenity instance to this Place.""" + from models.amenity import Amenity # Avoid circular import issues + + if isinstance(obj, Amenity): + if obj.id not in self.amenity_ids: + self.amenity_ids.append(obj.id) diff --git a/models/review.py b/models/review.py old mode 100644 new mode 100755 index c487d90d34f0..8d8cdd4db975 --- a/models/review.py +++ b/models/review.py @@ -1,10 +1,40 @@ #!/usr/bin/python3 -""" Review module for the HBNB project """ -from models.base_model import BaseModel +# -*- coding: utf-8 -*- +""" +Review module for the HBNB project. +Defines the `Review` class, which stores user reviews related to places. +""" +import os +from models.base_model import BaseModel, Base +from sqlalchemy import Column, String, ForeignKey -class Review(BaseModel): - """ Review classto store review information """ - place_id = "" - user_id = "" - text = "" + +class Review(BaseModel, Base): + """ + Represents a review given by a user for a specific place. + + Inherits from_: + - BaseModel: Provides common attributes like `id`, `created_at`, and + `updated_at`. + - Base: Enables SQLAlchemy ORM functionality. + + Attributes_: + __tablename__ (str): The name of the MySQL table for reviews. + place_id (Column or str): The ID of the associated place (foreign key, + required). + user_id (Column or str): The ID of the user who wrote the review + (foreign key, required). + text (Column or str): The review content (string, required). + """ + + __tablename__ = "reviews" + + place_id = Column(String(60), ForeignKey('places.id'), nullable=False, ) + user_id = Column(String(60), ForeignKey('users.id'), nullable=False) + text = Column(String(1024), nullable=False) + + if os.getenv("HBNB_TYPE_STORAGE") != 'db': + place_id: str = "" + user_id: str = "" + text: str = "" diff --git a/models/state.py b/models/state.py old mode 100644 new mode 100755 index 583f041f07e4..1a6b6bd3bae3 --- a/models/state.py +++ b/models/state.py @@ -1,8 +1,56 @@ #!/usr/bin/python3 -""" State Module for HBNB project """ -from models.base_model import BaseModel +# -*- coding: utf-8 -*- +""" +State Module for HBNB project. +Defines the `State` class, which represents a state in the application. +""" +import os +from models.base_model import BaseModel, Base +from sqlalchemy.orm import relationship +from sqlalchemy import Column, String -class State(BaseModel): - """ State class """ - name = "" + +class State(BaseModel, Base): + """ + Represents a state in the HBNB project. + + Inherits from_: + - BaseModel: Provides common attributes like `id`, `created_at`, and + `updated_at`. + - Base: Enables SQLAlchemy ORM functionality. + + Attributes_: + __tablename__ (str): The name of the MySQL table for states. + name (Column or str): The name of the state (string, required). + cities (relationship or property): A relationship to `City` objects + if using DB storage, otherwise a + property returning related `City` + instances. + """ + + __tablename__ = "states" + + name = Column(String(128), nullable=False) + + if os.getenv("HBNB_TYPE_STORAGE") == 'db': + cities = relationship('City', + backref='state', + cascade='all, delete, delete-orphan') + else: + name: str = "" + + @property + def cities(self): + """ + Returns a list of `City` instances related to this `State` + when using file storage. + + Retrieves all `City` objects from storage and filters them + based on `state_id`. + """ + from models import storage + from models.city import City + + return [city for city in storage.all( + City).values() if city.state_id == self.id] diff --git a/models/user.py b/models/user.py index 4b54a6d24120..2fbff727b596 100644 --- a/models/user.py +++ b/models/user.py @@ -1,11 +1,54 @@ #!/usr/bin/python3 -"""This module defines a class User""" -from models.base_model import BaseModel +# -*- coding: utf-8 -*- +""" +User Module for HBNB project. +Defines the `User` class, which represents a user in the application. +""" +import os +from models.base_model import BaseModel, Base +from sqlalchemy.orm import relationship +from sqlalchemy import Column, String -class User(BaseModel): - """This class defines a user by various attributes""" - email = '' - password = '' - first_name = '' - last_name = '' + +class User(BaseModel, Base): + """ + Represents a user in the HBNB project. + + Inherits from_: + - BaseModel: Provides common attributes like `id`, `created_at`, and + `updated_at`. + - Base: Enables SQLAlchemy ORM functionality. + + Attributes_: + __tablename__ (str): The name of the MySQL table for users. + email (Column or str): The user's email address (string, required). + password (Column or str): The user's password (string, required). + first_name (Column or str): The user's first name (string, optional). + last_name (Column or str): The user's last name (string, optional). + places (relationship or list): Relationship to `Place` objects if + using DB storage. + reviews (relationship or list): Relationship to `Review` objects if + using DB storage. + """ + + __tablename__ = "users" + + email = Column(String(128), nullable=False) + password = Column(String(128), nullable=False) + first_name = Column(String(128), nullable=True) + last_name = Column(String(128), nullable=True) + + if os.getenv("HBNB_TYPE_STORAGE") == 'db': + places = relationship('Place', + backref='user', + cascade='all, delete, delete-orphan') + + reviews = relationship('Review', + backref='user', + cascade='all, delete, delete-orphan') + else: + email = '' + password = '' + first_name = '' + last_name = '' diff --git a/setup_mysql_dev.sql b/setup_mysql_dev.sql new file mode 100644 index 000000000000..aa64d15c9944 --- /dev/null +++ b/setup_mysql_dev.sql @@ -0,0 +1,7 @@ +-- Prepares the server for the project +-- Creates a database hbnb_dev_db and a new user hbnb_dev +CREATE DATABASE IF NOT EXISTS hbnb_dev_db; +CREATE USER IF NOT EXISTS 'hbnb_dev'@'localhost' IDENTIFIED BY 'hbnb_dev_pwd'; +GRANT ALL PRIVILEGES ON hbnb_dev_db.* TO 'hbnb_dev'@'localhost'; +GRANT SELECT ON performance_schema.* TO 'hbnb_dev'@'localhost'; +FLUSH PRIVILEGES; diff --git a/setup_mysql_test.sql b/setup_mysql_test.sql new file mode 100644 index 000000000000..3e35533d508d --- /dev/null +++ b/setup_mysql_test.sql @@ -0,0 +1,8 @@ +-- Prepares the server for the project +-- Creates a database hbnb_test_db and a new user hbnb_test +CREATE DATABASE IF NOT EXISTS hbnb_test_db; +CREATE USER IF NOT EXISTS 'hbnb_test'@'localhost' IDENTIFIED BY 'hbnb_test_pwd'; +GRANT USAGE ON *.* TO 'hbnb_test'@'localhost'; +GRANT ALL PRIVILEGES ON hbnb_test_db.* TO 'hbnb_test'@'localhost'; +GRANT SELECT ON performance_schema.* TO 'hbnb_test'@'localhost'; +FLUSH PRIVILEGES; diff --git a/tests/__init__.py b/tests/__init__.py old mode 100644 new mode 100755 diff --git a/tests/test_console.py b/tests/test_console.py new file mode 100755 index 000000000000..671ff72c266c --- /dev/null +++ b/tests/test_console.py @@ -0,0 +1,1374 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Unittest suites for the HBNBCommand class for FileStorage and DBStorage. +""" +import os +import time +import unittest +import MySQLdb +from sqlalchemy.exc import IntegrityError +from io import StringIO +from unittest.mock import patch +from console import HBNBCommand +from models import storage +from models.amenity import Amenity +from models.city import City +from models.place import Place +from models.review import Review +from models.state import State +from models.user import User +from models.engine.db_storage import DBStorage +from tests.test_models.test_engine.test_db_storage import BaseTestDBStorage + + +__author__ = "Albert Mwanza" +__license__ = "MIT" +__date__ = "2025-02-11" +__version__ = "2.1" + + +class TestBuiltInConsoleCommands(unittest.TestCase): + """Test cases for the HBNBCommand class.""" + + def test_quit_command(self): + """Test the quit command.""" + with patch('sys.stdout', new=StringIO()) as output: + self.assertTrue(HBNBCommand().onecmd("quit")) + + def test_eof_command(self): + """Test the EOF command.""" + with patch('sys.stdout', new=StringIO()) as output: + self.assertTrue(HBNBCommand().onecmd("EOF")) + + def test_empty_line(self): + """Test empty line input.""" + with patch('sys.stdout', new=StringIO()) as output: + self.assertFalse(HBNBCommand().onecmd("")) + + def test_help_command(self): + """Test the help command.""" + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd("help") + self.assertIn("Documented commands", output.getvalue()) + + @patch('sys.stdout', new_callable=StringIO) + def test_empty_line(self, mock_stdout): + """Test that empty line does nothing.""" + # Simulate pressing ENTER with an empty line + HBNBCommand().onecmd("") + self.assertEqual(mock_stdout.getvalue(), "") # No output expected + + @patch('sys.stdout', new_callable=StringIO) + def test_empty_line_with_spaces(self, mock_stdout): + """Test that a line with only spaces does nothing.""" + # Simulate pressing ENTER with a line of spaces + HBNBCommand().onecmd(" ") + self.assertEqual(mock_stdout.getvalue(), "") # No output expected + + +@unittest.skipIf(os.getenv("HBNB_TYPE_STORAGE") == "db", + "Skipping: not using DBStorage") +class TestFileStorageConsole(unittest.TestCase): + _classes = {'BaseModel', + 'User', + 'Place', + 'State', + 'City', + 'Amenity', + 'Review' + } + + @classmethod + def setUpClass(cls): + """Set up the test environment by creating a FileStorage instance and + clearing the file.""" + cls.file_path = "file.json" + cls.storage = storage + cls.storage.reload() # Ensure the database session is reloaded + cls.storage._FileStorage__objects.clear() + + @classmethod + def tearDownClass(cls): + """Clean up after each test by removing the test file.""" + if os.path.exists(cls.file_path): + os.remove(cls.file_path) + + def setUp(self): + """Set up test environment""" + self.storage.reload() + + def tearDown(self): + """Clean up after each test""" + # Clear the in-memory cache of objects + self.storage.all().clear() + + +class TestCreateCommand(TestFileStorageConsole): + """Test cases for the create command.""" + + def test_create_each_class_without_kwargs(self): + """Test create command for each class.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + self.assertRegex(instance_id, + r'^[0-9a-f-]{36}$') + key = cls + "." + instance_id + print(key) + self.assertIn(key, self.storage.all()) + + def test_create_invalid_class(self): + """Test create with an invalid class.""" + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd("create FakeClass") + self.assertEqual(output.getvalue().strip(), + "** class doesn't exist **") + + def test_storage_entries_count(self): + """Test Number of record in storage for each class.""" + self.assertEqual(len(self.storage.all()), len(self._classes)) + + for model in self._classes: + if model != "BaseModel": + self.assertEqual(len(self.storage.all(model)), 1) + + +class TestShowCommand(TestFileStorageConsole): + """Test cases for the show command.""" + + def test_show_valid_instance(self): + """Test show with a valid instance.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + with patch('sys.stdout', new=StringIO()) as show_output: + HBNBCommand().onecmd(f"show {cls} {instance_id}") + self.assertIn(instance_id, show_output.getvalue().strip()) + + def test_show_invalid_class(self): + """Test show with an invalid class.""" + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd("show FakeClass 1234") + self.assertEqual(output.getvalue().strip(), + "** class doesn't exist **") + + def test_show_missing_id(self): + """Test show with a missing ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"show {cls}") + self.assertEqual(output.getvalue().strip(), + "** instance id missing **") + + def test_show_nonexistent_id(self): + """Test show with a nonexistent ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"show {cls} 1234") + self.assertEqual(output.getvalue().strip(), + "** no instance found **") + + +class TestUpdateCommand(TestFileStorageConsole): + """Test cases for the show command.""" + + def test_update_valid_instance(self): + """Test update command for each class.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + with patch('sys.stdout', new=StringIO()) as update_output: + HBNBCommand().onecmd( + f"update {cls} {instance_id} author 'Albert'") + self.assertEqual(update_output.getvalue().strip(), "") + + def test_update_invalid_class(self): + """Test show with an invalid class.""" + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd("update FakeClass 1234 author 'Albert'") + self.assertEqual(output.getvalue().strip(), + "** class doesn't exist **") + + def test_update_nonexistent_id(self): + """Test show with a nonexistent ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + with patch('sys.stdout', new=StringIO()) as update_output: + HBNBCommand().onecmd(f"update {cls} 1234 author 'Albert'") + self.assertEqual(update_output.getvalue().strip(), + "** no instance found **") + + def test_update_missing_id(self): + """Test show with a missing ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"update {cls}") + self.assertEqual(output.getvalue().strip(), + "** instance id missing **") + + def test_update_missing_attr_name(self): + """Test show with a missing ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + with patch('sys.stdout', new=StringIO()) as update_output: + HBNBCommand().onecmd( + f"update {cls} {instance_id}") + self.assertEqual(update_output.getvalue().strip(), + "** attribute name missing **") + + def test_update_missing_attr_value(self): + """Test show with a missing ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + with patch('sys.stdout', new=StringIO()) as update_output: + HBNBCommand().onecmd(f"update {cls} {instance_id} author") + self.assertEqual(update_output.getvalue().strip(), + "** value missing **") + + +class TestDestroyCommand(TestFileStorageConsole): + """Test cases for the destroy command.""" + + def test_destroy_valid_instance(self): + """Test destroy with a valid instance.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + with patch('sys.stdout', new=StringIO()) as destroy_output: + HBNBCommand().onecmd(f"destroy {cls} {instance_id}") + self.assertEqual(destroy_output.getvalue().strip(), "") + + def test_destroy_invalid_class(self): + """Test destroy with an invalid class.""" + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd("destroy FakeClass 1234") + self.assertEqual(output.getvalue().strip(), + "** class doesn't exist **") + + def test_destroy_missing_id(self): + """Test destroy with a missing ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"destroy {cls}") + self.assertEqual(output.getvalue().strip(), + "** instance id missing **") + + def test_destroy_nonexistent_id(self): + """Test destroy with a nonexistent ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + with patch('sys.stdout', new=StringIO()) as destroy_output: + HBNBCommand().onecmd(f"destroy {cls} 1234") + self.assertEqual(destroy_output.getvalue().strip(), + "** no instance found **") + + +class TestAllCommand(TestFileStorageConsole): + """Test cases for the all command.""" + + def setUp(self): + """Set up the test environment by creating a FileStorage instance and + clearing the file.""" + self.storage.reload() + + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + + def test_all(self): + """Test all command without class.""" + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd("all") + self.assertIsInstance(output.getvalue().strip(), str) + + def test_all_with_class(self): + """Test all command with a valid class.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"all {cls}") + self.assertIsInstance(output.getvalue().strip(), str) + + def test_all_invalid_class(self): + """Test all command with an invalid class.""" + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd("all FakeClass") + self.assertEqual(output.getvalue().strip(), + "** class doesn't exist **") + + +class TestDotCommands(TestFileStorageConsole): + """Test cases for count, show, and update commands for each class.""" + + def setUp(self): + """Set up the test environment by creating a FileStorage instance and + clearing the file.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + + def test_count(self): + """Test count command for each class.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as count_output: + command = HBNBCommand().precmd( + f"{cls}.count()") # Manually process command + HBNBCommand().onecmd(command) # Execute transformed command + self.assertRegex(count_output.getvalue().strip(), r'^\d+$') + + def test_show_nonexistent(self): + """Test show command with nonexistent ID for each class.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as show_output: + command = HBNBCommand().precmd( + f"{cls}.show(1234)") # Manually process command + HBNBCommand().onecmd(command) # Execute transformed command + self.assertEqual(show_output.getvalue().strip(), + "** no instance found **") + + def test_all(self): + """Test all command without class.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + command = HBNBCommand().precmd( + f"{cls}.all()") # Manually process command + HBNBCommand().onecmd(command) + self.assertIsInstance(output.getvalue().strip(), str) + self.assertNotEqual(len(output.getvalue().strip()), len('[]')) + self.assertTrue(output.getvalue().strip().startswith("[") + and + output.getvalue().strip().endswith("]")) + + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + + all_instance_entry = f"[{cls}] ({instance_id})" + + with patch('sys.stdout', new=StringIO()) as all_output: + command = HBNBCommand().precmd( + f"{cls}.all()") # Manually process command + + # Execute transformed command + HBNBCommand().onecmd(command) + self.assertIn(all_instance_entry, + all_output.getvalue().strip()) + + # clear storage and remove json file to test for empty storage call + self.tearDown() + self.tearDownClass() + + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + command = HBNBCommand().precmd( + f"{cls}.all()") # Manually process command + HBNBCommand().onecmd(command) + + self.assertEqual(output.getvalue().strip(), '[]') + self.assertTrue(len(output.getvalue().strip()), 2) + + def test_all_invalid_class(self): + """Test all command with an invalid class.""" + with patch('sys.stdout', new=StringIO()) as output: + # Manually process command + command = HBNBCommand().precmd("FakeClass.all()") + HBNBCommand().onecmd(command) # Execute transformed command + self.assertEqual(output.getvalue().strip(), + "** class doesn't exist **") + + def test_update(self): + """Test update command for each class.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + + with patch('sys.stdout', new=StringIO()) as update_output: + command = HBNBCommand().precmd( + f"{cls}.update({instance_id}, attr_name, 'value')") + HBNBCommand().onecmd(command) + + self.assertEqual(update_output.getvalue().strip(), "") + + with patch('sys.stdout', new=StringIO()) as all_output: + command = HBNBCommand().precmd( + f"{cls}.all()") # Manually process command + + # Execute transformed command + HBNBCommand().onecmd(command) + self.assertTrue(all(item in all_output.getvalue().strip() + for item in ('attr_name', 'value'))) + + def test_update_nonexistent(self): + """Test update command with nonexistent ID for each class.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as update_output: + command = HBNBCommand().precmd( + f"{cls}.update(1234, attr_name, 'value')") + HBNBCommand().onecmd(command) + self.assertEqual(update_output.getvalue().strip(), + "** no instance found **") + + def test_destroy_valid_instance(self): + """Test destroy with a valid instance.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + + with patch('sys.stdout', new=StringIO()) as destroy_output: + command = HBNBCommand().precmd( + f"{cls}.destroy({instance_id})") + HBNBCommand().onecmd(command) + + self.assertEqual(destroy_output.getvalue().strip(), "") + + def test_destroy_invalid_class(self): + """Test destroy with an invalid class.""" + with patch('sys.stdout', new=StringIO()) as output: + command = HBNBCommand().precmd("FakeClass.destroy(1234)") + HBNBCommand().onecmd(command) + + self.assertEqual(output.getvalue().strip(), + "** class doesn't exist **") + + def test_destroy_missing_id(self): + """Test destroy with a missing ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + command = HBNBCommand().precmd(f"{cls}.destroy()") + HBNBCommand().onecmd(command) + + self.assertEqual(output.getvalue().strip(), + "** instance id missing **") + + def test_destroy_nonexistent_id(self): + """Test destroy with a nonexistent ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as destroy_output: + command = HBNBCommand().precmd(f"{cls}.destroy(1234)") + HBNBCommand().onecmd(command) + + self.assertEqual(destroy_output.getvalue().strip(), + "** no instance found **") + + +class TestShowUpdateCommandsWithDict(TestFileStorageConsole): + """Test cases for show and update commands with dictionary input for each + class.""" + + def test_show_with_dict(self): + """ + Test show command when a dictionary is used in update for each class. + """ + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + + with patch('sys.stdout', new=StringIO()) as update_output: + command = HBNBCommand().precmd( + f"{cls}.update({instance_id}, {{'author': 'Albert'}})") + HBNBCommand().onecmd(command) + self.assertEqual(update_output.getvalue().strip(), "") + + with patch('sys.stdout', new=StringIO()) as show_output: + command = HBNBCommand().precmd( + f"{cls}.show({instance_id})") + HBNBCommand().onecmd(command) + self.assertIn("'author': 'Albert'", + show_output.getvalue().strip()) + + def test_update_with_dict_nonexistent_id(self): + """ + Test update command with dictionary for nonexistent ID in each class. + """ + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + command = HBNBCommand().precmd( + f"{cls}.update(1234, {{'author': 'Albert'}})") + HBNBCommand().onecmd(command) + + self.assertEqual(output.getvalue().strip(), + "** no instance found **") + + +class TestDBStorageConsole(BaseTestDBStorage): + + DB_CLASSES = {'users': User, + 'places': Place, + 'states': State, + 'cities': City, + 'amenities': Amenity, + 'reviews': Review + } + + @classmethod + def get_model_class(cls, tablename): + """Get the model class for a given table name""" + return cls.DB_CLASSES.get(tablename) + + @classmethod + def create_attr_for_entries_created_based_table_name(cls, tablename): + """Get the model class for a given table name""" + setattr(cls, tablename, []) + + def test_01_DBStorage__object_has_no_object_of_the_model(self): + """Confirm __objects is empty""" + self.assertEqual(len(self.storage.all(self.model)), 0) + + def test_02_table_is_empty(self): + """Confirm table is empty""" + self.assertEqual(self.get_table_entries_count(), 0) + + def test_04_DBStorage__objects_not_empty_and_has_one_object(self): + """Confirm __objects is not empty""" + entries = getattr(self, self.tablename) + + self.assertTrue(len(self.storage.all(self.model)) > 0) + self.assertEqual(len(self.storage.all(self.model)), + self.get_table_entries_count()) + self.assertEqual(len(self.storage.all(self.model)), len(entries)) + self.assertEqual(len(self.storage.all(self.model)), 1) + self.assertEqual(len(entries), 1) + self.assertEqual(self.get_table_entries_count(), 1) + self.assertIn(entries[-1], self.storage.all(self.model).keys()) + + def test_05_table_is_not_empty_and_has_one_entry(self): + """Confirm the model's table is not empty""" + entries = getattr(self, self.tablename) + + self.assertTrue(self.get_table_entries_count() > + 0, "Entry was not added to DB") + self.assertEqual(len(self.storage.all(self.model)), + self.get_table_entries_count()) + self.assertEqual(len(self.storage.all(self.model)), len(entries)) + self.assertEqual(self.get_table_entries_count(), len(entries)) + self.assertEqual(len(self.storage.all(self.model)), 1) + self.assertEqual(len(entries), 1) + self.assertEqual(self.get_table_entries_count(), 1) + + def test_07_DBStorage__objects_has_two_objects_of_the_model(self): + """Confirm __objects size is greater thant 1""" + # update table entries count + entries = getattr(self, self.tablename) + + self.assertTrue(len(self.storage.all(self.model)) + > 1, "Entry was not added to DB") + self.assertEqual(len(self.storage.all(self.model)), + len(entries), "Entry was not added to DB") + self.assertEqual(len(self.storage.all(self.model)), + self.get_table_entries_count()) + self.assertIn(entries[-1], self.storage.all(self.model).keys()) + self.assertEqual(len(self.storage.all(self.model)), 2) + self.assertEqual(len(entries), 2) + self.assertEqual(self.get_table_entries_count(), 2) + + def test_08_table_has_two_entries(self): + """Confirm users table has more than one row""" + entries = getattr(self, self.tablename) + + self.assertTrue(self.get_table_entries_count() > + 1, "Entry was not added to DB") + self.assertEqual(self.get_table_entries_count(), + len(entries), "Entry was not added to DB") + self.assertEqual(len(self.storage.all(self.model)), + self.get_table_entries_count()) + self.assertEqual(self.get_table_entries_count(), len(entries)) + self.assertEqual(len(self.storage.all(self.model)), 2) + self.assertEqual(len(entries), 2) + self.assertEqual(self.get_table_entries_count(), 2) + + def test_09_deletion_of_last_table_entry(self): + """Cleanup: Delete the last created table entry from the database""" + entries = getattr(self, self.tablename) + initial_count = len(entries) + + self.assertEqual(self.get_table_entries_count(), initial_count) + self.assertEqual(len(self.storage.all(self.model)), + self.get_table_entries_count()) + self.assertEqual(self.get_table_entries_count(), initial_count) + self.assertEqual(len(self.storage.all(self.model)), 2) + self.assertEqual(len(entries), 2) + self.assertEqual(self.get_table_entries_count(), 2) + + key = entries[-1] + + self.storage.delete(storage.all(self.model)[key]) + + entry_id = key.split('.')[-1] + + # Refresh the MySQLdb connection to see the changes + self.conn.commit() + + entries.pop(-1) + + self.assertTrue(len(self.users) < initial_count, + "Entry not deleted from the DB") + self.assertEqual(self.get_table_entries_count(), + initial_count-1, "Entry not deleted from the DB") + self.assertEqual(len(self.storage.all(self.model)), + self.get_table_entries_count(), + "Entry not deleted from the DB") + self.assertEqual(len(self.storage.all(self.model)), + initial_count-1, "Entry not deleted from the DB") + self.assertEqual(len(entries), + self.get_table_entries_count(), + "Entry not deleted from the DB") + self.assertEqual(len(self.storage.all(self.model)), len( + entries), "Entry not deleted from the DB") + self.assertEqual(len(self.storage.all(self.model)), 1) + self.assertEqual(len(entries), 1) + self.assertEqual(self.get_table_entries_count(), 1) + + query = f"SELECT * FROM {self.tablename} WHERE id=%s" + self.cursor.execute(query, (entry_id,)) + result = self.cursor.fetchone() + self.assertTrue(result is None) + + def test_10_only_one_entry_exists_after_last_deletion(self): + """Helper function to count records in users table""" + query = f"SELECT * FROM {self.tablename}" + self.cursor.execute(query) + result = self.cursor.fetchone() + self.assertTrue(result is not None) + + def test_11_create_a_table_entry_without_kwargs(self): + """Test that creating a record with missing attributes via the console + does not add it to the database""" + models = {'users': "User", + 'places': "Place", + 'states': "State", + 'cities': "City", + 'amenities': "Amenity", + 'reviews': "Review" + } + + with patch("sys.stdout", new=StringIO()) as output: + args = f'create {models[self.tablename]}' + HBNBCommand().onecmd(args) + instance_id = output.getvalue().strip() + + self.assertEqual(instance_id, "") + self.assertFalse(instance_id) + + +class Test_02_User(TestDBStorageConsole): + """Tests DBStorage integration with console commands for the User model""" + tablename = "users" + + model = TestDBStorageConsole.get_model_class(tablename) + + TestDBStorageConsole.create_attr_for_entries_created_based_table_name( + tablename) + + def test_03_create_user_all_valid_attributes(self): + """Test that creating a User via the console adds it to the database""" + # Capture console output + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd( + 'create User email="gui@hbtn.io" password="guipwd" ' + + 'first_name="Guillaume" last_name="Snow"') + user_id = output.getvalue().strip() + + self.assertNotEqual(user_id, "", + "Failed to get User ID from console output") + self.assertTrue(len(user_id) > 0, + "Failed to get User ID from console output") + self.assertRegex(user_id, r'^[0-9a-f-]{36}$') + self.users.append("User." + user_id) + + def test_06_create_user_missing_nullable_attributes(self): + """Test that creating a User with missing nullable attributes via the + console does not add it to the database""" + # Test user creation missing last_name & first_name attributes + with patch("sys.stdout", new=StringIO()) as output: + args = 'create User email="a@a.com" password="pwd"' + HBNBCommand().onecmd(args) + + user_id = output.getvalue().strip() + + self.assertNotEqual(user_id, "", + "Failed to get User ID from console output") + self.assertTrue(len(user_id) > 0, + "Failed to get User ID from console output") + self.assertRegex(user_id, r'^[0-9a-f-]{36}$') + self.users.append("User." + user_id) + + def test_13_create_user_missing_non_nullable_attributes(self): + """ + Test that creating a User via the console with missing non_nullable + attributes does not add it to the database + """ + # Test missing email and password attributes + with patch("sys.stdout", new=StringIO()) as output: + args = 'create User first_name="Guillaume" last_name="Snow"' + HBNBCommand().onecmd(args) + user_id = output.getvalue().strip() + + self.assertEqual(user_id, "") + self.assertFalse(user_id) + + # Test missing password attribute + with patch("sys.stdout", new=StringIO()) as output: + args = 'create User email="a@a.com"' + HBNBCommand().onecmd(args) + user_id = output.getvalue().strip() + + self.assertEqual(user_id, "") + self.assertFalse(user_id) + + # Test missing email attribute + with patch("sys.stdout", new=StringIO()) as output: + args = 'create User password="pwd"' + HBNBCommand().onecmd(args) + user_id = output.getvalue().strip() + + self.assertEqual(user_id, "") + self.assertFalse(user_id) + + +class Test_03_State(TestDBStorageConsole): + """ + Tests DBStorage integration with console commands for the State model + """ + tablename = "states" + + model = TestDBStorageConsole.get_model_class(tablename) + + TestDBStorageConsole.create_attr_for_entries_created_based_table_name( + tablename) + + def test_03_create_state_valid_attributes(self): + """Test that creating a User via the console adds it to the database""" + # Capture console output + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd('create State name="California"') + state_id = output.getvalue().strip() + + self.assertNotEqual(state_id, "", + "Failed to get State ID from console output") + self.assertTrue(len(state_id) > 0, + "Failed to get State ID from console output") + self.assertRegex(state_id, r'^[0-9a-f-]{36}$') + self.states.append("State." + state_id) + + def test_06_create_additional_state_with_valid_attributes(self): + """Test that creating a User via the console adds it to the database""" + # Capture console output + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd('create State name="Nevada"') + state_id = output.getvalue().strip() + + self.assertNotEqual(state_id, "", + "Failed to get State ID from console output") + self.assertTrue(len(state_id) > 0, + "Failed to get State ID from console output") + self.assertRegex(state_id, r'^[0-9a-f-]{36}$') + self.states.append("State." + state_id) + + +class Test_04_City(TestDBStorageConsole): + """ + Tests DBStorage integration with console commands for the City model + """ + tablename = "cities" + + model = TestDBStorageConsole.get_model_class(tablename) + + TestDBStorageConsole.create_attr_for_entries_created_based_table_name( + tablename) + + def test_03_create_city_existing_state_id(self): + """ + Test that creating a City via the console adds it to the database + """ + + self.assertNotEqual(self.get_table_entries_count( + "states"), 0, "states table is empty") + self.assertTrue(self.get_table_entries_count( + "states") >= 1, "states table is empty") + + # 1. Get a valid state_id from and existing state + query = "SELECT id from states;" + self.cursor.execute(query) + state_id = self.cursor.fetchone()[0] + + # Create city from an existing state + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create City name="San_Francisco" state_id="{state_id}"' + HBNBCommand().onecmd(arg) + city_id = output.getvalue().strip() + + self.assertNotEqual(city_id, "", + "Failed to get City ID from console output") + self.assertTrue(len(city_id) > 0, + "Failed to get City ID from console output") + self.assertRegex(city_id, r'^[0-9a-f-]{36}$') + self.cities.append("City." + city_id) + + def test_06_create_city_all_existing_attributes(self): + """ + Test that creating a City via the console adds it to the database + """ + initial_state_count = self.get_table_entries_count("states") + + # 1. Create a new State first since City requires a valid state_id + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd('create State name="Nevada"') + state_id = output.getvalue().strip() + + self.assertNotEqual(state_id, "", + "Failed to get State ID from console output") + self.assertTrue(len(state_id) > 0, + "Failed to get State ID from console output") + self.assertRegex(state_id, r'^[0-9a-f-]{36}$') + + self.conn.commit() + + new_state_count = self.get_table_entries_count("states") + self.assertEqual(new_state_count, initial_state_count + + 1, "State was not added to DB") + + # 2. Create a City using the new state_id + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd( + f'create City name="Reno" state_id="{state_id}"') + city_id = output.getvalue().strip() + + self.assertNotEqual(city_id, "", + "Failed to get City ID from console output") + self.assertTrue(len(city_id) > 0, + "Failed to get City ID from console output") + self.assertRegex(city_id, r'^[0-9a-f-]{36}$') + self.cities.append("City." + city_id) + + def test_12_create_city_non_existent_state_id(self): + """ + Test that creating a City via the console with non_existent state_id + does not add it to the database + """ + with patch("sys.stdout", new=StringIO()) as output: + args = 'create City name="Fremont" state_id="12345"' + HBNBCommand().onecmd(args) + city_id = output.getvalue().strip() + + self.assertEqual(city_id, "") + self.assertFalse(city_id) + + def test_13_create_city_missing_state_id(self): + """ + Test that creating a City via the console with missing state_id + does not add it to the database + """ + with patch("sys.stdout", new=StringIO()) as output: + args = 'create City name="Fremont"' + HBNBCommand().onecmd(args) + city_id = output.getvalue().strip() + + self.assertEqual(city_id, "") + self.assertFalse(city_id) + + +class Test_05_Place(TestDBStorageConsole): + """Tests DBStorage integration with console commands for the Place model""" + tablename = "places" + + model = TestDBStorageConsole.get_model_class(tablename) + + TestDBStorageConsole.create_attr_for_entries_created_based_table_name( + tablename) + + def test_03_create_place_all_existing_attributes(self): + """ + Test that creating a Place via the console adds it to the database. + """ + + self.assertNotEqual(self.get_table_entries_count( + "users"), 0, "users table is empty") + self.assertTrue(self.get_table_entries_count( + "users") >= 1, "users table is empty") + + self.assertNotEqual(self.get_table_entries_count( + "cities"), 0, "cities table is empty") + self.assertTrue(self.get_table_entries_count( + "cities") >= 1, "cities table is empty") + + # 1. Get a valid city_id from an existing city (required for Place) + query = 'SELECT id from cities WHERE name=%s;' + self.cursor.execute(query, ("San Francisco",)) + city_id = self.cursor.fetchone()[0] + + # 2. Get a valid user_id from an existing user (required for Place) + query = 'SELECT id from users WHERE last_name=%s;' + self.cursor.execute(query, ("Snow",)) + user_id = self.cursor.fetchone()[0] + + # 3. Create a Place using the user_id and city_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Place city_id="{city_id}" user_id="{user_id}"' +\ + ' name="Happy_place" description="No_description_provided"' +\ + ' number_rooms=3 number_bathrooms=1 ' +\ + 'max_guest=6 price_by_night=120 latitude=37.773972 ' +\ + 'longitude=-122.431297' + HBNBCommand().onecmd(arg) + place_id = output.getvalue().strip() + + self.assertNotEqual(place_id, "", + "Failed to get Place ID from console output") + self.assertTrue(len(place_id) > 0, + "Failed to get Place ID from console output") + self.assertRegex(place_id, r'^[0-9a-f-]{36}$') + self.places.append("Place." + place_id) + + def test_06_create_place_missing_nullable_and_default_attributes(self): + """ + Test that creating a Place via the console with + missing_nullable_and_default_attributes adds it to the database. + """ + initial_state_count = self.get_table_entries_count("states") + initial_user_count = self.get_table_entries_count("users") + initial_city_count = self.get_table_entries_count("cities") + + # 1. Create a User (required for Place) + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd( + 'create User email="johnsmith@example.com" ' + + 'password="password" first_name="John" last_name="Smith"') + user_id = output.getvalue().strip() + + self.assertTrue(len(user_id) > 0, + "Failed to get State ID from console output") + self.assertRegex(user_id, r'^[0-9a-f-]{36}$') + + # 2. Create a City (required for Place) + + # Create a new State first since City requires a valid state_id + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd('create State name="Georgia"') + state_id = output.getvalue().strip() + + self.assertTrue(len(state_id) > 0, + "Failed to get State ID from console output") + self.assertRegex(state_id, r'^[0-9a-f-]{36}$') + + # create a new city using the state_id + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd( + f'create City name="Atlanta" state_id="{state_id}"') + city_id = output.getvalue().strip() + + self.assertTrue(len(city_id) > 0, + "Failed to get City ID from console output") + self.assertRegex(city_id, r'^[0-9a-f-]{36}$') + + self.conn.commit() + + new_state_count = self.get_table_entries_count("states") + new_user_count = self.get_table_entries_count("users") + new_city_count = self.get_table_entries_count("cities") + + self.assertEqual(new_state_count, initial_state_count + + 1, "State was not added to DB") + self.assertEqual(new_user_count, initial_user_count + + 1, "User was not added to DB") + self.assertEqual(new_city_count, initial_city_count + + 1, "City was not added to DB") + + # 3. Create a Place using the user_id and city_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Place city_id="{city_id}" ' +\ + f'user_id="{user_id}" name="Lovely_place"' + HBNBCommand().onecmd(arg) + place_id = output.getvalue().strip() + + self.assertNotEqual(place_id, "", + "Failed to get Place ID from console output") + self.assertTrue(len(place_id) > 0, + "Failed to get Place ID from console output") + self.assertRegex(place_id, r'^[0-9a-f-]{36}$') + self.places.append("Place." + place_id) + + def test_12_create_place_missing_user_id(self): + """ + Test that creating a Place via the console with missing user_id + doesn't add it to the database. + """ + self.assertNotEqual(self.get_table_entries_count( + "cities"), 0, "cities table is empty") + self.assertTrue(self.get_table_entries_count( + "cities") >= 1, "cities table is empty") + + # 1. Get an existing city_id + query = 'SELECT id from cities WHERE name=%s;' + self.cursor.execute(query, ("San Francisco",)) + city_id = self.cursor.fetchone()[0] + + # 2. Create a Place without a user_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Place city_id="{city_id}" name="Wonderful_place"' + HBNBCommand().onecmd(arg) + place_id = output.getvalue().strip() + + self.assertEqual(place_id, "") + self.assertFalse(place_id) + + def test_13_create_place_non_existent_user_id(self): + """ + Test that creating a Place via the console with non_existent user_id + doesn't add it to the database. + """ + self.assertNotEqual(self.get_table_entries_count( + "cities"), 0, "cities table is empty") + self.assertTrue(self.get_table_entries_count( + "cities") >= 1, "cities table is empty") + + # 1. Get an existing city_id + query = 'SELECT id from cities WHERE name=%s;' + self.cursor.execute(query, ("San Francisco",)) + city_id = self.cursor.fetchone()[0] + + # 2. Create a Place with a non-existent user_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Place city_id="{city_id}" user_id="12345" ' +\ + 'name="Wonderful_place"' + HBNBCommand().onecmd(arg) + place_id = output.getvalue().strip() + + self.assertEqual(place_id, "") + self.assertFalse(place_id) + + def test_14_create_place_missing_city_id(self): + """ + Test that creating a Place via the console with missing city_id + doesn't add it to the database. + """ + self.assertNotEqual(self.get_table_entries_count( + "users"), 0, "users table is empty") + self.assertTrue(self.get_table_entries_count( + "users") >= 1, "users table is empty") + + # 2. Get an existing user_id + query = 'SELECT id from users WHERE last_name=%s;' + self.cursor.execute(query, ("Snow",)) + user_id = self.cursor.fetchone()[0] + + # 3. Create a Place without a city_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Place user_id="{user_id}" name="Joyful_place"' + HBNBCommand().onecmd(arg) + place_id = output.getvalue().strip() + + self.assertEqual(place_id, "") + self.assertFalse(place_id) + + def test_15_create_place_non_existent_city_id(self): + """ + Test that creating a Place via the console with non-existent city_id + doesn't add it to the database. + """ + self.assertNotEqual(self.get_table_entries_count( + "users"), 0, "users table is empty") + self.assertTrue(self.get_table_entries_count( + "users") >= 1, "users table is empty") + + # 2. Get an existing user_id + query = 'SELECT id from users WHERE last_name=%s;' + self.cursor.execute(query, ("Snow",)) + user_id = self.cursor.fetchone()[0] + + # 3. Create a Place without a city_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Place city_id="12345" user_id="{user_id}" ' +\ + 'name="Joyful_place"' + HBNBCommand().onecmd(arg) + place_id = output.getvalue().strip() + + self.assertEqual(place_id, "") + self.assertFalse(place_id) + + +class Test_06_Review(TestDBStorageConsole): + """ + Tests DBStorage integration with console commands for the Review model. + """ + tablename = "reviews" + + model = TestDBStorageConsole.get_model_class(tablename) + + TestDBStorageConsole.create_attr_for_entries_created_based_table_name( + tablename) + + def test_03_create_review_all_existing_attributes(self): + """ + Test that creating a Review via the console adds it to the database. + """ + + self.assertNotEqual(self.get_table_entries_count( + "users"), 0, "users table is empty") + self.assertTrue(self.get_table_entries_count( + "users") >= 1, "users table is empty") + + self.assertNotEqual(self.get_table_entries_count( + "places"), 0, "places table is empty") + self.assertTrue(self.get_table_entries_count( + "places") >= 1, "places table is empty") + + # 1. Get a valid place_id from an existing place (required for Review) + query = 'SELECT id from places WHERE name=%s;' + self.cursor.execute(query, ("Happy place",)) + place_id = self.cursor.fetchone()[0] + print("city_id one place", place_id) + + # 2. Get a valid user_id from an existing user (required for Place) + query = 'SELECT id from users WHERE last_name=%s;' + self.cursor.execute(query, ("Snow",)) + user_id = self.cursor.fetchone()[0] + print("user_id one place", user_id) + + # 3. Create a Review using the user_id and city_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Review place_id="{place_id}" user_id="{user_id}"' +\ + ' text="Amazing_place,_beautiful_beach"' + HBNBCommand().onecmd(arg) + review_id = output.getvalue().strip() + + self.assertNotEqual(review_id, "", + "Failed to get Review ID from console output") + self.assertTrue(len(review_id) > 0, + "Failed to get Review ID from console output") + self.assertRegex(review_id, r'^[0-9a-f-]{36}$') + self.reviews.append("Review." + review_id) + + def test_06_create_review_all_valid_attributes(self): + """ + Test that creating a Place via the console adds it to the database. + """ + initial_state_count = self.get_table_entries_count("states") + initial_user_count = self.get_table_entries_count("users") + initial_city_count = self.get_table_entries_count("cities") + initial_place_count = self.get_table_entries_count("places") + + # 1. Create a User (required for Place) + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd( + 'create User email="janesmith@example.com" ' + + 'password="password" first_name="Jane" last_name="Smith"') + user_id = output.getvalue().strip() + + self.assertTrue(len(user_id) > 0, + "Failed to get State ID from console output") + self.assertRegex(user_id, r'^[0-9a-f-]{36}$') + + # 2. Create a Place (required for Review) + + # Create a new State first since City requires a valid state_id + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd('create State name="Louisiana"') + state_id = output.getvalue().strip() + + self.assertTrue(len(state_id) > 0, + "Failed to get State ID from console output") + self.assertRegex(state_id, r'^[0-9a-f-]{36}$') + + # create a new city using state_id since Place requires a valid city_id + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd( + f'create City name="New_Orleans" state_id="{state_id}"') + city_id = output.getvalue().strip() + + self.assertTrue(len(city_id) > 0, + "Failed to get City ID from console output") + self.assertRegex(city_id, r'^[0-9a-f-]{36}$') + + # Create a Place using the user_id and city_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Place city_id="{city_id}" user_id="{user_id}" ' +\ + 'name="Lovely_place"' + HBNBCommand().onecmd(arg) + place_id = output.getvalue().strip() + + self.conn.commit() + + new_state_count = self.get_table_entries_count("states") + new_user_count = self.get_table_entries_count("users") + new_city_count = self.get_table_entries_count("cities") + new_place_count = self.get_table_entries_count("places") + + self.assertEqual(new_state_count, initial_state_count + + 1, "State was not added to DB") + self.assertEqual(new_user_count, initial_user_count + + 1, "User was not added to DB") + self.assertEqual(new_city_count, initial_city_count + + 1, "City was not added to DB") + self.assertEqual(new_place_count, initial_place_count + + 1, "Place was not added to DB") + + # 3. Create a Review using the user_id and place_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Review place_id="{place_id}" ' +\ + f'user_id="{user_id}" text="Amazing_place,_beautiful_lounge"' + HBNBCommand().onecmd(arg) + review_id = output.getvalue().strip() + + self.reviews.append("Review." + review_id) + self.assertTrue(len(review_id) > 0, + "Failed to get Review ID from console output") + self.assertRegex(review_id, r'^[0-9a-f-]{36}$') + + def test_12_create_review_missing_user_id(self): + """ + Test that creating a Review via the console with missing + user_id doesn't add it to the database. + """ + self.assertNotEqual(self.get_table_entries_count( + "places"), 0, "places table is empty") + self.assertTrue(self.get_table_entries_count( + "places") >= 1, "places table is empty") + + # 1. Get a existing place_id + query = 'SELECT id from places WHERE name=%s;' + self.cursor.execute(query, ("Happy place",)) + place_id = self.cursor.fetchone()[0] + + # 3. Create a Review without a user_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Review place_id="{place_id}" ' +\ + 'text="Amazing_place,_beautiful_beach"' + HBNBCommand().onecmd(arg) + review_id = output.getvalue().strip() + + self.assertEqual(review_id, "") + self.assertFalse(review_id) + + def test_13_create_review_non_existent_user_id(self): + """ + Test that creating a Review via the console with non_existent user_id + doesn't add it to the database. + """ + self.assertNotEqual(self.get_table_entries_count( + "places"), 0, "places table is empty") + self.assertTrue(self.get_table_entries_count( + "places") >= 1, "places table is empty") + + # 1. Get a existing place_id + query = 'SELECT id from places WHERE name=%s;' + self.cursor.execute(query, ("Happy place",)) + place_id = self.cursor.fetchone()[0] + + # 3. Create a Review with a non-existent user_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Review place_id="{place_id}" user_id="12345" ' +\ + 'text="Amazing_place,_beautiful_beach"' + HBNBCommand().onecmd(arg) + review_id = output.getvalue().strip() + + self.assertEqual(review_id, "") + self.assertFalse(review_id) + + def test_14_create_review_missing_place_id(self): + """ + Test that creating a Review via the console with missing + place_id doesn't add it to the database + """ + + self.assertNotEqual(self.get_table_entries_count( + "users"), 0, "users table is empty") + self.assertTrue(self.get_table_entries_count( + "users") >= 1, "users table is empty") + + # 1. Get an exisiting user_id + query = 'SELECT id from users WHERE last_name=%s;' + self.cursor.execute(query, ("Snow",)) + user_id = self.cursor.fetchone()[0] + + # 2. Create a Review without a place_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Review user_id="{user_id}" ' +\ + 'text="Amazing_place,_beautiful_beach"' + HBNBCommand().onecmd(arg) + review_id = output.getvalue().strip() + + self.assertEqual(review_id, "") + self.assertFalse(review_id) + + def test_15_create_review_non_existent_place_id(self): + """ + Test that creating a Review via the console with non_existent + place_id doesn't add it to the database. + """ + + self.assertNotEqual(self.get_table_entries_count( + "users"), 0, "users table is empty") + self.assertTrue(self.get_table_entries_count( + "users") >= 1, "users table is empty") + + # 1. Get an exisiting user_id + query = 'SELECT id from users WHERE last_name=%s;' + self.cursor.execute(query, ("Snow",)) + user_id = self.cursor.fetchone()[0] + + # 2. Create a Review with non-existent place_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Review place_id="12345" user_id="{user_id}" ' +\ + 'text="Amazing_place,_beautiful_beach"' + HBNBCommand().onecmd(arg) + review_id = output.getvalue().strip() + + self.assertEqual(review_id, "") + self.assertFalse(review_id) + + +class Test_07_Amenity(TestDBStorageConsole): + """ + Tests DBStorage integration with console commands for the Amenity model. + """ + tablename = "amenities" + + model = TestDBStorageConsole.get_model_class(tablename) + + TestDBStorageConsole.create_attr_for_entries_created_based_table_name( + tablename) + + def test_03_create_amenity_with_valid_attributes(self): + """ + Test that creating an Amenity via the console adds it to the database + """ + # Capture console output + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd('create Amenity name="Wifi"') + amenity_id = output.getvalue().strip() + + self.amenities.append("Amenity." + amenity_id) + self.assertTrue(len(amenity_id) > 0, + "Failed to get Amenity ID from console output") + self.assertRegex(amenity_id, r'^[0-9a-f-]{36}$') + + def test_06_create_additional_amenity_with_valid_attributes(self): + """ + Test that creating an Amenity via the console adds it to the database + """ + # Capture console output + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd('create Amenity name="Oven"') + amenity_id = output.getvalue().strip() + + self.amenities.append("Amenity." + amenity_id) + self.assertTrue(len(amenity_id) > 0, + "Failed to get State ID from console output") + self.assertRegex(amenity_id, r'^[0-9a-f-]{36}$') + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_models/test_amenity.py b/tests/test_models/test_amenity.py index e47ab0d2e09a..62353ac910b0 100755 --- a/tests/test_models/test_amenity.py +++ b/tests/test_models/test_amenity.py @@ -1,19 +1,21 @@ #!/usr/bin/python3 -""" """ -from tests.test_models.test_base_model import test_basemodel +"""Unit tests for Amenity""" +from tests.test_models.test_base_model import TestBaseModel from models.amenity import Amenity -class test_Amenity(test_basemodel): - """ """ +class TestAmenity(TestBaseModel): + """Test cases for the Amenity class""" def __init__(self, *args, **kwargs): - """ """ + """Initialize test class""" super().__init__(*args, **kwargs) self.name = "Amenity" self.value = Amenity def test_name2(self): - """ """ + """Test that name is a string""" new = self.value() self.assertEqual(type(new.name), str) + self.assertTrue(hasattr(new, "name")) + self.assertEqual(new.name, "") diff --git a/tests/test_models/test_base_model.py b/tests/test_models/test_base_model.py index b6fef535c595..a9ebea4954e2 100755 --- a/tests/test_models/test_base_model.py +++ b/tests/test_models/test_base_model.py @@ -1,46 +1,51 @@ #!/usr/bin/python3 -""" """ -from models.base_model import BaseModel +# -*- coding: utf-8 -*- +"""Unit tests for BaseModel""" +import os import unittest -import datetime -from uuid import UUID import json -import os +from datetime import datetime, timedelta +from unittest.mock import patch +from models.base_model import BaseModel -class test_basemodel(unittest.TestCase): - """ """ +@unittest.skipIf(os.getenv("HBNB_TYPE_STORAGE") == "db", + "Skipping: not using DBStorage") +class TestBaseModel(unittest.TestCase): + """Test cases for the BaseModel class""" def __init__(self, *args, **kwargs): - """ """ + """Initialize test cases""" super().__init__(*args, **kwargs) self.name = 'BaseModel' self.value = BaseModel def setUp(self): - """ """ - pass + """Set up test environment by removing file.json""" + if os.path.exists("file.json"): + os.remove('file.json') def tearDown(self): + """Clean up the test file after each test.""" try: os.remove('file.json') - except: + except FileNotFoundError as e: pass def test_default(self): - """ """ + """Test if a new instance is correctly created""" i = self.value() self.assertEqual(type(i), self.value) def test_kwargs(self): - """ """ + """Test instantiation with **kwargs""" i = self.value() copy = i.to_dict() new = BaseModel(**copy) self.assertFalse(new is i) def test_kwargs_int(self): - """ """ + """Test passing an integer in kwargs""" i = self.value() copy = i.to_dict() copy.update({1: 2}) @@ -48,52 +53,95 @@ def test_kwargs_int(self): new = BaseModel(**copy) def test_save(self): - """ Testing save """ + """Test the save method to ensure correct JSON serialization""" i = self.value() i.save() key = self.name + "." + i.id + + self.assertTrue(os.path.exists("file.json"), + "file.json was not created") + with open('file.json', 'r') as f: j = json.load(f) + self.assertIn(key, j) self.assertEqual(j[key], i.to_dict()) def test_str(self): - """ """ + """Test the string representation of an instance""" i = self.value() + + try: + delattr(i, '_sa_instance_state') + except AttributeError: + pass + self.assertEqual(str(i), '[{}] ({}) {}'.format(self.name, i.id, i.__dict__)) def test_todict(self): - """ """ + """Test conversion to dictionary""" i = self.value() n = i.to_dict() self.assertEqual(i.to_dict(), n) def test_kwargs_none(self): - """ """ + """Test passing None as kwargs""" n = {None: None} with self.assertRaises(TypeError): new = self.value(**n) def test_kwargs_one(self): - """ """ + """Test instantiation with a single named attribute""" n = {'Name': 'test'} - with self.assertRaises(KeyError): - new = self.value(**n) + + new = self.value(**n) + self.assertIn('Name', dir(new)) + self.assertEqual(getattr(new, 'Name'), 'test') def test_id(self): - """ """ + """Test that id is a string""" new = self.value() self.assertEqual(type(new.id), str) def test_created_at(self): - """ """ + """Test that created_at is a datetime object""" new = self.value() - self.assertEqual(type(new.created_at), datetime.datetime) + self.assertIsInstance(new.updated_at, datetime) + + @patch('models.base_model.datetime') + def test_updated_at(self, mock_datetime): + """Test that updated_at is updated correctly""" + # Configure the mock to return a specific datetime object + mock_now = datetime.now() + mock_datetime.now.return_value = mock_now - def test_updated_at(self): - """ """ + # Create a new BaseModel instance new = self.value() - self.assertEqual(type(new.updated_at), datetime.datetime) + + # Check that updated_at is a datetime object + self.assertIsInstance(new.updated_at, datetime) + + # Check that created_at and updated_at are initially the same + self.assertEqual(new.created_at, new.updated_at) + + # Save the old updated_at value + old_updated_at = new.updated_at + + # Simulate 5 seconds later + mock_datetime.now.return_value = old_updated_at + timedelta(seconds=5) + new.save() + + # Convert the instance to a dictionary and back to a BaseModel instance n = new.to_dict() - new = BaseModel(**n) - self.assertFalse(new.created_at == new.updated_at) + + # Temporarily un-mock datetime to allow datetime.strptime + # to work correctly + with patch('models.base_model.datetime', + wraps=datetime) as mock_datetime_unmocked: + new = BaseModel(**n) + + # Check that created_at and updated_at are no longer the same + self.assertNotEqual(new.created_at, new.updated_at) + + # Check that updated_at is greater than the old updated_at + self.assertTrue(new.updated_at > old_updated_at) diff --git a/tests/test_models/test_city.py b/tests/test_models/test_city.py index 2673225808c0..699432e70c47 100755 --- a/tests/test_models/test_city.py +++ b/tests/test_models/test_city.py @@ -1,24 +1,32 @@ #!/usr/bin/python3 -""" """ -from tests.test_models.test_base_model import test_basemodel +"""Unit tests for City""" +from tests.test_models.test_base_model import TestBaseModel from models.city import City -class test_City(test_basemodel): - """ """ +class TestCity(TestBaseModel): + """Test cases for the City class""" def __init__(self, *args, **kwargs): - """ """ + """Initialize test class""" super().__init__(*args, **kwargs) self.name = "City" self.value = City def test_state_id(self): - """ """ + """ + Test that state_id exists, is a string, and defaults to an empty string + """ new = self.value() - self.assertEqual(type(new.state_id), str) + self.assertTrue(hasattr(new, "state_id")) + self.assertIsInstance(new.state_id, str) + self.assertEqual(new.state_id, "") def test_name(self): - """ """ + """ + Test that name exists, is a string, and defaults to an empty string + """ new = self.value() - self.assertEqual(type(new.name), str) + self.assertTrue(hasattr(new, "name")) + self.assertIsInstance(new.name, str) + self.assertEqual(new.name, "") diff --git a/tests/test_models/test_engine/test_db_storage.py b/tests/test_models/test_engine/test_db_storage.py new file mode 100755 index 000000000000..864ffecefd79 --- /dev/null +++ b/tests/test_models/test_engine/test_db_storage.py @@ -0,0 +1,758 @@ +#!/usr/bin/python3 +""" Module for testing database storage""" + +import os +import sys +import unittest +import MySQLdb +import sqlalchemy +from unittest.mock import patch, MagicMock +from models.user import User +from models.state import State +from models.city import City +from models.amenity import Amenity +from models.place import Place +from models.review import Review +from models import storage +from models.engine.db_storage import DBStorage +from sqlalchemy.orm import make_transient + +__author__ = "Albert Mwanza" +__license__ = "MIT" +__date__ = "2025-02-12" +__version__ = "2.1" + + +@unittest.skipIf(os.getenv("HBNB_TYPE_STORAGE") != "db", + "Skipping: not using DBStorage") +class BaseTestDBStorage(unittest.TestCase): + """Test cases for the DBStorage class""" + + @classmethod + def setUpClass(cls): + """Set up database connection""" + cls.storage = storage + cls.storage.reload() # Ensure the database session is reloaded + cls.storage._DBStorage__objects.clear() + + try: + # Setup MySQL connection using real credentials + cls.conn = MySQLdb.connect( + host=os.getenv("HBNB_MYSQL_HOST"), + user=os.getenv("HBNB_MYSQL_USER"), + password=os.getenv("HBNB_MYSQL_PWD"), + database=os.getenv("HBNB_MYSQL_DB"), + charset='utf8', + port=3306) + + except MySQLdb.OperationalError as e: + print(str(e).strip('()').split(',')[-1].strip()) + sys.exit(1) + + else: + cls.cursor = cls.conn.cursor() + + @classmethod + def tearDownClass(cls): + """Close database connection""" + cls.cursor.close() + cls.conn.close() + cls.storage.close() + + def setUp(self): + """Set up test environment""" + if not hasattr(self, 'tablename') or\ + self.tablename is None or\ + self.model is None: + self.skipTest("Skipping test since it has no table name") + + # Clear the in-memory cache of objects + self.storage._DBStorage__objects.clear() + + def tearDown(self): + """Clean up after each test""" + self.conn.commit() + + def test_01_type_objects(self): + """ Confirm __objects is a dict """ + self.assertTrue(type(self.storage.all()) is dict) + + def test_03_storage_var_created(self): + """ DBStorage object storage created """ + self.assertIsInstance(self.storage, DBStorage) + + def get_table_entries_count(self, table=None): + """Helper function to count records in a table""" + allowed_tables = ('users', + 'places', + 'states', + 'cities', + 'amenities', + 'reviews') + + if table is not None and table in allowed_tables: + query = f"SELECT COUNT(*) FROM {table}" + else: + query = f"SELECT COUNT(*) FROM {self.tablename}" + + self.cursor.execute(query) + return self.cursor.fetchone()[0] + + +class Test_01_TablesAreEmptyTestEnvironment(BaseTestDBStorage): + """Confirm the tables are empty i.e storage instance __objects is empty + when a storage instance is created and the test environment is 'test'.""" + + def setUp(self): + """Set up test environment""" + if os.getenv("HBNB_ENV") != "test": + self.skipTest( + "Skipping test since development environ is not test") + + self.storage._DBStorage__objects.clear() + + @patch.dict(os.environ, {"HBNB_ENV": "test"}) + # Mock DBStorage.__init__ + @patch('models.engine.db_storage.DBStorage.__init__', return_value=None) + def test_02_empty(self, mock_db_storage_init): + """Confirm __objects is empty when HBNB_ENV is 'test'.""" + # Create a mock DBStorage instance + mock_storage = DBStorage() + + # Ensure __objects is empty + self.assertEqual(len(mock_storage._DBStorage__objects), 0) + + # Verify that __init__ was called (optional) + mock_db_storage_init.assert_called_once() + + +class BaseTableTests(BaseTestDBStorage): + model = None + + new_record = None + + def create_a_new_record_instance(self, **kwargs): + if kwargs: + self.new_record = self.model(**kwargs) + else: + self.new_record = self.model() + + def get_object_record_from_db_by_id(self, obj=None, table=None): + """Helper function to count records in a table""" + tables = ('users', + 'places', + 'states', + 'cities', + 'amenities', + 'reviews') + + if table is not None and table in tables: + query = f"SELECT * FROM {table} where id=%s" + else: + query = f"SELECT * FROM {self.tablename} where id=%s" + + if obj is not None: + self.cursor.execute(query, (obj.id,)) + + return self.cursor.fetchone() + + def assert_object_in_db(self, obj): + """ + Helper method to assert that a record instance object exists in the + database. + """ + self.conn.commit() + + # Query the database directly to check if the object exists + result = self.get_object_record_from_db_by_id(obj) + + self.assertIsNotNone(result, f"Object {obj} not found in the database") + self.assertTrue(self.get_table_entries_count() >= 1) + return result + + def assert_object_not_in_db(self, obj): + """ + Helper method to assert that a record instance object exists in the + database. + """ + + self.conn.commit() + + # Query the database directly to check if the object exists + result = self.get_object_record_from_db_by_id(obj) + + self.assertIsNone( + result, + f"{self.new_record} object was not deleted from the database") + self.assertNotIn(obj, self.storage.all().values()) + + def update_record(self, **kwargs): + """Update a new_record instance with provided kwargs""" + for attr, value in kwargs.items(): + setattr(self.new_record, attr, value) + + def test_02_record_creation_without_kwargs(self): + """Test creating and saving a new_record object without kwargs to +the database""" + self.create_a_new_record_instance() + + try: + self.new_record.save() + except sqlalchemy.exc.IntegrityError: + self.storage.rollback() # clear the invalid state + finally: + self.assert_object_not_in_db(self.new_record) + + def test_04_table_creation_valid_kwargs(self): + """Test creating and saving a new_record object to the database""" + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + def test_05_new_record_attributes_property(self): + # Verify the object is in the database + + new_record_attr = [attr + for attr, val in self.new_record.__dict__.items() + if not attr.startswith("_") + and attr not in ("created_at", "updated_at") + and val != [] + and not isinstance(val, (User, + Place, + City, + Review, + Amenity, + State)) + ] + + result = self.assert_object_in_db(self.new_record) + + if self.model == Amenity: + try: + del new_record_attr[new_record_attr.index("state_id")] + except ValueError as e: + pass + + for attr in new_record_attr: + self.assertIn(getattr(self.new_record, attr), result) + + def test_06_record_deletion_from_db(self): + """Test deleting a new_record object from the database""" + initial_count = self.get_table_entries_count() + + self.assertTrue(self.get_table_entries_count() >= 1) + + # Delete new_record object and verify it is removed from the database + self.storage.delete(self.new_record) + + self.conn.commit() + + self.assert_object_not_in_db(self.new_record) + + new_count = self.get_table_entries_count() + self.assertTrue(new_count < initial_count) + + # Make the object transient + make_transient(self.new_record) + + +class Test_08_User(BaseTableTests): + """Test cases for the User model""" + tablename = "users" + + model = User + + new_record = User( + email="test@example1.com", + password="password", + first_name="Richard", + last_name="Quest", + ) + + def test_07_user_update(self): + """Test updating a User object in the database""" +# + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + self.assertEqual(self.new_record.email, "test@example1.com") + self.assertEqual(self.new_record.first_name, "Richard") + + self.test_05_new_record_attributes_property() + + # Update the user's email and name + self.update_record(first_name="Jane", email="test@example2.com") + + self.assertEqual(self.new_record.email, "test@example2.com") + self.assertEqual(self.new_record.first_name, "Jane") + + self.storage.save() + + # Verify the changes in the database + self.assert_object_in_db(self.new_record) + + self.test_05_new_record_attributes_property() + + query = F"SELECT * from {self.tablename} WHERE first_name=%s" + + self.cursor.execute(query, ("Jane",)) + + result = self.cursor.fetchone() + + self.assertIsNotNone(result, "User not updated") + + def test_08_user_creation_missing_kwargs(self): + # missing password + new_user1 = self.model(email="johndoe@example.com", + first_name="John", + last_name="Doe") + # missing email + new_user2 = self.model(password="password", + first_name="John", + last_name="Doe") + # missing first_name + new_user3 = self.model(email="bond@doubleoseven.com", + password="password", + last_name="bond") + # missing last_name + new_user4 = self.model(email="test@example.com", + password="password", + first_name="John") + + for user in (new_user1, new_user2, new_user3, new_user4): + try: + user.save() + except sqlalchemy.exc.IntegrityError: + self.storage.rollback() # Clear the invalid state + self.assert_object_not_in_db(user) + else: + self.assert_object_in_db(user) + + +class Test_09_State(BaseTableTests): + """Test cases for the State model""" + tablename = "states" + + model = State + + new_record = State(name="Texas") + + def test_07_state_update(self): + """Test updating a User object in the database""" + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + self.assertEqual(self.new_record.name, "Texas") + + self.test_05_new_record_attributes_property() + + # Update the state's name + self.update_record(name="Illinois") + + self.assertEqual(self.new_record.name, "Illinois") + + self.storage.save() + + # Verify the changes in the database + self.assert_object_in_db(self.new_record) + + self.test_05_new_record_attributes_property() + + query = F"SELECT * from {self.tablename} WHERE name=%s" + + self.cursor.execute(query, ("Illinois",)) + + result = self.cursor.fetchone() + + self.assertIsNotNone(result, "State not updated") + + +class Test_10_City(BaseTableTests): + """Test cases for the City model""" + tablename = "cities" + + model = City + + new_state = State(name="Massachusetts") + + new_record = City(name="Boston", state_id=new_state.id) + updated_record = None + + def test_04_table_creation_valid_kwargs(self): + """Test creating and saving a new_record object to the database""" + self.storage.new(self.new_state) + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + def test_07_city_update(self): + """Test updating a User object in the database""" + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + self.assertEqual(self.new_record.name, "Boston") + + self.test_05_new_record_attributes_property() + + # Update the city's name + self.update_record(name="Amherst") + self.assertEqual(self.new_record.name, "Amherst") + + self.storage.save() + + # Verify the changes in the database + self.assert_object_in_db(self.new_record) + + self.test_05_new_record_attributes_property() + + query = F"SELECT * from {self.tablename} WHERE name=%s" + + self.cursor.execute(query, ("Amherst",)) + + result = self.cursor.fetchone() + + self.assertIsNotNone(result, "City not updated") + + setattr(self, 'updated_record', self.new_record) + + def test_08_city_state_relationship(self): + """Test the relationship between City and State""" + state_id = self.new_state.id + + # Verify the relationship in the database + query = """SELECT cities.name, cities.id +FROM cities +JOIN states ON cities.state_id=states.id +WHERE states.id=%s;""" + self.cursor.execute(query, (state_id,)) + + result = self.cursor.fetchone() + + city_name, city_id = result + + self.assertEqual(self.new_record.name, city_name) + self.assertEqual(self.new_record.id, city_id) + + +class Test_11_Place(BaseTableTests): + """Test cases for the Place model""" + tablename = "places" + + model = Place + + new_user = User( + email="therock@wwe.ent", + password="therock", + first_name="Dwane", + last_name="Johnson", + ) + + new_state = State(name="Hawaii") + + new_city = City(name="Holualoa", state_id=new_state.id) + + new_record = Place(name="Grand Vacation Club", + city_id=new_city.id, + user_id=new_user.id, + description="Hilton Grand Vacations Club Kings" + + "Land Waikoloa") + + def test_04_table_creation_valid_kwargs(self): + """Test creating and saving a new_record object to the database""" + self.storage.new(self.new_user) + self.storage.new(self.new_state) + self.storage.new(self.new_city) + self.storage.save() + + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + def test_07_place_update(self): + """Test updating a User object in the database""" + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + self.assertEqual(self.new_record.name, "Grand Vacation Club") + + self.test_05_new_record_attributes_property() + + # Update the state's name + self.update_record(name="Sheraton", + description="Sheraton Waikiki Beach Resort") + + self.assertEqual(self.new_record.name, "Sheraton") + self.assertEqual(self.new_record.description, + "Sheraton Waikiki Beach Resort") + + self.storage.save() + + # Verify the changes in the database + self.assert_object_in_db(self.new_record) + + self.test_05_new_record_attributes_property() + + query = F"SELECT * from {self.tablename} WHERE name=%s" + + self.cursor.execute(query, ("Sheraton",)) + + result = self.cursor.fetchone() + + self.assertIsNotNone(result, "Place not updated") + + def test_08_place_city_user_relationship(self): + """Test the relationship between City, User and State""" + city_id = self.new_city.id + user_id = self.new_user.id + + # Verify the relationship in the database + query = """SELECT places.name, places.id +FROM places +JOIN cities ON places.city_id=cities.id +JOIN users ON places.user_id=users.id +WHERE cities.id=%s +AND users.id=%s;""" + self.cursor.execute(query, (city_id, user_id,)) + + result = self.cursor.fetchone() + + place_name, place_id = result + + self.assertEqual(self.new_record.name, place_name) + self.assertEqual(self.new_record.id, place_id) + + +class Test_12_Review(BaseTableTests): + """Test cases for the Review model""" + tablename = "reviews" + + model = Review + + new_user = User( + email="fred@futuristic.net", + password="theflintstones", + first_name="Fred", + last_name="Flintstone", + ) + + new_state = State(name="Florida") + + new_city = City(name="Orlando", state_id=new_state.id) + + new_place = Place(name="Futuristic Resorts", + city_id=new_city.id, + user_id=new_user.id, + description="Description not provided") + + new_record = Review(place_id=new_place.id, + user_id=new_user.id, + text="This is a great place!") + + def test_04_table_creation_valid_kwargs(self): + """Test creating and saving a new_record object to the database""" + self.storage.new(self.new_user) + self.storage.new(self.new_state) + self.storage.new(self.new_city) + self.storage.new(self.new_place) + self.storage.save() + + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + def test_07_review_update(self): + """Test updating a User object in the database""" + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + self.assertEqual(self.new_record.text, "This is a great place!") + + self.test_05_new_record_attributes_property() + + # Update the state's name + self.update_record(text="This place is amazing!") + + self.assertEqual(self.new_record.text, "This place is amazing!") + + self.storage.save() + + # Verify the changes in the database + self.assert_object_in_db(self.new_record) + + self.test_05_new_record_attributes_property() + + query = F"SELECT * from {self.tablename} WHERE text=%s" + + self.cursor.execute(query, ("This place is amazing!",)) + + result = self.cursor.fetchone() + + self.assertIsNotNone(result, "Review not updated") + + def test_08_review_place_user_relationship(self): + """Test the relationship between City, User and State""" + place_id = self.new_place.id + user_id = self.new_user.id + + # Verify the relationship in the database + query = """SELECT reviews.text, reviews.id +FROM reviews +JOIN places ON reviews.place_id=places.id +JOIN users ON reviews.user_id=users.id +WHERE places.id=%s +AND users.id=%s;""" + self.cursor.execute(query, (place_id, user_id,)) + + result = self.cursor.fetchone() + + review, review_id = result + + self.assertEqual(self.new_record.text, review) + self.assertEqual(self.new_record.id, review_id) + + +class Test_13_Amenity(BaseTableTests): + """Test cases for the Amenity model""" + tablename = "amenities" + + model = Amenity + + new_user = User( + email="jj@spageage.com", + password="theflintstones", + first_name="Judy", + last_name="Jetson", + ) + + new_state = State(name="Michigan") + + new_city = City(name="Detroit", state_id=new_state.id) + + new_place = Place(name="Space Age World", + city_id=new_city.id, + user_id=new_user.id, + description="A futuristic place") + + new_review = Review(place_id=new_place.id, + user_id=new_user.id, + text="This is a fantastic place!") + + new_record = Amenity(name="Wifi") + + def test_04_table_creation_valid_kwargs(self): + """Test creating and saving a new_record object to the database""" + self.storage.new(self.new_user) + self.storage.new(self.new_state) + self.storage.new(self.new_city) + self.storage.new(self.new_place) + self.storage.new(self.new_review) + self.storage.save() + + self.new_record.state_id = self.new_state.id + + self.storage.new(self.new_record) + self.storage.save() + self.new_record.save() + + self.assert_object_in_db(self.new_record) + + def test_07_amenity_update(self): + """Test updating a User object in the database""" + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + self.assertEqual(self.new_record.name, "Wifi") + + self.test_05_new_record_attributes_property() + + # Update the state's name + self.update_record(name="Cable") + + self.assertEqual(self.new_record.name, "Cable") + + self.storage.save() + + # Verify the changes in the database + self.assert_object_in_db(self.new_record) + + self.test_05_new_record_attributes_property() + + query = F"SELECT * from {self.tablename} WHERE name=%s" + + self.cursor.execute(query, ("Cable",)) + + result = self.cursor.fetchone() + + self.assertIsNotNone(result, "Amenity not updated") + + def test_08_place_amenities_relationship(self): + """Test the relationship between City, User and State""" + new_user = User( + email="sales@hpconfectionaries.com", + password="sweethoney", + first_name="Dwight", + last_name="Malone", + ) + + new_state = State(name="Oklahoma") + + new_city = City(name="Tulsa", state_id=new_state.id) + + new_place = Place(name="Higher Plane", + city_id=new_city.id, + user_id=new_user.id) + + new_review = Review(place_id=new_place.id, + user_id=new_user.id, + text="I love the weed edibles!") + + new_amenity = Amenity(name="Meditation Lounge") + + self.storage.new(new_user) + self.storage.new(new_state) + self.storage.new(new_city) + self.storage.new(new_place) + self.storage.new(new_review) + self.storage.new(new_amenity) + self.storage.save() + + place_id = new_place.id + + new_place.amenities.append(new_amenity) + + self.storage.save() + + # Verify the relationship in the database + query = """SELECT amenities.name, amenities.id +FROM amenities +JOIN place_amenity ON place_amenity.amenity_id=amenities.id +JOIN places ON place_amenity.place_id=places.id +WHERE places.id=%s;""" + self.cursor.execute(query, (place_id,)) + + result = self.cursor.fetchone() + + amenity, amenity_id = result + + self.assertEqual(new_amenity.name, amenity) + self.assertEqual(new_amenity.id, amenity_id) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_models/test_engine/test_file_storage.py b/tests/test_models/test_engine/test_file_storage.py index e1de7198b697..354ef61ffe12 100755 --- a/tests/test_models/test_engine/test_file_storage.py +++ b/tests/test_models/test_engine/test_file_storage.py @@ -1,27 +1,41 @@ #!/usr/bin/python3 """ Module for testing file storage""" +import os import unittest from models.base_model import BaseModel +from models.user import User +from models.state import State +from models.city import City +from models.amenity import Amenity +from models.place import Place +from models.review import Review from models import storage -import os +from models.engine.file_storage import FileStorage -class test_fileStorage(unittest.TestCase): +@unittest.skipIf(os.getenv("HBNB_TYPE_STORAGE") == "db", + "Skipping: not using DBStorage") +class BaseTestFileStorage(unittest.TestCase): """ Class to test the file storage method """ + model = None + __file = "file.json" + def setUp(self): """ Set up test environment """ - del_list = [] - for key in storage._FileStorage__objects.keys(): - del_list.append(key) - for key in del_list: - del storage._FileStorage__objects[key] + if self.model is None: + self.skipTest("Skipping BaseTestFileStorage since it has no model") + + if os.path.exists(self.__file): + os.remove(self.__file) + + storage._FileStorage__objects.clear() def tearDown(self): """ Remove storage file at end of tests """ try: - os.remove('file.json') - except: + os.remove(self.__file) + except FileNotFoundError: pass def test_obj_list_empty(self): @@ -30,48 +44,53 @@ def test_obj_list_empty(self): def test_new(self): """ New object is correctly added to __objects """ - new = BaseModel() - for obj in storage.all().values(): - temp = obj - self.assertTrue(temp is obj) + new = self.model() + storage.new(new) # added + + self.assertIn(new, storage.all().values()) def test_all(self): """ __objects is properly returned """ - new = BaseModel() + new = self.model() + storage.new(new) # added + storage.save() # added temp = storage.all() self.assertIsInstance(temp, dict) + self.assertIn(new, storage.all().values()) - def test_base_model_instantiation(self): - """ File is not created on BaseModel save """ - new = BaseModel() - self.assertFalse(os.path.exists('file.json')) + def test_model_instantiation(self): + """ Json file is not created on a model instantiation """ + new = self.model() + self.assertFalse(os.path.exists(self.__file)) def test_empty(self): """ Data is saved to file """ - new = BaseModel() + new = self.model() thing = new.to_dict() new.save() - new2 = BaseModel(**thing) - self.assertNotEqual(os.path.getsize('file.json'), 0) + new2 = self.model(**thing) + self.assertNotEqual(os.path.getsize(self.__file), 0) def test_save(self): """ FileStorage save method """ - new = BaseModel() + new = self.model() + storage.new(new) # added storage.save() - self.assertTrue(os.path.exists('file.json')) + self.assertTrue(os.path.exists(self.__file)) def test_reload(self): """ Storage file is successfully loaded to __objects """ - new = BaseModel() + new = self.model() + storage.new(new) # added storage.save() storage.reload() - for obj in storage.all().values(): - loaded = obj - self.assertEqual(new.to_dict()['id'], loaded.to_dict()['id']) + + self.assertEqual(new.to_dict()['id'], + list(storage.all().values())[0].id) def test_reload_empty(self): """ Load from an empty file """ - with open('file.json', 'w') as f: + with open(self.__file, 'w') as f: pass with self.assertRaises(ValueError): storage.reload() @@ -82,9 +101,9 @@ def test_reload_from_nonexistent(self): def test_base_model_save(self): """ BaseModel save method calls storage save """ - new = BaseModel() + new = self.model() new.save() - self.assertTrue(os.path.exists('file.json')) + self.assertTrue(os.path.exists(self.__file)) def test_type_path(self): """ Confirm __file_path is string """ @@ -92,18 +111,85 @@ def test_type_path(self): def test_type_objects(self): """ Confirm __objects is a dict """ - self.assertEqual(type(storage.all()), dict) + self.assertTrue(type(storage.all()) is dict) def test_key_format(self): """ Key is properly formatted """ - new = BaseModel() + new = self.model() + storage.new(new) # added _id = new.to_dict()['id'] - for key in storage.all().keys(): - temp = key - self.assertEqual(temp, 'BaseModel' + '.' + _id) + + self.assertIn(f'{self.model.__name__}' + '.' + _id, + storage.all().keys()) def test_storage_var_created(self): """ FileStorage object storage created """ - from models.engine.file_storage import FileStorage - print(type(storage)) - self.assertEqual(type(storage), FileStorage) + self.assertIsInstance(storage, FileStorage) + + def test_delete(self): + """ Object is successfully deleted from __objects """ + new = self.model() + storage.new(new) + storage.save() + self.assertIn(new, storage.all().values()) + storage.delete(new) + self.assertNotIn(new, storage.all().values()) + + def test_delete_nonexistent(self): + """ Deleting a nonexistent object should not raise an error """ + new = self.model() + storage.new(new) + storage.save() + self.assertIn(new, storage.all().values()) + storage.delete(new) # Should not raise an error + self.assertNotIn(new, storage.all().values()) + + def test_all_with_class(self): + """ all() method with class argument returns only matching objects """ + new1 = self.model() + new2 = self.model() + storage.new(new1) + storage.new(new2) + storage.save() + self.assertIn(new1, storage.all(self.model).values()) + self.assertIn(new2, storage.all(self.model).values()) + self.assertTrue(all(self.assertIn(obj, + storage.all().values())) + for obj in (new1, new2)) + + def test_get_number_of_records(self): + """ Get the number of current records in storage """ + new1 = self.model() + new2 = self.model() + storage.new(new1) + storage.new(new2) + storage.save() + self.assertEqual(len(storage.all()), 2) + + +class TestBaseModelStorage(BaseTestFileStorage): + model = BaseModel + + +class TestCityStorage(BaseTestFileStorage): + model = City + + +class TestAmenityStorage(BaseTestFileStorage): + model = Amenity + + +class TestPlaceStorage(BaseTestFileStorage): + model = Place + + +class TestReviewStorage(BaseTestFileStorage): + model = Review + + +class TestUserStorage(BaseTestFileStorage): + model = User + + +class TestStateStorage(BaseTestFileStorage): + model = State diff --git a/tests/test_models/test_place.py b/tests/test_models/test_place.py index ec133d104ef5..048021054172 100755 --- a/tests/test_models/test_place.py +++ b/tests/test_models/test_place.py @@ -1,69 +1,85 @@ #!/usr/bin/python3 -""" """ -from tests.test_models.test_base_model import test_basemodel +# -*- coding: utf-8 -*- +"""Unit tests for Place""" +from tests.test_models.test_base_model import TestBaseModel from models.place import Place -class test_Place(test_basemodel): - """ """ +class TestPlace(TestBaseModel): + """Test cases for the Place class""" def __init__(self, *args, **kwargs): - """ """ + """Initialize test class""" super().__init__(*args, **kwargs) self.name = "Place" self.value = Place + def setUp(self): + """Set up test environment""" + self.place = self.value() # Create a Place instance for testing + def test_city_id(self): - """ """ - new = self.value() - self.assertEqual(type(new.city_id), str) + """Test that city_id exists and is a string""" + self.assertTrue(hasattr(self.place, "city_id")) + self.assertIsInstance(self.place.city_id, str) + self.assertEqual(self.place.city_id, "") # Assuming default is empty def test_user_id(self): - """ """ - new = self.value() - self.assertEqual(type(new.user_id), str) + """Test that user_id exists and is a string""" + self.assertTrue(hasattr(self.place, "user_id")) + self.assertIsInstance(self.place.user_id, str) + self.assertEqual(self.place.user_id, "") def test_name(self): - """ """ - new = self.value() - self.assertEqual(type(new.name), str) + """Test that name exists and is a string""" + self.assertTrue(hasattr(self.place, "name")) + self.assertIsInstance(self.place.name, str) + self.assertEqual(self.place.name, "") def test_description(self): - """ """ - new = self.value() - self.assertEqual(type(new.description), str) + """Test that description exists and is a string""" + self.assertTrue(hasattr(self.place, "description")) + self.assertIsInstance(self.place.description, str) + self.assertEqual(self.place.description, "") def test_number_rooms(self): - """ """ - new = self.value() - self.assertEqual(type(new.number_rooms), int) + """Test that number_rooms exists and is an integer""" + self.assertTrue(hasattr(self.place, "number_rooms")) + self.assertIsInstance(self.place.number_rooms, int) + self.assertEqual(self.place.number_rooms, 0) def test_number_bathrooms(self): - """ """ - new = self.value() - self.assertEqual(type(new.number_bathrooms), int) + """Test that number_bathrooms exists and is an integer""" + self.assertTrue(hasattr(self.place, "number_bathrooms")) + self.assertIsInstance(self.place.number_bathrooms, int) + self.assertEqual(self.place.number_bathrooms, 0) def test_max_guest(self): - """ """ - new = self.value() - self.assertEqual(type(new.max_guest), int) + """Test that max_guest exists and is an integer""" + self.assertTrue(hasattr(self.place, "max_guest")) + self.assertIsInstance(self.place.max_guest, int) + self.assertEqual(self.place.max_guest, 0) def test_price_by_night(self): - """ """ - new = self.value() - self.assertEqual(type(new.price_by_night), int) + """Test that price_by_night exists and is an integer""" + self.assertTrue(hasattr(self.place, "price_by_night")) + self.assertIsInstance(self.place.price_by_night, int) + self.assertEqual(self.place.price_by_night, 0) def test_latitude(self): - """ """ - new = self.value() - self.assertEqual(type(new.latitude), float) + """Test that latitude exists and is a float""" + self.assertTrue(hasattr(self.place, "latitude")) + self.assertIsInstance(self.place.latitude, float) + self.assertEqual(self.place.latitude, 0.0) def test_longitude(self): - """ """ - new = self.value() - self.assertEqual(type(new.latitude), float) + """Test that longitude exists and is a float""" + self.assertTrue(hasattr(self.place, "longitude")) + self.assertIsInstance(self.place.longitude, float) + self.assertEqual(self.place.longitude, 0.0) # Fixed the mistake def test_amenity_ids(self): - """ """ - new = self.value() - self.assertEqual(type(new.amenity_ids), list) + """Test that amenity_ids exists and is a list""" + self.assertTrue(hasattr(self.place, "amenity_ids")) + self.assertIsInstance(self.place.amenity_ids, list) + self.assertEqual(self.place.amenity_ids, []) diff --git a/tests/test_models/test_review.py b/tests/test_models/test_review.py index 23fbc61529e8..2c3c09c6565c 100755 --- a/tests/test_models/test_review.py +++ b/tests/test_models/test_review.py @@ -1,29 +1,37 @@ #!/usr/bin/python3 -""" """ -from tests.test_models.test_base_model import test_basemodel +# -*- coding: utf-8 -*- +"""Unit tests for Review""" +from tests.test_models.test_base_model import TestBaseModel from models.review import Review -class test_review(test_basemodel): - """ """ +class TestReview(TestBaseModel): + """Test cases for the Review class""" def __init__(self, *args, **kwargs): - """ """ + """Initialize test class""" super().__init__(*args, **kwargs) self.name = "Review" self.value = Review + def setUp(self): + """Set up test environment""" + self.review = self.value() # Create a Review instance for testing + def test_place_id(self): - """ """ - new = self.value() - self.assertEqual(type(new.place_id), str) + """Test that place_id exists and is a string""" + self.assertTrue(hasattr(self.review, "place_id")) + self.assertIsInstance(self.review.place_id, str) + self.assertEqual(self.review.place_id, "") def test_user_id(self): - """ """ - new = self.value() - self.assertEqual(type(new.user_id), str) + """Test that user_id exists and is a string""" + self.assertTrue(hasattr(self.review, "user_id")) + self.assertIsInstance(self.review.user_id, str) + self.assertEqual(self.review.user_id, "") def test_text(self): - """ """ - new = self.value() - self.assertEqual(type(new.text), str) + """Test that text exists and is a string""" + self.assertTrue(hasattr(self.review, "text")) + self.assertIsInstance(self.review.text, str) + self.assertEqual(self.review.text, "") diff --git a/tests/test_models/test_state.py b/tests/test_models/test_state.py index 719e096d8633..894936971f56 100755 --- a/tests/test_models/test_state.py +++ b/tests/test_models/test_state.py @@ -1,19 +1,21 @@ #!/usr/bin/python3 -""" """ -from tests.test_models.test_base_model import test_basemodel +"""Unit tests for the State class""" +from tests.test_models.test_base_model import TestBaseModel from models.state import State -class test_state(test_basemodel): - """ """ +class test_state(TestBaseModel): + """Test cases for the State class""" def __init__(self, *args, **kwargs): - """ """ + """Initialize test class""" super().__init__(*args, **kwargs) self.name = "State" self.value = State def test_name3(self): - """ """ + """Test that name exists and is a string""" new = self.value() - self.assertEqual(type(new.name), str) + self.assertTrue(hasattr(new, "name")) + self.assertIsInstance(new.name, str) + self.assertEqual(new.name, "") # Default value check diff --git a/tests/test_models/test_user.py b/tests/test_models/test_user.py index 8660300f8bbc..3261133ee78b 100755 --- a/tests/test_models/test_user.py +++ b/tests/test_models/test_user.py @@ -1,34 +1,43 @@ #!/usr/bin/python3 -""" """ -from tests.test_models.test_base_model import test_basemodel +# -*- coding: utf-8 -*- +"""Unit tests for the User class""" +from tests.test_models.test_base_model import TestBaseModel from models.user import User -class test_User(test_basemodel): - """ """ +class test_User(TestBaseModel): + """Test cases for the User class""" def __init__(self, *args, **kwargs): - """ """ + """Initialize test class""" super().__init__(*args, **kwargs) self.name = "User" self.value = User + def setUp(self): + """Set up test environment""" + self.user = self.value() # Create a User instance for testing + def test_first_name(self): - """ """ - new = self.value() - self.assertEqual(type(new.first_name), str) + """Test that first_name exists and is a string""" + self.assertTrue(hasattr(self.user, "first_name")) + self.assertIsInstance(self.user.first_name, str) + self.assertEqual(self.user.first_name, "") # Default value check def test_last_name(self): - """ """ - new = self.value() - self.assertEqual(type(new.last_name), str) + """Test that last_name exists and is a string""" + self.assertTrue(hasattr(self.user, "last_name")) + self.assertIsInstance(self.user.last_name, str) + self.assertEqual(self.user.last_name, "") # Default value check def test_email(self): - """ """ - new = self.value() - self.assertEqual(type(new.email), str) + """Test that email exists and is a string""" + self.assertTrue(hasattr(self.user, "email")) + self.assertIsInstance(self.user.email, str) + self.assertEqual(self.user.email, "") # Default value check def test_password(self): - """ """ - new = self.value() - self.assertEqual(type(new.password), str) + """Test that password exists and is a string""" + self.assertTrue(hasattr(self.user, "password")) + self.assertIsInstance(self.user.password, str) + self.assertEqual(self.user.password, "") # Default value check