From 1c84ecc4ccedf5aade3a4f8c2e87a60235b6541d Mon Sep 17 00:00:00 2001 From: AlexandersWrld Date: Tue, 26 Nov 2024 22:08:17 -0500 Subject: [PATCH 01/11] updating docker compose --- .dockerignore | 18 +- .env.sample | 6 +- .github/workflows/production.yml | 170 +++--- .gitignore | 16 +- Dockerfile | 84 +-- alembic.ini | 234 ++++---- alembic/env.py | 158 +++--- alembic/script.py.mako | 52 +- .../ef1d775276c0_initial_migration.py | 152 ++--- app/database.py | 50 +- app/dependencies.py | 104 ++-- app/main.py | 62 +-- app/models/user_model.py | 194 +++---- app/routers/user_routes.py | 488 ++++++++-------- app/schemas/link_schema.py | 34 +- app/schemas/pagination_schema.py | 70 +-- app/schemas/token_schema.py | 26 +- app/schemas/user_schemas.py | 174 +++--- app/services/email_service.py | 88 +-- app/services/jwt_service.py | 44 +- app/services/user_service.py | 388 ++++++------- app/utils/api_description.py | 72 +-- app/utils/common.py | 30 +- app/utils/link_generation.py | 96 ++-- app/utils/nickname_gen.py | 20 +- app/utils/security.py | 104 ++-- app/utils/smtp_connection.py | 62 +-- app/utils/template_manager.py | 92 +-- docker-compose.yml | 118 ++-- docker.md | 196 +++---- email_templates/email_verification.md | 16 +- email_templates/footer.md | 26 +- email_templates/header.md | 12 +- email_templates/test_email.md | 18 +- git.md | 276 ++++----- license.txt | 42 +- logging.conf | 44 +- nginx/nginx.conf | 22 +- pytest.ini | 48 +- readme.md | 340 ++++++------ requirements.txt | 124 ++--- settings/config.py | 102 ++-- tests/conftest.py | 524 +++++++++--------- tests/test_api/test_users_api.py | 382 ++++++------- tests/test_conftest.py | 128 ++--- tests/test_email.py | 28 +- tests/test_link_generation.py | 102 ++-- tests/test_models/test_user_model.py | 304 +++++----- tests/test_schemas/test_user_schemas.py | 136 ++--- tests/test_security.py | 134 ++--- tests/test_services/test_user_service.py | 316 +++++------ 51 files changed, 3263 insertions(+), 3263 deletions(-) diff --git a/.dockerignore b/.dockerignore index 095a53107..46b5b2859 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,9 @@ -venv -_*pycache*_ -qr_codes -*.db -*.sqlite -.vscode -.env -.coverage - +venv +_*pycache*_ +qr_codes +*.db +*.sqlite +.vscode +.env +.coverage + diff --git a/.env.sample b/.env.sample index 15f6ab343..99c9b7f33 100644 --- a/.env.sample +++ b/.env.sample @@ -1,4 +1,4 @@ -smtp_server=sandbox.smtp.mailtrap.io -smtp_port=2525 -smtp_username= +smtp_server=sandbox.smtp.mailtrap.io +smtp_port=2525 +smtp_username= smtp_password= \ No newline at end of file diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml index 1d6d27bb0..94b66b583 100644 --- a/.github/workflows/production.yml +++ b/.github/workflows/production.yml @@ -1,85 +1,85 @@ -name: CI/CD Pipeline - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - test: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: [3.10.12] # Define Python versions here - services: - postgres: - image: postgres:latest - env: - POSTGRES_USER: user - POSTGRES_PASSWORD: password - POSTGRES_DB: myappdb - ports: - - 5432:5432 - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 - steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Cache Python packages - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - - name: Run tests with Pytest - env: - DATABASE_URL: postgresql+asyncpg://user:password@localhost:5432/myappdb # Configure the DATABASE_URL environment variable for tests - run: pytest - - build-and-push-docker: - needs: test - runs-on: ubuntu-latest - environment: production - steps: - - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - push: true - tags: woffee/wis_club_api:${{ github.sha }} # Uses the Git SHA for tagging - platforms: linux/amd64,linux/arm64 # Multi-platform support - cache-from: type=registry,ref=woffee/wis_club_api:cache - cache-to: type=inline,mode=max - - - name: Scan the Docker image - uses: aquasecurity/trivy-action@master - with: - image-ref: 'woffee/wis_club_api:${{ github.sha }}' - format: 'table' - exit-code: '1' # Fail the job if vulnerabilities are found - ignore-unfixed: true - severity: 'CRITICAL,HIGH' +name: CI/CD Pipeline + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.10.12] # Define Python versions here + services: + postgres: + image: postgres:latest + env: + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: myappdb + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache Python packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run tests with Pytest + env: + DATABASE_URL: postgresql+asyncpg://user:password@localhost:5432/myappdb # Configure the DATABASE_URL environment variable for tests + run: pytest + + build-and-push-docker: + needs: test + runs-on: ubuntu-latest + environment: production + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + push: true + tags: woffee/wis_club_api:${{ github.sha }} # Uses the Git SHA for tagging + platforms: linux/amd64,linux/arm64 # Multi-platform support + cache-from: type=registry,ref=woffee/wis_club_api:cache + cache-to: type=inline,mode=max + + - name: Scan the Docker image + uses: aquasecurity/trivy-action@master + with: + image-ref: 'woffee/wis_club_api:${{ github.sha }}' + format: 'table' + exit-code: '1' # Fail the job if vulnerabilities are found + ignore-unfixed: true + severity: 'CRITICAL,HIGH' diff --git a/.gitignore b/.gitignore index 8b8bf1ba8..86f7d59a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ -venv -_*pycache*_ -qr_codes -*.db -*.sqlite -.vscode -.env -.coverage +venv +_*pycache*_ +qr_codes +*.db +*.sqlite +.vscode +.env +.coverage .DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a26347203..c896258da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,42 +1,42 @@ -# Use an official lightweight Python image. -# 3.12-slim variant is chosen for a balance between size and utility. -FROM python:3.12-slim-bullseye as base - -# Set environment variables to configure Python and pip. -# Prevents Python from buffering stdout and stderr, enables the fault handler, disables pip cache, -# sets default pip timeout, and suppresses pip version check messages. -ENV PYTHONUNBUFFERED=1 \ - PYTHONFAULTHANDLER=1 \ - PIP_NO_CACHE_DIR=true \ - PIP_DEFAULT_TIMEOUT=100 \ - PIP_DISABLE_PIP_VERSION_CHECK=on \ - QR_CODE_DIR=/myapp/qr_codes - -# Set the working directory inside the container -WORKDIR /myapp - -# Install system dependencies -RUN apt-get update \ - && apt-get install -y --no-install-recommends gcc libpq-dev \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Copy only the requirements, to cache them in Docker layer -COPY ./requirements.txt /myapp/requirements.txt - -# Upgrade pip and install Python dependencies from requirements file -RUN pip install --upgrade pip \ - && pip install -r requirements.txt - -# Add a non-root user and switch to it -RUN useradd -m myuser -USER myuser - -# Copy the rest of your application's code with appropriate ownership -COPY --chown=myuser:myuser . /myapp - -# Inform Docker that the container listens on the specified port at runtime. -EXPOSE 8000 - -# Use ENTRYPOINT to specify the executable when the container starts. -# ENTRYPOINT ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] +# Use an official lightweight Python image. +# 3.12-slim variant is chosen for a balance between size and utility. +FROM python:3.12-slim-bullseye as base + +# Set environment variables to configure Python and pip. +# Prevents Python from buffering stdout and stderr, enables the fault handler, disables pip cache, +# sets default pip timeout, and suppresses pip version check messages. +ENV PYTHONUNBUFFERED=1 \ + PYTHONFAULTHANDLER=1 \ + PIP_NO_CACHE_DIR=true \ + PIP_DEFAULT_TIMEOUT=100 \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + QR_CODE_DIR=/myapp/qr_codes + +# Set the working directory inside the container +WORKDIR /myapp + +# Install system dependencies +RUN apt-get update \ + && apt-get install -y --no-install-recommends gcc libpq-dev \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Copy only the requirements, to cache them in Docker layer +COPY ./requirements.txt /myapp/requirements.txt + +# Upgrade pip and install Python dependencies from requirements file +RUN pip install --upgrade pip \ + && pip install -r requirements.txt + +# Add a non-root user and switch to it +RUN useradd -m myuser +USER myuser + +# Copy the rest of your application's code with appropriate ownership +COPY --chown=myuser:myuser . /myapp + +# Inform Docker that the container listens on the specified port at runtime. +EXPOSE 8000 + +# Use ENTRYPOINT to specify the executable when the container starts. +# ENTRYPOINT ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/alembic.ini b/alembic.ini index 97bf80756..19e9240c6 100644 --- a/alembic.ini +++ b/alembic.ini @@ -1,117 +1,117 @@ -# A generic, single database configuration. - -[alembic] -# path to migration scripts -script_location = alembic - -# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s -# Uncomment the line below if you want the files to be prepended with date and time -# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file -# for all available tokens -# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s - -# sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. -prepend_sys_path = . - -# timezone to use when rendering the date within the migration file -# as well as the filename. -# If specified, requires the python>=3.9 or backports.zoneinfo library. -# Any required deps can installed by adding `alembic[tz]` to the pip requirements -# string value is passed to ZoneInfo() -# leave blank for localtime -# timezone = - -# max length of characters to apply to the -# "slug" field -# truncate_slug_length = 40 - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version location specification; This defaults -# to alembic/versions. When using multiple version -# directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "version_path_separator" below. -# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions - -# version path separator; As mentioned above, this is the character used to split -# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. -# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. -# Valid values for version_path_separator are: -# -# version_path_separator = : -# version_path_separator = ; -# version_path_separator = space -version_path_separator = os # Use os.pathsep. Default configuration used for new projects. - -# set to 'true' to search source files recursively -# in each "version_locations" directory -# new in Alembic version 1.10 -# recursive_version_locations = false - -# the output encoding used when revision files -# are written from script.py.mako -# output_encoding = utf-8 - -# SQLAlchemy connection string to your database. -sqlalchemy.url = postgresql://user:password@postgres/myappdb - - -[post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run -# on newly generated revision scripts. See the documentation for further -# detail and examples - -# format using "black" - use the console_scripts runner, against the "black" entrypoint -# hooks = black -# black.type = console_scripts -# black.entrypoint = black -# black.options = -l 79 REVISION_SCRIPT_FILENAME - -# lint with attempts to fix using "ruff" - use the exec runner, execute a binary -# hooks = ruff -# ruff.type = exec -# ruff.executable = %(here)s/.venv/bin/ruff -# ruff.options = --fix REVISION_SCRIPT_FILENAME - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# SQLAlchemy connection string to your database. +sqlalchemy.url = postgresql://user:password@postgres/myappdb + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/env.py b/alembic/env.py index 73d3b3ef8..56a52b79f 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -1,80 +1,80 @@ -from logging.config import fileConfig - -from sqlalchemy import engine_from_config -from sqlalchemy import pool - -from alembic import context -from app.models.user_model import Base # adjust "myapp.models" to the actual location of your Base - - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -if config.config_file_name is not None: - fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -target_metadata = Base.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline() -> None: - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online() -> None: - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - connectable = engine_from_config( - config.get_section(config.config_ini_section, {}), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) - - with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context +from app.models.user_model import Base # adjust "myapp.models" to the actual location of your Base + + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: run_migrations_online() \ No newline at end of file diff --git a/alembic/script.py.mako b/alembic/script.py.mako index fbc4b07dc..aa5053c91 100644 --- a/alembic/script.py.mako +++ b/alembic/script.py.mako @@ -1,26 +1,26 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision: str = ${repr(up_revision)} -down_revision: Union[str, None] = ${repr(down_revision)} -branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} -depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} - - -def upgrade() -> None: - ${upgrades if upgrades else "pass"} - - -def downgrade() -> None: - ${downgrades if downgrades else "pass"} +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/ef1d775276c0_initial_migration.py b/alembic/versions/ef1d775276c0_initial_migration.py index 23068895c..0c786d794 100644 --- a/alembic/versions/ef1d775276c0_initial_migration.py +++ b/alembic/versions/ef1d775276c0_initial_migration.py @@ -1,76 +1,76 @@ -"""initial migration - -Revision ID: ef1d775276c0 -Revises: -Create Date: 2024-04-20 21:20:32.839580 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import uuid - -# revision identifiers, used by Alembic. -revision: str = 'ef1d775276c0' -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('users', - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('nickname', sa.String(length=50), nullable=False), - sa.Column('email', sa.String(length=255), nullable=False), - sa.Column('first_name', sa.String(length=100), nullable=True), - sa.Column('last_name', sa.String(length=100), nullable=True), - sa.Column('bio', sa.String(length=500), nullable=True), - sa.Column('profile_picture_url', sa.String(length=255), nullable=True), - sa.Column('linkedin_profile_url', sa.String(length=255), nullable=True), - sa.Column('github_profile_url', sa.String(length=255), nullable=True), - sa.Column('role', sa.Enum('ANONYMOUS', 'AUTHENTICATED', 'MANAGER', 'ADMIN', name='UserRole'), nullable=False), - sa.Column('is_professional', sa.Boolean(), nullable=True), - sa.Column('professional_status_updated_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('last_login_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('failed_login_attempts', sa.Integer(), nullable=True), - sa.Column('is_locked', sa.Boolean(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('verification_token', sa.String(), nullable=True), - sa.Column('email_verified', sa.Boolean(), nullable=False), - sa.Column('hashed_password', sa.String(length=255), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) - op.create_index(op.f('ix_users_nickname'), 'users', ['nickname'], unique=True) - # ### end Alembic commands ### - - # ### Add an admin account ### - admin_id = str(uuid.uuid4()) # Generate a UUID for the admin user - admin_email = 'admin@example.com' - admin_nickname = 'admin' - admin_hash_password = '$2b$12$wMygvJfsJGS4UqdeKF1JGO6Sd7tQBg8uo6C946xgntDsstrdgTKVy' - - op.execute(f""" - INSERT INTO users (id, nickname, email, role, email_verified, hashed_password, created_at, updated_at) - VALUES ( - '{admin_id}', - '{admin_nickname}', - '{admin_email}', - 'ADMIN', - TRUE, - '{admin_hash_password}', - now(), - now() - ) - """) - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_users_nickname'), table_name='users') - op.drop_index(op.f('ix_users_email'), table_name='users') - op.drop_table('users') - # ### end Alembic commands ### +"""initial migration + +Revision ID: ef1d775276c0 +Revises: +Create Date: 2024-04-20 21:20:32.839580 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import uuid + +# revision identifiers, used by Alembic. +revision: str = 'ef1d775276c0' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('nickname', sa.String(length=50), nullable=False), + sa.Column('email', sa.String(length=255), nullable=False), + sa.Column('first_name', sa.String(length=100), nullable=True), + sa.Column('last_name', sa.String(length=100), nullable=True), + sa.Column('bio', sa.String(length=500), nullable=True), + sa.Column('profile_picture_url', sa.String(length=255), nullable=True), + sa.Column('linkedin_profile_url', sa.String(length=255), nullable=True), + sa.Column('github_profile_url', sa.String(length=255), nullable=True), + sa.Column('role', sa.Enum('ANONYMOUS', 'AUTHENTICATED', 'MANAGER', 'ADMIN', name='UserRole'), nullable=False), + sa.Column('is_professional', sa.Boolean(), nullable=True), + sa.Column('professional_status_updated_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('last_login_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('failed_login_attempts', sa.Integer(), nullable=True), + sa.Column('is_locked', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('verification_token', sa.String(), nullable=True), + sa.Column('email_verified', sa.Boolean(), nullable=False), + sa.Column('hashed_password', sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + op.create_index(op.f('ix_users_nickname'), 'users', ['nickname'], unique=True) + # ### end Alembic commands ### + + # ### Add an admin account ### + admin_id = str(uuid.uuid4()) # Generate a UUID for the admin user + admin_email = 'admin@example.com' + admin_nickname = 'admin' + admin_hash_password = '$2b$12$wMygvJfsJGS4UqdeKF1JGO6Sd7tQBg8uo6C946xgntDsstrdgTKVy' + + op.execute(f""" + INSERT INTO users (id, nickname, email, role, email_verified, hashed_password, created_at, updated_at) + VALUES ( + '{admin_id}', + '{admin_nickname}', + '{admin_email}', + 'ADMIN', + TRUE, + '{admin_hash_password}', + now(), + now() + ) + """) + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_users_nickname'), table_name='users') + op.drop_index(op.f('ix_users_email'), table_name='users') + op.drop_table('users') + # ### end Alembic commands ### diff --git a/app/database.py b/app/database.py index a8662934d..75cae29bd 100644 --- a/app/database.py +++ b/app/database.py @@ -1,25 +1,25 @@ -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession -from sqlalchemy.orm import declarative_base, sessionmaker - -Base = declarative_base() - -class Database: - """Handles database connections and sessions.""" - _engine = None - _session_factory = None - - @classmethod - def initialize(cls, database_url: str, echo: bool = False): - """Initialize the async engine and sessionmaker.""" - if cls._engine is None: # Ensure engine is created once - cls._engine = create_async_engine(database_url, echo=echo, future=True) - cls._session_factory = sessionmaker( - bind=cls._engine, class_=AsyncSession, expire_on_commit=False, future=True - ) - - @classmethod - def get_session_factory(cls): - """Returns the session factory, ensuring it's initialized.""" - if cls._session_factory is None: - raise ValueError("Database not initialized. Call `initialize()` first.") - return cls._session_factory +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import declarative_base, sessionmaker + +Base = declarative_base() + +class Database: + """Handles database connections and sessions.""" + _engine = None + _session_factory = None + + @classmethod + def initialize(cls, database_url: str, echo: bool = False): + """Initialize the async engine and sessionmaker.""" + if cls._engine is None: # Ensure engine is created once + cls._engine = create_async_engine(database_url, echo=echo, future=True) + cls._session_factory = sessionmaker( + bind=cls._engine, class_=AsyncSession, expire_on_commit=False, future=True + ) + + @classmethod + def get_session_factory(cls): + """Returns the session factory, ensuring it's initialized.""" + if cls._session_factory is None: + raise ValueError("Database not initialized. Call `initialize()` first.") + return cls._session_factory diff --git a/app/dependencies.py b/app/dependencies.py index 3dd3f7781..7b20801a6 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -1,52 +1,52 @@ -from builtins import Exception, dict, str -from fastapi import Depends, HTTPException -from fastapi.security import OAuth2PasswordBearer -from sqlalchemy.ext.asyncio import AsyncSession -from app.database import Database -from app.utils.template_manager import TemplateManager -from app.services.email_service import EmailService -from app.services.jwt_service import decode_token -from settings.config import Settings -from fastapi import Depends - -def get_settings() -> Settings: - """Return application settings.""" - return Settings() - -def get_email_service() -> EmailService: - template_manager = TemplateManager() - return EmailService(template_manager=template_manager) - -async def get_db() -> AsyncSession: - """Dependency that provides a database session for each request.""" - async_session_factory = Database.get_session_factory() - async with async_session_factory() as session: - try: - yield session - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login") - -def get_current_user(token: str = Depends(oauth2_scheme)): - credentials_exception = HTTPException( - status_code=401, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - payload = decode_token(token) - if payload is None: - raise credentials_exception - user_id: str = payload.get("sub") - user_role: str = payload.get("role") - if user_id is None or user_role is None: - raise credentials_exception - return {"user_id": user_id, "role": user_role} - -def require_role(role: str): - def role_checker(current_user: dict = Depends(get_current_user)): - if current_user["role"] not in role: - raise HTTPException(status_code=403, detail="Operation not permitted") - return current_user - return role_checker +from builtins import Exception, dict, str +from fastapi import Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.ext.asyncio import AsyncSession +from app.database import Database +from app.utils.template_manager import TemplateManager +from app.services.email_service import EmailService +from app.services.jwt_service import decode_token +from settings.config import Settings +from fastapi import Depends + +def get_settings() -> Settings: + """Return application settings.""" + return Settings() + +def get_email_service() -> EmailService: + template_manager = TemplateManager() + return EmailService(template_manager=template_manager) + +async def get_db() -> AsyncSession: + """Dependency that provides a database session for each request.""" + async_session_factory = Database.get_session_factory() + async with async_session_factory() as session: + try: + yield session + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login") + +def get_current_user(token: str = Depends(oauth2_scheme)): + credentials_exception = HTTPException( + status_code=401, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + payload = decode_token(token) + if payload is None: + raise credentials_exception + user_id: str = payload.get("sub") + user_role: str = payload.get("role") + if user_id is None or user_role is None: + raise credentials_exception + return {"user_id": user_id, "role": user_role} + +def require_role(role: str): + def role_checker(current_user: dict = Depends(get_current_user)): + if current_user["role"] not in role: + raise HTTPException(status_code=403, detail="Operation not permitted") + return current_user + return role_checker diff --git a/app/main.py b/app/main.py index 383efe906..fa6fdadac 100644 --- a/app/main.py +++ b/app/main.py @@ -1,31 +1,31 @@ -from builtins import Exception -from fastapi import FastAPI -from starlette.responses import JSONResponse -from app.database import Database -from app.dependencies import get_settings -from app.routers import user_routes -from app.utils.api_description import getDescription -app = FastAPI( - title="User Management", - description=getDescription(), - version="0.0.1", - contact={ - "name": "API Support", - "url": "http://www.example.com/support", - "email": "support@example.com", - }, - license_info={"name": "MIT", "url": "https://opensource.org/licenses/MIT"}, -) - -@app.on_event("startup") -async def startup_event(): - settings = get_settings() - Database.initialize(settings.database_url, settings.debug) - -@app.exception_handler(Exception) -async def exception_handler(request, exc): - return JSONResponse(status_code=500, content={"message": "An unexpected error occurred."}) - -app.include_router(user_routes.router) - - +from builtins import Exception +from fastapi import FastAPI +from starlette.responses import JSONResponse +from app.database import Database +from app.dependencies import get_settings +from app.routers import user_routes +from app.utils.api_description import getDescription +app = FastAPI( + title="User Management", + description=getDescription(), + version="0.0.1", + contact={ + "name": "API Support", + "url": "http://www.example.com/support", + "email": "support@example.com", + }, + license_info={"name": "MIT", "url": "https://opensource.org/licenses/MIT"}, +) + +@app.on_event("startup") +async def startup_event(): + settings = get_settings() + Database.initialize(settings.database_url, settings.debug) + +@app.exception_handler(Exception) +async def exception_handler(request, exc): + return JSONResponse(status_code=500, content={"message": "An unexpected error occurred."}) + +app.include_router(user_routes.router) + + diff --git a/app/models/user_model.py b/app/models/user_model.py index 283bb7ee5..4ddc7aad8 100644 --- a/app/models/user_model.py +++ b/app/models/user_model.py @@ -1,97 +1,97 @@ -from builtins import bool, int, str -from datetime import datetime -from enum import Enum -import uuid -from sqlalchemy import ( - Column, String, Integer, DateTime, Boolean, func, Enum as SQLAlchemyEnum -) -from sqlalchemy.dialects.postgresql import UUID, ENUM -from sqlalchemy.orm import Mapped, mapped_column -from app.database import Base - -class UserRole(Enum): - """Enumeration of user roles within the application, stored as ENUM in the database.""" - ANONYMOUS = "ANONYMOUS" - AUTHENTICATED = "AUTHENTICATED" - MANAGER = "MANAGER" - ADMIN = "ADMIN" - -class User(Base): - """ - Represents a user within the application, corresponding to the 'users' table in the database. - This class uses SQLAlchemy ORM for mapping attributes to database columns efficiently. - - Attributes: - id (UUID): Unique identifier for the user. - nickname (str): Unique nickname for privacy, required. - email (str): Unique email address, required. - email_verified (bool): Flag indicating if the email has been verified. - hashed_password (str): Hashed password for security, required. - first_name (str): Optional first name of the user. - last_name (str): Optional first name of the user. - - bio (str): Optional biographical information. - profile_picture_url (str): Optional URL to a profile picture. - linkedin_profile_url (str): Optional LinkedIn profile URL. - github_profile_url (str): Optional GitHub profile URL. - role (UserRole): Role of the user within the application. - is_professional (bool): Flag indicating professional status. - professional_status_updated_at (datetime): Timestamp of last professional status update. - last_login_at (datetime): Timestamp of the last login. - failed_login_attempts (int): Count of failed login attempts. - is_locked (bool): Flag indicating if the account is locked. - created_at (datetime): Timestamp when the user was created, set by the server. - updated_at (datetime): Timestamp of the last update, set by the server. - - Methods: - lock_account(): Locks the user account. - unlock_account(): Unlocks the user account. - verify_email(): Marks the user's email as verified. - has_role(role_name): Checks if the user has a specified role. - update_professional_status(status): Updates the professional status and logs the update time. - """ - __tablename__ = "users" - __mapper_args__ = {"eager_defaults": True} - - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - nickname: Mapped[str] = Column(String(50), unique=True, nullable=False, index=True) - email: Mapped[str] = Column(String(255), unique=True, nullable=False, index=True) - first_name: Mapped[str] = Column(String(100), nullable=True) - last_name: Mapped[str] = Column(String(100), nullable=True) - bio: Mapped[str] = Column(String(500), nullable=True) - profile_picture_url: Mapped[str] = Column(String(255), nullable=True) - linkedin_profile_url: Mapped[str] = Column(String(255), nullable=True) - github_profile_url: Mapped[str] = Column(String(255), nullable=True) - role: Mapped[UserRole] = Column(SQLAlchemyEnum(UserRole, name='UserRole', create_constraint=False), default=UserRole.ANONYMOUS, nullable=False) - is_professional: Mapped[bool] = Column(Boolean, default=False) - professional_status_updated_at: Mapped[datetime] = Column(DateTime(timezone=True), nullable=True) - last_login_at: Mapped[datetime] = Column(DateTime(timezone=True), nullable=True) - failed_login_attempts: Mapped[int] = Column(Integer, default=0) - is_locked: Mapped[bool] = Column(Boolean, default=False) - created_at: Mapped[datetime] = Column(DateTime(timezone=True), server_default=func.now()) - updated_at: Mapped[datetime] = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - verification_token = Column(String, nullable=True) - email_verified: Mapped[bool] = Column(Boolean, default=False, nullable=False) - hashed_password: Mapped[str] = Column(String(255), nullable=False) - - - def __repr__(self) -> str: - """Provides a readable representation of a user object.""" - return f"" - - def lock_account(self): - self.is_locked = True - - def unlock_account(self): - self.is_locked = False - - def verify_email(self): - self.email_verified = True - - def has_role(self, role_name: UserRole) -> bool: - return self.role == role_name - - def update_professional_status(self, status: bool): - """Updates the professional status and logs the update time.""" - self.is_professional = status - self.professional_status_updated_at = func.now() +from builtins import bool, int, str +from datetime import datetime +from enum import Enum +import uuid +from sqlalchemy import ( + Column, String, Integer, DateTime, Boolean, func, Enum as SQLAlchemyEnum +) +from sqlalchemy.dialects.postgresql import UUID, ENUM +from sqlalchemy.orm import Mapped, mapped_column +from app.database import Base + +class UserRole(Enum): + """Enumeration of user roles within the application, stored as ENUM in the database.""" + ANONYMOUS = "ANONYMOUS" + AUTHENTICATED = "AUTHENTICATED" + MANAGER = "MANAGER" + ADMIN = "ADMIN" + +class User(Base): + """ + Represents a user within the application, corresponding to the 'users' table in the database. + This class uses SQLAlchemy ORM for mapping attributes to database columns efficiently. + + Attributes: + id (UUID): Unique identifier for the user. + nickname (str): Unique nickname for privacy, required. + email (str): Unique email address, required. + email_verified (bool): Flag indicating if the email has been verified. + hashed_password (str): Hashed password for security, required. + first_name (str): Optional first name of the user. + last_name (str): Optional first name of the user. + + bio (str): Optional biographical information. + profile_picture_url (str): Optional URL to a profile picture. + linkedin_profile_url (str): Optional LinkedIn profile URL. + github_profile_url (str): Optional GitHub profile URL. + role (UserRole): Role of the user within the application. + is_professional (bool): Flag indicating professional status. + professional_status_updated_at (datetime): Timestamp of last professional status update. + last_login_at (datetime): Timestamp of the last login. + failed_login_attempts (int): Count of failed login attempts. + is_locked (bool): Flag indicating if the account is locked. + created_at (datetime): Timestamp when the user was created, set by the server. + updated_at (datetime): Timestamp of the last update, set by the server. + + Methods: + lock_account(): Locks the user account. + unlock_account(): Unlocks the user account. + verify_email(): Marks the user's email as verified. + has_role(role_name): Checks if the user has a specified role. + update_professional_status(status): Updates the professional status and logs the update time. + """ + __tablename__ = "users" + __mapper_args__ = {"eager_defaults": True} + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + nickname: Mapped[str] = Column(String(50), unique=True, nullable=False, index=True) + email: Mapped[str] = Column(String(255), unique=True, nullable=False, index=True) + first_name: Mapped[str] = Column(String(100), nullable=True) + last_name: Mapped[str] = Column(String(100), nullable=True) + bio: Mapped[str] = Column(String(500), nullable=True) + profile_picture_url: Mapped[str] = Column(String(255), nullable=True) + linkedin_profile_url: Mapped[str] = Column(String(255), nullable=True) + github_profile_url: Mapped[str] = Column(String(255), nullable=True) + role: Mapped[UserRole] = Column(SQLAlchemyEnum(UserRole, name='UserRole', create_constraint=False), default=UserRole.ANONYMOUS, nullable=False) + is_professional: Mapped[bool] = Column(Boolean, default=False) + professional_status_updated_at: Mapped[datetime] = Column(DateTime(timezone=True), nullable=True) + last_login_at: Mapped[datetime] = Column(DateTime(timezone=True), nullable=True) + failed_login_attempts: Mapped[int] = Column(Integer, default=0) + is_locked: Mapped[bool] = Column(Boolean, default=False) + created_at: Mapped[datetime] = Column(DateTime(timezone=True), server_default=func.now()) + updated_at: Mapped[datetime] = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + verification_token = Column(String, nullable=True) + email_verified: Mapped[bool] = Column(Boolean, default=False, nullable=False) + hashed_password: Mapped[str] = Column(String(255), nullable=False) + + + def __repr__(self) -> str: + """Provides a readable representation of a user object.""" + return f"" + + def lock_account(self): + self.is_locked = True + + def unlock_account(self): + self.is_locked = False + + def verify_email(self): + self.email_verified = True + + def has_role(self, role_name: UserRole) -> bool: + return self.role == role_name + + def update_professional_status(self, status: bool): + """Updates the professional status and logs the update time.""" + self.is_professional = status + self.professional_status_updated_at = func.now() diff --git a/app/routers/user_routes.py b/app/routers/user_routes.py index 737fd18e7..942ffe5b0 100644 --- a/app/routers/user_routes.py +++ b/app/routers/user_routes.py @@ -1,245 +1,245 @@ -""" -This Python file is part of a FastAPI application, demonstrating user management functionalities including creating, reading, -updating, and deleting (CRUD) user information. It uses OAuth2 with Password Flow for security, ensuring that only authenticated -users can perform certain operations. Additionally, the file showcases the integration of FastAPI with SQLAlchemy for asynchronous -database operations, enhancing performance by non-blocking database calls. - -The implementation emphasizes RESTful API principles, with endpoints for each CRUD operation and the use of HTTP status codes -and exceptions to communicate the outcome of operations. It introduces the concept of HATEOAS (Hypermedia as the Engine of -Application State) by including navigational links in API responses, allowing clients to discover other related operations dynamically. - -OAuth2PasswordBearer is employed to extract the token from the Authorization header and verify the user's identity, providing a layer -of security to the operations that manipulate user data. - -Key Highlights: -- Use of FastAPI's Dependency Injection system to manage database sessions and user authentication. -- Demonstrates how to perform CRUD operations in an asynchronous manner using SQLAlchemy with FastAPI. -- Implements HATEOAS by generating dynamic links for user-related actions, enhancing API discoverability. -- Utilizes OAuth2PasswordBearer for securing API endpoints, requiring valid access tokens for operations. -""" - -from builtins import dict, int, len, str -from datetime import timedelta -from uuid import UUID -from fastapi import APIRouter, Depends, HTTPException, Response, status, Request -from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm -from sqlalchemy.ext.asyncio import AsyncSession -from app.dependencies import get_current_user, get_db, get_email_service, require_role -from app.schemas.pagination_schema import EnhancedPagination -from app.schemas.token_schema import TokenResponse -from app.schemas.user_schemas import LoginRequest, UserBase, UserCreate, UserListResponse, UserResponse, UserUpdate -from app.services.user_service import UserService -from app.services.jwt_service import create_access_token -from app.utils.link_generation import create_user_links, generate_pagination_links -from app.dependencies import get_settings -from app.services.email_service import EmailService -router = APIRouter() -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") -settings = get_settings() -@router.get("/users/{user_id}", response_model=UserResponse, name="get_user", tags=["User Management Requires (Admin or Manager Roles)"]) -async def get_user(user_id: UUID, request: Request, db: AsyncSession = Depends(get_db), token: str = Depends(oauth2_scheme), current_user: dict = Depends(require_role(["ADMIN", "MANAGER"]))): - """ - Endpoint to fetch a user by their unique identifier (UUID). - - Utilizes the UserService to query the database asynchronously for the user and constructs a response - model that includes the user's details along with HATEOAS links for possible next actions. - - Args: - user_id: UUID of the user to fetch. - request: The request object, used to generate full URLs in the response. - db: Dependency that provides an AsyncSession for database access. - token: The OAuth2 access token obtained through OAuth2PasswordBearer dependency. - """ - user = await UserService.get_by_id(db, user_id) - if not user: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") - - return UserResponse.model_construct( - id=user.id, - nickname=user.nickname, - first_name=user.first_name, - last_name=user.last_name, - bio=user.bio, - profile_picture_url=user.profile_picture_url, - github_profile_url=user.github_profile_url, - linkedin_profile_url=user.linkedin_profile_url, - role=user.role, - email=user.email, - last_login_at=user.last_login_at, - created_at=user.created_at, - updated_at=user.updated_at, - links=create_user_links(user.id, request) - ) - -# Additional endpoints for update, delete, create, and list users follow a similar pattern, using -# asynchronous database operations, handling security with OAuth2PasswordBearer, and enhancing response -# models with dynamic HATEOAS links. - -# This approach not only ensures that the API is secure and efficient but also promotes a better client -# experience by adhering to REST principles and providing self-discoverable operations. - -@router.put("/users/{user_id}", response_model=UserResponse, name="update_user", tags=["User Management Requires (Admin or Manager Roles)"]) -async def update_user(user_id: UUID, user_update: UserUpdate, request: Request, db: AsyncSession = Depends(get_db), token: str = Depends(oauth2_scheme), current_user: dict = Depends(require_role(["ADMIN", "MANAGER"]))): - """ - Update user information. - - - **user_id**: UUID of the user to update. - - **user_update**: UserUpdate model with updated user information. - """ - user_data = user_update.model_dump(exclude_unset=True) - updated_user = await UserService.update(db, user_id, user_data) - if not updated_user: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") - - return UserResponse.model_construct( - id=updated_user.id, - bio=updated_user.bio, - first_name=updated_user.first_name, - last_name=updated_user.last_name, - nickname=updated_user.nickname, - email=updated_user.email, - last_login_at=updated_user.last_login_at, - profile_picture_url=updated_user.profile_picture_url, - github_profile_url=updated_user.github_profile_url, - linkedin_profile_url=updated_user.linkedin_profile_url, - created_at=updated_user.created_at, - updated_at=updated_user.updated_at, - links=create_user_links(updated_user.id, request) - ) - - -@router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT, name="delete_user", tags=["User Management Requires (Admin or Manager Roles)"]) -async def delete_user(user_id: UUID, db: AsyncSession = Depends(get_db), token: str = Depends(oauth2_scheme), current_user: dict = Depends(require_role(["ADMIN", "MANAGER"]))): - """ - Delete a user by their ID. - - - **user_id**: UUID of the user to delete. - """ - success = await UserService.delete(db, user_id) - if not success: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") - return Response(status_code=status.HTTP_204_NO_CONTENT) - - - -@router.post("/users/", response_model=UserResponse, status_code=status.HTTP_201_CREATED, tags=["User Management Requires (Admin or Manager Roles)"], name="create_user") -async def create_user(user: UserCreate, request: Request, db: AsyncSession = Depends(get_db), email_service: EmailService = Depends(get_email_service), token: str = Depends(oauth2_scheme), current_user: dict = Depends(require_role(["ADMIN", "MANAGER"]))): - """ - Create a new user. - - This endpoint creates a new user with the provided information. If the email - already exists, it returns a 400 error. On successful creation, it returns the - newly created user's information along with links to related actions. - - Parameters: - - user (UserCreate): The user information to create. - - request (Request): The request object. - - db (AsyncSession): The database session. - - Returns: - - UserResponse: The newly created user's information along with navigation links. - """ - existing_user = await UserService.get_by_email(db, user.email) - if existing_user: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already exists") - - created_user = await UserService.create(db, user.model_dump(), email_service) - if not created_user: - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create user") - - - return UserResponse.model_construct( - id=created_user.id, - bio=created_user.bio, - first_name=created_user.first_name, - last_name=created_user.last_name, - profile_picture_url=created_user.profile_picture_url, - nickname=created_user.nickname, - email=created_user.email, - last_login_at=created_user.last_login_at, - created_at=created_user.created_at, - updated_at=created_user.updated_at, - links=create_user_links(created_user.id, request) - ) - - -@router.get("/users/", response_model=UserListResponse, tags=["User Management Requires (Admin or Manager Roles)"]) -async def list_users( - request: Request, - skip: int = 0, - limit: int = 10, - db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_role(["ADMIN", "MANAGER"])) -): - total_users = await UserService.count(db) - users = await UserService.list_users(db, skip, limit) - - user_responses = [ - UserResponse.model_validate(user) for user in users - ] - - pagination_links = generate_pagination_links(request, skip, limit, total_users) - - # Construct the final response with pagination details - return UserListResponse( - items=user_responses, - total=total_users, - page=skip // limit + 1, - size=len(user_responses), - links=pagination_links # Ensure you have appropriate logic to create these links - ) - - -@router.post("/register/", response_model=UserResponse, tags=["Login and Registration"]) -async def register(user_data: UserCreate, session: AsyncSession = Depends(get_db), email_service: EmailService = Depends(get_email_service)): - user = await UserService.register_user(session, user_data.model_dump(), email_service) - if user: - return user - raise HTTPException(status_code=400, detail="Email already exists") - -@router.post("/login/", response_model=TokenResponse, tags=["Login and Registration"]) -async def login(form_data: OAuth2PasswordRequestForm = Depends(), session: AsyncSession = Depends(get_db)): - if await UserService.is_account_locked(session, form_data.username): - raise HTTPException(status_code=400, detail="Account locked due to too many failed login attempts.") - - user = await UserService.login_user(session, form_data.username, form_data.password) - if user: - access_token_expires = timedelta(minutes=settings.access_token_expire_minutes) - - access_token = create_access_token( - data={"sub": user.email, "role": str(user.role.name)}, - expires_delta=access_token_expires - ) - - return {"access_token": access_token, "token_type": "bearer"} - raise HTTPException(status_code=401, detail="The email or password is incorrect, the email is not verified, or the account is locked.") - -@router.post("/login/", include_in_schema=False, response_model=TokenResponse, tags=["Login and Registration"]) -async def login(form_data: OAuth2PasswordRequestForm = Depends(), session: AsyncSession = Depends(get_db)): - if await UserService.is_account_locked(session, form_data.username): - raise HTTPException(status_code=400, detail="Account locked due to too many failed login attempts.") - - user = await UserService.login_user(session, form_data.username, form_data.password) - if user: - access_token_expires = timedelta(minutes=settings.access_token_expire_minutes) - - access_token = create_access_token( - data={"sub": user.email, "role": str(user.role.name)}, - expires_delta=access_token_expires - ) - - return {"access_token": access_token, "token_type": "bearer"} - raise HTTPException(status_code=401, detail="The email or password is incorrect, the email is not verified, or the account is locked.") - - -@router.get("/verify-email/{user_id}/{token}", status_code=status.HTTP_200_OK, name="verify_email", tags=["Login and Registration"]) -async def verify_email(user_id: UUID, token: str, db: AsyncSession = Depends(get_db), email_service: EmailService = Depends(get_email_service)): - """ - Verify user's email with a provided token. - - - **user_id**: UUID of the user to verify. - - **token**: Verification token sent to the user's email. - """ - if await UserService.verify_email_with_token(db, user_id, token): - return {"message": "Email verified successfully"} +""" +This Python file is part of a FastAPI application, demonstrating user management functionalities including creating, reading, +updating, and deleting (CRUD) user information. It uses OAuth2 with Password Flow for security, ensuring that only authenticated +users can perform certain operations. Additionally, the file showcases the integration of FastAPI with SQLAlchemy for asynchronous +database operations, enhancing performance by non-blocking database calls. + +The implementation emphasizes RESTful API principles, with endpoints for each CRUD operation and the use of HTTP status codes +and exceptions to communicate the outcome of operations. It introduces the concept of HATEOAS (Hypermedia as the Engine of +Application State) by including navigational links in API responses, allowing clients to discover other related operations dynamically. + +OAuth2PasswordBearer is employed to extract the token from the Authorization header and verify the user's identity, providing a layer +of security to the operations that manipulate user data. + +Key Highlights: +- Use of FastAPI's Dependency Injection system to manage database sessions and user authentication. +- Demonstrates how to perform CRUD operations in an asynchronous manner using SQLAlchemy with FastAPI. +- Implements HATEOAS by generating dynamic links for user-related actions, enhancing API discoverability. +- Utilizes OAuth2PasswordBearer for securing API endpoints, requiring valid access tokens for operations. +""" + +from builtins import dict, int, len, str +from datetime import timedelta +from uuid import UUID +from fastapi import APIRouter, Depends, HTTPException, Response, status, Request +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from sqlalchemy.ext.asyncio import AsyncSession +from app.dependencies import get_current_user, get_db, get_email_service, require_role +from app.schemas.pagination_schema import EnhancedPagination +from app.schemas.token_schema import TokenResponse +from app.schemas.user_schemas import LoginRequest, UserBase, UserCreate, UserListResponse, UserResponse, UserUpdate +from app.services.user_service import UserService +from app.services.jwt_service import create_access_token +from app.utils.link_generation import create_user_links, generate_pagination_links +from app.dependencies import get_settings +from app.services.email_service import EmailService +router = APIRouter() +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") +settings = get_settings() +@router.get("/users/{user_id}", response_model=UserResponse, name="get_user", tags=["User Management Requires (Admin or Manager Roles)"]) +async def get_user(user_id: UUID, request: Request, db: AsyncSession = Depends(get_db), token: str = Depends(oauth2_scheme), current_user: dict = Depends(require_role(["ADMIN", "MANAGER"]))): + """ + Endpoint to fetch a user by their unique identifier (UUID). + + Utilizes the UserService to query the database asynchronously for the user and constructs a response + model that includes the user's details along with HATEOAS links for possible next actions. + + Args: + user_id: UUID of the user to fetch. + request: The request object, used to generate full URLs in the response. + db: Dependency that provides an AsyncSession for database access. + token: The OAuth2 access token obtained through OAuth2PasswordBearer dependency. + """ + user = await UserService.get_by_id(db, user_id) + if not user: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") + + return UserResponse.model_construct( + id=user.id, + nickname=user.nickname, + first_name=user.first_name, + last_name=user.last_name, + bio=user.bio, + profile_picture_url=user.profile_picture_url, + github_profile_url=user.github_profile_url, + linkedin_profile_url=user.linkedin_profile_url, + role=user.role, + email=user.email, + last_login_at=user.last_login_at, + created_at=user.created_at, + updated_at=user.updated_at, + links=create_user_links(user.id, request) + ) + +# Additional endpoints for update, delete, create, and list users follow a similar pattern, using +# asynchronous database operations, handling security with OAuth2PasswordBearer, and enhancing response +# models with dynamic HATEOAS links. + +# This approach not only ensures that the API is secure and efficient but also promotes a better client +# experience by adhering to REST principles and providing self-discoverable operations. + +@router.put("/users/{user_id}", response_model=UserResponse, name="update_user", tags=["User Management Requires (Admin or Manager Roles)"]) +async def update_user(user_id: UUID, user_update: UserUpdate, request: Request, db: AsyncSession = Depends(get_db), token: str = Depends(oauth2_scheme), current_user: dict = Depends(require_role(["ADMIN", "MANAGER"]))): + """ + Update user information. + + - **user_id**: UUID of the user to update. + - **user_update**: UserUpdate model with updated user information. + """ + user_data = user_update.model_dump(exclude_unset=True) + updated_user = await UserService.update(db, user_id, user_data) + if not updated_user: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") + + return UserResponse.model_construct( + id=updated_user.id, + bio=updated_user.bio, + first_name=updated_user.first_name, + last_name=updated_user.last_name, + nickname=updated_user.nickname, + email=updated_user.email, + last_login_at=updated_user.last_login_at, + profile_picture_url=updated_user.profile_picture_url, + github_profile_url=updated_user.github_profile_url, + linkedin_profile_url=updated_user.linkedin_profile_url, + created_at=updated_user.created_at, + updated_at=updated_user.updated_at, + links=create_user_links(updated_user.id, request) + ) + + +@router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT, name="delete_user", tags=["User Management Requires (Admin or Manager Roles)"]) +async def delete_user(user_id: UUID, db: AsyncSession = Depends(get_db), token: str = Depends(oauth2_scheme), current_user: dict = Depends(require_role(["ADMIN", "MANAGER"]))): + """ + Delete a user by their ID. + + - **user_id**: UUID of the user to delete. + """ + success = await UserService.delete(db, user_id) + if not success: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") + return Response(status_code=status.HTTP_204_NO_CONTENT) + + + +@router.post("/users/", response_model=UserResponse, status_code=status.HTTP_201_CREATED, tags=["User Management Requires (Admin or Manager Roles)"], name="create_user") +async def create_user(user: UserCreate, request: Request, db: AsyncSession = Depends(get_db), email_service: EmailService = Depends(get_email_service), token: str = Depends(oauth2_scheme), current_user: dict = Depends(require_role(["ADMIN", "MANAGER"]))): + """ + Create a new user. + + This endpoint creates a new user with the provided information. If the email + already exists, it returns a 400 error. On successful creation, it returns the + newly created user's information along with links to related actions. + + Parameters: + - user (UserCreate): The user information to create. + - request (Request): The request object. + - db (AsyncSession): The database session. + + Returns: + - UserResponse: The newly created user's information along with navigation links. + """ + existing_user = await UserService.get_by_email(db, user.email) + if existing_user: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already exists") + + created_user = await UserService.create(db, user.model_dump(), email_service) + if not created_user: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create user") + + + return UserResponse.model_construct( + id=created_user.id, + bio=created_user.bio, + first_name=created_user.first_name, + last_name=created_user.last_name, + profile_picture_url=created_user.profile_picture_url, + nickname=created_user.nickname, + email=created_user.email, + last_login_at=created_user.last_login_at, + created_at=created_user.created_at, + updated_at=created_user.updated_at, + links=create_user_links(created_user.id, request) + ) + + +@router.get("/users/", response_model=UserListResponse, tags=["User Management Requires (Admin or Manager Roles)"]) +async def list_users( + request: Request, + skip: int = 0, + limit: int = 10, + db: AsyncSession = Depends(get_db), + current_user: dict = Depends(require_role(["ADMIN", "MANAGER"])) +): + total_users = await UserService.count(db) + users = await UserService.list_users(db, skip, limit) + + user_responses = [ + UserResponse.model_validate(user) for user in users + ] + + pagination_links = generate_pagination_links(request, skip, limit, total_users) + + # Construct the final response with pagination details + return UserListResponse( + items=user_responses, + total=total_users, + page=skip // limit + 1, + size=len(user_responses), + links=pagination_links # Ensure you have appropriate logic to create these links + ) + + +@router.post("/register/", response_model=UserResponse, tags=["Login and Registration"]) +async def register(user_data: UserCreate, session: AsyncSession = Depends(get_db), email_service: EmailService = Depends(get_email_service)): + user = await UserService.register_user(session, user_data.model_dump(), email_service) + if user: + return user + raise HTTPException(status_code=400, detail="Email already exists") + +@router.post("/login/", response_model=TokenResponse, tags=["Login and Registration"]) +async def login(form_data: OAuth2PasswordRequestForm = Depends(), session: AsyncSession = Depends(get_db)): + if await UserService.is_account_locked(session, form_data.username): + raise HTTPException(status_code=400, detail="Account locked due to too many failed login attempts.") + + user = await UserService.login_user(session, form_data.username, form_data.password) + if user: + access_token_expires = timedelta(minutes=settings.access_token_expire_minutes) + + access_token = create_access_token( + data={"sub": user.email, "role": str(user.role.name)}, + expires_delta=access_token_expires + ) + + return {"access_token": access_token, "token_type": "bearer"} + raise HTTPException(status_code=401, detail="The email or password is incorrect, the email is not verified, or the account is locked.") + +@router.post("/login/", include_in_schema=False, response_model=TokenResponse, tags=["Login and Registration"]) +async def login(form_data: OAuth2PasswordRequestForm = Depends(), session: AsyncSession = Depends(get_db)): + if await UserService.is_account_locked(session, form_data.username): + raise HTTPException(status_code=400, detail="Account locked due to too many failed login attempts.") + + user = await UserService.login_user(session, form_data.username, form_data.password) + if user: + access_token_expires = timedelta(minutes=settings.access_token_expire_minutes) + + access_token = create_access_token( + data={"sub": user.email, "role": str(user.role.name)}, + expires_delta=access_token_expires + ) + + return {"access_token": access_token, "token_type": "bearer"} + raise HTTPException(status_code=401, detail="The email or password is incorrect, the email is not verified, or the account is locked.") + + +@router.get("/verify-email/{user_id}/{token}", status_code=status.HTTP_200_OK, name="verify_email", tags=["Login and Registration"]) +async def verify_email(user_id: UUID, token: str, db: AsyncSession = Depends(get_db), email_service: EmailService = Depends(get_email_service)): + """ + Verify user's email with a provided token. + + - **user_id**: UUID of the user to verify. + - **token**: Verification token sent to the user's email. + """ + if await UserService.verify_email_with_token(db, user_id, token): + return {"message": "Email verified successfully"} raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired verification token") \ No newline at end of file diff --git a/app/schemas/link_schema.py b/app/schemas/link_schema.py index 2cd90d22f..356cd057a 100644 --- a/app/schemas/link_schema.py +++ b/app/schemas/link_schema.py @@ -1,17 +1,17 @@ -from pydantic import BaseModel, Field, HttpUrl - -class Link(BaseModel): - rel: str = Field(..., description="Relation type of the link.") - href: HttpUrl = Field(..., description="The URL of the link.") - action: str = Field(..., description="HTTP method for the action this link represents.") - type: str = Field(default="application/json", description="Content type of the response for this link.") - - class Config: - json_schema_extra = { - "example": { - "rel": "self", - "href": "https://api.example.com/qr/123", - "action": "GET", - "type": "application/json" - } - } +from pydantic import BaseModel, Field, HttpUrl + +class Link(BaseModel): + rel: str = Field(..., description="Relation type of the link.") + href: HttpUrl = Field(..., description="The URL of the link.") + action: str = Field(..., description="HTTP method for the action this link represents.") + type: str = Field(default="application/json", description="Content type of the response for this link.") + + class Config: + json_schema_extra = { + "example": { + "rel": "self", + "href": "https://api.example.com/qr/123", + "action": "GET", + "type": "application/json" + } + } diff --git a/app/schemas/pagination_schema.py b/app/schemas/pagination_schema.py index 263aa209b..3956dca43 100644 --- a/app/schemas/pagination_schema.py +++ b/app/schemas/pagination_schema.py @@ -1,35 +1,35 @@ -import re -from datetime import datetime -from typing import List, Optional -from uuid import UUID -from pydantic import BaseModel, EmailStr, Field, HttpUrl, validator, conint - -# Pagination Model -class Pagination(BaseModel): - page: int = Field(..., description="Current page number.") - per_page: int = Field(..., description="Number of items per page.") - total_items: int = Field(..., description="Total number of items.") - total_pages: int = Field(..., description="Total number of pages.") - - class Config: - json_schema_extra = { - "example": { - "page": 1, - "per_page": 10, - "total_items": 50, - "total_pages": 5 - } - } - - - -class PaginationLink(BaseModel): - rel: str - href: HttpUrl - method: str = "GET" - -class EnhancedPagination(Pagination): - links: List[PaginationLink] = [] - - def add_link(self, rel: str, href: str): - self.links.append(PaginationLink(rel=rel, href=href)) +import re +from datetime import datetime +from typing import List, Optional +from uuid import UUID +from pydantic import BaseModel, EmailStr, Field, HttpUrl, validator, conint + +# Pagination Model +class Pagination(BaseModel): + page: int = Field(..., description="Current page number.") + per_page: int = Field(..., description="Number of items per page.") + total_items: int = Field(..., description="Total number of items.") + total_pages: int = Field(..., description="Total number of pages.") + + class Config: + json_schema_extra = { + "example": { + "page": 1, + "per_page": 10, + "total_items": 50, + "total_pages": 5 + } + } + + + +class PaginationLink(BaseModel): + rel: str + href: HttpUrl + method: str = "GET" + +class EnhancedPagination(Pagination): + links: List[PaginationLink] = [] + + def add_link(self, rel: str, href: str): + self.links.append(PaginationLink(rel=rel, href=href)) diff --git a/app/schemas/token_schema.py b/app/schemas/token_schema.py index 991d85252..af66f03cb 100644 --- a/app/schemas/token_schema.py +++ b/app/schemas/token_schema.py @@ -1,14 +1,14 @@ -from builtins import str -from pydantic import BaseModel - -class TokenResponse(BaseModel): - access_token: str - token_type: str = "bearer" - - class Config: - schema_extra = { - "example": { - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huZG9lQGV4YW1wbGUuY29tIiwicm9sZSI6IkFVVEhFTlRJQ0FURUQiLCJleHAiOjE2MjQzMzQ5ODR9.ZGNjNjI2ZjI4MmYzNTk0MjVjNDk0ZjI4MjdjNGEzNmI1", - "token_type": "bearer" - } +from builtins import str +from pydantic import BaseModel + +class TokenResponse(BaseModel): + access_token: str + token_type: str = "bearer" + + class Config: + schema_extra = { + "example": { + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huZG9lQGV4YW1wbGUuY29tIiwicm9sZSI6IkFVVEhFTlRJQ0FURUQiLCJleHAiOjE2MjQzMzQ5ODR9.ZGNjNjI2ZjI4MmYzNTk0MjVjNDk0ZjI4MjdjNGEzNmI1", + "token_type": "bearer" + } } \ No newline at end of file diff --git a/app/schemas/user_schemas.py b/app/schemas/user_schemas.py index 7877378cf..c5d7e0ccc 100644 --- a/app/schemas/user_schemas.py +++ b/app/schemas/user_schemas.py @@ -1,87 +1,87 @@ -from builtins import ValueError, any, bool, str -from pydantic import BaseModel, EmailStr, Field, validator, root_validator -from typing import Optional, List -from datetime import datetime -from enum import Enum -import uuid -import re - -from app.utils.nickname_gen import generate_nickname - -class UserRole(str, Enum): - ANONYMOUS = "ANONYMOUS" - AUTHENTICATED = "AUTHENTICATED" - MANAGER = "MANAGER" - ADMIN = "ADMIN" - -def validate_url(url: Optional[str]) -> Optional[str]: - if url is None: - return url - url_regex = r'^https?:\/\/[^\s/$.?#].[^\s]*$' - if not re.match(url_regex, url): - raise ValueError('Invalid URL format') - return url - -class UserBase(BaseModel): - email: EmailStr = Field(..., example="john.doe@example.com") - nickname: Optional[str] = Field(None, min_length=3, pattern=r'^[\w-]+$', example=generate_nickname()) - first_name: Optional[str] = Field(None, example="John") - last_name: Optional[str] = Field(None, example="Doe") - bio: Optional[str] = Field(None, example="Experienced software developer specializing in web applications.") - profile_picture_url: Optional[str] = Field(None, example="https://example.com/profiles/john.jpg") - linkedin_profile_url: Optional[str] =Field(None, example="https://linkedin.com/in/johndoe") - github_profile_url: Optional[str] = Field(None, example="https://github.com/johndoe") - - _validate_urls = validator('profile_picture_url', 'linkedin_profile_url', 'github_profile_url', pre=True, allow_reuse=True)(validate_url) - - class Config: - from_attributes = True - -class UserCreate(UserBase): - email: EmailStr = Field(..., example="john.doe@example.com") - password: str = Field(..., example="Secure*1234") - -class UserUpdate(UserBase): - email: Optional[EmailStr] = Field(None, example="john.doe@example.com") - nickname: Optional[str] = Field(None, min_length=3, pattern=r'^[\w-]+$', example="john_doe123") - first_name: Optional[str] = Field(None, example="John") - last_name: Optional[str] = Field(None, example="Doe") - bio: Optional[str] = Field(None, example="Experienced software developer specializing in web applications.") - profile_picture_url: Optional[str] = Field(None, example="https://example.com/profiles/john.jpg") - linkedin_profile_url: Optional[str] =Field(None, example="https://linkedin.com/in/johndoe") - github_profile_url: Optional[str] = Field(None, example="https://github.com/johndoe") - - @root_validator(pre=True) - def check_at_least_one_value(cls, values): - if not any(values.values()): - raise ValueError("At least one field must be provided for update") - return values - -class UserResponse(UserBase): - id: uuid.UUID = Field(..., example=uuid.uuid4()) - role: UserRole = Field(default=UserRole.AUTHENTICATED, example="AUTHENTICATED") - email: EmailStr = Field(..., example="john.doe@example.com") - nickname: Optional[str] = Field(None, min_length=3, pattern=r'^[\w-]+$', example=generate_nickname()) - role: UserRole = Field(default=UserRole.AUTHENTICATED, example="AUTHENTICATED") - is_professional: Optional[bool] = Field(default=False, example=True) - -class LoginRequest(BaseModel): - email: str = Field(..., example="john.doe@example.com") - password: str = Field(..., example="Secure*1234") - -class ErrorResponse(BaseModel): - error: str = Field(..., example="Not Found") - details: Optional[str] = Field(None, example="The requested resource was not found.") - -class UserListResponse(BaseModel): - items: List[UserResponse] = Field(..., example=[{ - "id": uuid.uuid4(), "nickname": generate_nickname(), "email": "john.doe@example.com", - "first_name": "John", "bio": "Experienced developer", "role": "AUTHENTICATED", - "last_name": "Doe", "bio": "Experienced developer", "role": "AUTHENTICATED", - "profile_picture_url": "https://example.com/profiles/john.jpg", - "linkedin_profile_url": "https://linkedin.com/in/johndoe", - "github_profile_url": "https://github.com/johndoe" - }]) - total: int = Field(..., example=100) - page: int = Field(..., example=1) - size: int = Field(..., example=10) +from builtins import ValueError, any, bool, str +from pydantic import BaseModel, EmailStr, Field, validator, root_validator +from typing import Optional, List +from datetime import datetime +from enum import Enum +import uuid +import re + +from app.utils.nickname_gen import generate_nickname + +class UserRole(str, Enum): + ANONYMOUS = "ANONYMOUS" + AUTHENTICATED = "AUTHENTICATED" + MANAGER = "MANAGER" + ADMIN = "ADMIN" + +def validate_url(url: Optional[str]) -> Optional[str]: + if url is None: + return url + url_regex = r'^https?:\/\/[^\s/$.?#].[^\s]*$' + if not re.match(url_regex, url): + raise ValueError('Invalid URL format') + return url + +class UserBase(BaseModel): + email: EmailStr = Field(..., example="john.doe@example.com") + nickname: Optional[str] = Field(None, min_length=3, pattern=r'^[\w-]+$', example=generate_nickname()) + first_name: Optional[str] = Field(None, example="John") + last_name: Optional[str] = Field(None, example="Doe") + bio: Optional[str] = Field(None, example="Experienced software developer specializing in web applications.") + profile_picture_url: Optional[str] = Field(None, example="https://example.com/profiles/john.jpg") + linkedin_profile_url: Optional[str] =Field(None, example="https://linkedin.com/in/johndoe") + github_profile_url: Optional[str] = Field(None, example="https://github.com/johndoe") + + _validate_urls = validator('profile_picture_url', 'linkedin_profile_url', 'github_profile_url', pre=True, allow_reuse=True)(validate_url) + + class Config: + from_attributes = True + +class UserCreate(UserBase): + email: EmailStr = Field(..., example="john.doe@example.com") + password: str = Field(..., example="Secure*1234") + +class UserUpdate(UserBase): + email: Optional[EmailStr] = Field(None, example="john.doe@example.com") + nickname: Optional[str] = Field(None, min_length=3, pattern=r'^[\w-]+$', example="john_doe123") + first_name: Optional[str] = Field(None, example="John") + last_name: Optional[str] = Field(None, example="Doe") + bio: Optional[str] = Field(None, example="Experienced software developer specializing in web applications.") + profile_picture_url: Optional[str] = Field(None, example="https://example.com/profiles/john.jpg") + linkedin_profile_url: Optional[str] =Field(None, example="https://linkedin.com/in/johndoe") + github_profile_url: Optional[str] = Field(None, example="https://github.com/johndoe") + + @root_validator(pre=True) + def check_at_least_one_value(cls, values): + if not any(values.values()): + raise ValueError("At least one field must be provided for update") + return values + +class UserResponse(UserBase): + id: uuid.UUID = Field(..., example=uuid.uuid4()) + role: UserRole = Field(default=UserRole.AUTHENTICATED, example="AUTHENTICATED") + email: EmailStr = Field(..., example="john.doe@example.com") + nickname: Optional[str] = Field(None, min_length=3, pattern=r'^[\w-]+$', example=generate_nickname()) + role: UserRole = Field(default=UserRole.AUTHENTICATED, example="AUTHENTICATED") + is_professional: Optional[bool] = Field(default=False, example=True) + +class LoginRequest(BaseModel): + email: str = Field(..., example="john.doe@example.com") + password: str = Field(..., example="Secure*1234") + +class ErrorResponse(BaseModel): + error: str = Field(..., example="Not Found") + details: Optional[str] = Field(None, example="The requested resource was not found.") + +class UserListResponse(BaseModel): + items: List[UserResponse] = Field(..., example=[{ + "id": uuid.uuid4(), "nickname": generate_nickname(), "email": "john.doe@example.com", + "first_name": "John", "bio": "Experienced developer", "role": "AUTHENTICATED", + "last_name": "Doe", "bio": "Experienced developer", "role": "AUTHENTICATED", + "profile_picture_url": "https://example.com/profiles/john.jpg", + "linkedin_profile_url": "https://linkedin.com/in/johndoe", + "github_profile_url": "https://github.com/johndoe" + }]) + total: int = Field(..., example=100) + page: int = Field(..., example=1) + size: int = Field(..., example=10) diff --git a/app/services/email_service.py b/app/services/email_service.py index 620b05cee..0301f31c9 100644 --- a/app/services/email_service.py +++ b/app/services/email_service.py @@ -1,45 +1,45 @@ -# email_service.py -from builtins import ValueError, dict, str -from settings.config import settings -from app.utils.smtp_connection import SMTPClient -from app.utils.template_manager import TemplateManager -from app.models.user_model import User - -class EmailService: - def __init__(self, template_manager: TemplateManager): - if not settings.smtp_server or not settings.smtp_port or not settings.smtp_username or not settings.smtp_password: - print("SMTP settings not configured. Email service will not work.") - self.smtp_client = None - else: - self.smtp_client = SMTPClient( - server=settings.smtp_server, - port=settings.smtp_port, - username=settings.smtp_username, - password=settings.smtp_password - ) - self.template_manager = template_manager - - async def send_user_email(self, user_data: dict, email_type: str): - if not self.smtp_client: - return - subject_map = { - 'email_verification': "Verify Your Account", - 'password_reset': "Password Reset Instructions", - 'account_locked': "Account Locked Notification" - } - - if email_type not in subject_map: - raise ValueError("Invalid email type") - - html_content = self.template_manager.render_template(email_type, **user_data) - self.smtp_client.send_email(subject_map[email_type], html_content, user_data['email']) - - async def send_verification_email(self, user: User): - if not self.smtp_client: - return - verification_url = f"{settings.server_base_url}verify-email/{user.id}/{user.verification_token}" - await self.send_user_email({ - "name": user.first_name, - "verification_url": verification_url, - "email": user.email +# email_service.py +from builtins import ValueError, dict, str +from settings.config import settings +from app.utils.smtp_connection import SMTPClient +from app.utils.template_manager import TemplateManager +from app.models.user_model import User + +class EmailService: + def __init__(self, template_manager: TemplateManager): + if not settings.smtp_server or not settings.smtp_port or not settings.smtp_username or not settings.smtp_password: + print("SMTP settings not configured. Email service will not work.") + self.smtp_client = None + else: + self.smtp_client = SMTPClient( + server=settings.smtp_server, + port=settings.smtp_port, + username=settings.smtp_username, + password=settings.smtp_password + ) + self.template_manager = template_manager + + async def send_user_email(self, user_data: dict, email_type: str): + if not self.smtp_client: + return + subject_map = { + 'email_verification': "Verify Your Account", + 'password_reset': "Password Reset Instructions", + 'account_locked': "Account Locked Notification" + } + + if email_type not in subject_map: + raise ValueError("Invalid email type") + + html_content = self.template_manager.render_template(email_type, **user_data) + self.smtp_client.send_email(subject_map[email_type], html_content, user_data['email']) + + async def send_verification_email(self, user: User): + if not self.smtp_client: + return + verification_url = f"{settings.server_base_url}verify-email/{user.id}/{user.verification_token}" + await self.send_user_email({ + "name": user.first_name, + "verification_url": verification_url, + "email": user.email }, 'email_verification') \ No newline at end of file diff --git a/app/services/jwt_service.py b/app/services/jwt_service.py index c19b8fcc3..b0a898453 100644 --- a/app/services/jwt_service.py +++ b/app/services/jwt_service.py @@ -1,22 +1,22 @@ -# app/services/jwt_service.py -from builtins import dict, str -import jwt -from datetime import datetime, timedelta -from settings.config import settings - -def create_access_token(*, data: dict, expires_delta: timedelta = None): - to_encode = data.copy() - # Convert role to uppercase before encoding the JWT - if 'role' in to_encode: - to_encode['role'] = to_encode['role'].upper() - expire = datetime.utcnow() + (expires_delta if expires_delta else timedelta(minutes=settings.access_token_expire_minutes)) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, settings.jwt_secret_key, algorithm=settings.jwt_algorithm) - return encoded_jwt - -def decode_token(token: str): - try: - decoded = jwt.decode(token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm]) - return decoded - except jwt.PyJWTError: - return None +# app/services/jwt_service.py +from builtins import dict, str +import jwt +from datetime import datetime, timedelta +from settings.config import settings + +def create_access_token(*, data: dict, expires_delta: timedelta = None): + to_encode = data.copy() + # Convert role to uppercase before encoding the JWT + if 'role' in to_encode: + to_encode['role'] = to_encode['role'].upper() + expire = datetime.utcnow() + (expires_delta if expires_delta else timedelta(minutes=settings.access_token_expire_minutes)) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, settings.jwt_secret_key, algorithm=settings.jwt_algorithm) + return encoded_jwt + +def decode_token(token: str): + try: + decoded = jwt.decode(token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm]) + return decoded + except jwt.PyJWTError: + return None diff --git a/app/services/user_service.py b/app/services/user_service.py index e22842505..ce0ec3aff 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -1,194 +1,194 @@ -from builtins import Exception, bool, classmethod, int, str -from datetime import datetime, timezone -import secrets -from typing import Optional, Dict, List -from pydantic import ValidationError -from sqlalchemy import func, null, update, select -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.ext.asyncio import AsyncSession -from app.dependencies import get_email_service, get_settings -from app.models.user_model import User -from app.schemas.user_schemas import UserCreate, UserUpdate -from app.utils.nickname_gen import generate_nickname -from app.utils.security import generate_verification_token, hash_password, verify_password -from uuid import UUID -from app.services.email_service import EmailService -from app.models.user_model import UserRole -import logging - -settings = get_settings() -logger = logging.getLogger(__name__) - -class UserService: - @classmethod - async def _execute_query(cls, session: AsyncSession, query): - try: - result = await session.execute(query) - await session.commit() - return result - except SQLAlchemyError as e: - logger.error(f"Database error: {e}") - await session.rollback() - return None - - @classmethod - async def _fetch_user(cls, session: AsyncSession, **filters) -> Optional[User]: - query = select(User).filter_by(**filters) - result = await cls._execute_query(session, query) - return result.scalars().first() if result else None - - @classmethod - async def get_by_id(cls, session: AsyncSession, user_id: UUID) -> Optional[User]: - return await cls._fetch_user(session, id=user_id) - - @classmethod - async def get_by_nickname(cls, session: AsyncSession, nickname: str) -> Optional[User]: - return await cls._fetch_user(session, nickname=nickname) - - @classmethod - async def get_by_email(cls, session: AsyncSession, email: str) -> Optional[User]: - return await cls._fetch_user(session, email=email) - - @classmethod - async def create(cls, session: AsyncSession, user_data: Dict[str, str], email_service: EmailService) -> Optional[User]: - try: - validated_data = UserCreate(**user_data).model_dump() - existing_user = await cls.get_by_email(session, validated_data['email']) - if existing_user: - logger.error("User with given email already exists.") - return None - validated_data['hashed_password'] = hash_password(validated_data.pop('password')) - new_user = User(**validated_data) - new_user.verification_token = generate_verification_token() - new_nickname = generate_nickname() - while await cls.get_by_nickname(session, new_nickname): - new_nickname = generate_nickname() - new_user.nickname = new_nickname - session.add(new_user) - await session.commit() - await email_service.send_verification_email(new_user) - - return new_user - except ValidationError as e: - logger.error(f"Validation error during user creation: {e}") - return None - - @classmethod - async def update(cls, session: AsyncSession, user_id: UUID, update_data: Dict[str, str]) -> Optional[User]: - try: - # validated_data = UserUpdate(**update_data).dict(exclude_unset=True) - validated_data = UserUpdate(**update_data).dict(exclude_unset=True) - - if 'password' in validated_data: - validated_data['hashed_password'] = hash_password(validated_data.pop('password')) - query = update(User).where(User.id == user_id).values(**validated_data).execution_options(synchronize_session="fetch") - await cls._execute_query(session, query) - updated_user = await cls.get_by_id(session, user_id) - if updated_user: - session.refresh(updated_user) # Explicitly refresh the updated user object - logger.info(f"User {user_id} updated successfully.") - return updated_user - else: - logger.error(f"User {user_id} not found after update attempt.") - return None - except Exception as e: # Broad exception handling for debugging - logger.error(f"Error during user update: {e}") - return None - - @classmethod - async def delete(cls, session: AsyncSession, user_id: UUID) -> bool: - user = await cls.get_by_id(session, user_id) - if not user: - logger.info(f"User with ID {user_id} not found.") - return False - await session.delete(user) - await session.commit() - return True - - @classmethod - async def list_users(cls, session: AsyncSession, skip: int = 0, limit: int = 10) -> List[User]: - query = select(User).offset(skip).limit(limit) - result = await cls._execute_query(session, query) - return result.scalars().all() if result else [] - - @classmethod - async def register_user(cls, session: AsyncSession, user_data: Dict[str, str], get_email_service) -> Optional[User]: - return await cls.create(session, user_data, get_email_service) - - - @classmethod - async def login_user(cls, session: AsyncSession, email: str, password: str) -> Optional[User]: - user = await cls.get_by_email(session, email) - if user: - if user.email_verified is False: - return None - if user.is_locked: - return None - if verify_password(password, user.hashed_password): - user.failed_login_attempts = 0 - user.last_login_at = datetime.now(timezone.utc) - session.add(user) - await session.commit() - return user - else: - user.failed_login_attempts += 1 - if user.failed_login_attempts >= settings.max_login_attempts: - user.is_locked = True - session.add(user) - await session.commit() - return None - - @classmethod - async def is_account_locked(cls, session: AsyncSession, email: str) -> bool: - user = await cls.get_by_email(session, email) - return user.is_locked if user else False - - - @classmethod - async def reset_password(cls, session: AsyncSession, user_id: UUID, new_password: str) -> bool: - hashed_password = hash_password(new_password) - user = await cls.get_by_id(session, user_id) - if user: - user.hashed_password = hashed_password - user.failed_login_attempts = 0 # Resetting failed login attempts - user.is_locked = False # Unlocking the user account, if locked - session.add(user) - await session.commit() - return True - return False - - @classmethod - async def verify_email_with_token(cls, session: AsyncSession, user_id: UUID, token: str) -> bool: - user = await cls.get_by_id(session, user_id) - if user and user.verification_token == token: - user.email_verified = True - user.verification_token = None # Clear the token once used - user.role = UserRole.AUTHENTICATED - session.add(user) - await session.commit() - return True - return False - - @classmethod - async def count(cls, session: AsyncSession) -> int: - """ - Count the number of users in the database. - - :param session: The AsyncSession instance for database access. - :return: The count of users. - """ - query = select(func.count()).select_from(User) - result = await session.execute(query) - count = result.scalar() - return count - - @classmethod - async def unlock_user_account(cls, session: AsyncSession, user_id: UUID) -> bool: - user = await cls.get_by_id(session, user_id) - if user and user.is_locked: - user.is_locked = False - user.failed_login_attempts = 0 # Optionally reset failed login attempts - session.add(user) - await session.commit() - return True - return False +from builtins import Exception, bool, classmethod, int, str +from datetime import datetime, timezone +import secrets +from typing import Optional, Dict, List +from pydantic import ValidationError +from sqlalchemy import func, null, update, select +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.ext.asyncio import AsyncSession +from app.dependencies import get_email_service, get_settings +from app.models.user_model import User +from app.schemas.user_schemas import UserCreate, UserUpdate +from app.utils.nickname_gen import generate_nickname +from app.utils.security import generate_verification_token, hash_password, verify_password +from uuid import UUID +from app.services.email_service import EmailService +from app.models.user_model import UserRole +import logging + +settings = get_settings() +logger = logging.getLogger(__name__) + +class UserService: + @classmethod + async def _execute_query(cls, session: AsyncSession, query): + try: + result = await session.execute(query) + await session.commit() + return result + except SQLAlchemyError as e: + logger.error(f"Database error: {e}") + await session.rollback() + return None + + @classmethod + async def _fetch_user(cls, session: AsyncSession, **filters) -> Optional[User]: + query = select(User).filter_by(**filters) + result = await cls._execute_query(session, query) + return result.scalars().first() if result else None + + @classmethod + async def get_by_id(cls, session: AsyncSession, user_id: UUID) -> Optional[User]: + return await cls._fetch_user(session, id=user_id) + + @classmethod + async def get_by_nickname(cls, session: AsyncSession, nickname: str) -> Optional[User]: + return await cls._fetch_user(session, nickname=nickname) + + @classmethod + async def get_by_email(cls, session: AsyncSession, email: str) -> Optional[User]: + return await cls._fetch_user(session, email=email) + + @classmethod + async def create(cls, session: AsyncSession, user_data: Dict[str, str], email_service: EmailService) -> Optional[User]: + try: + validated_data = UserCreate(**user_data).model_dump() + existing_user = await cls.get_by_email(session, validated_data['email']) + if existing_user: + logger.error("User with given email already exists.") + return None + validated_data['hashed_password'] = hash_password(validated_data.pop('password')) + new_user = User(**validated_data) + new_user.verification_token = generate_verification_token() + new_nickname = generate_nickname() + while await cls.get_by_nickname(session, new_nickname): + new_nickname = generate_nickname() + new_user.nickname = new_nickname + session.add(new_user) + await session.commit() + await email_service.send_verification_email(new_user) + + return new_user + except ValidationError as e: + logger.error(f"Validation error during user creation: {e}") + return None + + @classmethod + async def update(cls, session: AsyncSession, user_id: UUID, update_data: Dict[str, str]) -> Optional[User]: + try: + # validated_data = UserUpdate(**update_data).dict(exclude_unset=True) + validated_data = UserUpdate(**update_data).dict(exclude_unset=True) + + if 'password' in validated_data: + validated_data['hashed_password'] = hash_password(validated_data.pop('password')) + query = update(User).where(User.id == user_id).values(**validated_data).execution_options(synchronize_session="fetch") + await cls._execute_query(session, query) + updated_user = await cls.get_by_id(session, user_id) + if updated_user: + session.refresh(updated_user) # Explicitly refresh the updated user object + logger.info(f"User {user_id} updated successfully.") + return updated_user + else: + logger.error(f"User {user_id} not found after update attempt.") + return None + except Exception as e: # Broad exception handling for debugging + logger.error(f"Error during user update: {e}") + return None + + @classmethod + async def delete(cls, session: AsyncSession, user_id: UUID) -> bool: + user = await cls.get_by_id(session, user_id) + if not user: + logger.info(f"User with ID {user_id} not found.") + return False + await session.delete(user) + await session.commit() + return True + + @classmethod + async def list_users(cls, session: AsyncSession, skip: int = 0, limit: int = 10) -> List[User]: + query = select(User).offset(skip).limit(limit) + result = await cls._execute_query(session, query) + return result.scalars().all() if result else [] + + @classmethod + async def register_user(cls, session: AsyncSession, user_data: Dict[str, str], get_email_service) -> Optional[User]: + return await cls.create(session, user_data, get_email_service) + + + @classmethod + async def login_user(cls, session: AsyncSession, email: str, password: str) -> Optional[User]: + user = await cls.get_by_email(session, email) + if user: + if user.email_verified is False: + return None + if user.is_locked: + return None + if verify_password(password, user.hashed_password): + user.failed_login_attempts = 0 + user.last_login_at = datetime.now(timezone.utc) + session.add(user) + await session.commit() + return user + else: + user.failed_login_attempts += 1 + if user.failed_login_attempts >= settings.max_login_attempts: + user.is_locked = True + session.add(user) + await session.commit() + return None + + @classmethod + async def is_account_locked(cls, session: AsyncSession, email: str) -> bool: + user = await cls.get_by_email(session, email) + return user.is_locked if user else False + + + @classmethod + async def reset_password(cls, session: AsyncSession, user_id: UUID, new_password: str) -> bool: + hashed_password = hash_password(new_password) + user = await cls.get_by_id(session, user_id) + if user: + user.hashed_password = hashed_password + user.failed_login_attempts = 0 # Resetting failed login attempts + user.is_locked = False # Unlocking the user account, if locked + session.add(user) + await session.commit() + return True + return False + + @classmethod + async def verify_email_with_token(cls, session: AsyncSession, user_id: UUID, token: str) -> bool: + user = await cls.get_by_id(session, user_id) + if user and user.verification_token == token: + user.email_verified = True + user.verification_token = None # Clear the token once used + user.role = UserRole.AUTHENTICATED + session.add(user) + await session.commit() + return True + return False + + @classmethod + async def count(cls, session: AsyncSession) -> int: + """ + Count the number of users in the database. + + :param session: The AsyncSession instance for database access. + :return: The count of users. + """ + query = select(func.count()).select_from(User) + result = await session.execute(query) + count = result.scalar() + return count + + @classmethod + async def unlock_user_account(cls, session: AsyncSession, user_id: UUID) -> bool: + user = await cls.get_by_id(session, user_id) + if user and user.is_locked: + user.is_locked = False + user.failed_login_attempts = 0 # Optionally reset failed login attempts + session.add(user) + await session.commit() + return True + return False diff --git a/app/utils/api_description.py b/app/utils/api_description.py index 2cd892456..cb06c3163 100644 --- a/app/utils/api_description.py +++ b/app/utils/api_description.py @@ -1,37 +1,37 @@ -def getDescription(): - description = """ -Application Overview: - -This application is a robust user management system designed to facilitate the administration of user credentials and profiles in a secure and efficient manner. It leverages the capabilities of FastAPI to provide a high-performance, scalable API that adheres to the best practices of modern web service development. - -Key Features: - -- User Authentication: Implements OAuth2 with Password Flow to ensure secure access to the API. Users are required to authenticate using a JWT (JSON Web Token) which provides a secure and efficient means of user identification and authorization. - -- CRUD Operations: Offers comprehensive endpoints for creating, reading, updating, and deleting user information. This includes management of user details such as email, passwords, and personal profiles. - -- Role-Based Access Control: Enforces different access levels using a role-based mechanism that restricts certain operations to users with appropriate privileges. Supported roles include Admin, Manager, and regular Users, each with different permissions. - -- Email Integration: Integrates with email services for account verification and notifications, enhancing the registration and password recovery processes. - -- HATEOAS (Hypermedia as the Engine of Application State): Each response from the API includes hypermedia links to guide the client to other relevant endpoints based on the context of the current interaction, promoting discoverability and ease of navigation within the API. - -- Secure Password Handling: Implements best practices for password security, including hashing and salting techniques, to ensure that user credentials are stored securely. - -- Error Handling: Provides clear and informative error responses that help clients properly handle issues such as authentication failures, access violations, and data conflicts. - -Security Features: - -The application incorporates several security measures to protect data and ensure the integrity and confidentiality of user information: - -- Data Encryption: Uses advanced encryption standards to secure sensitive data in transit and at rest. -- Input Validation: Employs rigorous validation checks to prevent SQL injection, XSS, and other common security threats. -- Rate Limiting: Protects against brute-force attacks by limiting the number of requests a user can make to the API within a given timeframe. - -User Experience: - -Designed with a focus on user experience, the API provides detailed documentation, descriptive error messages, and consistent interface patterns that make it intuitive and straightforward for developers to integrate with their applications. - -This API is ideal for businesses and developers looking for a reliable and secure way to manage user authentication and authorization in their applications. It is particularly suited to environments where security and data privacy are paramount. -""" +def getDescription(): + description = """ +Application Overview: + +This application is a robust user management system designed to facilitate the administration of user credentials and profiles in a secure and efficient manner. It leverages the capabilities of FastAPI to provide a high-performance, scalable API that adheres to the best practices of modern web service development. + +Key Features: + +- User Authentication: Implements OAuth2 with Password Flow to ensure secure access to the API. Users are required to authenticate using a JWT (JSON Web Token) which provides a secure and efficient means of user identification and authorization. + +- CRUD Operations: Offers comprehensive endpoints for creating, reading, updating, and deleting user information. This includes management of user details such as email, passwords, and personal profiles. + +- Role-Based Access Control: Enforces different access levels using a role-based mechanism that restricts certain operations to users with appropriate privileges. Supported roles include Admin, Manager, and regular Users, each with different permissions. + +- Email Integration: Integrates with email services for account verification and notifications, enhancing the registration and password recovery processes. + +- HATEOAS (Hypermedia as the Engine of Application State): Each response from the API includes hypermedia links to guide the client to other relevant endpoints based on the context of the current interaction, promoting discoverability and ease of navigation within the API. + +- Secure Password Handling: Implements best practices for password security, including hashing and salting techniques, to ensure that user credentials are stored securely. + +- Error Handling: Provides clear and informative error responses that help clients properly handle issues such as authentication failures, access violations, and data conflicts. + +Security Features: + +The application incorporates several security measures to protect data and ensure the integrity and confidentiality of user information: + +- Data Encryption: Uses advanced encryption standards to secure sensitive data in transit and at rest. +- Input Validation: Employs rigorous validation checks to prevent SQL injection, XSS, and other common security threats. +- Rate Limiting: Protects against brute-force attacks by limiting the number of requests a user can make to the API within a given timeframe. + +User Experience: + +Designed with a focus on user experience, the API provides detailed documentation, descriptive error messages, and consistent interface patterns that make it intuitive and straightforward for developers to integrate with their applications. + +This API is ideal for businesses and developers looking for a reliable and secure way to manage user authentication and authorization in their applications. It is particularly suited to environments where security and data privacy are paramount. +""" return description \ No newline at end of file diff --git a/app/utils/common.py b/app/utils/common.py index 1281b46bc..82c605771 100644 --- a/app/utils/common.py +++ b/app/utils/common.py @@ -1,16 +1,16 @@ -import logging.config -import os -from app.dependencies import get_settings - -settings = get_settings() -def setup_logging(): - """ - Sets up logging for the application using a configuration file. - This ensures standardized logging across the entire application. - """ - # Construct the path to 'logging.conf', assuming it's in the project's root. - logging_config_path = os.path.join(os.path.dirname(__file__), '..', '..', 'logging.conf') - # Normalize the path to handle any '..' correctly. - normalized_path = os.path.normpath(logging_config_path) - # Apply the logging configuration. +import logging.config +import os +from app.dependencies import get_settings + +settings = get_settings() +def setup_logging(): + """ + Sets up logging for the application using a configuration file. + This ensures standardized logging across the entire application. + """ + # Construct the path to 'logging.conf', assuming it's in the project's root. + logging_config_path = os.path.join(os.path.dirname(__file__), '..', '..', 'logging.conf') + # Normalize the path to handle any '..' correctly. + normalized_path = os.path.normpath(logging_config_path) + # Apply the logging configuration. logging.config.fileConfig(normalized_path, disable_existing_loggers=False) \ No newline at end of file diff --git a/app/utils/link_generation.py b/app/utils/link_generation.py index 1f4ae4756..ff545ef43 100644 --- a/app/utils/link_generation.py +++ b/app/utils/link_generation.py @@ -1,48 +1,48 @@ -from builtins import dict, int, max, str -from typing import List, Callable -from urllib.parse import urlencode -from uuid import UUID - -from fastapi import Request -from app.schemas.link_schema import Link -from app.schemas.pagination_schema import PaginationLink - -# Utility function to create a link -def create_link(rel: str, href: str, method: str = "GET", action: str = None) -> Link: - return Link(rel=rel, href=href, method=method, action=action) - -def create_pagination_link(rel: str, base_url: str, params: dict) -> PaginationLink: - # Ensure parameters are added in a specific order - query_string = f"skip={params['skip']}&limit={params['limit']}" - return PaginationLink(rel=rel, href=f"{base_url}?{query_string}") - -def create_user_links(user_id: UUID, request: Request) -> List[Link]: - """ - Generate navigation links for user actions. - """ - actions = [ - ("self", "get_user", "GET", "view"), - ("update", "update_user", "PUT", "update"), - ("delete", "delete_user", "DELETE", "delete") - ] - return [ - create_link(rel, str(request.url_for(action, user_id=str(user_id))), method, action_desc) - for rel, action, method, action_desc in actions - ] - -def generate_pagination_links(request: Request, skip: int, limit: int, total_items: int) -> List[PaginationLink]: - base_url = str(request.url) - total_pages = (total_items + limit - 1) // limit - links = [ - create_pagination_link("self", base_url, {'skip': skip, 'limit': limit}), - create_pagination_link("first", base_url, {'skip': 0, 'limit': limit}), - create_pagination_link("last", base_url, {'skip': max(0, (total_pages - 1) * limit), 'limit': limit}) - ] - - if skip + limit < total_items: - links.append(create_pagination_link("next", base_url, {'skip': skip + limit, 'limit': limit})) - - if skip > 0: - links.append(create_pagination_link("prev", base_url, {'skip': max(skip - limit, 0), 'limit': limit})) - - return links +from builtins import dict, int, max, str +from typing import List, Callable +from urllib.parse import urlencode +from uuid import UUID + +from fastapi import Request +from app.schemas.link_schema import Link +from app.schemas.pagination_schema import PaginationLink + +# Utility function to create a link +def create_link(rel: str, href: str, method: str = "GET", action: str = None) -> Link: + return Link(rel=rel, href=href, method=method, action=action) + +def create_pagination_link(rel: str, base_url: str, params: dict) -> PaginationLink: + # Ensure parameters are added in a specific order + query_string = f"skip={params['skip']}&limit={params['limit']}" + return PaginationLink(rel=rel, href=f"{base_url}?{query_string}") + +def create_user_links(user_id: UUID, request: Request) -> List[Link]: + """ + Generate navigation links for user actions. + """ + actions = [ + ("self", "get_user", "GET", "view"), + ("update", "update_user", "PUT", "update"), + ("delete", "delete_user", "DELETE", "delete") + ] + return [ + create_link(rel, str(request.url_for(action, user_id=str(user_id))), method, action_desc) + for rel, action, method, action_desc in actions + ] + +def generate_pagination_links(request: Request, skip: int, limit: int, total_items: int) -> List[PaginationLink]: + base_url = str(request.url) + total_pages = (total_items + limit - 1) // limit + links = [ + create_pagination_link("self", base_url, {'skip': skip, 'limit': limit}), + create_pagination_link("first", base_url, {'skip': 0, 'limit': limit}), + create_pagination_link("last", base_url, {'skip': max(0, (total_pages - 1) * limit), 'limit': limit}) + ] + + if skip + limit < total_items: + links.append(create_pagination_link("next", base_url, {'skip': skip + limit, 'limit': limit})) + + if skip > 0: + links.append(create_pagination_link("prev", base_url, {'skip': max(skip - limit, 0), 'limit': limit})) + + return links diff --git a/app/utils/nickname_gen.py b/app/utils/nickname_gen.py index 3491ff96c..a452be371 100644 --- a/app/utils/nickname_gen.py +++ b/app/utils/nickname_gen.py @@ -1,10 +1,10 @@ -from builtins import str -import random - - -def generate_nickname() -> str: - """Generate a URL-safe nickname using adjectives and animal names.""" - adjectives = ["clever", "jolly", "brave", "sly", "gentle"] - animals = ["panda", "fox", "raccoon", "koala", "lion"] - number = random.randint(0, 999) - return f"{random.choice(adjectives)}_{random.choice(animals)}_{number}" +from builtins import str +import random + + +def generate_nickname() -> str: + """Generate a URL-safe nickname using adjectives and animal names.""" + adjectives = ["clever", "jolly", "brave", "sly", "gentle"] + animals = ["panda", "fox", "raccoon", "koala", "lion"] + number = random.randint(0, 999) + return f"{random.choice(adjectives)}_{random.choice(animals)}_{number}" diff --git a/app/utils/security.py b/app/utils/security.py index eceeb8e8a..fab17b098 100644 --- a/app/utils/security.py +++ b/app/utils/security.py @@ -1,53 +1,53 @@ -# app/security.py -from builtins import Exception, ValueError, bool, int, str -import secrets -import bcrypt -from logging import getLogger - -# Set up logging -logger = getLogger(__name__) - -def hash_password(password: str, rounds: int = 12) -> str: - """ - Hashes a password using bcrypt with a specified cost factor. - - Args: - password (str): The plain text password to hash. - rounds (int): The cost factor that determines the computational cost of hashing. - - Returns: - str: The hashed password. - - Raises: - ValueError: If hashing the password fails. - """ - try: - salt = bcrypt.gensalt(rounds=rounds) - hashed_password = bcrypt.hashpw(password.encode('utf-8'), salt) - return hashed_password.decode('utf-8') - except Exception as e: - logger.error("Failed to hash password: %s", e) - raise ValueError("Failed to hash password") from e - -def verify_password(plain_password: str, hashed_password: str) -> bool: - """ - Verifies a plain text password against a hashed password. - - Args: - plain_password (str): The plain text password to verify. - hashed_password (str): The bcrypt hashed password. - - Returns: - bool: True if the password is correct, False otherwise. - - Raises: - ValueError: If the hashed password format is incorrect or the function fails to verify. - """ - try: - return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8')) - except Exception as e: - logger.error("Error verifying password: %s", e) - raise ValueError("Authentication process encountered an unexpected error") from e - -def generate_verification_token(): +# app/security.py +from builtins import Exception, ValueError, bool, int, str +import secrets +import bcrypt +from logging import getLogger + +# Set up logging +logger = getLogger(__name__) + +def hash_password(password: str, rounds: int = 12) -> str: + """ + Hashes a password using bcrypt with a specified cost factor. + + Args: + password (str): The plain text password to hash. + rounds (int): The cost factor that determines the computational cost of hashing. + + Returns: + str: The hashed password. + + Raises: + ValueError: If hashing the password fails. + """ + try: + salt = bcrypt.gensalt(rounds=rounds) + hashed_password = bcrypt.hashpw(password.encode('utf-8'), salt) + return hashed_password.decode('utf-8') + except Exception as e: + logger.error("Failed to hash password: %s", e) + raise ValueError("Failed to hash password") from e + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """ + Verifies a plain text password against a hashed password. + + Args: + plain_password (str): The plain text password to verify. + hashed_password (str): The bcrypt hashed password. + + Returns: + bool: True if the password is correct, False otherwise. + + Raises: + ValueError: If the hashed password format is incorrect or the function fails to verify. + """ + try: + return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8')) + except Exception as e: + logger.error("Error verifying password: %s", e) + raise ValueError("Authentication process encountered an unexpected error") from e + +def generate_verification_token(): return secrets.token_urlsafe(16) # Generates a secure 16-byte URL-safe token \ No newline at end of file diff --git a/app/utils/smtp_connection.py b/app/utils/smtp_connection.py index b13c04a2a..e3b38d1ca 100644 --- a/app/utils/smtp_connection.py +++ b/app/utils/smtp_connection.py @@ -1,31 +1,31 @@ -# smtp_client.py -from builtins import Exception, int, str -import smtplib -from email.mime.text import MIMEText -from email.mime.multipart import MIMEMultipart -from settings.config import settings -import logging - -class SMTPClient: - def __init__(self, server: str, port: int, username: str, password: str): - self.server = server - self.port = port - self.username = username - self.password = password - - def send_email(self, subject: str, html_content: str, recipient: str): - try: - message = MIMEMultipart('alternative') - message['Subject'] = subject - message['From'] = self.username - message['To'] = recipient - message.attach(MIMEText(html_content, 'html')) - - with smtplib.SMTP(self.server, self.port) as server: - server.starttls() # Use TLS - server.login(self.username, self.password) - server.sendmail(self.username, recipient, message.as_string()) - logging.info(f"Email sent to {recipient}") - except Exception as e: - logging.error(f"Failed to send email: {str(e)}") - raise +# smtp_client.py +from builtins import Exception, int, str +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from settings.config import settings +import logging + +class SMTPClient: + def __init__(self, server: str, port: int, username: str, password: str): + self.server = server + self.port = port + self.username = username + self.password = password + + def send_email(self, subject: str, html_content: str, recipient: str): + try: + message = MIMEMultipart('alternative') + message['Subject'] = subject + message['From'] = self.username + message['To'] = recipient + message.attach(MIMEText(html_content, 'html')) + + with smtplib.SMTP(self.server, self.port) as server: + server.starttls() # Use TLS + server.login(self.username, self.password) + server.sendmail(self.username, recipient, message.as_string()) + logging.info(f"Email sent to {recipient}") + except Exception as e: + logging.error(f"Failed to send email: {str(e)}") + raise diff --git a/app/utils/template_manager.py b/app/utils/template_manager.py index f57d239fc..80e36f92d 100644 --- a/app/utils/template_manager.py +++ b/app/utils/template_manager.py @@ -1,46 +1,46 @@ -import markdown2 -from pathlib import Path - -class TemplateManager: - def __init__(self): - # Dynamically determine the root path of the project - self.root_dir = Path(__file__).resolve().parent.parent.parent # Adjust this depending on the structure - self.templates_dir = self.root_dir / 'email_templates' - - def _read_template(self, filename: str) -> str: - """Private method to read template content.""" - template_path = self.templates_dir / filename - with open(template_path, 'r', encoding='utf-8') as file: - return file.read() - - def _apply_email_styles(self, html: str) -> str: - """Apply advanced CSS styles inline for email compatibility with excellent typography.""" - styles = { - 'body': 'font-family: Arial, sans-serif; font-size: 16px; color: #333333; background-color: #ffffff; line-height: 1.5;', - 'h1': 'font-size: 24px; color: #333333; font-weight: bold; margin-top: 20px; margin-bottom: 10px;', - 'p': 'font-size: 16px; color: #666666; margin: 10px 0; line-height: 1.6;', - 'a': 'color: #0056b3; text-decoration: none; font-weight: bold;', - 'footer': 'font-size: 12px; color: #777777; padding: 20px 0;', - 'ul': 'list-style-type: none; padding: 0;', - 'li': 'margin-bottom: 10px;' - } - # Wrap entire HTML content in
with body style - styled_html = f'
{html}
' - # Apply styles to each HTML element - for tag, style in styles.items(): - if tag != 'body': # Skip the body style since it's already applied to the
- styled_html = styled_html.replace(f'<{tag}>', f'<{tag} style="{style}">') - return styled_html - - def render_template(self, template_name: str, **context) -> str: - """Render a markdown template with given context, applying advanced email styles.""" - header = self._read_template('header.md') - footer = self._read_template('footer.md') - - # Read main template and format it with provided context - main_template = self._read_template(f'{template_name}.md') - main_content = main_template.format(**context) - - full_markdown = f"{header}\n{main_content}\n{footer}" - html_content = markdown2.markdown(full_markdown) - return self._apply_email_styles(html_content) +import markdown2 +from pathlib import Path + +class TemplateManager: + def __init__(self): + # Dynamically determine the root path of the project + self.root_dir = Path(__file__).resolve().parent.parent.parent # Adjust this depending on the structure + self.templates_dir = self.root_dir / 'email_templates' + + def _read_template(self, filename: str) -> str: + """Private method to read template content.""" + template_path = self.templates_dir / filename + with open(template_path, 'r', encoding='utf-8') as file: + return file.read() + + def _apply_email_styles(self, html: str) -> str: + """Apply advanced CSS styles inline for email compatibility with excellent typography.""" + styles = { + 'body': 'font-family: Arial, sans-serif; font-size: 16px; color: #333333; background-color: #ffffff; line-height: 1.5;', + 'h1': 'font-size: 24px; color: #333333; font-weight: bold; margin-top: 20px; margin-bottom: 10px;', + 'p': 'font-size: 16px; color: #666666; margin: 10px 0; line-height: 1.6;', + 'a': 'color: #0056b3; text-decoration: none; font-weight: bold;', + 'footer': 'font-size: 12px; color: #777777; padding: 20px 0;', + 'ul': 'list-style-type: none; padding: 0;', + 'li': 'margin-bottom: 10px;' + } + # Wrap entire HTML content in
with body style + styled_html = f'
{html}
' + # Apply styles to each HTML element + for tag, style in styles.items(): + if tag != 'body': # Skip the body style since it's already applied to the
+ styled_html = styled_html.replace(f'<{tag}>', f'<{tag} style="{style}">') + return styled_html + + def render_template(self, template_name: str, **context) -> str: + """Render a markdown template with given context, applying advanced email styles.""" + header = self._read_template('header.md') + footer = self._read_template('footer.md') + + # Read main template and format it with provided context + main_template = self._read_template(f'{template_name}.md') + main_content = main_template.format(**context) + + full_markdown = f"{header}\n{main_content}\n{footer}" + html_content = markdown2.markdown(full_markdown) + return self._apply_email_styles(html_content) diff --git a/docker-compose.yml b/docker-compose.yml index bb4392056..8c69de764 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,60 +1,60 @@ -version: '3.8' - -services: - postgres: - image: postgres:16.2 - environment: - POSTGRES_DB: myappdb - POSTGRES_USER: user - POSTGRES_PASSWORD: password - healthcheck: - test: ["CMD-SHELL", "pg_isready -U user -d myappdb"] - interval: 10s - timeout: 5s - retries: 5 - networks: - - app-network - - pgadmin: - image: dpage/pgadmin4 - environment: - PGADMIN_DEFAULT_EMAIL: admin@example.com - PGADMIN_DEFAULT_PASSWORD: adminpassword - PGADMIN_LISTEN_PORT: 80 - depends_on: - - postgres - ports: - - "5050:80" # Expose PgAdmin on port 5050 of the host - volumes: - - pgadmin-data:/var/lib/pgadmin - networks: - - app-network - - fastapi: - build: . - volumes: - - ./:/myapp/ - depends_on: - postgres: - condition: service_healthy - networks: - - app-network - command: ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload"] - - nginx: - image: nginx:latest - ports: - - "80:80" - volumes: - - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf - depends_on: - - fastapi - networks: - - app-network - -volumes: - postgres-data: - pgadmin-data: - -networks: +# version: '3.8' + +services: + postgres: + image: postgres:16.2 + environment: + POSTGRES_DB: myappdb + POSTGRES_USER: user + POSTGRES_PASSWORD: password + healthcheck: + test: ["CMD-SHELL", "pg_isready -U user -d myappdb"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - app-network + + pgadmin: + image: dpage/pgadmin4 + environment: + PGADMIN_DEFAULT_EMAIL: admin@example.com + PGADMIN_DEFAULT_PASSWORD: adminpassword + PGADMIN_LISTEN_PORT: 80 + depends_on: + - postgres + ports: + - "5050:80" # Expose PgAdmin on port 5050 of the host + volumes: + - pgadmin-data:/var/lib/pgadmin + networks: + - app-network + + fastapi: + build: . + volumes: + - ./:/myapp/ + depends_on: + postgres: + condition: service_healthy + networks: + - app-network + command: ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload"] + + nginx: + image: nginx:latest + ports: + - "80:80" + volumes: + - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf + depends_on: + - fastapi + networks: + - app-network + +volumes: + postgres-data: + pgadmin-data: + +networks: app-network: \ No newline at end of file diff --git a/docker.md b/docker.md index b594bbb26..fccdb8d73 100644 --- a/docker.md +++ b/docker.md @@ -1,98 +1,98 @@ -# Comprehensive Docker Compose Guide for Students - -## Overview -This guide will walk you through the process of using Docker Compose to manage a multi-container application consisting of a PostgreSQL database, PgAdmin for database management, a FastAPI application, and Nginx as a reverse proxy. - -## Prerequisites -- Ensure Docker and Docker Compose are installed on your computer. -- Basic understanding of Docker, PostgreSQL, FastAPI, and how web applications work. - -## Docker Compose Setup - -### Starting the Services -- To start all services defined in the Docker Compose file, navigate to the directory containing your `docker-compose.yml` file and run: - - **`docker-compose up -d`** - - This command starts the containers in the background. - -### Accessing PgAdmin -- Open your web browser and visit `http://localhost:5050` to access PgAdmin. -- Login with the following credentials: - - **Email**: `admin@example.com` - - **Password**: `adminpassword` -- Configure the PostgreSQL server in PgAdmin: - - **Right-click** on 'Servers' in the left pane and select **'Create' > 'Server'**. - - In the 'General' tab, give your server a name (e.g., `MyAppDB`). - - Switch to the 'Connection' tab and enter: - - **Hostname/address**: `postgres` - - **Port**: `5432` - - **Username**: `user` - - **Password**: `password` - - These credentials correspond to the environment variables set in the `docker-compose.yml` for the PostgreSQL service. - -## Managing Application Data with Docker - -### Running Database Migrations -- Execute database migrations within the FastAPI container: - - **`docker-compose exec fastapi alembic upgrade head`** - - This command runs the Alembic upgrade command to apply migrations to your PostgreSQL database. - -### Running Tests with Pytest -- To run tests inside the FastAPI container, ensuring they interact with the PostgreSQL service: - - **`docker-compose exec fastapi pytest`** - - This command runs all tests defined in your FastAPI application. - -### Specific Test Execution -- To run a specific test file: - - **`docker-compose exec fastapi pytest /myapp/tests/test_specific_file.py`** - -### Running Tests with Coverage -- For executing tests with coverage reports: - - **`docker-compose exec fastapi pytest --cov=myapp`** - - To generate an HTML coverage report: - - **`docker-compose exec fastapi pytest --cov=myapp --cov-report=html`** - -## Resetting the Testing Environment -- If you need to reset your environment, e.g., to clear test data: - - **Stop all services and remove volumes**: - - **`docker-compose down -v`** - - **Restart the services**: - - **`docker-compose up -d`** - -## Docker Basics - -### Building Docker Images -- To build a Docker image for your FastAPI application, ensure you have a Dockerfile in the same directory as your `docker-compose.yml`. Then run: - - **`docker-compose build`** - -## Pushing Images to Docker Hub - -### Creating a Docker Hub Account -- Visit [Docker Hub](https://hub.docker.com/) and sign up for an account. -- Once registered, you can create repositories to store your Docker images. - -### Logging into Docker Hub from the Command Line -- **`docker login`** -- Enter your Docker Hub username and password. - -### Tagging Your Docker Image -- **`docker tag local-image:tagname username/repository:tag`** - - For example: - - **`docker tag myfastapi:latest john/myfastapi:latest`** - -### Pushing the Image -- **`docker push username/repository:tag`** - - For example: - - **`docker push john/myfastapi:latest`** - -## Additional Tips - -### Viewing Logs -- To view logs for troubleshooting or monitoring application behavior: - - **`docker-compose logs -f`** - - The `-f` flag tails the log output. - -### Shutting Down -- To stop and remove all running containers: - - **`docker-compose down`** - -This guide is structured to provide clear, step-by-step instructions on how to interact with the Dockerized environment defined by your Docker Compose setup, ideal for educational purposes and ensuring students are well-equipped to manage their development environment effectively. +# Comprehensive Docker Compose Guide for Students + +## Overview +This guide will walk you through the process of using Docker Compose to manage a multi-container application consisting of a PostgreSQL database, PgAdmin for database management, a FastAPI application, and Nginx as a reverse proxy. + +## Prerequisites +- Ensure Docker and Docker Compose are installed on your computer. +- Basic understanding of Docker, PostgreSQL, FastAPI, and how web applications work. + +## Docker Compose Setup + +### Starting the Services +- To start all services defined in the Docker Compose file, navigate to the directory containing your `docker-compose.yml` file and run: + - **`docker-compose up -d`** + - This command starts the containers in the background. + +### Accessing PgAdmin +- Open your web browser and visit `http://localhost:5050` to access PgAdmin. +- Login with the following credentials: + - **Email**: `admin@example.com` + - **Password**: `adminpassword` +- Configure the PostgreSQL server in PgAdmin: + - **Right-click** on 'Servers' in the left pane and select **'Create' > 'Server'**. + - In the 'General' tab, give your server a name (e.g., `MyAppDB`). + - Switch to the 'Connection' tab and enter: + - **Hostname/address**: `postgres` + - **Port**: `5432` + - **Username**: `user` + - **Password**: `password` + - These credentials correspond to the environment variables set in the `docker-compose.yml` for the PostgreSQL service. + +## Managing Application Data with Docker + +### Running Database Migrations +- Execute database migrations within the FastAPI container: + - **`docker-compose exec fastapi alembic upgrade head`** + - This command runs the Alembic upgrade command to apply migrations to your PostgreSQL database. + +### Running Tests with Pytest +- To run tests inside the FastAPI container, ensuring they interact with the PostgreSQL service: + - **`docker-compose exec fastapi pytest`** + - This command runs all tests defined in your FastAPI application. + +### Specific Test Execution +- To run a specific test file: + - **`docker-compose exec fastapi pytest /myapp/tests/test_specific_file.py`** + +### Running Tests with Coverage +- For executing tests with coverage reports: + - **`docker-compose exec fastapi pytest --cov=myapp`** + - To generate an HTML coverage report: + - **`docker-compose exec fastapi pytest --cov=myapp --cov-report=html`** + +## Resetting the Testing Environment +- If you need to reset your environment, e.g., to clear test data: + - **Stop all services and remove volumes**: + - **`docker-compose down -v`** + - **Restart the services**: + - **`docker-compose up -d`** + +## Docker Basics + +### Building Docker Images +- To build a Docker image for your FastAPI application, ensure you have a Dockerfile in the same directory as your `docker-compose.yml`. Then run: + - **`docker-compose build`** + +## Pushing Images to Docker Hub + +### Creating a Docker Hub Account +- Visit [Docker Hub](https://hub.docker.com/) and sign up for an account. +- Once registered, you can create repositories to store your Docker images. + +### Logging into Docker Hub from the Command Line +- **`docker login`** +- Enter your Docker Hub username and password. + +### Tagging Your Docker Image +- **`docker tag local-image:tagname username/repository:tag`** + - For example: + - **`docker tag myfastapi:latest john/myfastapi:latest`** + +### Pushing the Image +- **`docker push username/repository:tag`** + - For example: + - **`docker push john/myfastapi:latest`** + +## Additional Tips + +### Viewing Logs +- To view logs for troubleshooting or monitoring application behavior: + - **`docker-compose logs -f`** + - The `-f` flag tails the log output. + +### Shutting Down +- To stop and remove all running containers: + - **`docker-compose down`** + +This guide is structured to provide clear, step-by-step instructions on how to interact with the Dockerized environment defined by your Docker Compose setup, ideal for educational purposes and ensuring students are well-equipped to manage their development environment effectively. diff --git a/email_templates/email_verification.md b/email_templates/email_verification.md index 042f923e8..7e80ded16 100644 --- a/email_templates/email_verification.md +++ b/email_templates/email_verification.md @@ -1,8 +1,8 @@ -Hello {name}, - -Thank you for registering at OurSite. Please click the following link to verify your email address: - -[Verify Email]({verification_url}) - -Thanks, -The OurSite Team +Hello {name}, + +Thank you for registering at OurSite. Please click the following link to verify your email address: + +[Verify Email]({verification_url}) + +Thanks, +The OurSite Team diff --git a/email_templates/footer.md b/email_templates/footer.md index 3ff6d440d..cebf95c67 100644 --- a/email_templates/footer.md +++ b/email_templates/footer.md @@ -1,13 +1,13 @@ -Sincerely, -[Your Company Name] -[Your Company Address] -[City, State, Zip] - -You are receiving this email because you have opted in at our website. If you no longer wish to receive these emails, you can unsubscribe at any time by clicking [here](#). - -Company Registration Number: [Company Registration Number] -VAT Number: [VAT Number] - -This email and any files transmitted with it are confidential and intended solely for the use of the individual or entity to whom they are addressed. If you have received this email in error, please notify the system manager. This message contains confidential information and is intended only for the individual named. If you are not the named addressee you should not disseminate, distribute or copy this e-mail. - -Please consider the environment before printing this email. +Sincerely, +[Your Company Name] +[Your Company Address] +[City, State, Zip] + +You are receiving this email because you have opted in at our website. If you no longer wish to receive these emails, you can unsubscribe at any time by clicking [here](#). + +Company Registration Number: [Company Registration Number] +VAT Number: [VAT Number] + +This email and any files transmitted with it are confidential and intended solely for the use of the individual or entity to whom they are addressed. If you have received this email in error, please notify the system manager. This message contains confidential information and is intended only for the individual named. If you are not the named addressee you should not disseminate, distribute or copy this e-mail. + +Please consider the environment before printing this email. diff --git a/email_templates/header.md b/email_templates/header.md index d64f8deb7..5e2c87b48 100644 --- a/email_templates/header.md +++ b/email_templates/header.md @@ -1,6 +1,6 @@ -![Your Company Logo](http://example.com/path/to/your/logo.png) - -# Welcome to [Your Company Name] - ---- - +![Your Company Logo](http://example.com/path/to/your/logo.png) + +# Welcome to [Your Company Name] + +--- + diff --git a/email_templates/test_email.md b/email_templates/test_email.md index 8916a59f8..3ec1156e8 100644 --- a/email_templates/test_email.md +++ b/email_templates/test_email.md @@ -1,9 +1,9 @@ -# Hello, Email - -This is a *simple* test email sent from a Markdown file. - -- Point 1 -- Point 2 -- Point 3 - -**Thank you!** +# Hello, Email + +This is a *simple* test email sent from a Markdown file. + +- Point 1 +- Point 2 +- Point 3 + +**Thank you!** diff --git a/git.md b/git.md index d8ac97c2f..f8742c568 100644 --- a/git.md +++ b/git.md @@ -1,138 +1,138 @@ -# Project Management Manual - -## Overview -This manual provides detailed instructions and commands for managing the development environment and workflow using Git, Docker, and pytest. It is tailored for a project that uses a Docker Compose setup involving multiple services including PostgreSQL, PGAdmin, FastAPI, and Nginx. - -## Git Commands - -### Basic Operations - -#### Clone the repository: - - git clone - -#### Create a new branch: - - git checkout -b - -#### Switch to an existing branch: - - git checkout - -#### Check the status of changes: - - git status - -#### Add changes to the staging area: - - git add . - -#### Commit changes: - - git commit -m "Commit message" - -#### Push changes to the remote repository: - - git push origin - -#### Pull changes from the remote repository: - - git pull origin - -### Advanced Branch Management - - -#### List all branches, local and remote: - - git branch -a - -#### Merge another branch into your current branch: - - git merge - -#### Delete a local branch: - - git branch -d - -#### Delete a remote branch: - - git push origin --delete - -#### Stash changes in a dirty working directory: - - git stash - -#### Apply stashed changes back to your working directory: - - git stash pop - -## Working with GitHub Issues - -#### Link a commit to an issue: - - git commit -m "Fixes #123 - commit message" - -#### Close an issue via commit message: - - git commit -m "Closes #123 - commit message" - -## GitFlow Workflow - -GitFlow is a branching model for Git, designed around the project release. This workflow defines a strict branching model designed around the project release. Here’s how it typically works: - -- Master/Main Branch: Stores the official release history. -- Develop Branch: Serves as an integration branch for features. -- Feature Branches: Each new feature should reside in its own branch, which can be pushed to the GitHub repository for backups/collaboration. Feature branches use develop as their parent branch. When a feature is complete, it gets merged back into develop. -- Release Branches: Once develop has acquired enough features for a release (or a predetermined release date is nearing), you fork a release branch off of develop. -- Hotfix Branches: Maintenance or “hotfix” branches are used to quickly patch production releases. Hotfix branches are a lot like release branches and feature branches except they're based on master/main instead of develop. - - -In collaborative environments, especially on platforms like GitHub, GitFlow provides a robust framework where multiple developers can work on various features independently without disrupting the main codebase. Testing should be integral at various stages: - -- Feature Testing: Each feature branch should be thoroughly tested before it is merged back into develop. -- Release Testing: Before a release is finalized, comprehensive testing should be conducted to ensure all integrated features work well together. -- Post-release: Hotfix branches allow quick patches to be applied to production, ensuring any issues that slip through are addressed swiftly. - -This workflow supports continuous integration and deployment practices by allowing for regular merges from development to production branches, with testing checkpoints at crucial stages. - - -### Example GitFlow Commands - -#### Starting development on a new feature: - - git checkout -b feature/ develop - -#### Finishing a feature branch: - - git checkout develop - git merge feature/ --no-ff - git branch -d feature/ - git push origin develop - -#### Preparing a release: - - git checkout -b release/ develop - -#### Make necessary adjustments in the release branch - - git commit -m "Final changes for release " - -#### Completing a release: - - git checkout master - git merge release/ --no-ff - git tag -a - git push origin master - -#### Hotfix branch: - - git checkout -b hotfix/ master - -#### Fix issues - - git commit -m "Fixed " - git checkout master - git merge hotfix/ --no-ff - git tag -a - git push origin master - +# Project Management Manual + +## Overview +This manual provides detailed instructions and commands for managing the development environment and workflow using Git, Docker, and pytest. It is tailored for a project that uses a Docker Compose setup involving multiple services including PostgreSQL, PGAdmin, FastAPI, and Nginx. + +## Git Commands + +### Basic Operations + +#### Clone the repository: + + git clone + +#### Create a new branch: + + git checkout -b + +#### Switch to an existing branch: + + git checkout + +#### Check the status of changes: + + git status + +#### Add changes to the staging area: + + git add . + +#### Commit changes: + + git commit -m "Commit message" + +#### Push changes to the remote repository: + + git push origin + +#### Pull changes from the remote repository: + + git pull origin + +### Advanced Branch Management + + +#### List all branches, local and remote: + + git branch -a + +#### Merge another branch into your current branch: + + git merge + +#### Delete a local branch: + + git branch -d + +#### Delete a remote branch: + + git push origin --delete + +#### Stash changes in a dirty working directory: + + git stash + +#### Apply stashed changes back to your working directory: + + git stash pop + +## Working with GitHub Issues + +#### Link a commit to an issue: + + git commit -m "Fixes #123 - commit message" + +#### Close an issue via commit message: + + git commit -m "Closes #123 - commit message" + +## GitFlow Workflow + +GitFlow is a branching model for Git, designed around the project release. This workflow defines a strict branching model designed around the project release. Here’s how it typically works: + +- Master/Main Branch: Stores the official release history. +- Develop Branch: Serves as an integration branch for features. +- Feature Branches: Each new feature should reside in its own branch, which can be pushed to the GitHub repository for backups/collaboration. Feature branches use develop as their parent branch. When a feature is complete, it gets merged back into develop. +- Release Branches: Once develop has acquired enough features for a release (or a predetermined release date is nearing), you fork a release branch off of develop. +- Hotfix Branches: Maintenance or “hotfix” branches are used to quickly patch production releases. Hotfix branches are a lot like release branches and feature branches except they're based on master/main instead of develop. + + +In collaborative environments, especially on platforms like GitHub, GitFlow provides a robust framework where multiple developers can work on various features independently without disrupting the main codebase. Testing should be integral at various stages: + +- Feature Testing: Each feature branch should be thoroughly tested before it is merged back into develop. +- Release Testing: Before a release is finalized, comprehensive testing should be conducted to ensure all integrated features work well together. +- Post-release: Hotfix branches allow quick patches to be applied to production, ensuring any issues that slip through are addressed swiftly. + +This workflow supports continuous integration and deployment practices by allowing for regular merges from development to production branches, with testing checkpoints at crucial stages. + + +### Example GitFlow Commands + +#### Starting development on a new feature: + + git checkout -b feature/ develop + +#### Finishing a feature branch: + + git checkout develop + git merge feature/ --no-ff + git branch -d feature/ + git push origin develop + +#### Preparing a release: + + git checkout -b release/ develop + +#### Make necessary adjustments in the release branch + + git commit -m "Final changes for release " + +#### Completing a release: + + git checkout master + git merge release/ --no-ff + git tag -a + git push origin master + +#### Hotfix branch: + + git checkout -b hotfix/ master + +#### Fix issues + + git commit -m "Fixed " + git checkout master + git merge hotfix/ --no-ff + git tag -a + git push origin master + diff --git a/license.txt b/license.txt index ab6bd63ff..61f32171f 100644 --- a/license.txt +++ b/license.txt @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) [year] [full name] - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) [year] [full name] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/logging.conf b/logging.conf index 82ff096ae..686dc67e8 100644 --- a/logging.conf +++ b/logging.conf @@ -1,22 +1,22 @@ -[loggers] -keys=root - -[handlers] -keys=consoleHandler - -[formatters] -keys=detailedFormatter - -[logger_root] -level=INFO -handlers=consoleHandler - -[handler_consoleHandler] -class=StreamHandler -level=DEBUG -formatter=detailedFormatter -args=(sys.stdout,) - -[formatter_detailedFormatter] -format=%(asctime)s - %(name)s - %(levelname)s - %(message)s -datefmt=%Y-%m-%d %H:%M:%S +[loggers] +keys=root + +[handlers] +keys=consoleHandler + +[formatters] +keys=detailedFormatter + +[logger_root] +level=INFO +handlers=consoleHandler + +[handler_consoleHandler] +class=StreamHandler +level=DEBUG +formatter=detailedFormatter +args=(sys.stdout,) + +[formatter_detailedFormatter] +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s +datefmt=%Y-%m-%d %H:%M:%S diff --git a/nginx/nginx.conf b/nginx/nginx.conf index cca5cd0e8..ab5e96d77 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,11 +1,11 @@ -server { - listen 80; - - location / { - proxy_pass http://fastapi:8000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} +server { + listen 80; + + location / { + proxy_pass http://fastapi:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/pytest.ini b/pytest.ini index 4851d4948..6b9a43549 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,24 +1,24 @@ -# pytest.ini -[pytest] -testpaths = tests -addopts = -v -python_files = test_*.py *_test.py -python_classes = Test* -python_functions = test_* -asyncio_mode = auto -markers = - slow: marks tests as slow (deselect with '-m "not slow"') - fast: marks tests as fast (deselect with '-m "not fast"') -# log_cli=true -# log_cli_level=DEBUG -# Suppresses specific known warnings or globally ignores certain categories of warnings -filterwarnings = - ignore::DeprecationWarning - ignore::UserWarning - ignore::RuntimeWarning - # Ignore specific warnings from libraries - ignore:the imp module is deprecated in favour of importlib:DeprecationWarning - ignore:Using or importing the ABCs from 'collections':DeprecationWarning - -# Customize logging level if needed -# log_level = INFO +# pytest.ini +[pytest] +testpaths = tests +addopts = -v +python_files = test_*.py *_test.py +python_classes = Test* +python_functions = test_* +asyncio_mode = auto +markers = + slow: marks tests as slow (deselect with '-m "not slow"') + fast: marks tests as fast (deselect with '-m "not fast"') +# log_cli=true +# log_cli_level=DEBUG +# Suppresses specific known warnings or globally ignores certain categories of warnings +filterwarnings = + ignore::DeprecationWarning + ignore::UserWarning + ignore::RuntimeWarning + # Ignore specific warnings from libraries + ignore:the imp module is deprecated in favour of importlib:DeprecationWarning + ignore:Using or importing the ABCs from 'collections':DeprecationWarning + +# Customize logging level if needed +# log_level = INFO diff --git a/readme.md b/readme.md index 9b0d27151..f4d0ee2cb 100644 --- a/readme.md +++ b/readme.md @@ -1,170 +1,170 @@ -# Event Manager Company: Software QA Analyst/Developer Onboarding Assignment - -Welcome to the Event Manager Company! As a newly hired Software QA Analyst/Developer and a student in software engineering, you are embarking on an exciting journey to contribute to our project aimed at developing a secure, robust REST API that supports JWT (JSON Web Token ) token-based OAuth2 authentication. This API serves as the backbone of our user management system and will eventually expand to include features for event management and registration. - -## Instructor Videos - - - [Introduction to REST API with Postgres](https://youtu.be/dgMCSND2FQw) - This video provides an overview of the REST API you'll be working with, including its structure, endpoints, and interaction with the PostgreSQL database. - - [Assignment Instructions](https://youtu.be/TFblm7QrF6o) - Detailed instructions on your tasks, guiding you through the assignment step by step. - -# Commands - -1. Start and build a multi-container application: - -``` -docker compose up --build -``` - -2. Goto http://localhost/docs to view openapi spec documentation - -Click "authorize" input username: `admin@example.com` password: `secret` - -3. Goto http://localhost:5050 to connect and manage the database. - -The following information must match the ones in the `docker-compose.yml` file. - -Login: - -- Email address / Username: `admin@example.com` -- Password: `adminpassword` - -When add new server: - -- Host name/address: `postgres` -- Port: `5432` -- Maintenance database: `myappdb` -- Username: `user` -- Password: `password` - -## Optional Commands - -### Run `pytest` inside the containers: - -Run all tests: - -``` -docker compose exec fastapi pytest -``` - -Run a single test: - -``` -docker compose exec fastapi pytest tests/test_services/test_user_service.py::test_list_users -``` - -### Creating database migration: - -``` -docker compose exec fastapi alembic revision --autogenerate -m 'added admin' -``` - - -### Apply database migrations: - -``` -docker compose exec fastapi alembic upgrade head -``` - -## Assignment Objectives - -1. **Familiarize with REST API functionality and structure**: Gain hands-on experience working with a REST API, understanding its endpoints, request/response formats, and authentication mechanisms. - -2. **Implement and refine documentation**: Critically analyze and improve existing documentation based on issues identified in the instructor videos. Ensure that the documentation is up-to-date and accurately reflects the current state of the software. - -3. **Engage in manual and automated testing**: Develop comprehensive test cases and leverage automated testing tools like pytest to push the project's test coverage towards 90%. Gain experience with different types of testing, such as unit testing, integration testing, and end-to-end testing. - -4. **Explore and debug issues**: Dive deep into the codebase to investigate and resolve issues related to user profile updates and OAuth token generation. Utilize debugging tools, interpret error messages, and trace the flow of execution to identify the root cause of problems. - -5. **Collaborate effectively**: Experience the power of collaboration using Git for version control and GitHub for code reviews and issue tracking. Work with issues, branches, create pull requests, and merge code while following best practices. - -## Setup and Preliminary Steps - -1. **Fork the Project Repository**: Fork the [project repository](https://github.com/woffee/event_manager) to your own GitHub account. This creates a copy of the repository under your account, allowing you to work on the project independently. - -2. **Clone the Forked Repository**: Clone the forked repository to your local machine using the `git clone` command. This creates a local copy of the repository on your computer, enabling you to make changes and run the project locally. - -3. **Verify the Project Setup**: Follow the steps in the instructor video to set up the project using [Docker](https://www.docker.com/). Docker allows you to package the application with all its dependencies into a standardized unit called a container. Verify that you can access the API documentation at [http://localhost/docs](http://localhost/docs) and the database using [PGAdmin](https://www.pgadmin.org/) at [http://localhost:5050](http://localhost:5050). - -## Testing and Database Management - -1. **Explore the API**: Use [http://localhost/docs](http://localhost/docs) to familiarize yourself with the API endpoints, request/response formats, and authentication mechanisms. It provides an interactive interface to explore and test the API endpoints. - -2. **Run Tests**: Execute the provided test suite using pytest, a popular testing framework for Python. Running tests ensures that the existing functionality of the API is working as expected. Note that running tests will drop the database tables, so you may need to manually drop the Alembic version table using PGAdmin and re-run migrations to ensure a clean state. - -3. **Increase Test Coverage**: To enhance the reliability of the API, aim to increase the project's test coverage to 90%. Write additional tests for various scenarios and edge cases to ensure that the API handles different situations correctly. - -## Collaborative Development Using Git - -1. **Enable Issue Tracking**: Enable GitHub issues in your repository settings. [GitHub Issues](https://guides.github.com/features/issues/) is a powerful tool for tracking bugs, enhancements, and other tasks related to the project. It allows you to create, assign, and prioritize issues, facilitating effective collaboration among team members. - -2. **Create Branches**: For each issue or task you work on, create a new branch with a descriptive name using the `git checkout -b` command. Branching allows you to work on different features or fixes independently without affecting the main codebase. It enables parallel development and helps maintain a stable main branch. - -3. **Pull Requests and Code Reviews**: When you have completed work on an issue, create a [pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) to merge your changes into the main branch. Pull requests provide an opportunity for code review, where your team members can examine your changes, provide feedback, and suggest improvements. Code reviews help maintain code quality, catch potential issues, and promote knowledge sharing among the team. - -## Specific Issues to Address - -In this assignment, you will identify, document, and resolve five specific issues related to: - -1. **Username validation**: Investigate and resolve any issues related to username validation. This may involve handling special characters, enforcing length constraints, or ensuring uniqueness. Proper username validation is essential to maintain data integrity and prevent potential security vulnerabilities. - -2. **Password validation**: Ensure that password validation follows security best practices, such as enforcing minimum length, requiring complexity (e.g., a mix of uppercase, lowercase, numbers, and special characters), and properly hashing passwords before storing them in the database. Robust password validation protects user accounts and mitigates the risk of unauthorized access. - -3. **Profile field edge cases**: Test and handle various scenarios related to updating profile fields. This may include updating the bio and profile picture URL simultaneously or individually. Consider different combinations of fields being updated and ensure that the API handles these cases gracefully. Edge case testing helps uncover potential issues and ensures a smooth user experience. - -Additionally, you will resolve a sixth issue demonstrated in the instructor video. These issues will test various combinations and scenarios to simulate real-world usage and potential edge cases. By addressing these specific issues, you will gain experience in identifying and resolving common challenges in API development. - -## Submission Requirements - -To complete this assignment, submit the following: - -1. **GitHub Repository Link**: Ensure that your repository is well-organized and includes: - - Five closed issues, each with accompanying test code and necessary application code modifications. - - Each issue should be well-documented, explaining the problem, the steps taken to resolve it, and the outcome. Proper documentation helps others understand your work and facilitates future maintenance. - - All issues should be merged into the main branch, following the Git workflow and best practices. - -2. **Updated README**: Replace the existing README with: - - Links to the closed issues, providing easy access to your work. - - Link to project image deployed to Dockerhub. - - A 2-3 paragraph reflection on what you learned from this assignment, focusing on both technical skills and collaborative processes. Reflect on the challenges you faced, the solutions you implemented, and the insights you gained. This reflection helps solidify your learning and provides valuable feedback for improving the assignment in the future. - -## Grading Rubric - -| Criteria | Points | -|-------------------------------------------------------------------------------------------------------------------------|--------| -| Resolved 5 issues related to username validation, password validation, and profile field edge cases | 30 | -| Resolved the issue demonstrated in the instructor video | 20 | -| Increased test coverage to 90% by writing comprehensive test cases | 20 | -| Followed collaborative development practices using Git and GitHub (branching, pull requests, code reviews) | 15 | -| Submitted a well-organized GitHub repository with clear documentation, links to closed issues, and a reflective summary | 15 | -| **Total** | **100**| - -## Resources and Documentation - -- **Important Links**: - - [Git Command Reference I created and some explanation for collaboration with git](git.md) - - [Docker Commands and Running The Tests in the Application](docker.md) - - Look at the code comments: - - [Test Configuration and Fixtures](tests/conftest.py) - - [API User Routes](app/routers/user_routes.py) - - [API Oauth Routes - Connection to HTTP](app/routers/oauth.py) - - [User Service - Business Logic - This implements whats called the service repository pattern](app/services/user_service.py) - - [User Schema - Pydantic models](app/schemas/user_schemas.py) - - [User Model - SQl Alchemy Model ](app/models/user_model.py) - - [Alembic Migration - this is what runs to create the tables when you do alembic upgrade head](alembic/versions/628adcb2d60e_initial_migration.py) - - See the tests folder for all the tests - - - API Documentation: [http://localhost/docs](http://localhost/docs) - Provides information on endpoints, request/response formats, and authentication. - - Database Management: [http://localhost:5050](http://localhost:5050) - The PGAdmin interface for managing the PostgreSQL database, allowing you to view and manipulate the database tables. - - Email service: [https://mailtrap.io/](https://mailtrap.io/) - Email Delivery Platform that delivers just in time. Great for dev, and marketing teams. After registered, put the credentials in `.env` file. - -- **Code Documentation**: - The project codebase includes docstrings and comments explaining various concepts and functionalities. Take the time to read through the code and understand how different components work together. Pay attention to the structure of the code, the naming conventions used, and the purpose of each function or class. Understanding the existing codebase will help you write code that is consistent and integrates well with the project. - -- **Additional Resources**: - - [SQLAlchemy Library](https://www.sqlalchemy.org/) - SQLAlchemy is a powerful SQL toolkit and Object-Relational Mapping (ORM) library for Python. It provides a set of tools for interacting with databases, including query building, database schema management, and data serialization. Familiarize yourself with SQLAlchemy's documentation to understand how it is used in the project for database operations. - - [Pydantic Documentation](https://docs.pydantic.dev/latest/) - Pydantic is a data validation and settings management library for Python. It allows you to define data models with type annotations and provides automatic validation, serialization, and deserialization. Consult the Pydantic documentation to understand how it is used in the project for request/response validation and serialization. - - [FastAPI Framework](https://fastapi.tiangolo.com/) - FastAPI is a modern, fast (high-performance) Python web framework for building APIs. It leverages Python's type hints and provides automatic API documentation, request/response validation, and easy integration with other libraries. Explore the FastAPI documentation to gain a deeper understanding of its features and how it is used in the project. - - [Alembic Documentation](https://alembic.sqlalchemy.org/en/latest/index.html) - Alembic is a lightweight database migration tool for usage with SQLAlchemy. It allows you to define and manage database schema changes over time, ensuring that the database structure remains consistent across different environments. Refer to the Alembic documentation to learn how to create and apply database migrations in the project. - -These resources will provide you with a solid foundation to understand the tools, technologies, and concepts used in the project. Don't hesitate to explore them further and consult the documentation whenever you encounter challenges or need clarification. - +# Event Manager Company: Software QA Analyst/Developer Onboarding Assignment + +Welcome to the Event Manager Company! As a newly hired Software QA Analyst/Developer and a student in software engineering, you are embarking on an exciting journey to contribute to our project aimed at developing a secure, robust REST API that supports JWT (JSON Web Token ) token-based OAuth2 authentication. This API serves as the backbone of our user management system and will eventually expand to include features for event management and registration. + +## Instructor Videos + + - [Introduction to REST API with Postgres](https://youtu.be/dgMCSND2FQw) - This video provides an overview of the REST API you'll be working with, including its structure, endpoints, and interaction with the PostgreSQL database. + - [Assignment Instructions](https://youtu.be/TFblm7QrF6o) - Detailed instructions on your tasks, guiding you through the assignment step by step. + +# Commands + +1. Start and build a multi-container application: + +``` +docker compose up --build +``` + +2. Goto http://localhost/docs to view openapi spec documentation + +Click "authorize" input username: `admin@example.com` password: `secret` + +3. Goto http://localhost:5050 to connect and manage the database. + +The following information must match the ones in the `docker-compose.yml` file. + +Login: + +- Email address / Username: `admin@example.com` +- Password: `adminpassword` + +When add new server: + +- Host name/address: `postgres` +- Port: `5432` +- Maintenance database: `myappdb` +- Username: `user` +- Password: `password` + +## Optional Commands + +### Run `pytest` inside the containers: + +Run all tests: + +``` +docker compose exec fastapi pytest +``` + +Run a single test: + +``` +docker compose exec fastapi pytest tests/test_services/test_user_service.py::test_list_users +``` + +### Creating database migration: + +``` +docker compose exec fastapi alembic revision --autogenerate -m 'added admin' +``` + + +### Apply database migrations: + +``` +docker compose exec fastapi alembic upgrade head +``` + +## Assignment Objectives + +1. **Familiarize with REST API functionality and structure**: Gain hands-on experience working with a REST API, understanding its endpoints, request/response formats, and authentication mechanisms. + +2. **Implement and refine documentation**: Critically analyze and improve existing documentation based on issues identified in the instructor videos. Ensure that the documentation is up-to-date and accurately reflects the current state of the software. + +3. **Engage in manual and automated testing**: Develop comprehensive test cases and leverage automated testing tools like pytest to push the project's test coverage towards 90%. Gain experience with different types of testing, such as unit testing, integration testing, and end-to-end testing. + +4. **Explore and debug issues**: Dive deep into the codebase to investigate and resolve issues related to user profile updates and OAuth token generation. Utilize debugging tools, interpret error messages, and trace the flow of execution to identify the root cause of problems. + +5. **Collaborate effectively**: Experience the power of collaboration using Git for version control and GitHub for code reviews and issue tracking. Work with issues, branches, create pull requests, and merge code while following best practices. + +## Setup and Preliminary Steps + +1. **Fork the Project Repository**: Fork the [project repository](https://github.com/woffee/event_manager) to your own GitHub account. This creates a copy of the repository under your account, allowing you to work on the project independently. + +2. **Clone the Forked Repository**: Clone the forked repository to your local machine using the `git clone` command. This creates a local copy of the repository on your computer, enabling you to make changes and run the project locally. + +3. **Verify the Project Setup**: Follow the steps in the instructor video to set up the project using [Docker](https://www.docker.com/). Docker allows you to package the application with all its dependencies into a standardized unit called a container. Verify that you can access the API documentation at [http://localhost/docs](http://localhost/docs) and the database using [PGAdmin](https://www.pgadmin.org/) at [http://localhost:5050](http://localhost:5050). + +## Testing and Database Management + +1. **Explore the API**: Use [http://localhost/docs](http://localhost/docs) to familiarize yourself with the API endpoints, request/response formats, and authentication mechanisms. It provides an interactive interface to explore and test the API endpoints. + +2. **Run Tests**: Execute the provided test suite using pytest, a popular testing framework for Python. Running tests ensures that the existing functionality of the API is working as expected. Note that running tests will drop the database tables, so you may need to manually drop the Alembic version table using PGAdmin and re-run migrations to ensure a clean state. + +3. **Increase Test Coverage**: To enhance the reliability of the API, aim to increase the project's test coverage to 90%. Write additional tests for various scenarios and edge cases to ensure that the API handles different situations correctly. + +## Collaborative Development Using Git + +1. **Enable Issue Tracking**: Enable GitHub issues in your repository settings. [GitHub Issues](https://guides.github.com/features/issues/) is a powerful tool for tracking bugs, enhancements, and other tasks related to the project. It allows you to create, assign, and prioritize issues, facilitating effective collaboration among team members. + +2. **Create Branches**: For each issue or task you work on, create a new branch with a descriptive name using the `git checkout -b` command. Branching allows you to work on different features or fixes independently without affecting the main codebase. It enables parallel development and helps maintain a stable main branch. + +3. **Pull Requests and Code Reviews**: When you have completed work on an issue, create a [pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) to merge your changes into the main branch. Pull requests provide an opportunity for code review, where your team members can examine your changes, provide feedback, and suggest improvements. Code reviews help maintain code quality, catch potential issues, and promote knowledge sharing among the team. + +## Specific Issues to Address + +In this assignment, you will identify, document, and resolve five specific issues related to: + +1. **Username validation**: Investigate and resolve any issues related to username validation. This may involve handling special characters, enforcing length constraints, or ensuring uniqueness. Proper username validation is essential to maintain data integrity and prevent potential security vulnerabilities. + +2. **Password validation**: Ensure that password validation follows security best practices, such as enforcing minimum length, requiring complexity (e.g., a mix of uppercase, lowercase, numbers, and special characters), and properly hashing passwords before storing them in the database. Robust password validation protects user accounts and mitigates the risk of unauthorized access. + +3. **Profile field edge cases**: Test and handle various scenarios related to updating profile fields. This may include updating the bio and profile picture URL simultaneously or individually. Consider different combinations of fields being updated and ensure that the API handles these cases gracefully. Edge case testing helps uncover potential issues and ensures a smooth user experience. + +Additionally, you will resolve a sixth issue demonstrated in the instructor video. These issues will test various combinations and scenarios to simulate real-world usage and potential edge cases. By addressing these specific issues, you will gain experience in identifying and resolving common challenges in API development. + +## Submission Requirements + +To complete this assignment, submit the following: + +1. **GitHub Repository Link**: Ensure that your repository is well-organized and includes: + - Five closed issues, each with accompanying test code and necessary application code modifications. + - Each issue should be well-documented, explaining the problem, the steps taken to resolve it, and the outcome. Proper documentation helps others understand your work and facilitates future maintenance. + - All issues should be merged into the main branch, following the Git workflow and best practices. + +2. **Updated README**: Replace the existing README with: + - Links to the closed issues, providing easy access to your work. + - Link to project image deployed to Dockerhub. + - A 2-3 paragraph reflection on what you learned from this assignment, focusing on both technical skills and collaborative processes. Reflect on the challenges you faced, the solutions you implemented, and the insights you gained. This reflection helps solidify your learning and provides valuable feedback for improving the assignment in the future. + +## Grading Rubric + +| Criteria | Points | +|-------------------------------------------------------------------------------------------------------------------------|--------| +| Resolved 5 issues related to username validation, password validation, and profile field edge cases | 30 | +| Resolved the issue demonstrated in the instructor video | 20 | +| Increased test coverage to 90% by writing comprehensive test cases | 20 | +| Followed collaborative development practices using Git and GitHub (branching, pull requests, code reviews) | 15 | +| Submitted a well-organized GitHub repository with clear documentation, links to closed issues, and a reflective summary | 15 | +| **Total** | **100**| + +## Resources and Documentation + +- **Important Links**: + - [Git Command Reference I created and some explanation for collaboration with git](git.md) + - [Docker Commands and Running The Tests in the Application](docker.md) + - Look at the code comments: + - [Test Configuration and Fixtures](tests/conftest.py) + - [API User Routes](app/routers/user_routes.py) + - [API Oauth Routes - Connection to HTTP](app/routers/oauth.py) + - [User Service - Business Logic - This implements whats called the service repository pattern](app/services/user_service.py) + - [User Schema - Pydantic models](app/schemas/user_schemas.py) + - [User Model - SQl Alchemy Model ](app/models/user_model.py) + - [Alembic Migration - this is what runs to create the tables when you do alembic upgrade head](alembic/versions/628adcb2d60e_initial_migration.py) + - See the tests folder for all the tests + + - API Documentation: [http://localhost/docs](http://localhost/docs) - Provides information on endpoints, request/response formats, and authentication. + - Database Management: [http://localhost:5050](http://localhost:5050) - The PGAdmin interface for managing the PostgreSQL database, allowing you to view and manipulate the database tables. + - Email service: [https://mailtrap.io/](https://mailtrap.io/) - Email Delivery Platform that delivers just in time. Great for dev, and marketing teams. After registered, put the credentials in `.env` file. + +- **Code Documentation**: + The project codebase includes docstrings and comments explaining various concepts and functionalities. Take the time to read through the code and understand how different components work together. Pay attention to the structure of the code, the naming conventions used, and the purpose of each function or class. Understanding the existing codebase will help you write code that is consistent and integrates well with the project. + +- **Additional Resources**: + - [SQLAlchemy Library](https://www.sqlalchemy.org/) - SQLAlchemy is a powerful SQL toolkit and Object-Relational Mapping (ORM) library for Python. It provides a set of tools for interacting with databases, including query building, database schema management, and data serialization. Familiarize yourself with SQLAlchemy's documentation to understand how it is used in the project for database operations. + - [Pydantic Documentation](https://docs.pydantic.dev/latest/) - Pydantic is a data validation and settings management library for Python. It allows you to define data models with type annotations and provides automatic validation, serialization, and deserialization. Consult the Pydantic documentation to understand how it is used in the project for request/response validation and serialization. + - [FastAPI Framework](https://fastapi.tiangolo.com/) - FastAPI is a modern, fast (high-performance) Python web framework for building APIs. It leverages Python's type hints and provides automatic API documentation, request/response validation, and easy integration with other libraries. Explore the FastAPI documentation to gain a deeper understanding of its features and how it is used in the project. + - [Alembic Documentation](https://alembic.sqlalchemy.org/en/latest/index.html) - Alembic is a lightweight database migration tool for usage with SQLAlchemy. It allows you to define and manage database schema changes over time, ensuring that the database structure remains consistent across different environments. Refer to the Alembic documentation to learn how to create and apply database migrations in the project. + +These resources will provide you with a solid foundation to understand the tools, technologies, and concepts used in the project. Don't hesitate to explore them further and consult the documentation whenever you encounter challenges or need clarification. + diff --git a/requirements.txt b/requirements.txt index 4306e8ed1..398665d18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,63 +1,63 @@ -aiofiles==23.2.1 -aiomysql==0.2.0 -alembic==1.13.1 -annotated-types==0.6.0 -anyio==4.3.0 -async-sqlalchemy==1.0.0 -async-timeout==4.0.3 -asyncio==3.4.3 -asyncpg==0.29.0 -bcrypt==4.1.2 -certifi==2024.2.2 -cffi==1.16.0 -click==8.1.7 -coverage==7.4.4 -cryptography==42.0.5 -dnspython==2.6.1 -ecdsa==0.18.0 -email_validator==2.1.1 -exceptiongroup==1.2.0 -factory-boy==3.3.0 -Faker==24.4.0 -fastapi==0.110.0 -greenlet==3.0.3 -gunicorn==21.2.0 -h11==0.14.0 -httpcore==1.0.5 -httpx==0.27.0 -idna==3.6 -iniconfig==2.0.0 -Mako==1.3.2 -MarkupSafe==2.1.5 -packaging==24.0 -passlib==1.7.4 -pluggy==1.4.0 -psycopg==3.1.18 -psycopg2-binary==2.9.9 -pyasn1==0.6.0 -pycparser==2.22 -pydantic==2.6.4 -pydantic-settings==2.2.1 -pydantic_core==2.16.3 -PyMySQL==1.1.0 -pypng==0.20220715.0 -pytest==8.1.1 -pytest-asyncio==0.23.6 -pytest-cov==5.0.0 -pytest-mock==3.14.0 -python-dateutil==2.9.0.post0 -python-dotenv==1.0.1 -python-jose==3.3.0 -python-multipart==0.0.9 -qrcode==7.4.2 -rsa==4.9 -six==1.16.0 -sniffio==1.3.1 -SQLAlchemy==2.0.29 -starlette==0.36.3 -tomli==2.0.1 -typing_extensions==4.10.0 -uvicorn==0.29.0 -validators==0.24.0 -markdown2 +aiofiles==23.2.1 +aiomysql==0.2.0 +alembic==1.13.1 +annotated-types==0.6.0 +anyio==4.3.0 +async-sqlalchemy==1.0.0 +async-timeout==4.0.3 +asyncio==3.4.3 +asyncpg==0.29.0 +bcrypt==4.1.2 +certifi==2024.2.2 +cffi==1.16.0 +click==8.1.7 +coverage==7.4.4 +cryptography==42.0.5 +dnspython==2.6.1 +ecdsa==0.18.0 +email_validator==2.1.1 +exceptiongroup==1.2.0 +factory-boy==3.3.0 +Faker==24.4.0 +fastapi==0.110.0 +greenlet==3.0.3 +gunicorn==21.2.0 +h11==0.14.0 +httpcore==1.0.5 +httpx==0.27.0 +idna==3.6 +iniconfig==2.0.0 +Mako==1.3.2 +MarkupSafe==2.1.5 +packaging==24.0 +passlib==1.7.4 +pluggy==1.4.0 +psycopg==3.1.18 +psycopg2-binary==2.9.9 +pyasn1==0.6.0 +pycparser==2.22 +pydantic==2.6.4 +pydantic-settings==2.2.1 +pydantic_core==2.16.3 +PyMySQL==1.1.0 +pypng==0.20220715.0 +pytest==8.1.1 +pytest-asyncio==0.23.6 +pytest-cov==5.0.0 +pytest-mock==3.14.0 +python-dateutil==2.9.0.post0 +python-dotenv==1.0.1 +python-jose==3.3.0 +python-multipart==0.0.9 +qrcode==7.4.2 +rsa==4.9 +six==1.16.0 +sniffio==1.3.1 +SQLAlchemy==2.0.29 +starlette==0.36.3 +tomli==2.0.1 +typing_extensions==4.10.0 +uvicorn==0.29.0 +validators==0.24.0 +markdown2 pyjwt \ No newline at end of file diff --git a/settings/config.py b/settings/config.py index 5b5cf8771..3827feb66 100644 --- a/settings/config.py +++ b/settings/config.py @@ -1,51 +1,51 @@ -from builtins import bool, int, str -from pathlib import Path -from pydantic import Field, AnyUrl, DirectoryPath -from pydantic_settings import BaseSettings - -class Settings(BaseSettings): - max_login_attempts: int = Field(default=3, description="Background color of QR codes") - # Server configuration - server_base_url: AnyUrl = Field(default='http://localhost', description="Base URL of the server") - server_download_folder: str = Field(default='downloads', description="Folder for storing downloaded files") - - # Security and authentication configuration - secret_key: str = Field(default="secret-key", description="Secret key for encryption") - algorithm: str = Field(default="HS256", description="Algorithm used for encryption") - access_token_expire_minutes: int = Field(default=30, description="Expiration time for access tokens in minutes") - admin_user: str = Field(default='admin', description="Default admin username") - admin_password: str = Field(default='secret', description="Default admin password") - debug: bool = Field(default=False, description="Debug mode outputs errors and sqlalchemy queries") - jwt_secret_key: str = "a_very_secret_key" - jwt_algorithm: str = "HS256" - access_token_expire_minutes: int = 15 # 15 minutes for access token - refresh_token_expire_minutes: int = 1440 # 24 hours for refresh token - # Database configuration - database_url: str = Field(default='postgresql+asyncpg://user:password@postgres/myappdb', description="URL for connecting to the database") - - # Optional: If preferring to construct the SQLAlchemy database URL from components - postgres_user: str = Field(default='user', description="PostgreSQL username") - postgres_password: str = Field(default='password', description="PostgreSQL password") - postgres_server: str = Field(default='localhost', description="PostgreSQL server address") - postgres_port: str = Field(default='5432', description="PostgreSQL port") - postgres_db: str = Field(default='myappdb', description="PostgreSQL database name") - # Discord configuration - discord_bot_token: str = Field(default='NONE', description="Discord bot token") - discord_channel_id: int = Field(default=1234567890, description="Default Discord channel ID for the bot to interact", example=1234567890) - #Open AI Key - openai_api_key: str = Field(default='NONE', description="Open AI Api Key") - send_real_mail: bool = Field(default=False, description="use mock") - # Email settings for Mailtrap - smtp_server: str = Field(default='smtp.mailtrap.io', description="SMTP server for sending emails") - smtp_port: int = Field(default=2525, description="SMTP port for sending emails") - smtp_username: str = Field(default='your-mailtrap-username', description="Username for SMTP server") - smtp_password: str = Field(default='your-mailtrap-password', description="Password for SMTP server") - - - class Config: - # If your .env file is not in the root directory, adjust the path accordingly. - env_file = ".env" - env_file_encoding = 'utf-8' - -# Instantiate settings to be imported in your application -settings = Settings() +from builtins import bool, int, str +from pathlib import Path +from pydantic import Field, AnyUrl, DirectoryPath +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + max_login_attempts: int = Field(default=3, description="Background color of QR codes") + # Server configuration + server_base_url: AnyUrl = Field(default='http://localhost', description="Base URL of the server") + server_download_folder: str = Field(default='downloads', description="Folder for storing downloaded files") + + # Security and authentication configuration + secret_key: str = Field(default="secret-key", description="Secret key for encryption") + algorithm: str = Field(default="HS256", description="Algorithm used for encryption") + access_token_expire_minutes: int = Field(default=30, description="Expiration time for access tokens in minutes") + admin_user: str = Field(default='admin', description="Default admin username") + admin_password: str = Field(default='secret', description="Default admin password") + debug: bool = Field(default=False, description="Debug mode outputs errors and sqlalchemy queries") + jwt_secret_key: str = "a_very_secret_key" + jwt_algorithm: str = "HS256" + access_token_expire_minutes: int = 15 # 15 minutes for access token + refresh_token_expire_minutes: int = 1440 # 24 hours for refresh token + # Database configuration + database_url: str = Field(default='postgresql+asyncpg://user:password@postgres/myappdb', description="URL for connecting to the database") + + # Optional: If preferring to construct the SQLAlchemy database URL from components + postgres_user: str = Field(default='user', description="PostgreSQL username") + postgres_password: str = Field(default='password', description="PostgreSQL password") + postgres_server: str = Field(default='localhost', description="PostgreSQL server address") + postgres_port: str = Field(default='5432', description="PostgreSQL port") + postgres_db: str = Field(default='myappdb', description="PostgreSQL database name") + # Discord configuration + discord_bot_token: str = Field(default='NONE', description="Discord bot token") + discord_channel_id: int = Field(default=1234567890, description="Default Discord channel ID for the bot to interact", example=1234567890) + #Open AI Key + openai_api_key: str = Field(default='NONE', description="Open AI Api Key") + send_real_mail: bool = Field(default=False, description="use mock") + # Email settings for Mailtrap + smtp_server: str = Field(default='smtp.mailtrap.io', description="SMTP server for sending emails") + smtp_port: int = Field(default=2525, description="SMTP port for sending emails") + smtp_username: str = Field(default='your-mailtrap-username', description="Username for SMTP server") + smtp_password: str = Field(default='your-mailtrap-password', description="Password for SMTP server") + + + class Config: + # If your .env file is not in the root directory, adjust the path accordingly. + env_file = ".env" + env_file_encoding = 'utf-8' + +# Instantiate settings to be imported in your application +settings = Settings() diff --git a/tests/conftest.py b/tests/conftest.py index 6bd7998ac..9e4cb125a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,263 +1,263 @@ -""" -File: test_database_operations.py - -Overview: -This Python test file utilizes pytest to manage database states and HTTP clients for testing a web application built with FastAPI and SQLAlchemy. It includes detailed fixtures to mock the testing environment, ensuring each test is run in isolation with a consistent setup. - -Fixtures: -- `async_client`: Manages an asynchronous HTTP client for testing interactions with the FastAPI application. -- `db_session`: Handles database transactions to ensure a clean database state for each test. -- User fixtures (`user`, `locked_user`, `verified_user`, etc.): Set up various user states to test different behaviors under diverse conditions. -- `token`: Generates an authentication token for testing secured endpoints. -- `initialize_database`: Prepares the database at the session start. -- `setup_database`: Sets up and tears down the database before and after each test. -""" - -# Standard library imports -from builtins import range -from datetime import datetime -from unittest.mock import patch -from uuid import uuid4 - -# Third-party imports -import pytest -from fastapi.testclient import TestClient -from httpx import AsyncClient -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession -from sqlalchemy.orm import sessionmaker, scoped_session -from faker import Faker - -# Application-specific imports -from app.main import app -from app.database import Base, Database -from app.models.user_model import User, UserRole -from app.dependencies import get_db, get_settings -from app.utils.security import hash_password -from app.utils.template_manager import TemplateManager -from app.services.email_service import EmailService -from app.services.jwt_service import create_access_token - -fake = Faker() - -settings = get_settings() -TEST_DATABASE_URL = settings.database_url.replace("postgresql://", "postgresql+asyncpg://") -engine = create_async_engine(TEST_DATABASE_URL, echo=settings.debug) -AsyncTestingSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) -AsyncSessionScoped = scoped_session(AsyncTestingSessionLocal) - - -@pytest.fixture -def email_service(): - # Assuming the TemplateManager does not need any arguments for initialization - template_manager = TemplateManager() - email_service = EmailService(template_manager=template_manager) - return email_service - - -# this is what creates the http client for your api tests -@pytest.fixture(scope="function") -async def async_client(db_session): - async with AsyncClient(app=app, base_url="http://testserver") as client: - app.dependency_overrides[get_db] = lambda: db_session - try: - yield client - finally: - app.dependency_overrides.clear() - -@pytest.fixture(scope="session", autouse=True) -def initialize_database(): - try: - Database.initialize(settings.database_url) - except Exception as e: - pytest.fail(f"Failed to initialize the database: {str(e)}") - -# this function setup and tears down (drops tales) for each test function, so you have a clean database for each test. -@pytest.fixture(scope="function", autouse=True) -async def setup_database(): - async with engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - yield - async with engine.begin() as conn: - # you can comment out this line during development if you are debugging a single test - await conn.run_sync(Base.metadata.drop_all) - await engine.dispose() - -@pytest.fixture(scope="function") -async def db_session(setup_database): - async with AsyncSessionScoped() as session: - try: - yield session - finally: - await session.close() - -@pytest.fixture(scope="function") -async def locked_user(db_session): - unique_email = fake.email() - user_data = { - "nickname": fake.user_name(), - "first_name": fake.first_name(), - "last_name": fake.last_name(), - "email": unique_email, - "hashed_password": hash_password("MySuperPassword$1234"), - "role": UserRole.AUTHENTICATED, - "email_verified": False, - "is_locked": True, - "failed_login_attempts": settings.max_login_attempts, - } - user = User(**user_data) - db_session.add(user) - await db_session.commit() - return user - -@pytest.fixture(scope="function") -async def user(db_session): - user_data = { - "nickname": fake.user_name(), - "first_name": fake.first_name(), - "last_name": fake.last_name(), - "email": fake.email(), - "hashed_password": hash_password("MySuperPassword$1234"), - "role": UserRole.AUTHENTICATED, - "email_verified": False, - "is_locked": False, - } - user = User(**user_data) - db_session.add(user) - await db_session.commit() - return user - -@pytest.fixture(scope="function") -async def verified_user(db_session): - user_data = { - "nickname": fake.user_name(), - "first_name": fake.first_name(), - "last_name": fake.last_name(), - "email": fake.email(), - "hashed_password": hash_password("MySuperPassword$1234"), - "role": UserRole.AUTHENTICATED, - "email_verified": True, - "is_locked": False, - } - user = User(**user_data) - db_session.add(user) - await db_session.commit() - return user - -@pytest.fixture(scope="function") -async def unverified_user(db_session): - user_data = { - "nickname": fake.user_name(), - "first_name": fake.first_name(), - "last_name": fake.last_name(), - "email": fake.email(), - "hashed_password": hash_password("MySuperPassword$1234"), - "role": UserRole.AUTHENTICATED, - "email_verified": False, - "is_locked": False, - } - user = User(**user_data) - db_session.add(user) - await db_session.commit() - return user - -@pytest.fixture(scope="function") -async def users_with_same_role_50_users(db_session): - users = [] - for _ in range(50): - user_data = { - "nickname": fake.user_name(), - "first_name": fake.first_name(), - "last_name": fake.last_name(), - "email": fake.email(), - "hashed_password": fake.password(), - "role": UserRole.AUTHENTICATED, - "email_verified": False, - "is_locked": False, - } - user = User(**user_data) - db_session.add(user) - users.append(user) - await db_session.commit() - return users - -@pytest.fixture -async def admin_user(db_session: AsyncSession): - user = User( - nickname="admin_user", - email="admin@example.com", - first_name="John", - last_name="Doe", - hashed_password="securepassword", - role=UserRole.ADMIN, - is_locked=False, - ) - db_session.add(user) - await db_session.commit() - return user - -@pytest.fixture -async def manager_user(db_session: AsyncSession): - user = User( - nickname="manager_john", - first_name="John", - last_name="Doe", - email="manager_user@example.com", - hashed_password="securepassword", - role=UserRole.MANAGER, - is_locked=False, - ) - db_session.add(user) - await db_session.commit() - return user - - -# Fixtures for common test data -@pytest.fixture -def user_base_data(): - return { - "username": "john_doe_123", - "email": "john.doe@example.com", - "full_name": "John Doe", - "bio": "I am a software engineer with over 5 years of experience.", - "profile_picture_url": "https://example.com/profile_pictures/john_doe.jpg" - } - -@pytest.fixture -def user_base_data_invalid(): - return { - "username": "john_doe_123", - "email": "john.doe.example.com", - "full_name": "John Doe", - "bio": "I am a software engineer with over 5 years of experience.", - "profile_picture_url": "https://example.com/profile_pictures/john_doe.jpg" - } - - -@pytest.fixture -def user_create_data(user_base_data): - return {**user_base_data, "password": "SecurePassword123!"} - -@pytest.fixture -def user_update_data(): - return { - "email": "john.doe.new@example.com", - "full_name": "John H. Doe", - "bio": "I specialize in backend development with Python and Node.js.", - "profile_picture_url": "https://example.com/profile_pictures/john_doe_updated.jpg" - } - -@pytest.fixture -def user_response_data(): - return { - "id": "unique-id-string", - "username": "testuser", - "email": "test@example.com", - "last_login_at": datetime.now(), - "created_at": datetime.now(), - "updated_at": datetime.now(), - "links": [] - } - -@pytest.fixture -def login_request_data(): +""" +File: test_database_operations.py + +Overview: +This Python test file utilizes pytest to manage database states and HTTP clients for testing a web application built with FastAPI and SQLAlchemy. It includes detailed fixtures to mock the testing environment, ensuring each test is run in isolation with a consistent setup. + +Fixtures: +- `async_client`: Manages an asynchronous HTTP client for testing interactions with the FastAPI application. +- `db_session`: Handles database transactions to ensure a clean database state for each test. +- User fixtures (`user`, `locked_user`, `verified_user`, etc.): Set up various user states to test different behaviors under diverse conditions. +- `token`: Generates an authentication token for testing secured endpoints. +- `initialize_database`: Prepares the database at the session start. +- `setup_database`: Sets up and tears down the database before and after each test. +""" + +# Standard library imports +from builtins import range +from datetime import datetime +from unittest.mock import patch +from uuid import uuid4 + +# Third-party imports +import pytest +from fastapi.testclient import TestClient +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker, scoped_session +from faker import Faker + +# Application-specific imports +from app.main import app +from app.database import Base, Database +from app.models.user_model import User, UserRole +from app.dependencies import get_db, get_settings +from app.utils.security import hash_password +from app.utils.template_manager import TemplateManager +from app.services.email_service import EmailService +from app.services.jwt_service import create_access_token + +fake = Faker() + +settings = get_settings() +TEST_DATABASE_URL = settings.database_url.replace("postgresql://", "postgresql+asyncpg://") +engine = create_async_engine(TEST_DATABASE_URL, echo=settings.debug) +AsyncTestingSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) +AsyncSessionScoped = scoped_session(AsyncTestingSessionLocal) + + +@pytest.fixture +def email_service(): + # Assuming the TemplateManager does not need any arguments for initialization + template_manager = TemplateManager() + email_service = EmailService(template_manager=template_manager) + return email_service + + +# this is what creates the http client for your api tests +@pytest.fixture(scope="function") +async def async_client(db_session): + async with AsyncClient(app=app, base_url="http://testserver") as client: + app.dependency_overrides[get_db] = lambda: db_session + try: + yield client + finally: + app.dependency_overrides.clear() + +@pytest.fixture(scope="session", autouse=True) +def initialize_database(): + try: + Database.initialize(settings.database_url) + except Exception as e: + pytest.fail(f"Failed to initialize the database: {str(e)}") + +# this function setup and tears down (drops tales) for each test function, so you have a clean database for each test. +@pytest.fixture(scope="function", autouse=True) +async def setup_database(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + yield + async with engine.begin() as conn: + # you can comment out this line during development if you are debugging a single test + await conn.run_sync(Base.metadata.drop_all) + await engine.dispose() + +@pytest.fixture(scope="function") +async def db_session(setup_database): + async with AsyncSessionScoped() as session: + try: + yield session + finally: + await session.close() + +@pytest.fixture(scope="function") +async def locked_user(db_session): + unique_email = fake.email() + user_data = { + "nickname": fake.user_name(), + "first_name": fake.first_name(), + "last_name": fake.last_name(), + "email": unique_email, + "hashed_password": hash_password("MySuperPassword$1234"), + "role": UserRole.AUTHENTICATED, + "email_verified": False, + "is_locked": True, + "failed_login_attempts": settings.max_login_attempts, + } + user = User(**user_data) + db_session.add(user) + await db_session.commit() + return user + +@pytest.fixture(scope="function") +async def user(db_session): + user_data = { + "nickname": fake.user_name(), + "first_name": fake.first_name(), + "last_name": fake.last_name(), + "email": fake.email(), + "hashed_password": hash_password("MySuperPassword$1234"), + "role": UserRole.AUTHENTICATED, + "email_verified": False, + "is_locked": False, + } + user = User(**user_data) + db_session.add(user) + await db_session.commit() + return user + +@pytest.fixture(scope="function") +async def verified_user(db_session): + user_data = { + "nickname": fake.user_name(), + "first_name": fake.first_name(), + "last_name": fake.last_name(), + "email": fake.email(), + "hashed_password": hash_password("MySuperPassword$1234"), + "role": UserRole.AUTHENTICATED, + "email_verified": True, + "is_locked": False, + } + user = User(**user_data) + db_session.add(user) + await db_session.commit() + return user + +@pytest.fixture(scope="function") +async def unverified_user(db_session): + user_data = { + "nickname": fake.user_name(), + "first_name": fake.first_name(), + "last_name": fake.last_name(), + "email": fake.email(), + "hashed_password": hash_password("MySuperPassword$1234"), + "role": UserRole.AUTHENTICATED, + "email_verified": False, + "is_locked": False, + } + user = User(**user_data) + db_session.add(user) + await db_session.commit() + return user + +@pytest.fixture(scope="function") +async def users_with_same_role_50_users(db_session): + users = [] + for _ in range(50): + user_data = { + "nickname": fake.user_name(), + "first_name": fake.first_name(), + "last_name": fake.last_name(), + "email": fake.email(), + "hashed_password": fake.password(), + "role": UserRole.AUTHENTICATED, + "email_verified": False, + "is_locked": False, + } + user = User(**user_data) + db_session.add(user) + users.append(user) + await db_session.commit() + return users + +@pytest.fixture +async def admin_user(db_session: AsyncSession): + user = User( + nickname="admin_user", + email="admin@example.com", + first_name="John", + last_name="Doe", + hashed_password="securepassword", + role=UserRole.ADMIN, + is_locked=False, + ) + db_session.add(user) + await db_session.commit() + return user + +@pytest.fixture +async def manager_user(db_session: AsyncSession): + user = User( + nickname="manager_john", + first_name="John", + last_name="Doe", + email="manager_user@example.com", + hashed_password="securepassword", + role=UserRole.MANAGER, + is_locked=False, + ) + db_session.add(user) + await db_session.commit() + return user + + +# Fixtures for common test data +@pytest.fixture +def user_base_data(): + return { + "username": "john_doe_123", + "email": "john.doe@example.com", + "full_name": "John Doe", + "bio": "I am a software engineer with over 5 years of experience.", + "profile_picture_url": "https://example.com/profile_pictures/john_doe.jpg" + } + +@pytest.fixture +def user_base_data_invalid(): + return { + "username": "john_doe_123", + "email": "john.doe.example.com", + "full_name": "John Doe", + "bio": "I am a software engineer with over 5 years of experience.", + "profile_picture_url": "https://example.com/profile_pictures/john_doe.jpg" + } + + +@pytest.fixture +def user_create_data(user_base_data): + return {**user_base_data, "password": "SecurePassword123!"} + +@pytest.fixture +def user_update_data(): + return { + "email": "john.doe.new@example.com", + "full_name": "John H. Doe", + "bio": "I specialize in backend development with Python and Node.js.", + "profile_picture_url": "https://example.com/profile_pictures/john_doe_updated.jpg" + } + +@pytest.fixture +def user_response_data(): + return { + "id": "unique-id-string", + "username": "testuser", + "email": "test@example.com", + "last_login_at": datetime.now(), + "created_at": datetime.now(), + "updated_at": datetime.now(), + "links": [] + } + +@pytest.fixture +def login_request_data(): return {"username": "john_doe_123", "password": "SecurePassword123!"} \ No newline at end of file diff --git a/tests/test_api/test_users_api.py b/tests/test_api/test_users_api.py index 7b497e1bd..b49cc3209 100644 --- a/tests/test_api/test_users_api.py +++ b/tests/test_api/test_users_api.py @@ -1,191 +1,191 @@ -from builtins import str -import pytest -from httpx import AsyncClient -from app.main import app -from app.models.user_model import User -from app.utils.nickname_gen import generate_nickname -from app.utils.security import hash_password -from app.services.jwt_service import decode_token # Import your FastAPI app - -# Example of a test function using the async_client fixture -@pytest.mark.asyncio -async def test_create_user_access_denied(async_client, user_token, email_service): - headers = {"Authorization": f"Bearer {user_token}"} - # Define user data for the test - user_data = { - "nickname": generate_nickname(), - "email": "test@example.com", - "password": "sS#fdasrongPassword123!", - } - # Send a POST request to create a user - response = await async_client.post("/users/", json=user_data, headers=headers) - # Asserts - assert response.status_code == 403 - -# You can similarly refactor other test functions to use the async_client fixture -@pytest.mark.asyncio -async def test_retrieve_user_access_denied(async_client, verified_user, user_token): - headers = {"Authorization": f"Bearer {user_token}"} - response = await async_client.get(f"/users/{verified_user.id}", headers=headers) - assert response.status_code == 403 - -@pytest.mark.asyncio -async def test_retrieve_user_access_allowed(async_client, admin_user, admin_token): - headers = {"Authorization": f"Bearer {admin_token}"} - response = await async_client.get(f"/users/{admin_user.id}", headers=headers) - assert response.status_code == 200 - assert response.json()["id"] == str(admin_user.id) - -@pytest.mark.asyncio -async def test_update_user_email_access_denied(async_client, verified_user, user_token): - updated_data = {"email": f"updated_{verified_user.id}@example.com"} - headers = {"Authorization": f"Bearer {user_token}"} - response = await async_client.put(f"/users/{verified_user.id}", json=updated_data, headers=headers) - assert response.status_code == 403 - -@pytest.mark.asyncio -async def test_update_user_email_access_allowed(async_client, admin_user, admin_token): - updated_data = {"email": f"updated_{admin_user.id}@example.com"} - headers = {"Authorization": f"Bearer {admin_token}"} - response = await async_client.put(f"/users/{admin_user.id}", json=updated_data, headers=headers) - assert response.status_code == 200 - assert response.json()["email"] == updated_data["email"] - - -@pytest.mark.asyncio -async def test_delete_user(async_client, admin_user, admin_token): - headers = {"Authorization": f"Bearer {admin_token}"} - delete_response = await async_client.delete(f"/users/{admin_user.id}", headers=headers) - assert delete_response.status_code == 204 - # Verify the user is deleted - fetch_response = await async_client.get(f"/users/{admin_user.id}", headers=headers) - assert fetch_response.status_code == 404 - -@pytest.mark.asyncio -async def test_create_user_duplicate_email(async_client, verified_user): - user_data = { - "email": verified_user.email, - "password": "AnotherPassword123!", - } - response = await async_client.post("/register/", json=user_data) - assert response.status_code == 400 - assert "Email already exists" in response.json().get("detail", "") - -@pytest.mark.asyncio -async def test_create_user_invalid_email(async_client): - user_data = { - "email": "notanemail", - "password": "ValidPassword123!", - } - response = await async_client.post("/register/", json=user_data) - assert response.status_code == 422 - -import pytest -from app.services.jwt_service import decode_token -from urllib.parse import urlencode - -@pytest.mark.asyncio -async def test_login_success(async_client, verified_user): - # Attempt to login with the test user - form_data = { - "username": verified_user.email, - "password": "MySuperPassword$1234" - } - response = await async_client.post("/login/", data=urlencode(form_data), headers={"Content-Type": "application/x-www-form-urlencoded"}) - - # Check for successful login response - assert response.status_code == 200 - data = response.json() - assert "access_token" in data - assert data["token_type"] == "bearer" - - # Use the decode_token method from jwt_service to decode the JWT - decoded_token = decode_token(data["access_token"]) - assert decoded_token is not None, "Failed to decode token" - assert decoded_token["role"] == "AUTHENTICATED", "The user role should be AUTHENTICATED" - -@pytest.mark.asyncio -async def test_login_user_not_found(async_client): - form_data = { - "username": "nonexistentuser@here.edu", - "password": "DoesNotMatter123!" - } - response = await async_client.post("/login/", data=urlencode(form_data), headers={"Content-Type": "application/x-www-form-urlencoded"}) - assert response.status_code == 401 - assert "Incorrect email or password." in response.json().get("detail", "") - -@pytest.mark.asyncio -async def test_login_incorrect_password(async_client, verified_user): - form_data = { - "username": verified_user.email, - "password": "IncorrectPassword123!" - } - response = await async_client.post("/login/", data=urlencode(form_data), headers={"Content-Type": "application/x-www-form-urlencoded"}) - assert response.status_code == 401 - assert "Incorrect email or password." in response.json().get("detail", "") - -@pytest.mark.asyncio -async def test_login_unverified_user(async_client, unverified_user): - form_data = { - "username": unverified_user.email, - "password": "MySuperPassword$1234" - } - response = await async_client.post("/login/", data=urlencode(form_data), headers={"Content-Type": "application/x-www-form-urlencoded"}) - assert response.status_code == 401 - -@pytest.mark.asyncio -async def test_login_locked_user(async_client, locked_user): - form_data = { - "username": locked_user.email, - "password": "MySuperPassword$1234" - } - response = await async_client.post("/login/", data=urlencode(form_data), headers={"Content-Type": "application/x-www-form-urlencoded"}) - assert response.status_code == 400 - assert "Account locked due to too many failed login attempts." in response.json().get("detail", "") -@pytest.mark.asyncio -async def test_delete_user_does_not_exist(async_client, admin_token): - non_existent_user_id = "00000000-0000-0000-0000-000000000000" # Valid UUID format - headers = {"Authorization": f"Bearer {admin_token}"} - delete_response = await async_client.delete(f"/users/{non_existent_user_id}", headers=headers) - assert delete_response.status_code == 404 - -@pytest.mark.asyncio -async def test_update_user_github(async_client, admin_user, admin_token): - updated_data = {"github_profile_url": "http://www.github.com/kaw393939"} - headers = {"Authorization": f"Bearer {admin_token}"} - response = await async_client.put(f"/users/{admin_user.id}", json=updated_data, headers=headers) - assert response.status_code == 200 - assert response.json()["github_profile_url"] == updated_data["github_profile_url"] - -@pytest.mark.asyncio -async def test_update_user_linkedin(async_client, admin_user, admin_token): - updated_data = {"linkedin_profile_url": "http://www.linkedin.com/kaw393939"} - headers = {"Authorization": f"Bearer {admin_token}"} - response = await async_client.put(f"/users/{admin_user.id}", json=updated_data, headers=headers) - assert response.status_code == 200 - assert response.json()["linkedin_profile_url"] == updated_data["linkedin_profile_url"] - -@pytest.mark.asyncio -async def test_list_users_as_admin(async_client, admin_token): - response = await async_client.get( - "/users/", - headers={"Authorization": f"Bearer {admin_token}"} - ) - assert response.status_code == 200 - assert 'items' in response.json() - -@pytest.mark.asyncio -async def test_list_users_as_manager(async_client, manager_token): - response = await async_client.get( - "/users/", - headers={"Authorization": f"Bearer {manager_token}"} - ) - assert response.status_code == 200 - -@pytest.mark.asyncio -async def test_list_users_unauthorized(async_client, user_token): - response = await async_client.get( - "/users/", - headers={"Authorization": f"Bearer {user_token}"} - ) - assert response.status_code == 403 # Forbidden, as expected for regular user +from builtins import str +import pytest +from httpx import AsyncClient +from app.main import app +from app.models.user_model import User +from app.utils.nickname_gen import generate_nickname +from app.utils.security import hash_password +from app.services.jwt_service import decode_token # Import your FastAPI app + +# Example of a test function using the async_client fixture +@pytest.mark.asyncio +async def test_create_user_access_denied(async_client, user_token, email_service): + headers = {"Authorization": f"Bearer {user_token}"} + # Define user data for the test + user_data = { + "nickname": generate_nickname(), + "email": "test@example.com", + "password": "sS#fdasrongPassword123!", + } + # Send a POST request to create a user + response = await async_client.post("/users/", json=user_data, headers=headers) + # Asserts + assert response.status_code == 403 + +# You can similarly refactor other test functions to use the async_client fixture +@pytest.mark.asyncio +async def test_retrieve_user_access_denied(async_client, verified_user, user_token): + headers = {"Authorization": f"Bearer {user_token}"} + response = await async_client.get(f"/users/{verified_user.id}", headers=headers) + assert response.status_code == 403 + +@pytest.mark.asyncio +async def test_retrieve_user_access_allowed(async_client, admin_user, admin_token): + headers = {"Authorization": f"Bearer {admin_token}"} + response = await async_client.get(f"/users/{admin_user.id}", headers=headers) + assert response.status_code == 200 + assert response.json()["id"] == str(admin_user.id) + +@pytest.mark.asyncio +async def test_update_user_email_access_denied(async_client, verified_user, user_token): + updated_data = {"email": f"updated_{verified_user.id}@example.com"} + headers = {"Authorization": f"Bearer {user_token}"} + response = await async_client.put(f"/users/{verified_user.id}", json=updated_data, headers=headers) + assert response.status_code == 403 + +@pytest.mark.asyncio +async def test_update_user_email_access_allowed(async_client, admin_user, admin_token): + updated_data = {"email": f"updated_{admin_user.id}@example.com"} + headers = {"Authorization": f"Bearer {admin_token}"} + response = await async_client.put(f"/users/{admin_user.id}", json=updated_data, headers=headers) + assert response.status_code == 200 + assert response.json()["email"] == updated_data["email"] + + +@pytest.mark.asyncio +async def test_delete_user(async_client, admin_user, admin_token): + headers = {"Authorization": f"Bearer {admin_token}"} + delete_response = await async_client.delete(f"/users/{admin_user.id}", headers=headers) + assert delete_response.status_code == 204 + # Verify the user is deleted + fetch_response = await async_client.get(f"/users/{admin_user.id}", headers=headers) + assert fetch_response.status_code == 404 + +@pytest.mark.asyncio +async def test_create_user_duplicate_email(async_client, verified_user): + user_data = { + "email": verified_user.email, + "password": "AnotherPassword123!", + } + response = await async_client.post("/register/", json=user_data) + assert response.status_code == 400 + assert "Email already exists" in response.json().get("detail", "") + +@pytest.mark.asyncio +async def test_create_user_invalid_email(async_client): + user_data = { + "email": "notanemail", + "password": "ValidPassword123!", + } + response = await async_client.post("/register/", json=user_data) + assert response.status_code == 422 + +import pytest +from app.services.jwt_service import decode_token +from urllib.parse import urlencode + +@pytest.mark.asyncio +async def test_login_success(async_client, verified_user): + # Attempt to login with the test user + form_data = { + "username": verified_user.email, + "password": "MySuperPassword$1234" + } + response = await async_client.post("/login/", data=urlencode(form_data), headers={"Content-Type": "application/x-www-form-urlencoded"}) + + # Check for successful login response + assert response.status_code == 200 + data = response.json() + assert "access_token" in data + assert data["token_type"] == "bearer" + + # Use the decode_token method from jwt_service to decode the JWT + decoded_token = decode_token(data["access_token"]) + assert decoded_token is not None, "Failed to decode token" + assert decoded_token["role"] == "AUTHENTICATED", "The user role should be AUTHENTICATED" + +@pytest.mark.asyncio +async def test_login_user_not_found(async_client): + form_data = { + "username": "nonexistentuser@here.edu", + "password": "DoesNotMatter123!" + } + response = await async_client.post("/login/", data=urlencode(form_data), headers={"Content-Type": "application/x-www-form-urlencoded"}) + assert response.status_code == 401 + assert "Incorrect email or password." in response.json().get("detail", "") + +@pytest.mark.asyncio +async def test_login_incorrect_password(async_client, verified_user): + form_data = { + "username": verified_user.email, + "password": "IncorrectPassword123!" + } + response = await async_client.post("/login/", data=urlencode(form_data), headers={"Content-Type": "application/x-www-form-urlencoded"}) + assert response.status_code == 401 + assert "Incorrect email or password." in response.json().get("detail", "") + +@pytest.mark.asyncio +async def test_login_unverified_user(async_client, unverified_user): + form_data = { + "username": unverified_user.email, + "password": "MySuperPassword$1234" + } + response = await async_client.post("/login/", data=urlencode(form_data), headers={"Content-Type": "application/x-www-form-urlencoded"}) + assert response.status_code == 401 + +@pytest.mark.asyncio +async def test_login_locked_user(async_client, locked_user): + form_data = { + "username": locked_user.email, + "password": "MySuperPassword$1234" + } + response = await async_client.post("/login/", data=urlencode(form_data), headers={"Content-Type": "application/x-www-form-urlencoded"}) + assert response.status_code == 400 + assert "Account locked due to too many failed login attempts." in response.json().get("detail", "") +@pytest.mark.asyncio +async def test_delete_user_does_not_exist(async_client, admin_token): + non_existent_user_id = "00000000-0000-0000-0000-000000000000" # Valid UUID format + headers = {"Authorization": f"Bearer {admin_token}"} + delete_response = await async_client.delete(f"/users/{non_existent_user_id}", headers=headers) + assert delete_response.status_code == 404 + +@pytest.mark.asyncio +async def test_update_user_github(async_client, admin_user, admin_token): + updated_data = {"github_profile_url": "http://www.github.com/kaw393939"} + headers = {"Authorization": f"Bearer {admin_token}"} + response = await async_client.put(f"/users/{admin_user.id}", json=updated_data, headers=headers) + assert response.status_code == 200 + assert response.json()["github_profile_url"] == updated_data["github_profile_url"] + +@pytest.mark.asyncio +async def test_update_user_linkedin(async_client, admin_user, admin_token): + updated_data = {"linkedin_profile_url": "http://www.linkedin.com/kaw393939"} + headers = {"Authorization": f"Bearer {admin_token}"} + response = await async_client.put(f"/users/{admin_user.id}", json=updated_data, headers=headers) + assert response.status_code == 200 + assert response.json()["linkedin_profile_url"] == updated_data["linkedin_profile_url"] + +@pytest.mark.asyncio +async def test_list_users_as_admin(async_client, admin_token): + response = await async_client.get( + "/users/", + headers={"Authorization": f"Bearer {admin_token}"} + ) + assert response.status_code == 200 + assert 'items' in response.json() + +@pytest.mark.asyncio +async def test_list_users_as_manager(async_client, manager_token): + response = await async_client.get( + "/users/", + headers={"Authorization": f"Bearer {manager_token}"} + ) + assert response.status_code == 200 + +@pytest.mark.asyncio +async def test_list_users_unauthorized(async_client, user_token): + response = await async_client.get( + "/users/", + headers={"Authorization": f"Bearer {user_token}"} + ) + assert response.status_code == 403 # Forbidden, as expected for regular user diff --git a/tests/test_conftest.py b/tests/test_conftest.py index f86b2da36..b7161bddb 100644 --- a/tests/test_conftest.py +++ b/tests/test_conftest.py @@ -1,64 +1,64 @@ -# test_users.py - -from builtins import len -import pytest -from httpx import AsyncClient -from sqlalchemy.future import select - -from app.models.user_model import User, UserRole -from app.utils.security import verify_password - -@pytest.mark.asyncio -async def test_user_creation(db_session, verified_user): - """Test that a user is correctly created and stored in the database.""" - result = await db_session.execute(select(User).filter_by(email=verified_user.email)) - stored_user = result.scalars().first() - assert stored_user is not None - assert stored_user.email == verified_user.email - assert verify_password("MySuperPassword$1234", stored_user.hashed_password) - -# Apply similar corrections to other test functions -@pytest.mark.asyncio -async def test_locked_user(db_session, locked_user): - result = await db_session.execute(select(User).filter_by(email=locked_user.email)) - stored_user = result.scalars().first() - assert stored_user.is_locked - -@pytest.mark.asyncio -async def test_verified_user(db_session, verified_user): - result = await db_session.execute(select(User).filter_by(email=verified_user.email)) - stored_user = result.scalars().first() - assert stored_user.email_verified - -@pytest.mark.asyncio -async def test_user_role(db_session, admin_user): - result = await db_session.execute(select(User).filter_by(email=admin_user.email)) - stored_user = result.scalars().first() - assert stored_user.role == UserRole.ADMIN - -@pytest.mark.asyncio -async def test_bulk_user_creation_performance(db_session, users_with_same_role_50_users): - result = await db_session.execute(select(User).filter_by(role=UserRole.AUTHENTICATED)) - users = result.scalars().all() - assert len(users) == 50 - -@pytest.mark.asyncio -async def test_password_hashing(user): - assert verify_password("MySuperPassword$1234", user.hashed_password) - -@pytest.mark.asyncio -async def test_user_unlock(db_session, locked_user): - locked_user.unlock_account() - await db_session.commit() - result = await db_session.execute(select(User).filter_by(email=locked_user.email)) - updated_user = result.scalars().first() - assert not updated_user.is_locked - -@pytest.mark.asyncio -async def test_update_professional_status(db_session, verified_user): - verified_user.update_professional_status(True) - await db_session.commit() - result = await db_session.execute(select(User).filter_by(email=verified_user.email)) - updated_user = result.scalars().first() - assert updated_user.is_professional - assert updated_user.professional_status_updated_at is not None +# test_users.py + +from builtins import len +import pytest +from httpx import AsyncClient +from sqlalchemy.future import select + +from app.models.user_model import User, UserRole +from app.utils.security import verify_password + +@pytest.mark.asyncio +async def test_user_creation(db_session, verified_user): + """Test that a user is correctly created and stored in the database.""" + result = await db_session.execute(select(User).filter_by(email=verified_user.email)) + stored_user = result.scalars().first() + assert stored_user is not None + assert stored_user.email == verified_user.email + assert verify_password("MySuperPassword$1234", stored_user.hashed_password) + +# Apply similar corrections to other test functions +@pytest.mark.asyncio +async def test_locked_user(db_session, locked_user): + result = await db_session.execute(select(User).filter_by(email=locked_user.email)) + stored_user = result.scalars().first() + assert stored_user.is_locked + +@pytest.mark.asyncio +async def test_verified_user(db_session, verified_user): + result = await db_session.execute(select(User).filter_by(email=verified_user.email)) + stored_user = result.scalars().first() + assert stored_user.email_verified + +@pytest.mark.asyncio +async def test_user_role(db_session, admin_user): + result = await db_session.execute(select(User).filter_by(email=admin_user.email)) + stored_user = result.scalars().first() + assert stored_user.role == UserRole.ADMIN + +@pytest.mark.asyncio +async def test_bulk_user_creation_performance(db_session, users_with_same_role_50_users): + result = await db_session.execute(select(User).filter_by(role=UserRole.AUTHENTICATED)) + users = result.scalars().all() + assert len(users) == 50 + +@pytest.mark.asyncio +async def test_password_hashing(user): + assert verify_password("MySuperPassword$1234", user.hashed_password) + +@pytest.mark.asyncio +async def test_user_unlock(db_session, locked_user): + locked_user.unlock_account() + await db_session.commit() + result = await db_session.execute(select(User).filter_by(email=locked_user.email)) + updated_user = result.scalars().first() + assert not updated_user.is_locked + +@pytest.mark.asyncio +async def test_update_professional_status(db_session, verified_user): + verified_user.update_professional_status(True) + await db_session.commit() + result = await db_session.execute(select(User).filter_by(email=verified_user.email)) + updated_user = result.scalars().first() + assert updated_user.is_professional + assert updated_user.professional_status_updated_at is not None diff --git a/tests/test_email.py b/tests/test_email.py index 9c0c22f81..a21ccd622 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -1,14 +1,14 @@ -import pytest -from app.services.email_service import EmailService -from app.utils.template_manager import TemplateManager - - -@pytest.mark.asyncio -async def test_send_markdown_email(email_service): - user_data = { - "email": "test@example.com", - "name": "Test User", - "verification_url": "http://example.com/verify?token=abc123" - } - await email_service.send_user_email(user_data, 'email_verification') - # Manual verification in Mailtrap +import pytest +from app.services.email_service import EmailService +from app.utils.template_manager import TemplateManager + + +@pytest.mark.asyncio +async def test_send_markdown_email(email_service): + user_data = { + "email": "test@example.com", + "name": "Test User", + "verification_url": "http://example.com/verify?token=abc123" + } + await email_service.send_user_email(user_data, 'email_verification') + # Manual verification in Mailtrap diff --git a/tests/test_link_generation.py b/tests/test_link_generation.py index 37faa88ac..bdac3ec63 100644 --- a/tests/test_link_generation.py +++ b/tests/test_link_generation.py @@ -1,51 +1,51 @@ -from builtins import len, max, sorted, str -from unittest.mock import MagicMock -from urllib.parse import parse_qs, urlparse, parse_qsl, urlunparse, urlencode -from uuid import uuid4 - -import pytest -from fastapi import Request - -from app.utils.link_generation import create_link, create_pagination_link, create_user_links, generate_pagination_links - -from urllib.parse import urlparse, parse_qs, urlunparse, urlencode - -def normalize_url(url): - """Normalize the URL for consistent comparison by sorting query parameters.""" - parsed_url = urlparse(url) - query_params = parse_qs(parsed_url.query, keep_blank_values=True) - # Sort the query parameters by key, and sort their values if there are multiple for a single key - sorted_query_items = sorted((k, sorted(v)) for k, v in query_params.items()) - # Convert the sorted query parameters back to a query string - encoded_query = urlencode(sorted_query_items, doseq=True) - normalized_url = urlunparse(parsed_url._replace(query=encoded_query)) - return normalized_url.rstrip('/') - - -@pytest.fixture -def mock_request(): - request = MagicMock(spec=Request) - request.url_for = MagicMock(side_effect=lambda action, user_id: f"http://testserver/{action}/{user_id}") - request.url = "http://testserver/users" - return request - -def test_create_link(): - link = create_link("self", "http://example.com", "GET", "view") - assert normalize_url(str(link.href)) == "http://example.com" - -def test_create_user_links(mock_request): - user_id = uuid4() - links = create_user_links(user_id, mock_request) - assert len(links) == 3 - assert normalize_url(str(links[0].href)) == f"http://testserver/get_user/{user_id}" - assert normalize_url(str(links[1].href)) == f"http://testserver/update_user/{user_id}" - assert normalize_url(str(links[2].href)) == f"http://testserver/delete_user/{user_id}" - -def test_generate_pagination_links(mock_request): - skip = 10 - limit = 5 - total_items = 50 - links = generate_pagination_links(mock_request, skip, limit, total_items) - assert len(links) >= 4 - expected_self_url = "http://testserver/users?limit=5&skip=10" - assert normalize_url(str(links[0].href)) == normalize_url(expected_self_url), "Self link should match expected URL" +from builtins import len, max, sorted, str +from unittest.mock import MagicMock +from urllib.parse import parse_qs, urlparse, parse_qsl, urlunparse, urlencode +from uuid import uuid4 + +import pytest +from fastapi import Request + +from app.utils.link_generation import create_link, create_pagination_link, create_user_links, generate_pagination_links + +from urllib.parse import urlparse, parse_qs, urlunparse, urlencode + +def normalize_url(url): + """Normalize the URL for consistent comparison by sorting query parameters.""" + parsed_url = urlparse(url) + query_params = parse_qs(parsed_url.query, keep_blank_values=True) + # Sort the query parameters by key, and sort their values if there are multiple for a single key + sorted_query_items = sorted((k, sorted(v)) for k, v in query_params.items()) + # Convert the sorted query parameters back to a query string + encoded_query = urlencode(sorted_query_items, doseq=True) + normalized_url = urlunparse(parsed_url._replace(query=encoded_query)) + return normalized_url.rstrip('/') + + +@pytest.fixture +def mock_request(): + request = MagicMock(spec=Request) + request.url_for = MagicMock(side_effect=lambda action, user_id: f"http://testserver/{action}/{user_id}") + request.url = "http://testserver/users" + return request + +def test_create_link(): + link = create_link("self", "http://example.com", "GET", "view") + assert normalize_url(str(link.href)) == "http://example.com" + +def test_create_user_links(mock_request): + user_id = uuid4() + links = create_user_links(user_id, mock_request) + assert len(links) == 3 + assert normalize_url(str(links[0].href)) == f"http://testserver/get_user/{user_id}" + assert normalize_url(str(links[1].href)) == f"http://testserver/update_user/{user_id}" + assert normalize_url(str(links[2].href)) == f"http://testserver/delete_user/{user_id}" + +def test_generate_pagination_links(mock_request): + skip = 10 + limit = 5 + total_items = 50 + links = generate_pagination_links(mock_request, skip, limit, total_items) + assert len(links) >= 4 + expected_self_url = "http://testserver/users?limit=5&skip=10" + assert normalize_url(str(links[0].href)) == normalize_url(expected_self_url), "Self link should match expected URL" diff --git a/tests/test_models/test_user_model.py b/tests/test_models/test_user_model.py index 7deb6e393..81dea3d47 100644 --- a/tests/test_models/test_user_model.py +++ b/tests/test_models/test_user_model.py @@ -1,152 +1,152 @@ -from builtins import repr -from datetime import datetime, timezone -import pytest -from sqlalchemy.ext.asyncio import AsyncSession -from app.models.user_model import User, UserRole - -@pytest.mark.asyncio -async def test_user_role(db_session: AsyncSession, user: User, admin_user: User, manager_user: User): - """ - Tests that the default role is assigned correctly and can be updated. - """ - assert user.role == UserRole.AUTHENTICATED, "Default role should be USER" - assert admin_user.role == UserRole.ADMIN, "Admin role should be correctly assigned" - assert manager_user.role == UserRole.MANAGER, "Pro role should be correctly assigned" - -@pytest.mark.asyncio -async def test_has_role(user: User, admin_user: User, manager_user: User): - """ - Tests the has_role method to ensure it accurately checks the user's role. - """ - assert user.has_role(UserRole.AUTHENTICATED), "User should have USER role" - assert not user.has_role(UserRole.ADMIN), "User should not have ADMIN role" - assert admin_user.has_role(UserRole.ADMIN), "Admin user should have ADMIN role" - assert manager_user.has_role(UserRole.MANAGER), "Pro user should have PRO role" - -@pytest.mark.asyncio -async def test_user_repr(user: User): - """ - Tests the __repr__ method for accurate representation of the User object. - """ - assert repr(user) == f"", "__repr__ should include nickname and role" - -@pytest.mark.asyncio -async def test_failed_login_attempts_increment(db_session: AsyncSession, user: User): - """ - Tests that failed login attempts can be incremented and persisted correctly. - """ - initial_attempts = user.failed_login_attempts - user.failed_login_attempts += 1 - await db_session.commit() - await db_session.refresh(user) - assert user.failed_login_attempts == initial_attempts + 1, "Failed login attempts should increment" - -@pytest.mark.asyncio -async def test_last_login_update(db_session: AsyncSession, user: User): - """ - Tests updating the last login timestamp. - """ - new_last_login = datetime.now(timezone.utc) - user.last_login_at = new_last_login - await db_session.commit() - await db_session.refresh(user) - assert user.last_login_at == new_last_login, "Last login timestamp should update correctly" - -@pytest.mark.asyncio -async def test_account_lock_and_unlock(db_session: AsyncSession, user: User): - """ - Tests locking and unlocking the user account. - """ - # Initially, the account should not be locked. - assert not user.is_locked, "Account should initially be unlocked" - - # Lock the account and verify. - user.lock_account() - await db_session.commit() - await db_session.refresh(user) - assert user.is_locked, "Account should be locked after calling lock_account()" - - # Unlock the account and verify. - user.unlock_account() - await db_session.commit() - await db_session.refresh(user) - assert not user.is_locked, "Account should be unlocked after calling unlock_account()" - -@pytest.mark.asyncio -async def test_email_verification(db_session: AsyncSession, user: User): - """ - Tests the email verification functionality. - """ - # Initially, the email should not be verified. - assert not user.email_verified, "Email should initially be unverified" - - # Verify the email and check. - user.verify_email() - await db_session.commit() - await db_session.refresh(user) - assert user.email_verified, "Email should be verified after calling verify_email()" - -@pytest.mark.asyncio -async def test_user_profile_pic_url_update(db_session: AsyncSession, user: User): - """ - Tests the profile pic update functionality. - """ - # Initially, the profile pic should be updated. - - # Verify the email and check. - profile_pic_url = "http://myprofile/picture.png" - user.profile_picture_url = profile_pic_url - await db_session.commit() - await db_session.refresh(user) - assert user.profile_picture_url == profile_pic_url, "The profile pic did not update" - -@pytest.mark.asyncio -async def test_user_linkedin_url_update(db_session: AsyncSession, user: User): - """ - Tests the profile pic update functionality. - """ - # Initially, the linkedin should be updated. - - # Verify the linkedin profile url. - profile_linkedin_url = "http://www.linkedin.com/profile" - user.linkedin_profile_url = profile_linkedin_url - await db_session.commit() - await db_session.refresh(user) - assert user.linkedin_profile_url == profile_linkedin_url, "The profile pic did not update" - - -@pytest.mark.asyncio -async def test_user_github_url_update(db_session: AsyncSession, user: User): - """ - Tests the profile pic update functionality. - """ - # Initially, the linkedin should be updated. - - # Verify the linkedin profile url. - profile_github_url = "http://www.github.com/profile" - user.github_profile_url = profile_github_url - await db_session.commit() - await db_session.refresh(user) - assert user.github_profile_url == profile_github_url, "The github did not update" - - -@pytest.mark.asyncio -async def test_default_role_assignment(db_session: AsyncSession): - """ - Tests that a user without a specified role defaults to 'anonymous' or the expected default role. - """ - user = User(nickname="noob", email="newuser@example.com", hashed_password="hashed_password") - db_session.add(user) - await db_session.commit() - await db_session.refresh(user) - assert user.role == UserRole.ANONYMOUS, "Default role should be 'anonymous' if not specified" - -@pytest.mark.asyncio -async def test_update_user_role(db_session: AsyncSession, user: User): - """ - Tests updating the user's role and ensuring it persists correctly. - """ - user.role = UserRole.ADMIN - await db_session.commit() - await db_session.refresh(user) - assert user.role == UserRole.ADMIN, "Role update should persist correctly in the database" +from builtins import repr +from datetime import datetime, timezone +import pytest +from sqlalchemy.ext.asyncio import AsyncSession +from app.models.user_model import User, UserRole + +@pytest.mark.asyncio +async def test_user_role(db_session: AsyncSession, user: User, admin_user: User, manager_user: User): + """ + Tests that the default role is assigned correctly and can be updated. + """ + assert user.role == UserRole.AUTHENTICATED, "Default role should be USER" + assert admin_user.role == UserRole.ADMIN, "Admin role should be correctly assigned" + assert manager_user.role == UserRole.MANAGER, "Pro role should be correctly assigned" + +@pytest.mark.asyncio +async def test_has_role(user: User, admin_user: User, manager_user: User): + """ + Tests the has_role method to ensure it accurately checks the user's role. + """ + assert user.has_role(UserRole.AUTHENTICATED), "User should have USER role" + assert not user.has_role(UserRole.ADMIN), "User should not have ADMIN role" + assert admin_user.has_role(UserRole.ADMIN), "Admin user should have ADMIN role" + assert manager_user.has_role(UserRole.MANAGER), "Pro user should have PRO role" + +@pytest.mark.asyncio +async def test_user_repr(user: User): + """ + Tests the __repr__ method for accurate representation of the User object. + """ + assert repr(user) == f"", "__repr__ should include nickname and role" + +@pytest.mark.asyncio +async def test_failed_login_attempts_increment(db_session: AsyncSession, user: User): + """ + Tests that failed login attempts can be incremented and persisted correctly. + """ + initial_attempts = user.failed_login_attempts + user.failed_login_attempts += 1 + await db_session.commit() + await db_session.refresh(user) + assert user.failed_login_attempts == initial_attempts + 1, "Failed login attempts should increment" + +@pytest.mark.asyncio +async def test_last_login_update(db_session: AsyncSession, user: User): + """ + Tests updating the last login timestamp. + """ + new_last_login = datetime.now(timezone.utc) + user.last_login_at = new_last_login + await db_session.commit() + await db_session.refresh(user) + assert user.last_login_at == new_last_login, "Last login timestamp should update correctly" + +@pytest.mark.asyncio +async def test_account_lock_and_unlock(db_session: AsyncSession, user: User): + """ + Tests locking and unlocking the user account. + """ + # Initially, the account should not be locked. + assert not user.is_locked, "Account should initially be unlocked" + + # Lock the account and verify. + user.lock_account() + await db_session.commit() + await db_session.refresh(user) + assert user.is_locked, "Account should be locked after calling lock_account()" + + # Unlock the account and verify. + user.unlock_account() + await db_session.commit() + await db_session.refresh(user) + assert not user.is_locked, "Account should be unlocked after calling unlock_account()" + +@pytest.mark.asyncio +async def test_email_verification(db_session: AsyncSession, user: User): + """ + Tests the email verification functionality. + """ + # Initially, the email should not be verified. + assert not user.email_verified, "Email should initially be unverified" + + # Verify the email and check. + user.verify_email() + await db_session.commit() + await db_session.refresh(user) + assert user.email_verified, "Email should be verified after calling verify_email()" + +@pytest.mark.asyncio +async def test_user_profile_pic_url_update(db_session: AsyncSession, user: User): + """ + Tests the profile pic update functionality. + """ + # Initially, the profile pic should be updated. + + # Verify the email and check. + profile_pic_url = "http://myprofile/picture.png" + user.profile_picture_url = profile_pic_url + await db_session.commit() + await db_session.refresh(user) + assert user.profile_picture_url == profile_pic_url, "The profile pic did not update" + +@pytest.mark.asyncio +async def test_user_linkedin_url_update(db_session: AsyncSession, user: User): + """ + Tests the profile pic update functionality. + """ + # Initially, the linkedin should be updated. + + # Verify the linkedin profile url. + profile_linkedin_url = "http://www.linkedin.com/profile" + user.linkedin_profile_url = profile_linkedin_url + await db_session.commit() + await db_session.refresh(user) + assert user.linkedin_profile_url == profile_linkedin_url, "The profile pic did not update" + + +@pytest.mark.asyncio +async def test_user_github_url_update(db_session: AsyncSession, user: User): + """ + Tests the profile pic update functionality. + """ + # Initially, the linkedin should be updated. + + # Verify the linkedin profile url. + profile_github_url = "http://www.github.com/profile" + user.github_profile_url = profile_github_url + await db_session.commit() + await db_session.refresh(user) + assert user.github_profile_url == profile_github_url, "The github did not update" + + +@pytest.mark.asyncio +async def test_default_role_assignment(db_session: AsyncSession): + """ + Tests that a user without a specified role defaults to 'anonymous' or the expected default role. + """ + user = User(nickname="noob", email="newuser@example.com", hashed_password="hashed_password") + db_session.add(user) + await db_session.commit() + await db_session.refresh(user) + assert user.role == UserRole.ANONYMOUS, "Default role should be 'anonymous' if not specified" + +@pytest.mark.asyncio +async def test_update_user_role(db_session: AsyncSession, user: User): + """ + Tests updating the user's role and ensuring it persists correctly. + """ + user.role = UserRole.ADMIN + await db_session.commit() + await db_session.refresh(user) + assert user.role == UserRole.ADMIN, "Role update should persist correctly in the database" diff --git a/tests/test_schemas/test_user_schemas.py b/tests/test_schemas/test_user_schemas.py index c5b92e148..a229a83e0 100644 --- a/tests/test_schemas/test_user_schemas.py +++ b/tests/test_schemas/test_user_schemas.py @@ -1,69 +1,69 @@ -from builtins import str -import pytest -from pydantic import ValidationError -from datetime import datetime -from app.schemas.user_schemas import UserBase, UserCreate, UserUpdate, UserResponse, UserListResponse, LoginRequest - -# Tests for UserBase -def test_user_base_valid(user_base_data): - user = UserBase(**user_base_data) - assert user.nickname == user_base_data["nickname"] - assert user.email == user_base_data["email"] - -# Tests for UserCreate -def test_user_create_valid(user_create_data): - user = UserCreate(**user_create_data) - assert user.nickname == user_create_data["nickname"] - assert user.password == user_create_data["password"] - -# Tests for UserUpdate -def test_user_update_valid(user_update_data): - user_update = UserUpdate(**user_update_data) - assert user_update.email == user_update_data["email"] - assert user_update.first_name == user_update_data["first_name"] - -# Tests for UserResponse -def test_user_response_valid(user_response_data): - user = UserResponse(**user_response_data) - assert user.id == user_response_data["id"] - # assert user.last_login_at == user_response_data["last_login_at"] - -# Tests for LoginRequest -def test_login_request_valid(login_request_data): - login = LoginRequest(**login_request_data) - assert login.email == login_request_data["email"] - assert login.password == login_request_data["password"] - -# Parametrized tests for nickname and email validation -@pytest.mark.parametrize("nickname", ["test_user", "test-user", "testuser123", "123test"]) -def test_user_base_nickname_valid(nickname, user_base_data): - user_base_data["nickname"] = nickname - user = UserBase(**user_base_data) - assert user.nickname == nickname - -@pytest.mark.parametrize("nickname", ["test user", "test?user", "", "us"]) -def test_user_base_nickname_invalid(nickname, user_base_data): - user_base_data["nickname"] = nickname - with pytest.raises(ValidationError): - UserBase(**user_base_data) - -# Parametrized tests for URL validation -@pytest.mark.parametrize("url", ["http://valid.com/profile.jpg", "https://valid.com/profile.png", None]) -def test_user_base_url_valid(url, user_base_data): - user_base_data["profile_picture_url"] = url - user = UserBase(**user_base_data) - assert user.profile_picture_url == url - -@pytest.mark.parametrize("url", ["ftp://invalid.com/profile.jpg", "http//invalid", "https//invalid"]) -def test_user_base_url_invalid(url, user_base_data): - user_base_data["profile_picture_url"] = url - with pytest.raises(ValidationError): - UserBase(**user_base_data) - -# Tests for UserBase -def test_user_base_invalid_email(user_base_data_invalid): - with pytest.raises(ValidationError) as exc_info: - user = UserBase(**user_base_data_invalid) - - assert "value is not a valid email address" in str(exc_info.value) +from builtins import str +import pytest +from pydantic import ValidationError +from datetime import datetime +from app.schemas.user_schemas import UserBase, UserCreate, UserUpdate, UserResponse, UserListResponse, LoginRequest + +# Tests for UserBase +def test_user_base_valid(user_base_data): + user = UserBase(**user_base_data) + assert user.nickname == user_base_data["nickname"] + assert user.email == user_base_data["email"] + +# Tests for UserCreate +def test_user_create_valid(user_create_data): + user = UserCreate(**user_create_data) + assert user.nickname == user_create_data["nickname"] + assert user.password == user_create_data["password"] + +# Tests for UserUpdate +def test_user_update_valid(user_update_data): + user_update = UserUpdate(**user_update_data) + assert user_update.email == user_update_data["email"] + assert user_update.first_name == user_update_data["first_name"] + +# Tests for UserResponse +def test_user_response_valid(user_response_data): + user = UserResponse(**user_response_data) + assert user.id == user_response_data["id"] + # assert user.last_login_at == user_response_data["last_login_at"] + +# Tests for LoginRequest +def test_login_request_valid(login_request_data): + login = LoginRequest(**login_request_data) + assert login.email == login_request_data["email"] + assert login.password == login_request_data["password"] + +# Parametrized tests for nickname and email validation +@pytest.mark.parametrize("nickname", ["test_user", "test-user", "testuser123", "123test"]) +def test_user_base_nickname_valid(nickname, user_base_data): + user_base_data["nickname"] = nickname + user = UserBase(**user_base_data) + assert user.nickname == nickname + +@pytest.mark.parametrize("nickname", ["test user", "test?user", "", "us"]) +def test_user_base_nickname_invalid(nickname, user_base_data): + user_base_data["nickname"] = nickname + with pytest.raises(ValidationError): + UserBase(**user_base_data) + +# Parametrized tests for URL validation +@pytest.mark.parametrize("url", ["http://valid.com/profile.jpg", "https://valid.com/profile.png", None]) +def test_user_base_url_valid(url, user_base_data): + user_base_data["profile_picture_url"] = url + user = UserBase(**user_base_data) + assert user.profile_picture_url == url + +@pytest.mark.parametrize("url", ["ftp://invalid.com/profile.jpg", "http//invalid", "https//invalid"]) +def test_user_base_url_invalid(url, user_base_data): + user_base_data["profile_picture_url"] = url + with pytest.raises(ValidationError): + UserBase(**user_base_data) + +# Tests for UserBase +def test_user_base_invalid_email(user_base_data_invalid): + with pytest.raises(ValidationError) as exc_info: + user = UserBase(**user_base_data_invalid) + + assert "value is not a valid email address" in str(exc_info.value) assert "john.doe.example.com" in str(exc_info.value) \ No newline at end of file diff --git a/tests/test_security.py b/tests/test_security.py index cedc7cef6..a9429af26 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -1,67 +1,67 @@ -# test_security.py -from builtins import RuntimeError, ValueError, isinstance, str -import pytest -from app.utils.security import hash_password, verify_password - -def test_hash_password(): - """Test that hashing password returns a bcrypt hashed string.""" - password = "secure_password" - hashed = hash_password(password) - assert hashed is not None - assert isinstance(hashed, str) - assert hashed.startswith('$2b$') - -def test_hash_password_with_different_rounds(): - """Test hashing with different cost factors.""" - password = "secure_password" - rounds = 10 - hashed_10 = hash_password(password, rounds) - rounds = 12 - hashed_12 = hash_password(password, rounds) - assert hashed_10 != hashed_12, "Hashes should differ with different cost factors" - -def test_verify_password_correct(): - """Test verifying the correct password.""" - password = "secure_password" - hashed = hash_password(password) - assert verify_password(password, hashed) is True - -def test_verify_password_incorrect(): - """Test verifying the incorrect password.""" - password = "secure_password" - hashed = hash_password(password) - wrong_password = "incorrect_password" - assert verify_password(wrong_password, hashed) is False - -def test_verify_password_invalid_hash(): - """Test verifying a password against an invalid hash format.""" - with pytest.raises(ValueError): - verify_password("secure_password", "invalid_hash_format") - -@pytest.mark.parametrize("password", [ - "", - " ", - "a"*100 # Long password -]) -def test_hash_password_edge_cases(password): - """Test hashing various edge cases.""" - hashed = hash_password(password) - assert isinstance(hashed, str) and hashed.startswith('$2b$'), "Should handle edge cases properly" - -def test_verify_password_edge_cases(): - """Test verifying passwords with edge cases.""" - password = " " - hashed = hash_password(password) - assert verify_password(password, hashed) is True - assert verify_password("not empty", hashed) is False - -# This function tests the error handling when an internal error occurs in bcrypt -def test_hash_password_internal_error(monkeypatch): - """Test proper error handling when an internal bcrypt error occurs.""" - def mock_bcrypt_gensalt(rounds): - raise RuntimeError("Simulated internal error") - - monkeypatch.setattr("bcrypt.gensalt", mock_bcrypt_gensalt) - with pytest.raises(ValueError): - hash_password("test") - +# test_security.py +from builtins import RuntimeError, ValueError, isinstance, str +import pytest +from app.utils.security import hash_password, verify_password + +def test_hash_password(): + """Test that hashing password returns a bcrypt hashed string.""" + password = "secure_password" + hashed = hash_password(password) + assert hashed is not None + assert isinstance(hashed, str) + assert hashed.startswith('$2b$') + +def test_hash_password_with_different_rounds(): + """Test hashing with different cost factors.""" + password = "secure_password" + rounds = 10 + hashed_10 = hash_password(password, rounds) + rounds = 12 + hashed_12 = hash_password(password, rounds) + assert hashed_10 != hashed_12, "Hashes should differ with different cost factors" + +def test_verify_password_correct(): + """Test verifying the correct password.""" + password = "secure_password" + hashed = hash_password(password) + assert verify_password(password, hashed) is True + +def test_verify_password_incorrect(): + """Test verifying the incorrect password.""" + password = "secure_password" + hashed = hash_password(password) + wrong_password = "incorrect_password" + assert verify_password(wrong_password, hashed) is False + +def test_verify_password_invalid_hash(): + """Test verifying a password against an invalid hash format.""" + with pytest.raises(ValueError): + verify_password("secure_password", "invalid_hash_format") + +@pytest.mark.parametrize("password", [ + "", + " ", + "a"*100 # Long password +]) +def test_hash_password_edge_cases(password): + """Test hashing various edge cases.""" + hashed = hash_password(password) + assert isinstance(hashed, str) and hashed.startswith('$2b$'), "Should handle edge cases properly" + +def test_verify_password_edge_cases(): + """Test verifying passwords with edge cases.""" + password = " " + hashed = hash_password(password) + assert verify_password(password, hashed) is True + assert verify_password("not empty", hashed) is False + +# This function tests the error handling when an internal error occurs in bcrypt +def test_hash_password_internal_error(monkeypatch): + """Test proper error handling when an internal bcrypt error occurs.""" + def mock_bcrypt_gensalt(rounds): + raise RuntimeError("Simulated internal error") + + monkeypatch.setattr("bcrypt.gensalt", mock_bcrypt_gensalt) + with pytest.raises(ValueError): + hash_password("test") + diff --git a/tests/test_services/test_user_service.py b/tests/test_services/test_user_service.py index d0642664e..9a60c062f 100644 --- a/tests/test_services/test_user_service.py +++ b/tests/test_services/test_user_service.py @@ -1,158 +1,158 @@ -from builtins import range -import pytest -from sqlalchemy import select -from app.dependencies import get_settings -from app.models.user_model import User -from app.services.user_service import UserService - -pytestmark = pytest.mark.asyncio - -# Test creating a user with valid data -async def test_create_user_with_valid_data(db_session, email_service): - user_data = { - "email": "valid_user@example.com", - "password": "ValidPassword123!", - } - user = await UserService.create(db_session, user_data, email_service) - assert user is not None - assert user.email == user_data["email"] - -# Test creating a user with invalid data -async def test_create_user_with_invalid_data(db_session, email_service): - user_data = { - "nickname": "", # Invalid nickname - "email": "invalidemail", # Invalid email - "password": "short", # Invalid password - } - user = await UserService.create(db_session, user_data, email_service) - assert user is None - -# Test fetching a user by ID when the user exists -async def test_get_by_id_user_exists(db_session, user): - retrieved_user = await UserService.get_by_id(db_session, user.id) - assert retrieved_user.id == user.id - -# Test fetching a user by ID when the user does not exist -async def test_get_by_id_user_does_not_exist(db_session): - non_existent_user_id = "non-existent-id" - retrieved_user = await UserService.get_by_id(db_session, non_existent_user_id) - assert retrieved_user is None - -# Test fetching a user by nickname when the user exists -async def test_get_by_nickname_user_exists(db_session, user): - retrieved_user = await UserService.get_by_nickname(db_session, user.nickname) - assert retrieved_user.nickname == user.nickname - -# Test fetching a user by nickname when the user does not exist -async def test_get_by_nickname_user_does_not_exist(db_session): - retrieved_user = await UserService.get_by_nickname(db_session, "non_existent_nickname") - assert retrieved_user is None - -# Test fetching a user by email when the user exists -async def test_get_by_email_user_exists(db_session, user): - retrieved_user = await UserService.get_by_email(db_session, user.email) - assert retrieved_user.email == user.email - -# Test fetching a user by email when the user does not exist -async def test_get_by_email_user_does_not_exist(db_session): - retrieved_user = await UserService.get_by_email(db_session, "non_existent_email@example.com") - assert retrieved_user is None - -# Test updating a user with valid data -async def test_update_user_valid_data(db_session, user): - new_email = "updated_email@example.com" - updated_user = await UserService.update(db_session, user.id, {"email": new_email}) - assert updated_user is not None - assert updated_user.email == new_email - -# Test updating a user with invalid data -async def test_update_user_invalid_data(db_session, user): - updated_user = await UserService.update(db_session, user.id, {"email": "invalidemail"}) - assert updated_user is None - -# Test deleting a user who exists -async def test_delete_user_exists(db_session, user): - deletion_success = await UserService.delete(db_session, user.id) - assert deletion_success is True - -# Test attempting to delete a user who does not exist -async def test_delete_user_does_not_exist(db_session): - non_existent_user_id = "non-existent-id" - deletion_success = await UserService.delete(db_session, non_existent_user_id) - assert deletion_success is False - -# Test listing users with pagination -async def test_list_users_with_pagination(db_session, users_with_same_role_50_users): - users_page_1 = await UserService.list_users(db_session, skip=0, limit=10) - users_page_2 = await UserService.list_users(db_session, skip=10, limit=10) - assert len(users_page_1) == 10 - assert len(users_page_2) == 10 - assert users_page_1[0].id != users_page_2[0].id - -# Test registering a user with valid data -async def test_register_user_with_valid_data(db_session, email_service): - user_data = { - "email": "register_valid_user@example.com", - "password": "RegisterValid123!", - } - user = await UserService.register_user(db_session, user_data, email_service) - assert user is not None - assert user.email == user_data["email"] - -# Test attempting to register a user with invalid data -async def test_register_user_with_invalid_data(db_session, email_service): - user_data = { - "email": "registerinvalidemail", # Invalid email - "password": "short", # Invalid password - } - user = await UserService.register_user(db_session, user_data, email_service) - assert user is None - -# Test successful user login -async def test_login_user_successful(db_session, verified_user): - user_data = { - "email": verified_user.email, - "password": "MySuperPassword$1234", - } - logged_in_user = await UserService.login_user(db_session, user_data["email"], user_data["password"]) - assert logged_in_user is not None - -# Test user login with incorrect email -async def test_login_user_incorrect_email(db_session): - user = await UserService.login_user(db_session, "nonexistentuser@noway.com", "Password123!") - assert user is None - -# Test user login with incorrect password -async def test_login_user_incorrect_password(db_session, user): - user = await UserService.login_user(db_session, user.email, "IncorrectPassword!") - assert user is None - -# Test account lock after maximum failed login attempts -async def test_account_lock_after_failed_logins(db_session, verified_user): - max_login_attempts = get_settings().max_login_attempts - for _ in range(max_login_attempts): - await UserService.login_user(db_session, verified_user.email, "wrongpassword") - - is_locked = await UserService.is_account_locked(db_session, verified_user.email) - assert is_locked, "The account should be locked after the maximum number of failed login attempts." - -# Test resetting a user's password -async def test_reset_password(db_session, user): - new_password = "NewPassword123!" - reset_success = await UserService.reset_password(db_session, user.id, new_password) - assert reset_success is True - -# Test verifying a user's email -async def test_verify_email_with_token(db_session, user): - token = "valid_token_example" # This should be set in your user setup if it depends on a real token - user.verification_token = token # Simulating setting the token in the database - await db_session.commit() - result = await UserService.verify_email_with_token(db_session, user.id, token) - assert result is True - -# Test unlocking a user's account -async def test_unlock_user_account(db_session, locked_user): - unlocked = await UserService.unlock_user_account(db_session, locked_user.id) - assert unlocked, "The account should be unlocked" - refreshed_user = await UserService.get_by_id(db_session, locked_user.id) - assert not refreshed_user.is_locked, "The user should no longer be locked" +from builtins import range +import pytest +from sqlalchemy import select +from app.dependencies import get_settings +from app.models.user_model import User +from app.services.user_service import UserService + +pytestmark = pytest.mark.asyncio + +# Test creating a user with valid data +async def test_create_user_with_valid_data(db_session, email_service): + user_data = { + "email": "valid_user@example.com", + "password": "ValidPassword123!", + } + user = await UserService.create(db_session, user_data, email_service) + assert user is not None + assert user.email == user_data["email"] + +# Test creating a user with invalid data +async def test_create_user_with_invalid_data(db_session, email_service): + user_data = { + "nickname": "", # Invalid nickname + "email": "invalidemail", # Invalid email + "password": "short", # Invalid password + } + user = await UserService.create(db_session, user_data, email_service) + assert user is None + +# Test fetching a user by ID when the user exists +async def test_get_by_id_user_exists(db_session, user): + retrieved_user = await UserService.get_by_id(db_session, user.id) + assert retrieved_user.id == user.id + +# Test fetching a user by ID when the user does not exist +async def test_get_by_id_user_does_not_exist(db_session): + non_existent_user_id = "non-existent-id" + retrieved_user = await UserService.get_by_id(db_session, non_existent_user_id) + assert retrieved_user is None + +# Test fetching a user by nickname when the user exists +async def test_get_by_nickname_user_exists(db_session, user): + retrieved_user = await UserService.get_by_nickname(db_session, user.nickname) + assert retrieved_user.nickname == user.nickname + +# Test fetching a user by nickname when the user does not exist +async def test_get_by_nickname_user_does_not_exist(db_session): + retrieved_user = await UserService.get_by_nickname(db_session, "non_existent_nickname") + assert retrieved_user is None + +# Test fetching a user by email when the user exists +async def test_get_by_email_user_exists(db_session, user): + retrieved_user = await UserService.get_by_email(db_session, user.email) + assert retrieved_user.email == user.email + +# Test fetching a user by email when the user does not exist +async def test_get_by_email_user_does_not_exist(db_session): + retrieved_user = await UserService.get_by_email(db_session, "non_existent_email@example.com") + assert retrieved_user is None + +# Test updating a user with valid data +async def test_update_user_valid_data(db_session, user): + new_email = "updated_email@example.com" + updated_user = await UserService.update(db_session, user.id, {"email": new_email}) + assert updated_user is not None + assert updated_user.email == new_email + +# Test updating a user with invalid data +async def test_update_user_invalid_data(db_session, user): + updated_user = await UserService.update(db_session, user.id, {"email": "invalidemail"}) + assert updated_user is None + +# Test deleting a user who exists +async def test_delete_user_exists(db_session, user): + deletion_success = await UserService.delete(db_session, user.id) + assert deletion_success is True + +# Test attempting to delete a user who does not exist +async def test_delete_user_does_not_exist(db_session): + non_existent_user_id = "non-existent-id" + deletion_success = await UserService.delete(db_session, non_existent_user_id) + assert deletion_success is False + +# Test listing users with pagination +async def test_list_users_with_pagination(db_session, users_with_same_role_50_users): + users_page_1 = await UserService.list_users(db_session, skip=0, limit=10) + users_page_2 = await UserService.list_users(db_session, skip=10, limit=10) + assert len(users_page_1) == 10 + assert len(users_page_2) == 10 + assert users_page_1[0].id != users_page_2[0].id + +# Test registering a user with valid data +async def test_register_user_with_valid_data(db_session, email_service): + user_data = { + "email": "register_valid_user@example.com", + "password": "RegisterValid123!", + } + user = await UserService.register_user(db_session, user_data, email_service) + assert user is not None + assert user.email == user_data["email"] + +# Test attempting to register a user with invalid data +async def test_register_user_with_invalid_data(db_session, email_service): + user_data = { + "email": "registerinvalidemail", # Invalid email + "password": "short", # Invalid password + } + user = await UserService.register_user(db_session, user_data, email_service) + assert user is None + +# Test successful user login +async def test_login_user_successful(db_session, verified_user): + user_data = { + "email": verified_user.email, + "password": "MySuperPassword$1234", + } + logged_in_user = await UserService.login_user(db_session, user_data["email"], user_data["password"]) + assert logged_in_user is not None + +# Test user login with incorrect email +async def test_login_user_incorrect_email(db_session): + user = await UserService.login_user(db_session, "nonexistentuser@noway.com", "Password123!") + assert user is None + +# Test user login with incorrect password +async def test_login_user_incorrect_password(db_session, user): + user = await UserService.login_user(db_session, user.email, "IncorrectPassword!") + assert user is None + +# Test account lock after maximum failed login attempts +async def test_account_lock_after_failed_logins(db_session, verified_user): + max_login_attempts = get_settings().max_login_attempts + for _ in range(max_login_attempts): + await UserService.login_user(db_session, verified_user.email, "wrongpassword") + + is_locked = await UserService.is_account_locked(db_session, verified_user.email) + assert is_locked, "The account should be locked after the maximum number of failed login attempts." + +# Test resetting a user's password +async def test_reset_password(db_session, user): + new_password = "NewPassword123!" + reset_success = await UserService.reset_password(db_session, user.id, new_password) + assert reset_success is True + +# Test verifying a user's email +async def test_verify_email_with_token(db_session, user): + token = "valid_token_example" # This should be set in your user setup if it depends on a real token + user.verification_token = token # Simulating setting the token in the database + await db_session.commit() + result = await UserService.verify_email_with_token(db_session, user.id, token) + assert result is True + +# Test unlocking a user's account +async def test_unlock_user_account(db_session, locked_user): + unlocked = await UserService.unlock_user_account(db_session, locked_user.id) + assert unlocked, "The account should be unlocked" + refreshed_user = await UserService.get_by_id(db_session, locked_user.id) + assert not refreshed_user.is_locked, "The user should no longer be locked" From 896ff0645b2b9d97e8ac8d545ef8dac527c1ef1f Mon Sep 17 00:00:00 2001 From: AlexandersWrld Date: Thu, 28 Nov 2024 12:03:57 -0500 Subject: [PATCH 02/11] resetting docker compose --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8c69de764..cbf3c8da4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -# version: '3.8' +version: '3.8' services: postgres: From f9a1a64efdb0c46bb137d8064dbb9602e7b88a87 Mon Sep 17 00:00:00 2001 From: AlexandersWrld Date: Thu, 28 Nov 2024 12:07:13 -0500 Subject: [PATCH 03/11] fixing version in docker compose file --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index cbf3c8da4..8c69de764 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.8' +# version: '3.8' services: postgres: From ab428e349135dd7e289b56985604cc6c6e525aba Mon Sep 17 00:00:00 2001 From: AlexandersWrld Date: Thu, 28 Nov 2024 12:27:26 -0500 Subject: [PATCH 04/11] fixing username mismatch --- app/services/user_service.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/services/user_service.py b/app/services/user_service.py index ce0ec3aff..242e53108 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -60,10 +60,10 @@ async def create(cls, session: AsyncSession, user_data: Dict[str, str], email_se validated_data['hashed_password'] = hash_password(validated_data.pop('password')) new_user = User(**validated_data) new_user.verification_token = generate_verification_token() - new_nickname = generate_nickname() - while await cls.get_by_nickname(session, new_nickname): - new_nickname = generate_nickname() - new_user.nickname = new_nickname + # new_nickname = generate_nickname() + # while await cls.get_by_nickname(session, new_nickname): + # new_nickname = generate_nickname() + # new_user.nickname = new_nickname session.add(new_user) await session.commit() await email_service.send_verification_email(new_user) From 26aba6d9c6b9c1316c34be3ee14ae8cd76a58100 Mon Sep 17 00:00:00 2001 From: AlexandersWrld Date: Thu, 28 Nov 2024 12:43:56 -0500 Subject: [PATCH 05/11] fixed user login not found assertation --- tests/test_api/test_users_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_api/test_users_api.py b/tests/test_api/test_users_api.py index b49cc3209..092e4f48f 100644 --- a/tests/test_api/test_users_api.py +++ b/tests/test_api/test_users_api.py @@ -112,7 +112,7 @@ async def test_login_user_not_found(async_client): } response = await async_client.post("/login/", data=urlencode(form_data), headers={"Content-Type": "application/x-www-form-urlencoded"}) assert response.status_code == 401 - assert "Incorrect email or password." in response.json().get("detail", "") + assert "The email or password is incorrect, the email is not verified, or the account is locked." in response.json().get("detail", "") @pytest.mark.asyncio async def test_login_incorrect_password(async_client, verified_user): From e52f81d83b05431ab743b6adc2094acb192d61e6 Mon Sep 17 00:00:00 2001 From: AlexandersWrld Date: Thu, 28 Nov 2024 13:31:15 -0500 Subject: [PATCH 06/11] fixed user base data test --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index 9e4cb125a..ad21d417c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -216,6 +216,7 @@ async def manager_user(db_session: AsyncSession): def user_base_data(): return { "username": "john_doe_123", + "nickname": "john_doe", "email": "john.doe@example.com", "full_name": "John Doe", "bio": "I am a software engineer with over 5 years of experience.", From 8ce6c0b7c7a692fa208b7dc5fd01853c4b729791 Mon Sep 17 00:00:00 2001 From: AlexandersWrld Date: Thu, 28 Nov 2024 13:40:38 -0500 Subject: [PATCH 07/11] fixed user update error --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index ad21d417c..71e5b3fd5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -243,6 +243,7 @@ def user_update_data(): return { "email": "john.doe.new@example.com", "full_name": "John H. Doe", + "first_name": "John", "bio": "I specialize in backend development with Python and Node.js.", "profile_picture_url": "https://example.com/profile_pictures/john_doe_updated.jpg" } From 8b773d8ee780c2e35f89cf6febc7488373847f3e Mon Sep 17 00:00:00 2001 From: AlexandersWrld Date: Thu, 28 Nov 2024 14:10:45 -0500 Subject: [PATCH 08/11] fixed incorrect password login error message --- tests/test_api/test_users_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_api/test_users_api.py b/tests/test_api/test_users_api.py index 092e4f48f..968ffc73d 100644 --- a/tests/test_api/test_users_api.py +++ b/tests/test_api/test_users_api.py @@ -122,7 +122,7 @@ async def test_login_incorrect_password(async_client, verified_user): } response = await async_client.post("/login/", data=urlencode(form_data), headers={"Content-Type": "application/x-www-form-urlencoded"}) assert response.status_code == 401 - assert "Incorrect email or password." in response.json().get("detail", "") + assert "The email or password is incorrect, the email is not verified, or the account is locked." in response.json().get("detail", "") @pytest.mark.asyncio async def test_login_unverified_user(async_client, unverified_user): @@ -142,6 +142,7 @@ async def test_login_locked_user(async_client, locked_user): response = await async_client.post("/login/", data=urlencode(form_data), headers={"Content-Type": "application/x-www-form-urlencoded"}) assert response.status_code == 400 assert "Account locked due to too many failed login attempts." in response.json().get("detail", "") + @pytest.mark.asyncio async def test_delete_user_does_not_exist(async_client, admin_token): non_existent_user_id = "00000000-0000-0000-0000-000000000000" # Valid UUID format From 4da000c8dd7897d7f3a2c07d533c1d0223c44891 Mon Sep 17 00:00:00 2001 From: AlexandersWrld Date: Thu, 28 Nov 2024 14:34:22 -0500 Subject: [PATCH 09/11] updating readme with closed issues for submission --- readme.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/readme.md b/readme.md index f4d0ee2cb..af1468fc5 100644 --- a/readme.md +++ b/readme.md @@ -115,6 +115,26 @@ Additionally, you will resolve a sixth issue demonstrated in the instructor vide ## Submission Requirements +### CLOSED ISSUES ### + +1. **Issues in docker compose preventing succesful application build** + https://github.com/AlexandersWrld/eventmanager/issues/1 + +2. **Nickname is user registration does not match output** + https://github.com/AlexandersWrld/eventmanager/issues/2 + +3. **User log in test assertation unsucceesful** + https://github.com/AlexandersWrld/eventmanager/issues/3 + +4. **KeyError in User Schema Test** + https://github.com/AlexandersWrld/eventmanager/issues/7 + +5. **Error in user update test - KeyError (first_name)** + https://github.com/AlexandersWrld/eventmanager/issues/9 + +6. **Error in user login with incorrect password test** + https://github.com/AlexandersWrld/eventmanager/issues/11 + To complete this assignment, submit the following: 1. **GitHub Repository Link**: Ensure that your repository is well-organized and includes: From 592b9744e239f2e73672d001c24fc6d367243235 Mon Sep 17 00:00:00 2001 From: AlexandersWrld Date: Thu, 28 Nov 2024 14:42:55 -0500 Subject: [PATCH 10/11] adding reflection to my markdown --- readme.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/readme.md b/readme.md index af1468fc5..f06214176 100644 --- a/readme.md +++ b/readme.md @@ -135,6 +135,10 @@ Additionally, you will resolve a sixth issue demonstrated in the instructor vide 6. **Error in user login with incorrect password test** https://github.com/AlexandersWrld/eventmanager/issues/11 +## Reflection ## +This assignment helped me get a better understanding of what it's like to work with a larger-scale project in a collaberative environment. I understand now the real purpose behind forking code from a repository, to create my own local copy of the repo. At first when I just started the assignment I was making pull requests to the main repo rather than to my fork. It took me two tries to get it right but I eventually caught the hang of the correct workflow. Through the creation of issues and through branching, I understand better now how to make clean changes without affecting the main repo. +I came to realize that in the case of making pull requests, that I would not be authorized to pull directly to the main repo, and that my requests were more mere suggesttions. This has given me some idea of what it might be like working with a team as a junior analyst. I'd imagine the senior analysts would be the ones approving my changes. + To complete this assignment, submit the following: 1. **GitHub Repository Link**: Ensure that your repository is well-organized and includes: From fef2e8cb5f0351d6c3fceb68d537ca9f0ca1758a Mon Sep 17 00:00:00 2001 From: AlexandersWrld Date: Thu, 28 Nov 2024 14:46:34 -0500 Subject: [PATCH 11/11] adding project image on docker --- Screenshot (388).png | Bin 0 -> 342108 bytes readme.md | 7 +++++++ 2 files changed, 7 insertions(+) create mode 100644 Screenshot (388).png diff --git a/Screenshot (388).png b/Screenshot (388).png new file mode 100644 index 0000000000000000000000000000000000000000..b0121062bce193a6dc5c8df38d4deacdc7fe6beb GIT binary patch literal 342108 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

s@(&qE$g;<;#KTrLno$j?>oJjc386R~8&B)`mk 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%0AnV5iTRI{&ugx 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%0DZhQDnea-hy zYFVEN zJqIjhUo)gzWFXfYnjPM9+_S9fdS3X9R8D$iS-gCBb6LhH{1hpkp8g+mipJ?U(A zvn{K`-V?iPT9SBF5VJ1WJisXzti~4mmv(UffK85+I3fL?vuwZP`YnKSA-U>!aTaI8 zl=%*Z>|aR+9&lj|rS(1Gcy_2RZR4vh&4Get&)dt5r8gr3Y;ZlvXyJ5=HhjK+X%P_5 zqwhbsrnVAEZIF+8_(c(VLDGI($HqfW4cmW=nb7*N(y_I~JYq?Vk zzq~N;vhd?T;L@%=BkN|hNgX}?OMdBZ9$*ewly3I)3%uEs7b){nmvcx9uf(9E>0Kp8 zwN!nsM{rjMMHI+UnSK^@&%amVAAsTQJ6Y>qr}`z66m+5RuB%s6DZ)zwT`L#Feu64# z79_uP1pH_>5v=l<^ENfsF+5S%!+!YP0-~=i7SR^R+_Tb+ZyL0mSX3M^r*8O`@u$eC z&$Swmj~gd7%BGD*cb-qJpEyLmFCoWWgod)uPmKB5mY)fi%`pqcqdkX)h8i$_IEYfL zZe*xcKI2mB9V!_YwV&T>l=?AcL3a1>dskq?^!6uk4_v0=W7!l z2i-q(xoD!l4@~b_Tw1aeHk0q!JHvWKW?&9+re^u+apU(sp`Ct?H5?rk4qu|?@i<)- zkYl|Pz^y%A6~u*ctAaD$qeY2ehaa#m!7Ty}`<&KTA(eNR)@Rq?n zm7IY524-`Kx-rw1tCb}LrCnBwhS84d3W(cGPG0H&r9QQ4ytpG&{OU4OG0130SdGQI zk(CDxh73!oi`l%l1;KmVAhp+b3@K^G;S~kmQJZ7hFw1X}a-d(goVVB;XSV||RZBaY zzBH>*<~OXtdy9tew9c%C69KpF0)FQ35wmisuY9mGRv(3tHzGZhrV*!i3w_^fh z{BQT5d;EHgPAU~FY}rSQ9OxtQ{n%&1GEm%YO>ZDY7g zUvF9rJA40-ap-7HYwf|SZAS{>dXSCO%RIx(I@e=v4S%o`80XHKtZBxTym2pB!_5Xt;8S zm-PS(l3I@fcZ$Q|N!8!mG8)`SvFy_%?9WxKA;<~%2`vSGKl$@g*~Y$`fv_Y?W%(4H zB2#ihE@(`>12i@O9$B@|@kOt6FZ4%AUqR^X?qp-+VA=R6SBc5AKewO(aFxXhBjweB z=rMx4=;sx78NW`kP!c*gQ|Rom008_k(GAWKe6jMyNW}+!^bK1){st+YP`!>DuOPkg z@{bui1$J(b#6^uzBt@>Hn;DTDvUz72Ba$i{78WM@vks}niCFl()%Bf$!d%iJk}R#T z&6Knz5XFO0J`u=}>2SQPN}4ixVeI%~tW!xcBz)Pp-Twx1svbTMuG!n>Phl!xDF`Kr2hIy}D3<66#1U1KoxIVG;5# z4X@%}tR6Dj6o-Wkm*iiIg>DwWHq5Fq1F|WU?WYq230d2M-rivB3IsxSP zFkF1xAUABm=%4_!0(9|^u?&PNL$I+1ISQ|&9pnj~zHUb%&gD_W8`*N50t?LrQOJ&9 zy4l`=2^AGfyYJ7O`gs~Wh`{Vvj*akM6qVs5#$CSs(Pw-d_=gRHZ?wmIeQjD=YeJoR z)f_b{cWyhfIP8H_m3|Vv*`nm&XKO|_kJn7SN{CW|uCivDsjoGZ@^g@EJM?$K(juyLVnaepN;OlQ-mDoq1w>64Ab+(EnNa z#}Y4YqfS8+c|48a^~d5O*@?ZlcyM)X{zX|cA^i0xukGrRtqzTmtPrBSaqG$NvRu`E;-F_TSx**A8h2WB-j3aX>&BICw*w>U`+p5{o3StMPYQe+4KPKeSf@h*d>E2OOw~WgtdbQzJ z=C5P~Cw?_Hi1SAzXZz?-PTW%cW9hBncIF7bs&tEPs}z*6MooDVwFrrzkLFbv ztQX5^Ajthg=X2WLo-Gh99h6Pf^!0>hONPD1_AQDq3!g%0j1N?z7K4%tLDDqRJ-)W@ zal5bdx$wEIQvZe6@WoE_0W|`F;{J`*Wv??MFL;gAFyhH|WYITMq@?f(w#GsRm#q9= z&P(6dEb@qH(e?^+pTBD+yRzDd1I4-Gx>WsBAq7Ejji<(?>3S!Bm64)@MojmhYGN#@ z)M(dO@%=abSNIJ(*IO5x+oe2q^6~rD0op0q7RNKf+bJSptwq@iXXv*i=PQMNY*JkA zWeii4x75Ln{*oRN4{XU7hO$ZR@XhBv>q|a!H8GKMwGm!(HMb+@>TdwD>IU9FA{L6w zj~#h`fF0lt6KcBdY3CSbw6!PQU^PS_YXrItDzt%obe5n33)R6Qw}h*I3|XdtyWTh} zNK|FQ=X2zZ!1%;S!@|&`@w5kbDW6Tn$jl5~cio*QzDt1b%xsKH>l1>Yez?cut18Oy zReb3KOd>S7ShLIercl*He77i={M=?u70O$D%7-$tFd@vrG;l_`1C3?&V-7Ctzj>5| z0aq)sFi(BK0TlADLyV~v6lZ6nkQ!elLqdoW*CZ}z6W^i$>hB%gV?G}0Q@(MEa<^I9 z4=GuD$PVt;2J6L!NMzj*X3=IR85b^>vwK*v5xB+3mGsOvnSbL8FO7<*5GFrax#@>W zr6ZTGi|%t8jjtBFAlx9bT^2Rh8}@H5frpKP!U^2I#cek>Q}7!%>!rR%8Tj?}#y*G( zQhdbeezNUycW4oJB*+}5aAx5hryz0tE$Rk-1zz<&-_NHw{0&vUS-ja=h-?@clLMnk zM@0{z|0;cvGG_4d9DK&-rdOh08{@hI9A*4CopRwD`DY=K5$Qt3taFjcYV1~;;Wq@9 zZMn%yIKl@e9agAV}su z<^fnh4=#W00#&`}yO7rt4U5>u&t)R%B#)&YK!_5#k6Rs-t_#Py53OoOrUBX~d4Lrf z%4k^7x@IIYeDxaU;A&uSG#Ys81;5^CHcHJPd@#aw3X2xn0dnc)#6;?ZaVFlok|beP z=j`5~Np6%1o7Ob2m?!lFGSvzxeE?|#N|$zlq3@6jp^H5{BB0x5p<=Q+C$_wS z^6}q(hkZH8&-U8fUFbD`*#}I}?Mk64$B15;Fyc}Z2lTQ4n!nuwm+IW)Vs@BVZP)A4 za)hk=>S#XfBs_QY!mz-xs#ImqTBAvCmB91ixGSXaF>n>DB?bj#HB3S6yaERWybrb0 zx#%iYp|2~7)>@Dlrtw^p(LGZzY$+Bz1yI24gVCxHm@N}aEUWl{;|a`|PfNw%G^`A$ zQjuIv8jPSVTH|d-Bs`IJ-c5G(Yjq>VUH;JK*>hfV)q^Ls7x81WmNT|>H!xQ+8ou20 zQocdskEDzi1eKrkj4IqyAv^)sTEXDfluJt?E1ehU&AOWR+U)HQsU1~zCB`Ci>->$5 zSc^WutXo{~pi@+3vXi-l-Op2Q$?9NA#8arpdroL+i4LnXT^K9j3;pAcy*I>kPgW!w zh7DBN0#qtLrHuw(%Five!OsnCDiPD|g!dS_j7-Pp*9~8WRg2Bc!nt~kdh#b*e@;?lS=a7}z*}EmX zJ42X}83v?xH0Q<+)i8`Ly|AuI#di76m*@SI<(}BxJyt<#q68=BWEzX6pSjzo<*b=2BR>8dJC}30%`fNOmbHQAl&eGC7F zODX*dP29j-%%b;hUHuOH*)v;ELY4B$tyaP#Efug63Xer)T3K0H|IZ`-q4w3H0`P1E z;-*j7yQ?d`(<(94wFvK5MpYRrZPlf@fc8IPH)_CK&w4e~MyoC1=z$E>Gt|Nz7q6j( ztg{m1KS8OMj!>?9kL0Gb6Bp@=+vna8j>QFr0BJk-1e^-AyGxvKoqUt~-D0&|H}@={ z6zeIPCq}mJOC*YK*()xNDq9jvY=q;z%lmd{^U!he8s*hQ&~(v7S**K@mL8_V{93~? zAw4(h&ZW}*y{ZT++$pc`86o)j`XGlbCYtIfxi7~glGIPG2U{sLQ~)E{IiODVA!fWWxI^zupfhYe^dQI z)8ZlbHtuQsB_6d3CyT<4iD5LXJ7xa4V)7o6jygIo5IyA47vjp3^ZEJ&w%3 zf;IBxt9XrG^JH_yl;pOU5K2Uau{35tA-~Stz#73N!MLeN0 zM9z2P#_ouxCs4PVe~}jS5(-pF8k5Pr8Awi;4SF+74|^$xW5j)MHwLHg z?RwO3a?(x^J&@6yA4q|RH^mKFVN`s^a_brF+aza7QkjY#8eKBHpdPe1*hheCgWo3% zL5Qi>t&^!VfZ5h>U?sbr20L8{4=W}7^PrDyjMHWc^UzD;X?V# zH>?b;ckW@z9^g>(U)E$CZkCf~gs4O8hKB0T39kf_6PD<92j zSBcg_JK&}WIUM`Kmif-T$^xhVWOqTBg;z^m)kat)TK8n_YV6vYjMdfJpUJ@8A~aST z?ya(jF4e`AVb=2d%h7(vT+2_Ysey%+h3T<#le5ca9c9%g+waqr5q|uAi%#_e%%r=tymjZIywK#n)w) zs+*HEtH-(of&YSe8mJ!wqpiL|uDxuGmiTi*1K|3y(~M_&!e4GCn+6>eFeod@=MnkF zag+?(*X_cbdB*7mYu5jDTF;hFz`%j@2wQ*JRQw&vNo9IiM* z*t=KNu+-p5TY5r~pK5wLLM1xBveOiAkH8GppZfPI~&h8U?KQ$x_Il*{@=nVTR zh%aWEfX+&=RYAryqGhYMc@Mf`i}Ti|=}_!YKs`dAe+@QYoM84|fKC{;$&#dVSovMs z0|rfrM~BQV<;XFPb9|usRu@;5X%w1e<&(hN+{lp?_$)J~M5M)+TU? zt7n45fSz#>dTC}}Iig_M(D!qb%MmM`#*8M|nT!X3+Ji|eR2wv3th(IPbWQFv&M)Ck zOthD#;SLt`IVlYp-^cn{rqY5-t;4M+peKI_@Tfa&q0h-D6apJv@B}MNLJRNdT{}Nx zo$E$^1BphOf4`b3oUJv6CYPeCwN&CDMtz}fXQl=p{5oQK>nfP^&0g zu2;N+T@1+X_5aE$;UYGPV^zbk3m-rUT7!^4wO_ZNOzQZbSgk>^=BYGgg)&y32WaAe zfc72u0s0&4owOasV49t^|22(yK*AGwo^9cY!k#vGcZ(HL8p5&jSY;u~2u?sL_sRg0 z+Ing>YnzprkX%OTyyY}$4#@lS&DBWCSS8DYJaHvpcqBA2(&K5kKGv_|5wvcmnokmTC^2XSNcAyQCAA2D;cBY zIwfgiNWdX9mOl8my%#3i3~`iJUq@a$cBVA#KW@Ey(1Qa7XrFtC6KaF5Apf7ONh;=l zo8A$&1&E0pK!&*yS#(Egp4K%B1AR?pF^IwbYPz^^2P>wjzjN!mO58ZYqH<88((s%W z8u-j&Z>bkwilEF^lT?e^bpT;t<=Q$cM9fs zOvuFtm0VqhWI4rKvj!Q$Heorzqjjg}?X6Alu203aKPJRvVfdcqR1bo2TyLkC4Jmc3 zyObmN!T=s7%RNG%*Md6VIs7ME&(B3%*2t6w;Vbk%O#&6$*%03wPTiKM(|$q z_T2(U2f*j+Rq7B4I6pnRs>gPIw!Tqp&}YFz<-ZT-NaYb&khl-Tj~9aey2v^+V8ecd z_6L0?K+05B^TJgIe%X>I`J6`qvPjBJAXq!SI9y@<*w7U!+&HuJ=~1^=qY+b50QW(Z zMK=_^Ef6l7^D7RrDms=-u*wSqZxx<U zJc>O-W=89zQ4+PVQ{d^oMW)Yo`2pPi~AL7sx>_1K`Jd(4;K#%evV3|bG9$) znon0QBh=gRJ3>7+_8ki#5sCM4EiG4IFul}3;AV2c7CQ@1HS{WbF|xTW|2(^cQ&S8I zv^Dtywgtdxcj9>~iSMy`=sAB~G{c7f^?Ve;{5R zcAo>hm4UxM%#b(zg}h+?Cr>Eb*H9#tb0<_xek7>J-hz4{Lk?(N{wpsf#>e~<+QU^p z@_)$jZ9C1ZmQUfs(}r^*kKCSD&3`6c^s&sv~do) zJ)3--432OgKfn?G@g&dhpcd9R*NFUMW#ncUWMzlV0>y8$bNlkXB2cTP)^&($((ZE} z%~9oZXPK_?CG+Ym$`hx*)~jLT-JOB*3F|9fqgj+0H%`e;42Uu5-}qQ%}ya9dWf+BE-DhE6QocP%PzxJJd^+~we-|wQw`Nh{sHUIbKi^gzL5GusCc^y9? zRNg5QpD_;Kbhdg+&+(L5%d1#sEL9p~`WuW;y%5a#8tCDvI`?7dFBeia&f!t2fUM(r za_jZQ*Dh;cgZIJtJu6(j-M^tW1}U}Cb1&PAVl=Hv`wG);h<%vYgH%w|&6}^KwdW>% zEETzu1sV%)KX)##La69rlnpixt`vc&QPCw_5f)Xf6xT2L5M^6Qu+K z(*OGo7aFWwTWz$GkIlYU50B0WS>9irnNl8Ie3Z^kYM4k2$ls`G${C!VKF+mUI0b;R zt7xa$yH@%Rg+M&hLGu6A=GEbw{{j;KZ%`bZqTJXT^60CupAfYJ&;B?_(YU2G|0~UR zQada@_T76ohim>$njez0cQ0ty5e8%V5yWn)%6st^UIh_*Cg-~5^-=x_wxuv(2<^s< z&f>-h2hmkW?1@Agj~tKz&A|*9UAw@7=4`BN9y%rs@R!Y`A_0Lmxd@cDpLO?ceC7s5 zLl7AB#d|R*Jld8UvFyz`%-1bS>;ajGLo}wQC6}2o;+ccxnhR2WU6TB+!Y;#cv=}o# zHt)`7)u%(z_(>2=Rrp0fM{{l3{#YBejIsg{-?W;y19ysv_PiGR7?U||(y@Pz3#IMz zv(3T=v?Cy4ZowYd4s+kv0BSO{^QHhiyBt$BKiO7#vBTmjxDf2aAK&+>TUbbx02!l? z-jL*!IZ~!BPk`o8T?K>9TuKKiS{Ff6Fu-Xb_OAZi#Bd$^^{5?Cv;(-qFNm%q{gp_s zZ(2VlfOY%k+V@u@(#)7K`fL43YdswD*huww;K4zqthu?AH=!YU`5j>0wtNQ_LFcJH+AhHWnsJ7`^2f zpGob^(LDg#DZnX-(v$nI@*h+s-d zb*b$Qf=Mq{{{P6HVX*(NeFh+alSy{gC;-8%2sAK?2=u0wSy|fZn?g93GCWaJ#TF#w z0pVCFq+B%MH4T}ZUg@;rXpS&_6&+lL@u~yz8uVR86AfD2uKj@ya6_%Ft|2kj%r31- z8~!$$A7bsEVGHyFP*qnP$_7#BRYb9VwS-+s4@i`fR*U_V;nv?8#W$%vU>p`lQFrgL zK2a!5TYd2<<&Z&SM@l=x%;Zht`82Na>H+Nn)*oaW;fzM0 zH=rZb_#L*Vr^Wr2o1Z)M#HIs^hFt$uzkswj3i6wBO%qf(WpehEb?|DX!SfOEU2R&3 zcse+C6=-%4hhDA$E&{02HpmUjnWI_*PF}v5kF26(>ywuBCL+b*3mi;0*$YT9Gs$J2 znxe3sinR1@BTn)e_9?!Dd5*j7O+hDh^_yNSb!Urq5KNxVQt0$|1YdwDgGZDk8K8rw z$E~KXw??I9X7+jcET@bC%_rzpfo?ni{mM*Mj=Fc?Z-yTgTKocZv=`8{102zdcob23wg7) zmqTYYlDs#rMb29nH<=}xR-aQ})y&*laV`C6zHWiPirjn+Uol$MhbfDb=G$5zVc6A& z@+#l+(8v!d6_mfc?^G3_YdOls);~J6dR7pv&-#bsHFnA&I>}xD`0_tpoproeh<9@e z4)Fz4e4=BQL;rL(EGJPv!H$M6t%^Q|W+WgmnLDL6=9-N_jtTSnzv`%qQ4fAdWjqSs zy7e|_z(`;h@!&WOaxr7P$vAYQ^Cfb6LtW#!p6(wzpaRct7lH;SG0YFcr~Q-20mtw zYv$Zt9R!E2X-rEZUau!`>tWcBif(SQ`6kBJ4t5R3JEX#jcm}^I+agqnE$K-rTi?k; zdU5}3}DA_mHj45-4?Q*B1X*Qh=Qb!7VUopxI9Js zUj_}+mQ_7g=&Vl@tbb%b+bZ$Y!DL0vjj6J+gnoCSgu_yHH`{RecHiR`03gc*s9Z>jdmmIcDns z9~hlohCL)KEskb4z={o%(2Nay$F$*JSTf_%I&=I5+D-kHTf;cLF1!_1lf&qo6ZwGf z0hX-9Tt9|vjnd>JS3vw1a379+9p)rbaEcAia?oYOA;8PQ zoHBQhI#vV{SBW^sKZ5}ktZhAEDEMT~(`)a(Zc2)qHC@?HE@L3nJ{(rXP4RzB64JGo z|LDuu5tb$f4P=^aVK3bUy#}=cElkq;k{k!3{glLlJm;v;E(5x+tsU(ZGIL5FBiEO# zZ;5$@G{26TS3jHKOg7{&jm6ig#J>+P|B;}mh>XS{{IYP{!mLMQH%?jO&#iLU%b72?$20n*tU?F*u0mXm=?s9rm*(Q3s(1 zF>`?Tbt6z&@}U+skjsxKz>N7=@f3J|Q)j@-%{n<+r@gOI79i+xCZK&XJb>8HN&qe= zVf**}wN+FodBN1pXL(+q_4?-q8tOj93Kn;sJo)u!?nqELm&;yctMLzA{rr!mM&kvY z#wPTI$Vw$gFT`$+TUUJn=^YaC?c-yvKM9-#5d<#n)1#sr;(e(BLE8xrg^YO*ol;W6 zX7I35r3y!9{!SLb_f2LWzfxQ^=d38w!PFt?7E3ht3=?2x)G_0SPNRHZ2eKNz1sD;I$Bm#(0NvtR;@ok8iu86560XHQ zE1*2Q{y(A~7z>^m#r6+Umx$JDWRmz~b;Fl|JHs*nD4lYk1qB7NnFp@tgVb2k2)_5fvNX7! z$w1<}hUY1g*I_d=GjI4kO_Sfpz4X3Z)W5CvD}+N5mvGId4Kc8hKk^HCO1-e^CQ`#a zHX9zXLiFmo|LCZoH}vBE$yW=(kxJIrG6sWDoUh0l2p-xA`&Mv>7oj=3`We&v$3m3= z*UJ|8((TK+PoMptE;?!e9Gt6^9<_R zs_T5?CYFokL;q$L4aXEQMajK(Nd;p-TSpcT}yJ7xafB4^q`Iqpge|z*P z=5xn7a~2r$mShCr6XTzneqkP@+YP4gp$Xh|D4yJ$XlrnIFMv!Z0j>*LY0~Clyre$U z4u;qSY{0t$w{NeyRS}#TaL;zn{w68T9F7V^7%>pNxBgdq*Z$Syea0b3q6J|q+W;kC zrK{~w5ItNHAly{+xT21ohDapTgwe^J5S2hk5HH7L09mDK^CDq*p$AMPHsKnGvwah} zyn!i+5DK>BCLsdmLLq|t(sf&V`oqr7+1XEf|A4&j_jx|glkao;JZk;YzatX`);|vd zV2;1ff1iAL*oDefvYvn37|=QE`l9VYbzWmC*iu2T*(;I(k>-8E$$u(|XQ;%DDz7GW zFBD{E>@d_)x0vBKrs)DMS!f>UsjtM(fRjH2d0S^@26v$|gtf~WcvUKk34c1W;}}Q%^&K(k3OM;1 z6BStH8VVW)4xKO8io84I=%}u9$Hkf5OHVWyd}DH|a`|bM`V-3;D6#%z?hQ}b>U26*2HRE0KHg#Jg&tN(&%^HRg|AgJ)5jqA1_ zFM`o7HaPXN7Xtat%tv@mmmR;JsAcLa52^w&mooEUR4gO7xsy}!Y&>32MOf5LREY&8 zBiFrl5hX6>FAU>wDrS&)p-q;DHSq%F>u1CaEImpYW!MIM=09QTOA{M%*N+&i}bQGJBpO@u8BuSsnhCLTPs5(IE;cy zi!3=CHvD0`;BdZsFUaJaQYQq^Goyx+%LKVxhOCU?<2I{m>%|pu`RQr$NW;li$kP4J z4EO$R0t(axm2=9c>07ej$YjxzpD|k=H!#4R;PxVLvWV|t;aw_3kNN;vI_AZINhtvp zy@0rLF)JtJa@aXhFK%0bbo99r5B|6wmx31?+?&;^P^1$ZWOyaGh@Wo#tR@cV!{XR2 z@m74AK$?QQIwohVSyNPZqGC~s-SSK~%|zp*tR z*+9-`EZ_G4c47sD;$Wx!PX)ME$6V!_T}8JKt0D~R1nu}0(ggr5CitZnkh5(Z598XY zI%q2FNEQ@k`?;~y!7gXRKbB=8z@Zd?{1n1r+R!`E3MU#4FXzxKOBelIMBrApd`=5# zqFt5A#G>xN?GT0E=7P5QL|6}1klA$Yr?7M6*<+|>JR0U_%Sj=Lv5htKovU3h z{{V{19lgepOg78=ufBo9pJ6P8I=>4@XC$CEtYwK}lpFZ%`Sh^{@fBO4`>7^f?P)5ed8|lH-LruxFh8!gwWi)HZ_N*P5 zEWd{&@5`2>E%k1Kbl$F=7$(iSw#SzT4m`O!#m==AdcYLoq`MVHsl8*mW`%PJc|u4R+lBL`10my46ila(5JQB?{m7z2*|Li^%|59Ln`C z$$vf9Cs~ffObdhZ(M)QE>5~63yZY2y^`WSG=FR{jQV6z}P%X_~W7{+q$cgC)zq&#? zND7b_s9d!_7Eae%*cDy_8qO9eX)LI6L0uQ&B5jk|_SOVDOx>3epNiy|Yc)ZJ$YSKn zwvs0oXFoQVBs)o2yn)r$Tdg(JNE50kJzGqFFq)l$*2mRw)4_fB6rGm@(B=@{B_I#J zHTCmAuky3yKPO#Xu^ro=#f`h;KPa!G`{51WJdK{&V}E3i-o}~i9m-^@ey5U$LJVh| zP(0rGY)1T^3!R^|PbgRmC5Hk59#_;K1Q;@2F?2(5`P&OvBTWOV%fUBy@8hR<>Zkj0 z9`*FX9Dl}E2d(smS6u#-UKE@Co@v?#>Eb}@qh@k}ZTt!#6bx)4uBC=1^tAx{tJy7o z$C*9WOj0goqnyy*{ck%Z(IN|6w5V~;-oc8E{oA&NuLl?YEmos3ix3OzQPmm|0oSlX zt-Ke%Rf*MZA~j5la$-t|^2&bdJQ21z(5`v5tBQ4*8of zAU!6@%vhCRZz!ERY?ih`KQ=#rR?g5qDtUCvLt%5p?Ze*Nr%}_>W2oOeuLyIsXC|+& zXQ5bwfzCPo^Aqh0#;RX(FOCMJeOE^~i34%+SL1NAi|Qi>6KTo%Z8Vtvf4%BBnot8o zXAoj$rB$=)>NbxqXfyVTxd>#+yTw&UFs&nY8$?uIJIw?xYh_V<)*mQON%4GIx_L~9 zrDBV55%_mCx;3Aey}8LFwaIM432%fhScidf=dw4%0QfZY`QKX%7g3^Mys;>+_^@OE zWTfX*;Xgb+FVpj##}C~?u_*_QBR1g3Y#eEO?aFwlDxT=D zMsbel(Q)ag*Eq%{@heWuUtRwaxVZ1PJe+8_qE!OjA2i+#n6e+XGD6yQu zq=l6~&E#B2bCK2GvE&(wq}<#@_URWJDOB#`2z5n};ixZ;FrY2Oa>ODJ{s7MG^|_(G za~V>nDzR37;(nU6uzwz++^C&?Hl}3scXB3taukH$IQwYOT~NzLv#(Jh+}_^nIB@M@ zG#nYi2aQFre#DcXRaKu7<*DeB2<4YUX?;d=EK3KZ=e_)pp8xG8D%9KQLB^^36$_+y zYD`Y8bquFt$R8W{F>`~Xh8~#tg1^|2u+>zRa^CRjCD#w5EEl98u39)F5u{D`) + + +## Instructions ## To complete this assignment, submit the following: 1. **GitHub Repository Link**: Ensure that your repository is well-organized and includes: