diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc3c029..bf9ca3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: working-directory: src - name: Run tests with coverage - run: PYTHONPATH=. pipenv run coverage run -m pytest -v --color=yes tests/ + run: PYTHONPATH=. PIPENV_DOTENV_LOCATION=.env.test pipenv run coverage run -m pytest -v --color=yes tests/ working-directory: src - name: Report coverage diff --git a/src/Pipfile b/src/Pipfile index 4423b6f..dabe443 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -26,6 +26,7 @@ watchdog = "*" pytest = "*" httpx = "*" coverage = "*" +pytest-asyncio = "*" [requires] python_version = "3.10" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 6e6c0f2..840330c 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c34b5745f3a6f67222d3f26e6c7f2d13615a3301d0ca4d1f2b0ec58474b1d43a" + "sha256": "da25332a2152541157c6873ec43ac771c5491bff7d60bb2714c26c4e6b40577f" }, "pipfile-spec": 6, "requires": { @@ -289,25 +289,26 @@ }, "boto3": { "hashes": [ - "sha256:a5b00f8b82dce62870759f04861747944da834d64a64355970120c475efdafc0", - "sha256:e1f36f8be453505cebcc3da178ea081b2a06c0e5e1cdee774f1067599b8d9c3e" + "sha256:18416d07b41e6094101a44f8b881047dcec6b846dad0b9f83b9bbf2f0cd93d07", + "sha256:7f8e8a252458d584d8cf7877c372c4f74ec103356eedf43d2dd9e479f47f3639" ], "markers": "python_version >= '3.8'", - "version": "==1.35.42" + "version": "==1.35.44" }, "botocore": { "hashes": [ - "sha256:05af0bb8b9cea7ce7bc589c332348d338a21b784e9d088a588fd10ec145007ff", - "sha256:af348636f73dc24b7e2dc760a34d08c8f2f94366e9b4c78d877307b128abecef" + "sha256:1fcd97b966ad8a88de4106fe1bd3bbd6d8dadabe99bbd4a6aadcf11cb6c66b39", + "sha256:55388e80624401d017a9a2b8109afd94814f7e666b53e28fce51375cfa8d9326" ], "markers": "python_version >= '3.8'", - "version": "==1.35.42" + "version": "==1.35.44" }, "brotli": { "hashes": [ "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208", "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48", "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354", + "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419", "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a", "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c", @@ -315,43 +316,67 @@ "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a", "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", + "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", + "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2", "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438", "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578", "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b", "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b", "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68", + "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d", + "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943", "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", + "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", + "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0", + "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547", "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", + "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", + "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a", + "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb", "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112", "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", + "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2", "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95", + "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec", "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", + "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c", + "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38", "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914", "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a", "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7", + "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", + "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c", "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", + "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f", "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f", + "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", + "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", + "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", + "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97", "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d", + "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf", "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac", + "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74", + "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60", "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c", "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1", @@ -362,27 +387,44 @@ "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460", "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751", "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9", + "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2", + "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474", + "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75", + "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5", + "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", + "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", + "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", + "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", + "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01", "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467", "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619", "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579", "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84", + "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7", + "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c", + "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", + "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52", "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b", "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59", "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", + "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1", "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", + "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2", "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3", "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64", + "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643", + "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985", "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596", @@ -3401,6 +3443,15 @@ "markers": "python_version >= '3.8'", "version": "==8.2.2" }, + "pytest-asyncio": { + "hashes": [ + "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", + "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.24.0" + }, "sniffio": { "hashes": [ "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", diff --git a/src/core/events.py b/src/core/events.py index abad52a..2fb649e 100644 --- a/src/core/events.py +++ b/src/core/events.py @@ -8,18 +8,16 @@ from loguru import logger from db import crud, models from db.database import get_db, make_engine -from shared.settings import Settings +from shared.settings import get_settings from utils.metrics import measure_regular_metrics, redis_subscribe_worker_exceptions -settings = Settings() - @asynccontextmanager async def lifespan(app: FastAPI): # see https://fastapi.tiangolo.com/advanced/events/#lifespan # STARTUP - engine = make_engine(settings.DATABASE_PATH) + engine = make_engine(get_settings().DATABASE_PATH) models.Base.metadata.create_all(bind=engine) alembic.config.main(argv=['--raiseerr', 'upgrade', 'head']) # disabling uvicorn logger since we use loguru in logging_middleware @@ -42,6 +40,6 @@ async def refresh_user_groups(): crud.upsert_user_groups(db) -@repeat_every(seconds=settings.REPEAT_COUNT_METRICS_SECONDS) +@repeat_every(seconds=get_settings().REPEAT_COUNT_METRICS_SECONDS) async def repeat_measure_regular_metrics(): - measure_regular_metrics(settings.DATABASE_PATH, settings.REPEAT_COUNT_METRICS_SECONDS) + measure_regular_metrics(get_settings().DATABASE_PATH, get_settings().REPEAT_COUNT_METRICS_SECONDS) diff --git a/src/db/crud.py b/src/db/crud.py index b251e6d..0d98a59 100644 --- a/src/db/crud.py +++ b/src/db/crud.py @@ -5,13 +5,13 @@ from loguru import logger from datetime import datetime, timedelta from web.security import ALLOW_ANY_EMAIL -from shared.settings import Settings +from shared.settings import get_settings from . import models, schemas import yaml DOMAIN_GROUPS = {} DOMAIN_GROUPS_LOADED = False -DATABASE_QUERY_LIMIT = Settings().DATABASE_QUERY_LIMIT +DATABASE_QUERY_LIMIT = get_settings().DATABASE_QUERY_LIMIT # --------------- TASK = Archive @@ -152,7 +152,7 @@ def upsert_user_groups(db: Session): along with new participation of users in groups """ logger.debug("Updating user-groups configuration.") - filename = Settings().USER_GROUPS_FILENAME + filename = get_settings().USER_GROUPS_FILENAME # read yaml safely try: diff --git a/src/db/database.py b/src/db/database.py index 8b72f70..d18333d 100644 --- a/src/db/database.py +++ b/src/db/database.py @@ -1,11 +1,9 @@ from sqlalchemy import Engine, create_engine, event -from sqlalchemy.orm import sessionmaker, declarative_base -from shared.settings import Settings +from sqlalchemy.orm import sessionmaker +from shared.settings import get_settings from contextlib import contextmanager -settings = Settings() - def make_engine(database_url: str): engine = create_engine(database_url, connect_args={"check_same_thread": False}) @@ -25,7 +23,7 @@ def make_session_local(engine: Engine): @contextmanager def get_db(): - session = make_session_local(make_engine(settings.DATABASE_PATH))() + session = make_session_local(make_engine(get_settings().DATABASE_PATH))() try: yield session finally: session.close() diff --git a/src/migrations/env.py b/src/migrations/env.py index cabd992..abd7c6e 100644 --- a/src/migrations/env.py +++ b/src/migrations/env.py @@ -5,12 +5,12 @@ from sqlalchemy import pool from alembic import context -from shared.settings import Settings +from shared.settings import get_settings # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config -config.set_main_option('sqlalchemy.url', Settings().DATABASE_PATH) +config.set_main_option('sqlalchemy.url', get_settings().DATABASE_PATH) # Interpret the config file for Python logging. # This line sets up loggers basically. if config.config_file_name is not None: diff --git a/src/shared/settings.py b/src/shared/settings.py index 2190726..56e138f 100644 --- a/src/shared/settings.py +++ b/src/shared/settings.py @@ -1,4 +1,5 @@ +from functools import lru_cache from pydantic_settings import BaseSettings from pydantic import ConfigDict from typing import Annotated, Set @@ -28,3 +29,7 @@ class Settings(BaseSettings): ALLOWED_ORIGINS: Annotated[set[str], Len(min_length=1)] CHROME_APP_IDS: Annotated[set[Annotated[str, Len(min_length=10)]], Len(min_length=1)] BLOCKED_EMAILS: Annotated[Set[str], Len(min_length=0)] = set() + +@lru_cache +def get_settings(): + return Settings() \ No newline at end of file diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 407616d..1b69916 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -13,7 +13,7 @@ def mock_logger_add(): @pytest.fixture() -def settings(): +def get_settings(): return Settings(_env_file=".env.test") @pytest.fixture(autouse=True) @@ -23,13 +23,13 @@ def mock_settings(): @pytest.fixture() -def test_db(settings: Settings): +def test_db(get_settings: Settings): from db.database import make_engine from db import models - engine = make_engine(settings.DATABASE_PATH) + engine = make_engine(get_settings.DATABASE_PATH) - fs = settings.DATABASE_PATH.replace("sqlite:///", "") + fs = get_settings.DATABASE_PATH.replace("sqlite:///", "") if not os.path.exists(fs): open(fs, 'w').close() diff --git a/src/tests/db/test_crud.py b/src/tests/db/test_crud.py index a4fdeff..13352db 100644 --- a/src/tests/db/test_crud.py +++ b/src/tests/db/test_crud.py @@ -135,7 +135,6 @@ def test_search_archives_by_url(test_data, db_session): def test_search_archives_by_email(test_data, db_session): from web.security import ALLOW_ANY_EMAIL from db import crud - from web.security import ALLOW_ANY_EMAIL # lower/upper case assert len(crud.search_archives_by_email(db_session, "rick@example.com")) == 34 @@ -363,13 +362,13 @@ def test_get_group(test_data, db_session): def test_upsert_user_groups(db_session): from db import crud - @patch('db.crud.Settings', new = lambda: bad_setings) + @patch('db.crud.get_settings', new = lambda: bad_setings) def test_missing_yaml(db_session): with pytest.raises(FileNotFoundError): crud.upsert_user_groups(db_session) - @patch('db.crud.Settings', new = lambda: bad_setings) + @patch('db.crud.get_settings', new = lambda: bad_setings) def test_broken_yaml(db_session): with pytest.raises(yaml.YAMLError): crud.upsert_user_groups(db_session) diff --git a/src/tests/web/test_main.py b/src/tests/web/test_main.py index a8c184f..c59b30a 100644 --- a/src/tests/web/test_main.py +++ b/src/tests/web/test_main.py @@ -1,11 +1,11 @@ import os from fastapi.testclient import TestClient -from shared.settings import Settings +from shared.settings import get_settings import shutil -def test_serve_local_archive_logic(settings: Settings): +def test_serve_local_archive_logic(get_settings): # create a test file first os.makedirs("local_archive_test", exist_ok=True) with open("local_archive_test/temp.txt", "w") as f: @@ -13,9 +13,9 @@ def test_serve_local_archive_logic(settings: Settings): try: # modify the settings - settings.SERVE_LOCAL_ARCHIVE = "/app/local_archive_test" + get_settings.SERVE_LOCAL_ARCHIVE = "/app/local_archive_test" from web.main import app_factory - app = app_factory(settings) + app = app_factory(get_settings) # test client = TestClient(app) diff --git a/src/tests/web/test_security.py b/src/tests/web/test_security.py new file mode 100644 index 0000000..29762d1 --- /dev/null +++ b/src/tests/web/test_security.py @@ -0,0 +1,104 @@ +from unittest.mock import patch + +from fastapi import HTTPException +from fastapi.security import HTTPAuthorizationCredentials +import pytest + + +def test_secure_compare(): + from web.security import secure_compare + + assert secure_compare("test", "test") + assert not secure_compare("test", "test2") + + +@pytest.mark.asyncio +async def test_get_token_or_user_auth_with_api(): + from web.security import get_token_or_user_auth, ALLOW_ANY_EMAIL + mock_api = HTTPAuthorizationCredentials(scheme="lorem", credentials="this_is_the_test_api_token") + assert await get_token_or_user_auth(mock_api) == ALLOW_ANY_EMAIL + + +@pytest.mark.asyncio +async def test_get_token_or_user_auth_with_user(): + from web.security import get_token_or_user_auth + bad_user = HTTPAuthorizationCredentials(scheme="ipsum", credentials="invalid") + with pytest.raises(HTTPException) as e: + await get_token_or_user_auth(bad_user) + assert e.status_code == 401 + assert e.detail == "invalid access_token" + + +@patch("web.security.authenticate_user", return_value=(True, "summer@example.com")) +@pytest.mark.asyncio +async def test_get_user_auth(m1): + from web.security import get_user_auth + bad_user = HTTPAuthorizationCredentials(scheme="ipsum", credentials="valid-and-good") + assert await get_user_auth(bad_user) == "summer@example.com" + + +@patch("web.security.secure_compare", return_value=False) +@pytest.mark.asyncio +async def test_token_api_key_auth_exception(m1): + from web.security import token_api_key_auth + + with pytest.raises(HTTPException) as e: + await token_api_key_auth(HTTPAuthorizationCredentials(scheme="ipsum", credentials="does-not-matter"), auto_error=True) + assert e.status_code == 401 + assert e.detail == "Wrong auth credentials" + + +@pytest.mark.asyncio +async def test_authenticate_user(): + from web.security import authenticate_user + + assert authenticate_user("test") == (False, "invalid access_token") + assert authenticate_user(123) == (False, "invalid access_token") + + with patch("web.security.requests.get") as mock_get: + # bad response from oauth2 + mock_get.return_value.status_code = 403 + assert authenticate_user("this-will-call-requests") == (False, "invalid token") + assert mock_get.call_count == 1 + + # 200 but invalid json + mock_get.return_value.status_code = 200 + assert authenticate_user("this-will-call-requests") == (False, "token does not belong to valid APP_ID") + assert mock_get.call_count == 2 + + # 200 but invalid azp and aud + mock_get.return_value.json.return_value = {"email": "summer@example.com", "azp": "not_an_app"} + assert authenticate_user("this-will-call-requests") == (False, "token does not belong to valid APP_ID") + + mock_get.return_value.json.return_value = {"email": "summer@example.com", "aud": "not_an_app"} + assert authenticate_user("this-will-call-requests") == (False, "token does not belong to valid APP_ID") + + mock_get.return_value.json.return_value = {"email": "summer@example.com", "azp": "not_an_app", "aud": "not_an_app"} + assert authenticate_user("this-will-call-requests") == (False, "token does not belong to valid APP_ID") + + # blocked email + mock_get.return_value.json.return_value = {"email": "blocked@example.com", "azp": "test_app_id_1", "aud": "not_an_app"} + assert authenticate_user("this-will-call-requests") == (False, "email 'blocked@example.com' not allowed") + + # not verified + mock_get.return_value.json.return_value = {"email": "summer@example.com", "azp": "not_an_app", "aud": "test_app_id_1"} + assert authenticate_user("this-will-call-requests") == (False, "email 'summer@example.com' not verified") + + # token expired + mock_get.return_value.json.return_value = {"email": "summer@example.com", "azp": "test_app_id_2", "email_verified": "true"} + assert authenticate_user("this-will-call-requests") == (False, "Token expired") + + # 200 and valid azp and aup and verified + mock_get.return_value.json.return_value = {"email": "summer@example.com", "azp": "test_app_id_2", "email_verified": "true", "expires_in": 100} + assert authenticate_user("this-will-call-requests") == (True, "summer@example.com") + assert mock_get.call_count == 9 + + +@pytest.mark.asyncio +async def test_authenticate_user_exception(): + from web.security import authenticate_user + + with patch("web.security.requests.get") as mock_get: + mock_get.return_value.status_code = 200 + mock_get.return_value.json.side_effect = Exception("mocked error") + assert authenticate_user("this-will-call-requests") == (False, "exception occurred") diff --git a/src/web/main.py b/src/web/main.py index be125c4..f70a281 100644 --- a/src/web/main.py +++ b/src/web/main.py @@ -19,14 +19,14 @@ from web.security import get_user_auth, token_api_key_auth, get_token_or_user_au from core.config import VERSION, API_DESCRIPTION from db.database import get_db_dependency from core.events import lifespan -from shared.settings import Settings +from shared.settings import get_settings from auto_archiver import Metadata from endpoints import default_router, url_router, sheet_router, task_router, interoperability_router -def app_factory(settings = Settings()): +def app_factory(settings = get_settings()): app = FastAPI( title="Auto-Archiver API", description=API_DESCRIPTION, diff --git a/src/web/security.py b/src/web/security.py index 26abadc..f4fc90e 100644 --- a/src/web/security.py +++ b/src/web/security.py @@ -1,12 +1,12 @@ from loguru import logger -import requests, os, secrets +import requests, secrets from fastapi import HTTPException, status, Depends from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from shared.settings import Settings +from shared.settings import get_settings ALLOW_ANY_EMAIL = "*" -settings = Settings() +settings = get_settings() bearer_security = HTTPBearer() @@ -39,15 +39,15 @@ token_api_key_auth = api_key_auth(settings.API_BEARER_TOKEN) async def get_token_or_user_auth(credentials: HTTPAuthorizationCredentials = Depends(bearer_security)): # tries to use the static API_KEY and defaults to google JWT auth - access_token = credentials.credentials - if token_api_key_auth(access_token, auto_error=False): return ALLOW_ANY_EMAIL + if await token_api_key_auth(credentials, auto_error=False): return ALLOW_ANY_EMAIL return await get_user_auth(credentials) async def get_user_auth(credentials: HTTPAuthorizationCredentials = Depends(bearer_security)): # validates the Bearer token in the case that it requires it valid_user, info = authenticate_user(credentials.credentials) - if valid_user: return info + if valid_user: + return info logger.debug(f"TOKEN FAILURE: {valid_user=} {info=}") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -73,5 +73,5 @@ def authenticate_user(access_token): return False, "Token expired" return True, j.get('email') except Exception as e: - logger.warning(f"EXCEPTION occurred: {e}") - return False, f"EXCEPTION occurred" + logger.warning(f"AUTH EXCEPTION occurred: {e}") + return False, "exception occurred" diff --git a/src/worker.py b/src/worker.py index 68d8aee..94fbb00 100644 --- a/src/worker.py +++ b/src/worker.py @@ -10,12 +10,12 @@ from loguru import logger from db import crud, schemas, models from db.database import get_db -from shared.settings import Settings +from shared.settings import get_settings import json import redis from sqlalchemy import exc -settings = Settings() +settings = get_settings() celery = Celery(__name__) celery.conf.broker_url = settings.CELERY_BROKER_URL