diff --git a/Makefile b/Makefile index 509f087..faa26fd 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,10 @@ testapi: @echo "Running api unit tests..." @docker compose up testapi --exit-code-from testapi +seed: + @echo "Seeding database..." + @docker compose up seeder --exit-code-from seeder + db: @echo "Starting db..." @docker compose up -d db diff --git a/app/api/seeder/Dockerfile.seed b/app/api/seeder/Dockerfile.seed new file mode 100644 index 0000000..f7b0658 --- /dev/null +++ b/app/api/seeder/Dockerfile.seed @@ -0,0 +1,21 @@ +# Dockerfile.seed for seeding development database +FROM python:3.9.1-slim + +RUN mkdir -p /app/api +WORKDIR /app/api + +COPY seeder/seeder_reqs.txt /app/api +RUN pip install --no-cache-dir -r seeder_reqs.txt +COPY main /app/api/ +COPY models /app/api/ +COPY seeder /app/api/ +WORKDIR /app + +# Setup user to represent developer permissions in container +ARG USERNAME=python +ARG USER_UID=1000 +ARG USER_GID=1000 +RUN useradd -rm -d /home/$USERNAME -s /bin/bash -g root -G sudo -u $USER_UID $USERNAME +USER $USERNAME + +EXPOSE 5000 \ No newline at end of file diff --git a/app/api/seeder/__init__.py b/app/api/seeder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/seeder/seed_database.py b/app/api/seeder/seed_database.py new file mode 100644 index 0000000..9cfb282 --- /dev/null +++ b/app/api/seeder/seed_database.py @@ -0,0 +1,110 @@ +from json import JSONDecodeError +from passlib.hash import argon2 +from typing import List, Dict + +from sqlalchemy.orm import Session +from fastapi.testclient import TestClient + +from api.main.app import api +from api.models import User +from api.main.database import get_db + +from api.seeder.seeds.users import USERS + +def create_authed_client(email: str, password: str, client: TestClient): + """ Create an authenticated client for testing + + Args: + email (str): User email + password (str): Plain text password + client (FastAPI.TestClient): Test client to be authenticated + + Returns: + FastAPI.TestClient: Authenticated test client + """ + + # Get token + print(f"Creating authed client for {email}") + login_data = {"email": email, "password": password} + r = client.post("/api/login", json=login_data) + + # Check if login was successful + if r.status_code != 200: + print("create_authed_client response: ", r.json()) + raise Exception("Login failed") + + print("create_authed_client response: ", r.json()) + token = r.json()["access_token"] + + # Add authorization token header + client.headers = {"Authorization": f"Bearer {token}"} + + + return client + + +def create_users(users: List[Dict], db: Session) -> List[User]: + """ Create users in database + + Args: + users (List[Dict]): List of users to create + db (Session): Database session + + Returns: + List[User]: List of created users + """ + + created_users = [] + + # Create users + for user in users: + + # Check if user already exists + user_obj = db.query(User).filter(User.email == user["email"]).first() + if user_obj: + print(f"User {user_obj.email} already exists") + created_users.append(user_obj) + continue + + # Hash password + hashed_password = argon2.hash(user["password"]) + # Create user + user_obj = User( + email=user["email"], + hashed_password=hashed_password, + ) + try: + ## Add user to database + db.add(user_obj) + db.commit() + db.refresh(user_obj) + print( + f"User {user_obj.email} created with uuid: {user_obj.user_uuid}" + ) + created_users.append(user_obj) + except Exception as e: + print(e) + db.rollback() + db.flush() + print(f"User {user_obj.email} already exists") + + return created_users + +def seed_database(): + """ Seed database with mock data. + """ + + db = next(get_db()) + + # Create test client and check if healthy + client = TestClient(api) + r = client.get("/api/") + if r.json()["status"] == "healthy": + print("API is healthy") + + # Create users + created_users = create_users(USERS, db) + + +if __name__ == "__main__": + seed_database() \ No newline at end of file diff --git a/app/api/seeder/seeder_reqs.txt b/app/api/seeder/seeder_reqs.txt new file mode 100644 index 0000000..2d0d1e3 --- /dev/null +++ b/app/api/seeder/seeder_reqs.txt @@ -0,0 +1,16 @@ +argon2-cffi==21.3.0 +argon2-cffi-bindings==21.2.0 +cffi==1.15.0 +email-validator==1.2.1 +fastapi==0.65.1 +passlib==1.7.4 +psycopg2-binary==2.8.6 +py==1.11.0 +pyasn1==0.4.8 +python-dateutil==2.8.2 +python-dotenv==0.20.0 +python-editor==1.0.4 +python-jose==3.3.0 +requests==2.25.1 +SQLAlchemy==1.4.15 +typing_extensions==4.2.0 \ No newline at end of file diff --git a/app/api/seeder/seeds/__init__.py b/app/api/seeder/seeds/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/seeder/seeds/users.py b/app/api/seeder/seeds/users.py new file mode 100644 index 0000000..d596be5 --- /dev/null +++ b/app/api/seeder/seeds/users.py @@ -0,0 +1,21 @@ +""" +Mock data for user model +""" + + +DEFAULT_USER = { + "email": "default_user@default.com", + "password": "Password123!" +} + +TEST_USER_1 = { + "email": "not_bruce_wayne@gothamcity.com", + "password": "B@tCaveSecret1" +} + +TEST_USER_2 = { + "email": "lethal@company.com", + "password": "WeMetQu0ta!" +} + +USERS = [DEFAULT_USER, TEST_USER_1, TEST_USER_2] \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 1d10afa..b6c4228 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,4 @@ -version: '3.9' +version: "3.9" # LOCAL DEV @@ -19,13 +19,14 @@ services: # Mount local codebase to reflect changes for local dev - ./app/api:/app/api healthcheck: - test: [ "CMD", "curl", "localhost:5000/api/health" ] + test: ["CMD", "curl", "localhost:5000/api/health"] interval: 5s timeout: 5s retries: 5 depends_on: - migs - testapi + - seeder migs: container_name: migs @@ -54,7 +55,7 @@ services: ports: - 5432:5432 healthcheck: - test: [ "CMD", "pg_isready" ] + test: ["CMD", "pg_isready"] interval: 5s timeout: 5s retries: 5 @@ -75,6 +76,22 @@ services: depends_on: - migs + seeder: + container_name: seeder + user: 1000:1000 + build: + context: app/api/ + dockerfile: seeder/Dockerfile.seed + env_file: app/api/.env + tty: true + environment: + - POSTGRES_HOST=db + volumes: + - ./app/api/:/app/api + command: python -m api.seeder.seed_database + depends_on: + - testapi + web: container_name: web user: node @@ -114,8 +131,7 @@ services: - ./app/api:/app/api command: bash -c "cd /app/api && alembic upgrade head" - -#PRODUCTION IMAGE VERIFICATION + #PRODUCTION IMAGE VERIFICATION apitarget: container_name: apitarget @@ -130,7 +146,7 @@ services: - 5000:5000 command: uvicorn api.main.app:api --reload --host=0.0.0.0 --port=5000 healthcheck: - test: [ "CMD", "curl", "localhost:5000/api/health" ] + test: ["CMD", "curl", "localhost:5000/api/health"] interval: 5s timeout: 5s retries: 5 @@ -138,9 +154,9 @@ services: - migstarget - testapitarget - migstarget: + migstarget: container_name: migstarget - build: + build: context: app/api dockerfile: Dockerfile.prod env_file: app/api/.env @@ -150,7 +166,7 @@ services: depends_on: db: condition: service_healthy - + testapitarget: container_name: testapitarget user: 1000:1000 @@ -162,22 +178,20 @@ services: - POSTGRES_HOST=db #run as module so basedir (root) is added to python path command: python -m pytest api/tests/ - depends_on: + depends_on: - migstarget - + webtarget: container_name: webtarget user: node - build: + build: context: app/web/ dockerfile: Dockerfile.prod ports: - 3000:3000 - depends_on: + depends_on: - apitarget - - networks: default: driver: "bridge"