diff --git a/.github/workflows/development_pipeline.yml b/.github/workflows/development_pipeline.yml index e6e49a0..f3dfd06 100644 --- a/.github/workflows/development_pipeline.yml +++ b/.github/workflows/development_pipeline.yml @@ -1,8 +1,46 @@ -name: hello-world -on: push +name: development +on: + pull_request: + branches: develop + + push: + branches: + - develop jobs: - my-job: + build-test: runs-on: ubuntu-latest + services: + postgres: + image: postgres:13.3 + env: + POSTGRES_USER: db_user + POSTGRES_PASSWORD: db_password + POSTGRES_DB: db_test + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - - name: my-step - run: echo "Hello World with Github Actions" + - name: Checkout code + uses: actions/checkout@v2 + + - name: Cache Python dependencies + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Set up Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: 3.9 + + - name: Install Python dependencies + run: python -m pip install -r requirements.txt + + - name: Unit Tests and Integration Tests + env: + DATABASE_TEST_URL: postgresql://db_user:db_password@localhost/db_test + run: python -m flask tests + \ No newline at end of file diff --git a/.github/workflows/production_pipeline.yaml b/.github/workflows/production_pipeline.yaml index e6e49a0..1f9d663 100644 --- a/.github/workflows/production_pipeline.yaml +++ b/.github/workflows/production_pipeline.yaml @@ -1,5 +1,7 @@ -name: hello-world -on: push +name: production +on: + pull_request: + branches: main jobs: my-job: runs-on: ubuntu-latest diff --git a/.github/workflows/staging_pipeline.yaml b/.github/workflows/staging_pipeline.yaml index e6e49a0..05afe6f 100644 --- a/.github/workflows/staging_pipeline.yaml +++ b/.github/workflows/staging_pipeline.yaml @@ -1,8 +1,41 @@ -name: hello-world -on: push +name: staging +on: + pull_request: + branches: staging jobs: - my-job: + build-test: runs-on: ubuntu-latest + services: + postgres: + image: postgres:13.3 + env: + POSTGRES_USER: db_user + POSTGRES_PASSWORD: db_password + POSTGRES_DB: db_test + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - - name: my-step - run: echo "Hello World with Github Actions" + - name: Checkout code + uses: actions/checkout@v2 + + - name: Cache Python dependencies + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Set up Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: 3.9 + + - name: Install Python dependencies + run: python -m pip install -r requirements.txt + + - name: Unit Tests and Integration Tests + env: + DATABASE_TEST_URL: postgresql://db_user:db_password@localhost/db_test + run: python -m flask tests \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index d04838d..6aaa34a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,6 +1,8 @@ import os from flask import Flask -from app.extention import migrate, jwt, cors +from app.extention import migrate, cors +from app.utils.auth import jwt +from app.utils.principal import principal from app.utils.logging import configure_logging from app.db import db from app.blueprint import register_routing @@ -15,6 +17,7 @@ def create_app(settings_module): migrate.init_app(app, db) jwt.init_app(app) cors.init_app(app, supports_credentials='true' ,resources={r"*": { "origins": "*" }}) + principal.init_app(app) manage.init_app(app) # Logging configuration @@ -25,6 +28,5 @@ def create_app(settings_module): return app - settings_module = os.getenv('APP_SETTINGS_MODULE') app = create_app(settings_module) \ No newline at end of file diff --git a/app/controllers/permission_controller.py b/app/controllers/permission_controller.py index 103bc4f..a213d1e 100644 --- a/app/controllers/permission_controller.py +++ b/app/controllers/permission_controller.py @@ -5,17 +5,23 @@ from flask_jwt_extended import jwt_required from app.schemas.user_schema import PermissionSchema, UpdatePermissionRoleSchema +# Define permissions +read_permission = Permission(RoleNeed('read')) +write_permission = Permission(RoleNeed('write')) + blp = Blueprint("Permission", __name__, description="Permission API") @blp.route("/permission") class PermissionList(MethodView): @jwt_required() + @read_permission.require(http_exception=403) @blp.response(200, PermissionSchema(many=True)) def get(self): result = permission_service.get_all_permission() return result @jwt_required() + @write_permission.require(http_exception=403) @blp.arguments(PermissionSchema) def post(self, qa_history_data): result = permission_service.post_permission(qa_history_data) @@ -24,12 +30,14 @@ def post(self, qa_history_data): @blp.route("/permission/") class Permission(MethodView): @jwt_required() + @read_permission.require(http_exception=403) @blp.response(200, PermissionSchema) def get(self, permission_id): result = permission_service.get_permission(permission_id) return result @jwt_required() + @write_permission.require(http_exception=403) @blp.arguments(PermissionSchema) def put(self, permission_data, permission_id): result = permission_service.update_permission(permission_data, permission_id) @@ -38,6 +46,7 @@ def put(self, permission_data, permission_id): @blp.route("/permission-role-update") class PermissionRole(MethodView): @jwt_required() + @write_permission.require(http_exception=403) @blp.arguments(UpdatePermissionRoleSchema) def put(self, permission_data): result = role_permission_service.update_roles_to_permission(permission_data) diff --git a/app/controllers/role_controller.py b/app/controllers/role_controller.py index c314bd3..bd28ca1 100644 --- a/app/controllers/role_controller.py +++ b/app/controllers/role_controller.py @@ -5,17 +5,24 @@ from flask_jwt_extended import jwt_required from app.schemas.user_schema import RoleSchema, UpdateRolePermissionSchema +# Define permissions +read_permission = Permission(RoleNeed('read')) +write_permission = Permission(RoleNeed('write')) +delete_permission = Permission(RoleNeed('delete')) + blp = Blueprint("Role", __name__, description="Role API") @blp.route("/role") class RoleList(MethodView): @jwt_required() + @read_permission.require(http_exception=403) @blp.response(200, RoleSchema(many=True)) def get(self): result = role_service.get_all_role() return result @jwt_required() + @write_permission.require(http_exception=403) @blp.arguments(UpdateRolePermissionSchema) def post(self, qa_history_data): result = role_service.post_role(qa_history_data) @@ -24,18 +31,21 @@ def post(self, qa_history_data): @blp.route("/role/") class Role(MethodView): @jwt_required() + @read_permission.require(http_exception=403) @blp.response(200, RoleSchema) def get(self, role_id): result = role_service.get_role(role_id) return result @jwt_required() + @write_permission.require(http_exception=403) @blp.arguments(UpdateRolePermissionSchema) def put(self, role_data, role_id): result = role_service.update_role(role_data, role_id) return result @jwt_required() + @delete_permission.require(http_exception=403) def delete(self, role_id): result = role_service.delete_role(role_id) return result \ No newline at end of file diff --git a/app/controllers/user_controller.py b/app/controllers/user_controller.py index aa1c99f..b88f80b 100644 --- a/app/controllers/user_controller.py +++ b/app/controllers/user_controller.py @@ -1,19 +1,21 @@ from app.services import user_service -from flask_jwt_extended import jwt_required, get_jwt, get_jwt_identity, create_access_token +from flask_jwt_extended import jwt_required, get_jwt from flask.views import MethodView -from flask_smorest import Blueprint, abort +from flask_smorest import Blueprint from flask_principal import Permission, RoleNeed from app.schemas.user_schema import * -# Define some permissions -# admin_permission = Permission(RoleNeed('user_management')) +# Define permissions +read_permission = Permission(RoleNeed('read')) +write_permission = Permission(RoleNeed('write')) +delete_permission = Permission(RoleNeed('delete')) blp = Blueprint("User", __name__, description="User API") @blp.route("/user") class UserList(MethodView): @jwt_required() - # @admin_permission.require(http_exception=403) + @read_permission.require(http_exception=403) @blp.response(200, UserSchema(many=True)) def get(self): result = user_service.get_all_user() @@ -22,26 +24,23 @@ def get(self): @blp.route("/user/") class User(MethodView): @jwt_required() + @read_permission.require(http_exception=403) @blp.response(200, UserSchema) def get(self, user_id): result = user_service.get_user(user_id) return result @jwt_required() + @write_permission.require(http_exception=403) @blp.arguments(UserUpdateSchema) def put(self, user_data, user_id): result = user_service.update_user(user_data, user_id) return result - - @jwt_required() - def delete(self, user_id): - result = user_service.delete_user(user_id) - return result @blp.route("/block-user/") class BlockUser(MethodView): @jwt_required() - # @admin_permission.require(http_exception=403) + @delete_permission.require(http_exception=403) @blp.arguments(UpdateBlockUserSchema) def put(self, user_data, user_id): result = user_service.update_block_user(user_data, user_id) diff --git a/app/extention.py b/app/extention.py index e6fd943..e524629 100644 --- a/app/extention.py +++ b/app/extention.py @@ -1,7 +1,9 @@ from flask_migrate import Migrate from flask_jwt_extended import JWTManager from flask_cors import CORS +from flask_principal import Principal migrate = Migrate() jwt = JWTManager() cors = CORS() +principal = Principal() diff --git a/app/models/permission_model.py b/app/models/permission_model.py index d714efa..b7dc2ff 100644 --- a/app/models/permission_model.py +++ b/app/models/permission_model.py @@ -5,6 +5,6 @@ class PermissionModel(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(), unique=True, nullable=False) - route = db.Column(db.String(), unique=True, nullable=False) + description = db.Column(db.String()) roles = db.relationship("RoleModel", back_populates="permissions", secondary="role_permission") \ No newline at end of file diff --git a/app/models/user_model.py b/app/models/user_model.py index e5a6196..9f975f7 100644 --- a/app/models/user_model.py +++ b/app/models/user_model.py @@ -4,7 +4,7 @@ class UserModel(db.Model): __tablename__ = "user" - id = db.Column(db.Integer, primary_key = True) + id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password = db.Column(db.String(), nullable=False) block = db.Column(db.Boolean, default=False, nullable=False) diff --git a/app/services/permission_service.py b/app/services/permission_service.py index bdfb03b..cfffa23 100644 --- a/app/services/permission_service.py +++ b/app/services/permission_service.py @@ -9,10 +9,10 @@ def get_all_permission(): def post_permission(permission_data): name = permission_data['name'] - route = permission_data['route'] + description = permission_data['description'] try: - new_row = PermissionModel(name=name, route=route) + new_row = PermissionModel(name=name, description=description) db.session.add(new_row) db.session.commit() @@ -36,8 +36,8 @@ def update_permission(permission_data, permission_id): if permission_data['name']: permission.name = permission_data['name'] - if permission_data['route']: - permission.route = permission_data['route'] + if permission_data['description']: + permission.description = permission_data['description'] db.session.commit() except: diff --git a/app/services/user_service.py b/app/services/user_service.py index 7187c65..c60f949 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -50,6 +50,11 @@ def update_user(user_data, user_id): return {"message": "Update successfully!"} def update_block_user(user_data, user_id): + # Only admin can delete user + jwt = get_jwt() + if not jwt.get("is_admin"): + abort(401, message="Admin privilege requierd.") + if user_id == 1: abort(401, message="Can not block Super Admin!") diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/auth.py b/app/utils/auth.py index 94feae7..e325df0 100644 --- a/app/utils/auth.py +++ b/app/utils/auth.py @@ -1,7 +1,6 @@ from app.extention import jwt -from app.models.user_model import UserModel +from app.models import UserModel, BlocklistModel from flask import jsonify -from models.blocklist_model import BlocklistModel @jwt.token_verification_loader def custom_token_verification_callback(jwt_header, jwt_data): diff --git a/app/utils/principal.py b/app/utils/principal.py index b966e30..107dba4 100644 --- a/app/utils/principal.py +++ b/app/utils/principal.py @@ -1,19 +1,16 @@ -# from app.models.user_model import UserModel -# from flask_principal import Principal, identity_loaded, RoleNeed -# from app import app +from app.models import UserModel +from app.extention import principal +from flask_principal import identity_loaded, RoleNeed -# # Initialize Flask-Principal -# principal = Principal(app) - -# # Define a function to load the user's identity -# @identity_loaded.connect_via(app) -# def on_identity_loaded(sender, identity): -# # Get User -# user = UserModel.query.filter_by(id=identity.id).first() +# Define a function to load the user's identity +@identity_loaded.connect +def on_identity_loaded(sender, identity): + # Get User + user = UserModel.query.filter_by(id=identity.id).first() -# # Get all unique permissions -# for role in user.roles: -# # get permission -# for permission in role.permissions: -# # Add the user's roles to the identity object -# identity.provides.add(RoleNeed(permission.route)) + # Get all unique permissions + for role in user.roles: + # get permission + for permission in role.permissions: + # Add the user's roles to the identity object + identity.provides.add(RoleNeed(permission.name)) \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh index 132b07f..9ee7f5e 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -29,10 +29,10 @@ if [ "$APP_ENV" = "local" ]; then echo "Done init user-admin" echo "Run app with gunicorn server..." - gunicorn --bind $API_HOST:$API_PORT $API_ENTRYPOINT --timeout 10 --workers 4; + gunicorn --bind $API_HOST:$API_PORT $API_ENTRYPOINT --timeout 10 --workers 1; fi if [ "$APP_ENV" = "production" ]; then echo "Run app with gunicorn server..." - gunicorn --bind $API_HOST:$API_PORT $API_ENTRYPOINT --timeout 10 --workers 4; + gunicorn --bind $API_HOST:$API_PORT $API_ENTRYPOINT --timeout 10 --workers 1; fi \ No newline at end of file diff --git a/manage.py b/manage.py index 4556913..413bc6f 100644 --- a/manage.py +++ b/manage.py @@ -1,7 +1,68 @@ from app import db +import unittest +import click +import coverage from app.models import UserModel, PermissionModel, RoleModel, RolePermissionModel, UserRoleModel from passlib.hash import pbkdf2_sha256 +@click.option("--pattern", default='tests_*.py', help='Test search pattern', required=False) +def cov(pattern): + """ + Run the unit tests with coverage + """ + cov = coverage.coverage( + branch=True, + include='app/*' + ) + cov.start() + tests = unittest.TestLoader().discover('tests', pattern=pattern) + result = unittest.TextTestRunner(verbosity=2).run(tests) + if result.wasSuccessful(): + cov.stop() + cov.save() + print('Coverage Summary:') + cov.report() + cov.erase() + return 0 + return 1 + +@click.option("--pattern", default='tests_*.py', help='Test search pattern', required=False) +def cov_html(pattern): + """ + Run the unit tests with coverage and generate an HTML report. + """ + cov = coverage.coverage( + branch=True, + include='app/*' + ) + cov.start() + + tests = unittest.TestLoader().discover('tests', pattern=pattern) + result = unittest.TextTestRunner(verbosity=2).run(tests) + + if result.wasSuccessful(): + cov.stop() + cov.save() + + print('Coverage Summary:') + cov.report() + cov.html_report(directory='report/htmlcov') + cov.erase() + return 0 + + return 1 + +@click.option("--pattern", default='tests_*.py', help='Test pattern', required=False) +def tests(pattern): + """ + Run the tests without code coverage + """ + tests = unittest.TestLoader().discover('tests', pattern=pattern) + result = unittest.TextTestRunner(verbosity=2).run(tests) + if result.wasSuccessful(): + return 0 + return 1 + def create_db(): """ Create Database. @@ -28,35 +89,43 @@ def drop_db(): def init_db_user(): # Insert Permission - new_perrmission1 = PermissionModel(name='User management', route='user_management') - db.session.add_all([new_perrmission1]) + read_perrmission = PermissionModel(name='read', description='Read data') + write_perrmission = PermissionModel(name='write', description='Write data') + delete_perrmission = PermissionModel(name='delete', description='Delete data') + db.session.add_all([read_perrmission, write_perrmission, delete_perrmission]) db.session.commit() # Insert Role - new_role1 = RoleModel(name='Super Admin', description='Full Permission') - new_role2 = RoleModel(name='Admin', description='Manage user') - new_role3 = RoleModel(name='Member', description='Member') - new_role4 = RoleModel(name='Guest',description='Guest') - db.session.add_all([new_role1, new_role2, new_role3, new_role4]) + admin_role = RoleModel(name='Admin', description='Full Permission') + user_role = RoleModel(name='User', description='Can read, write data') + guest_role = RoleModel(name='Guest',description='Just read data') + db.session.add_all([admin_role, user_role, guest_role]) db.session.commit() # Insert Role_Permission - new_permission1 = RolePermissionModel(role_id=1, permission_id=1) - new_permission2 = RolePermissionModel(role_id=2, permission_id=1) - db.session.add_all([new_permission1, new_permission2]) + role_permission_admin1 = RolePermissionModel(role_id=1, permission_id=1) + role_permission_admin2 = RolePermissionModel(role_id=1, permission_id=2) + role_permission_admin3 = RolePermissionModel(role_id=1, permission_id=3) + role_permission_user1 = RolePermissionModel(role_id=2, permission_id=1) + role_permission_user2 = RolePermissionModel(role_id=2, permission_id=2) + role_permission_guest = RolePermissionModel(role_id=3, permission_id=1) + db.session.add_all([role_permission_admin1, role_permission_admin2, role_permission_admin3, + role_permission_user1, role_permission_user2, role_permission_guest]) db.session.commit() # Insert User password = pbkdf2_sha256.hash("123456") - new_user1 = UserModel(username='admin', password=password) - db.session.add_all([new_user1]) + admin_user = UserModel(username='admin', password=password) + normal_user = UserModel(username='user', password=password) + guest_user = UserModel(username='guest', password=password) + db.session.add_all([admin_user, normal_user, guest_user]) db.session.commit() # Insert UserRole - new_userrole1 = UserRoleModel(user_id=1, role_id = 1) - new_userrole2 = UserRoleModel(user_id=1, role_id = 2) - new_userrole3 = UserRoleModel(user_id=1, role_id = 3) - db.session.add_all([new_userrole1, new_userrole2, new_userrole3]) + user_role1 = UserRoleModel(user_id=1, role_id=1) + user_role2 = UserRoleModel(user_id=2, role_id=2) + user_role3 = UserRoleModel(user_id=3, role_id=3) + db.session.add_all([user_role1, user_role2, user_role3]) db.session.commit() def create_user_admin(username='admin'): @@ -65,9 +134,8 @@ def create_user_admin(username='admin'): """ admin = UserModel.query.filter_by(username=username).first() - if admin is None: - print("user-admin is not created!") + print("user-admin is not created before!") init_db_user() else: print("user-admin is created!") @@ -76,8 +144,7 @@ def init_app(app): if app.config['APP_ENV'] == 'production': commands = [create_db, reset_db, drop_db, create_user_admin] else: - commands = [create_db, reset_db, drop_db, create_user_admin] + commands = [create_db, reset_db, drop_db, create_user_admin, tests, cov_html, cov] for command in commands: - app.cli.add_command(app.cli.command()(command)) - + app.cli.add_command(app.cli.command()(command)) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7722e1c..d30e594 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ certifi==2023.5.7 click==8.1.3 colorama==0.4.6 comm==0.1.2 +coverage==7.2.7 debugpy==1.6.4 decorator==5.1.1 distlib==0.3.6 @@ -25,7 +26,7 @@ greenlet==2.0.2 gunicorn==20.1.0 importlib-metadata==6.7.0 ipykernel==6.19.2 -ipython==8.10 +ipython==8.10.0 itsdangerous==2.1.2 jedi==0.18.2 Jinja2==3.1.2 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/tests_users_integration.py b/tests/integration/tests_users_integration.py new file mode 100644 index 0000000..29f11be --- /dev/null +++ b/tests/integration/tests_users_integration.py @@ -0,0 +1,46 @@ +import os +import unittest +from app.models import UserModel +from app import db, create_app +from passlib.hash import pbkdf2_sha256 + +class UsersUnitTests(unittest.TestCase): + def setUp(self): + """ + This method runs once before any test in this class. + It sets up the application context and creates the necessary database tables. + """ + self.app = create_app(settings_module=os.environ.get('APP_TEST_SETTINGS_MODULE')) + with self.app.app_context(): + db.create_all() + + def tearDown(self): + """ + This method runs once after all tests in this class have been executed. + It removes the database session and drops the database tables. + """ + with self.app.app_context(): + db.session.remove() + db.drop_all() + + def test_create_user(self): + """ + Test case to check if creating a user is successful. + """ + + with self.app.app_context(): + username = 'test_user' + password = "123456" + + user = UserModel(username=username, password=pbkdf2_sha256.hash(password)) + + # Add to database + db.session.add(user) + db.session.commit() + + # Assertions to check if the user object is created correctly + self.assertEqual(username, user.username) + self.assertTrue(pbkdf2_sha256.verify(password, user.password)) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/postman/Template REST API.postman_collection.json b/tests/postman/flask-api-rest-template.postman_collection.json similarity index 100% rename from tests/postman/Template REST API.postman_collection.json rename to tests/postman/flask-api-rest-template.postman_collection.json diff --git a/tests/postman/flask-api-rest-template.postman_environment.json b/tests/postman/flask-api-rest-template.postman_environment.json new file mode 100644 index 0000000..aead2c3 --- /dev/null +++ b/tests/postman/flask-api-rest-template.postman_environment.json @@ -0,0 +1,27 @@ +{ + "id": "25c79762-c6a7-41e5-997c-ac2c02ed715a", + "name": "flask-api-rest-template", + "values": [ + { + "key": "HOST", + "value": "http://127.0.0.1:5000", + "type": "default", + "enabled": true + }, + { + "key": "TOKEN", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "REFRESHTOKEN", + "value": "", + "type": "default", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2023-07-22T13:17:15.403Z", + "_postman_exported_using": "Postman/10.16.0" +} \ No newline at end of file diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/tests_users_unit.py b/tests/unit/tests_users_unit.py new file mode 100644 index 0000000..feae821 --- /dev/null +++ b/tests/unit/tests_users_unit.py @@ -0,0 +1,44 @@ +import os +import unittest +from app.models import UserModel +from app import db, create_app +from passlib.hash import pbkdf2_sha256 + +class UsersUnitTests(unittest.TestCase): + def setUp(self): + """ + This method runs once before any test in this class. + It sets up the application context and creates the necessary database tables. + """ + self.app = create_app(settings_module=os.environ.get('APP_TEST_SETTINGS_MODULE')) + with self.app.app_context(): + db.create_all() + + def tearDown(self): + """ + This method runs once after all tests in this class have been executed. + It removes the database session and drops the database tables. + """ + with self.app.app_context(): + db.session.remove() + db.drop_all() + + def test_create_user_success(self): + """ + Test case to check if creating a user is successful. + """ + # Given + username = 'test_user' + password = "123456" + + # When + with self.app.app_context(): + user = UserModel(username=username, password=pbkdf2_sha256.hash(password)) + + # Then + # Assertions to check if the user object is created correctly + self.assertEqual(username, user.username) + self.assertTrue(pbkdf2_sha256.verify(password, user.password)) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file