diff --git a/backend/Dockerfile b/backend/Dockerfile index d0051fe..ed22b24 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -7,7 +7,7 @@ RUN apt-get update \ && apt-get -y install netcat gcc postgresql \ && apt-get clean # install python dependencies -RUN pip install --upgrade pip +RUN pip3 install --upgrade pip COPY ./requirements.txt /backend/requirements.txt -RUN pip install -r requirements.txt +RUN pip3 install -r requirements.txt COPY . /backend \ No newline at end of file diff --git a/backend/app/core/config.py b/backend/app/core/config.py index dd46265..3cf46f7 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -31,7 +31,7 @@ POSTGRES_USER = config("POSTGRES_USER", cast=str) POSTGRES_PASSWORD = config("POSTGRES_PASSWORD", cast=Secret) POSTGRES_SERVER = config("POSTGRES_SERVER", cast=str, default="db") -POSTGRES_PORT = config("POSTGRES_PORT", cast=str, default="5432") +POSTGRES_PORT = config("POSTGRES_PORT", cast=str, default="5433") POSTGRES_DB = config("POSTGRES_DB", cast=str) DATABASE_URL = config( "DATABASE_URL", diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000..a919f38 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,261 @@ +# here we can create and configure things that are common to tests + +# we make fixtures for testing and then use them in our user/post/etc tests + +# e.g.: + +# we create common test_users with X privileges in the testing database +# we create a client using a test_user to make requests + +import warnings +import os + +from app.core.config import JWT_TOKEN_PREFIX + +from typing import List, Callable + +# testing +import pytest + +# Request + response +from httpx import AsyncClient # emulates a client +from fastapi import FastAPI +from asgi_lifespan import LifespanManager + +# Database +from databases import Database +import alembic +from alembic.config import Config + +# Repositories +from app.db.repositories.users import UsersRepository +from app.db.repositories.alerts import AlertsRepository +from app.db.repositories.assets import AssetsRepository + +# Models +from app.models.user import UserCreate, UserUpdate, User +from app.models.alert import AlertCreate, AlertUpdate, Alert +from app.models.asset import AssetCreate, Asset + +# Services (auth) +from app.services.authentication import AuthService + + +# CORE: all tests need this +@pytest.fixture(scope="session") # exists for duration of testing session +def apply_migrations(): + """ + Applies alembic migrations to the testing database prior to testing. Then downgrades applied migrations at end of testing session. + """ + warnings.filterwarnings("ignore", category=DeprecationWarning) + os.environ["TESTING"] = "1" # see app/core/config + config = Config("alembic.ini") + + alembic.command.upgrade(config, "head") + yield + alembic.command.downgrade(config, "base") + + +# CORE +@pytest.fixture +def app(apply_migrations: None) -> FastAPI: + """ + Creates and returns an app for use in tests. + """ + from app.api.server import get_application + + return get_application() + + +# CORE +@pytest.fixture +def db(app: FastAPI) -> Database: + """ + Uses the app's state to return the database. (testing db not live) + """ + return app.state._db + + +# CORE +@pytest.fixture +async def client(app: FastAPI) -> AsyncClient: + """ + Emulates a client (browser/mobileapp/etc) and sends to the application + """ + async with LifespanManager(app): + async with AsyncClient( + app=app, + base_url="http://testing", + headers={"Content-Type": "application/json"}, + ) as client: + yield client + + +# USERS - creates a test_user that exists throughout life of testing +@pytest.fixture +async def test_user(db: Database) -> User: + test_user = UserCreate( + email="conf@test.com", + phone_number = 1234567890, + ) + + user_repo = UsersRepository(db) + + # database persists for duration of the testing session + existing_user = await user_repo.get_user_by_email(email=test_user.email) + if existing_user: + return existing_user + # else + + return await user_repo.create_user(new_user=test_user) + + +# CORE - create an authorised client for test_user +@pytest.fixture +async def test_user_auth_client( + client: AsyncClient, test_user: User +) -> AsyncClient: + """ + Emulates a client with an authenticated user matching test_user. Used to test protected routes. + """ + # create_access_token has default vars from app.config for all but user + access_token = AuthService.create_access_token_for_user(user=test_user) + + # add additional auth headers to our client fixture + client.headers = { + **client.headers, + "Authorization": f"{JWT_TOKEN_PREFIX} {access_token}", + } + + return client + + +# CORE - create an authorised client for ANY user +@pytest.fixture +def create_auth_client(client: AsyncClient) -> Callable: + """ + Takes client (fixture defined already, not necessary to provide). + Returns a callable function that takes UserInDB. + Returns a client having Authorization headers with that user's token. + e.g. authorized_client_with_this_user = create_auth_client(user=this_user) + functionally we are injecting client into the function for when the user is provided. + """ + + def _create_authorised_client(*, user: User) -> AsyncClient: + """ + see user_auth_client + """ + # create_access_token has default vars from app.config for all but user + access_token = AuthService.create_access_token_for_user(user=user) + + # add additional auth headers to our client fixture + client.headers = { + **client.headers, + "Authorization": f"{JWT_TOKEN_PREFIX} {access_token}", + } + + return client + + return _create_authorised_client + + +# HELPER - create a user helper +async def helper_create_user( + *, db: Database, new_user: UserCreate +) -> User: + """ + Helper function that takes a UserCreate as input and creates that user/org + """ + user_repo = UsersRepository(db) + existing_user = await user_repo.get_user_by_email(email=new_user.email) + if existing_user: + return existing_user + + return await user_repo.create_user(new_user=new_user) + + +# USERS - creates test_user1 + 2 + 3 +@pytest.fixture +async def test_user1(db: Database) -> User: + new_user = UserCreate(email="testuser1@conf.test", phone_number="1111122222") + return await helper_create_user(db=db, new_user=new_user) + + +@pytest.fixture +async def test_user2(db: Database) -> User: + new_user = UserCreate(email="testuser2@conf.test", phone_number="1234512345") + return await helper_create_user(db=db, new_user=new_user) + + +@pytest.fixture +async def test_user3(db: Database) -> User: + new_user = UserCreate(email="testuser3@conf.test", phone_number="2345623456") + return await helper_create_user(db=db, new_user=new_user) + + +# LIST of USERS / ORGS for easy access +@pytest.fixture +async def test_user_list( + test_user1: User, test_user2: User, test_user3: User +) -> List[User]: + return [test_user1, test_user2, test_user3] + +# # ALERTS - creates a test_alert belonging to test_user1 +# @pytest.fixture +# async def org1_test_post(db: Database, test_user1: test_user1) -> Alert: +# """ +# Alerts owned created by test_user1 +# """ +# # get repo's from db +# alerts_repo = AlertsRepository(db) + +# # make a post with test_org1 as owner +# local_new_post = AlertCreate( +# asset_id=1, +# price=2, + +# ) + +# # create post in repo +# created_post = await post_repo.create_post( +# new_post=local_new_post, requesting_user=test_org1 +# ) + +# return created_post + + +# # POSTS + INTERESTED +# @pytest.fixture +# async def test_post_with_applications( +# db: Database, test_org1: UserInDB, test_user_list: List[UserInDB] +# ) -> PostInDB: +# """ +# Post owned by testorg1 with interest from testuser1,2,3 +# """ +# # get repo's from db +# post_repo = PostsRepository(db) + +# # make a post with test_org1 as owner +# local_new_post = PostCreate( +# title="fixture test post with interested", +# short_desc="fixture short desc", +# long_desc="fixture long desc", +# location="fixture location", +# ) + +# # create post in repo +# created_post = await post_repo.create_post( +# new_post=local_new_post, requesting_user=test_org1 +# ) + +# # create 'interested in this' for all of the testusers +# for user in test_user_list: +# # repo.create_interested_in_post() TODO: implement interested in post +# interested = user.is_org + +# return created_post + + +# # HELPER - creates a post belonging to the passed in user +# async def helper_create_post_with_applications() -> None: +# pass \ No newline at end of file diff --git a/issues.md b/issues.md new file mode 100644 index 0000000..e69de29