Skip to content

Commit 9a8a4fb

Browse files
Merge pull request #96 from socraticDevBlog/20250421-bootstrappingpostgresql
bootstraping posgresql allowing unit tests
2 parents 1447109 + 96d8d9e commit 9a8a4fb

File tree

7 files changed

+290
-9
lines changed

7 files changed

+290
-9
lines changed

backend/Pipfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ verify_ssl = true
44
name = "pypi"
55

66
[packages]
7-
fastapi = {extras = ["standard"], version = "*"}
87
exceptiongroup = "*"
98
tomli = "*"
9+
asyncpg = "*"
10+
python-dotenv = "*"
11+
fastapi = "*"
1012

1113
[dev-packages]
1214
black = "*"
15+
pytest = "*"

backend/Pipfile.lock

Lines changed: 141 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,9 @@ pipenv run uvicorn app.main:app --reload
5353
```
5454

5555
<http://127.0.0.1:8000/docs>
56+
57+
## unit tests
58+
59+
```bash
60+
pipenv run pytest
61+
```

backend/app/db_init.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from dotenv import load_dotenv
2+
import os
3+
from app.scripts.postgresql import create_schema_if_not_exists, create_paste_table
4+
5+
6+
async def initialize_database():
7+
"""
8+
Initialize the database by creating the schema and table if they don't exist.
9+
"""
10+
load_dotenv()
11+
conn_details = {
12+
"host": os.getenv("DB_HOST", "localhost"),
13+
"port": os.getenv("DB_PORT", "5432"),
14+
"user": os.getenv("DB_USER"),
15+
"password": os.getenv("DB_PASSWORD"),
16+
"database": os.getenv("DB_NAME"),
17+
}
18+
schema_name = os.getenv("DB_SCHEMA", "public")
19+
20+
# Ensure schema and table exist
21+
await create_schema_if_not_exists(
22+
conn_details=conn_details, schema_name=schema_name
23+
)
24+
await create_paste_table(conn_details=conn_details, schema_name=schema_name)

backend/app/main.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
11
from fastapi import FastAPI
22
from app.routes import router
3+
from app.db_init import initialize_database
4+
from contextlib import asynccontextmanager
5+
import os
36

4-
app = FastAPI()
7+
8+
@asynccontextmanager
9+
async def lifespan(app: FastAPI):
10+
"""
11+
Lifespan function to handle startup and shutdown events.
12+
"""
13+
if os.getenv("INIT_DB", "true").lower() == "true":
14+
await initialize_database()
15+
16+
yield # Yield control back to FastAPI
17+
18+
19+
app = FastAPI(lifespan=lifespan)
520

621
# Include the routes from the `routes.py` file
722
app.include_router(router)

backend/app/scripts/postgresql.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import asyncpg
2+
import os
3+
from dotenv import load_dotenv
4+
5+
6+
async def create_paste_table(conn_details, schema_name):
7+
"""
8+
Create the 'Paste' table in the database if it doesn't exist.
9+
"""
10+
conn = await asyncpg.connect(**conn_details)
11+
12+
create_table_query = f"""
13+
CREATE TABLE IF NOT EXISTS {schema_name}.paste (
14+
id TEXT PRIMARY KEY,
15+
content TEXT NOT NULL,
16+
client_id TEXT NOT NULL,
17+
created_at BIGINT NOT NULL
18+
);
19+
"""
20+
await conn.execute(create_table_query)
21+
print(
22+
f"Table 'paste' ensured in database {conn_details['database']} under schema {schema_name}."
23+
)
24+
25+
await conn.close()
26+
27+
28+
async def create_schema_if_not_exists(conn_details, schema_name):
29+
conn = await asyncpg.connect(**conn_details)
30+
31+
query = f"CREATE SCHEMA IF NOT EXISTS {schema_name};"
32+
await conn.execute(query)
33+
print(
34+
f"Schema '{schema_name}' ensured in database {conn_details['database']} under schema {schema_name}."
35+
)
36+
37+
await conn.close()
38+
39+
40+
if __name__ == "__main__":
41+
import asyncio
42+
43+
load_dotenv()
44+
conn_details = {
45+
"host": os.getenv("DB_HOST", "localhost"),
46+
"port": os.getenv("DB_PORT", "5432"),
47+
"user": os.getenv("DB_USER"),
48+
"password": os.getenv("DB_PASSWORD"),
49+
"database": os.getenv("DB_NAME"),
50+
}
51+
schema_name = os.getenv("DB_SCHEMA", "public")
52+
asyncio.run(
53+
create_schema_if_not_exists(conn_details=conn_details, schema_name=schema_name)
54+
)
55+
asyncio.run(create_paste_table(conn_details=conn_details, schema_name=schema_name))

backend/test/database_test.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from unittest.mock import AsyncMock, patch
2+
from app.main import app
3+
4+
from fastapi import Depends
5+
6+
from fastapi.testclient import TestClient
7+
8+
import os
9+
10+
client = TestClient(app) # Use FastAPI's TestClient for testing
11+
12+
13+
@patch("app.main.initialize_database", new_callable=AsyncMock)
14+
def test_app_initializes_database(mock_initialize_database):
15+
"""
16+
Test that the app initializes the database when INIT_DB=true.
17+
"""
18+
# Set the environment variable to simulate INIT_DB=true
19+
os.environ["INIT_DB"] = "true"
20+
client = TestClient(app)
21+
22+
with client:
23+
pass
24+
25+
# Assert that initialize_database was called
26+
mock_initialize_database.assert_called_once()
27+
28+
29+
@patch("app.main.initialize_database", new_callable=AsyncMock)
30+
def test_app_startup(mock_initialize_database):
31+
# Test that the app starts without initializing the database
32+
assert app # Ensure the app instance is created
33+
mock_initialize_database.assert_not_called()
34+
35+
36+
def get_db_connection():
37+
# Return a real or mock database connection
38+
pass
39+
40+
41+
@app.get("/example")
42+
async def example_route(db=Depends(get_db_connection)):
43+
# Use the db connection
44+
pass

0 commit comments

Comments
 (0)