diff --git a/backend/Pipfile b/backend/Pipfile index 9daa137..8bb64c0 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -4,9 +4,12 @@ verify_ssl = true name = "pypi" [packages] -fastapi = {extras = ["standard"], version = "*"} exceptiongroup = "*" tomli = "*" +asyncpg = "*" +python-dotenv = "*" +fastapi = "*" [dev-packages] black = "*" +pytest = "*" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 3c0b01f..623bd7e 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "44d6a12f3d4663c8dff7701dfebd282f43e70e5b72c5c754be86363db8f458ce" + "sha256": "a798c68e35825e36be6b4f73aab310a1862f22cf9760b2b348d82278ab2c5f59" }, "pipfile-spec": 6, "requires": {}, @@ -30,6 +30,70 @@ "markers": "python_version >= '3.9'", "version": "==4.9.0" }, + "async-timeout": { + "hashes": [ + "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", + "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3" + ], + "markers": "python_version >= '3.8'", + "version": "==5.0.1" + }, + "asyncpg": { + "hashes": [ + "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", + "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", + "sha256:0b448f0150e1c3b96cb0438a0d0aa4871f1472e58de14a3ec320dbb2798fb0d4", + "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", + "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", + "sha256:1b11a555a198b08f5c4baa8f8231c74a366d190755aa4f99aacec5970afe929a", + "sha256:1b982daf2441a0ed314bd10817f1606f1c28b1136abd9e4f11335358c2c631cb", + "sha256:1c06a3a50d014b303e5f6fc1e5f95eb28d2cee89cf58384b700da621e5d5e547", + "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a", + "sha256:26683d3b9a62836fad771a18ecf4659a30f348a561279d6227dab96182f46144", + "sha256:29ff1fc8b5bf724273782ff8b4f57b0f8220a1b2324184846b39d1ab4122031d", + "sha256:3152fef2e265c9c24eec4ee3d22b4f4d2703d30614b0b6753e9ed4115c8a146f", + "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956", + "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f", + "sha256:393af4e3214c8fa4c7b86da6364384c0d1b3298d45803375572f415b6f673f38", + "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", + "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056", + "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d", + "sha256:578445f09f45d1ad7abddbff2a3c7f7c291738fdae0abffbeb737d3fc3ab8b75", + "sha256:5b290f4726a887f75dcd1b3006f484252db37602313f806e9ffc4e5996cfe5cb", + "sha256:5df69d55add4efcd25ea2a3b02025b669a285b767bfbf06e356d68dbce4234ff", + "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a", + "sha256:64e899bce0600871b55368b8483e5e3e7f1860c9482e7f12e0a771e747988168", + "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", + "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", + "sha256:6f4e83f067b35ab5e6371f8a4c93296e0439857b4569850b178a01385e82e9ad", + "sha256:8b684a3c858a83cd876f05958823b68e8d14ec01bb0c0d14a6704c5bf9711773", + "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", + "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed", + "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", + "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", + "sha256:a3479a0d9a852c7c84e822c073622baca862d1217b10a02dd57ee4a7a081f708", + "sha256:aa403147d3e07a267ada2ae34dfc9324e67ccc4cdca35261c8c22792ba2b10cf", + "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", + "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", + "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454", + "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e", + "sha256:c42f6bb65a277ce4d93f3fba46b91a265631c8df7250592dd4f11f8b0152150f", + "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", + "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", + "sha256:c7255812ac85099a0e1ffb81b10dc477b9973345793776b128a23e60148dd1af", + "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", + "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", + "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0", + "sha256:f23b836dd90bea21104f69547923a02b167d999ce053f3d502081acea2fba15b", + "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", + "sha256:f86b0e2cd3f1249d6fe6fd6cfe0cd4538ba994e2d8249c0491925629b9104d0f", + "sha256:fb622c94db4e13137c4c7f98834185049cc50ee01d8f657ef898b6407c7b9c50", + "sha256:fd4406d09208d5b4a14db9a9dbb311b6d7aeeab57bded7ed2f8ea41aeef39b34" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.0'", + "version": "==0.30.0" + }, "certifi": { "hashes": [ "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", @@ -72,13 +136,11 @@ "version": "==1.2.2" }, "fastapi": { - "extras": [ - "standard" - ], "hashes": [ "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d" ], + "index": "pypi", "markers": "python_version >= '3.8'", "version": "==0.115.12" }, @@ -391,6 +453,7 @@ "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d" ], + "index": "pypi", "markers": "python_version >= '3.9'", "version": "==1.1.0" }, @@ -809,6 +872,22 @@ "markers": "python_version >= '3.7'", "version": "==8.1.8" }, + "exceptiongroup": { + "hashes": [ + "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", + "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" + ], + "markers": "python_version >= '3.7'", + "version": "==1.2.2" + }, + "iniconfig": { + "hashes": [ + "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", + "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760" + ], + "markers": "python_version >= '3.8'", + "version": "==2.1.0" + }, "mypy-extensions": { "hashes": [ "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", @@ -819,11 +898,11 @@ }, "packaging": { "hashes": [ - "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", - "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" ], "markers": "python_version >= '3.8'", - "version": "==24.2" + "version": "==25.0" }, "pathspec": { "hashes": [ @@ -840,6 +919,61 @@ ], "markers": "python_version >= '3.9'", "version": "==4.3.7" + }, + "pluggy": { + "hashes": [ + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "pytest": { + "hashes": [ + "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", + "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==8.3.5" + }, + "tomli": { + "hashes": [ + "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", + "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", + "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", + "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", + "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", + "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", + "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", + "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", + "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", + "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", + "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", + "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", + "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", + "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", + "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", + "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", + "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", + "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", + "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", + "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", + "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", + "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", + "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", + "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", + "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", + "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", + "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", + "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", + "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", + "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", + "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", + "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7" + ], + "markers": "python_version >= '3.8'", + "version": "==2.2.1" } } } diff --git a/backend/README.md b/backend/README.md index 716fbae..07937e8 100644 --- a/backend/README.md +++ b/backend/README.md @@ -53,3 +53,9 @@ pipenv run uvicorn app.main:app --reload ``` + +## unit tests + +```bash +pipenv run pytest +``` \ No newline at end of file diff --git a/backend/app/db_init.py b/backend/app/db_init.py new file mode 100644 index 0000000..88c1c25 --- /dev/null +++ b/backend/app/db_init.py @@ -0,0 +1,24 @@ +from dotenv import load_dotenv +import os +from app.scripts.postgresql import create_schema_if_not_exists, create_paste_table + + +async def initialize_database(): + """ + Initialize the database by creating the schema and table if they don't exist. + """ + load_dotenv() + conn_details = { + "host": os.getenv("DB_HOST", "localhost"), + "port": os.getenv("DB_PORT", "5432"), + "user": os.getenv("DB_USER"), + "password": os.getenv("DB_PASSWORD"), + "database": os.getenv("DB_NAME"), + } + schema_name = os.getenv("DB_SCHEMA", "public") + + # Ensure schema and table exist + await create_schema_if_not_exists( + conn_details=conn_details, schema_name=schema_name + ) + await create_paste_table(conn_details=conn_details, schema_name=schema_name) diff --git a/backend/app/main.py b/backend/app/main.py index 06565b0..3404487 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,7 +1,22 @@ from fastapi import FastAPI from app.routes import router +from app.db_init import initialize_database +from contextlib import asynccontextmanager +import os -app = FastAPI() + +@asynccontextmanager +async def lifespan(app: FastAPI): + """ + Lifespan function to handle startup and shutdown events. + """ + if os.getenv("INIT_DB", "true").lower() == "true": + await initialize_database() + + yield # Yield control back to FastAPI + + +app = FastAPI(lifespan=lifespan) # Include the routes from the `routes.py` file app.include_router(router) diff --git a/backend/app/scripts/postgresql.py b/backend/app/scripts/postgresql.py new file mode 100644 index 0000000..f80359a --- /dev/null +++ b/backend/app/scripts/postgresql.py @@ -0,0 +1,55 @@ +import asyncpg +import os +from dotenv import load_dotenv + + +async def create_paste_table(conn_details, schema_name): + """ + Create the 'Paste' table in the database if it doesn't exist. + """ + conn = await asyncpg.connect(**conn_details) + + create_table_query = f""" + CREATE TABLE IF NOT EXISTS {schema_name}.paste ( + id TEXT PRIMARY KEY, + content TEXT NOT NULL, + client_id TEXT NOT NULL, + created_at BIGINT NOT NULL + ); + """ + await conn.execute(create_table_query) + print( + f"Table 'paste' ensured in database {conn_details['database']} under schema {schema_name}." + ) + + await conn.close() + + +async def create_schema_if_not_exists(conn_details, schema_name): + conn = await asyncpg.connect(**conn_details) + + query = f"CREATE SCHEMA IF NOT EXISTS {schema_name};" + await conn.execute(query) + print( + f"Schema '{schema_name}' ensured in database {conn_details['database']} under schema {schema_name}." + ) + + await conn.close() + + +if __name__ == "__main__": + import asyncio + + load_dotenv() + conn_details = { + "host": os.getenv("DB_HOST", "localhost"), + "port": os.getenv("DB_PORT", "5432"), + "user": os.getenv("DB_USER"), + "password": os.getenv("DB_PASSWORD"), + "database": os.getenv("DB_NAME"), + } + schema_name = os.getenv("DB_SCHEMA", "public") + asyncio.run( + create_schema_if_not_exists(conn_details=conn_details, schema_name=schema_name) + ) + asyncio.run(create_paste_table(conn_details=conn_details, schema_name=schema_name)) diff --git a/backend/test/database_test.py b/backend/test/database_test.py new file mode 100644 index 0000000..ed78fc6 --- /dev/null +++ b/backend/test/database_test.py @@ -0,0 +1,44 @@ +from unittest.mock import AsyncMock, patch +from app.main import app + +from fastapi import Depends + +from fastapi.testclient import TestClient + +import os + +client = TestClient(app) # Use FastAPI's TestClient for testing + + +@patch("app.main.initialize_database", new_callable=AsyncMock) +def test_app_initializes_database(mock_initialize_database): + """ + Test that the app initializes the database when INIT_DB=true. + """ + # Set the environment variable to simulate INIT_DB=true + os.environ["INIT_DB"] = "true" + client = TestClient(app) + + with client: + pass + + # Assert that initialize_database was called + mock_initialize_database.assert_called_once() + + +@patch("app.main.initialize_database", new_callable=AsyncMock) +def test_app_startup(mock_initialize_database): + # Test that the app starts without initializing the database + assert app # Ensure the app instance is created + mock_initialize_database.assert_not_called() + + +def get_db_connection(): + # Return a real or mock database connection + pass + + +@app.get("/example") +async def example_route(db=Depends(get_db_connection)): + # Use the db connection + pass