Skip to content

Commit c07b4ea

Browse files
Merge pull request #8 from goldlabelapps/copilot/sub-pr-7
Fix DB integration quality: dependency injection, idempotent seeding, price precision, and test isolation
2 parents 1d445d3 + 0a8f519 commit c07b4ea

7 files changed

Lines changed: 200 additions & 118 deletions

File tree

app/api/routes.py

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,35 @@
11
"""API route definitions for NX AI."""
22

3-
from fastapi import APIRouter
4-
from pydantic import BaseModel
53
import os
6-
from dotenv import load_dotenv
4+
import time
5+
76
import psycopg2
8-
router = APIRouter()
7+
from dotenv import load_dotenv
8+
from fastapi import APIRouter, Depends
9+
from pydantic import BaseModel
910

11+
from app import __version__
12+
13+
load_dotenv()
1014

1115
router = APIRouter()
1216

1317

18+
def get_db_connection(): # type: ignore[return]
19+
"""Create and yield a PostgreSQL connection for use as a FastAPI dependency."""
20+
conn = psycopg2.connect(
21+
host=os.getenv('DB_HOST'),
22+
port=os.getenv('DB_PORT', '5432'),
23+
dbname=os.getenv('DB_NAME'),
24+
user=os.getenv('DB_USER'),
25+
password=os.getenv('DB_PASSWORD'),
26+
)
27+
try:
28+
yield conn
29+
finally:
30+
conn.close()
31+
32+
1433
class EchoRequest(BaseModel):
1534
"""Request body for the echo endpoint."""
1635

@@ -23,44 +42,32 @@ class EchoResponse(BaseModel):
2342
echo: str
2443

2544

26-
27-
import time
28-
import sys
29-
from app import __version__
30-
3145
@router.get("/")
32-
def root() -> dict:
46+
def root(conn=Depends(get_db_connection)) -> dict:
3347
"""Return a structured welcome message for the API root, including product data."""
34-
load_dotenv()
35-
conn = psycopg2.connect(
36-
host=os.getenv('DB_HOST'),
37-
port=os.getenv('DB_PORT', '5432'),
38-
dbname=os.getenv('DB_NAME'),
39-
user=os.getenv('DB_USER'),
40-
password=os.getenv('DB_PASSWORD')
41-
)
4248
cur = conn.cursor()
43-
cur.execute('SELECT id, name, description, price, in_stock, created_at FROM product;')
44-
products = [
45-
{
46-
"id": row[0],
47-
"name": row[1],
48-
"description": row[2],
49-
"price": float(row[3]),
50-
"in_stock": row[4],
51-
"created_at": row[5].isoformat() if row[5] else None
52-
}
53-
for row in cur.fetchall()
54-
]
55-
cur.close()
56-
conn.close()
49+
try:
50+
cur.execute('SELECT id, name, description, price, in_stock, created_at FROM product;')
51+
products = [
52+
{
53+
"id": row[0],
54+
"name": row[1],
55+
"description": row[2],
56+
"price": str(row[3]) if row[3] is not None else None,
57+
"in_stock": row[4],
58+
"created_at": row[5].isoformat() if row[5] else None,
59+
}
60+
for row in cur.fetchall()
61+
]
62+
finally:
63+
cur.close()
5764
epoch = int(time.time() * 1000)
5865
meta = {
5966
"version": __version__,
6067
"time": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()),
6168
"epoch": epoch,
6269
"severity": "success",
63-
"message": f"NX AI says hello. Returned {len(products)} products."
70+
"message": f"NX AI says hello. Returned {len(products)} products.",
6471
}
6572
return {"meta": meta, "data": products}
6673

app/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
from fastapi import FastAPI
44

5+
from app import __version__
56
from app.api.routes import router
67

78
app = FastAPI(
89
title="NX AI",
910
description="Production-ready Python FastAPI app for NX",
10-
version="1.0.0",
11+
version=__version__,
1112
)
1213

1314
app.include_router(router)

app/print_products.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
import os
2-
from dotenv import load_dotenv
2+
33
import psycopg2
4+
from dotenv import load_dotenv
45

5-
load_dotenv()
66

7-
conn = psycopg2.connect(
8-
host=os.getenv('DB_HOST'),
9-
port=os.getenv('DB_PORT', '5432'),
10-
dbname=os.getenv('DB_NAME'),
11-
user=os.getenv('DB_USER'),
12-
password=os.getenv('DB_PASSWORD')
13-
)
14-
cur = conn.cursor()
7+
def main() -> None:
8+
load_dotenv()
159

16-
cur.execute('SELECT * FROM product;')
17-
rows = cur.fetchall()
10+
conn = psycopg2.connect(
11+
host=os.getenv('DB_HOST'),
12+
port=os.getenv('DB_PORT', '5432'),
13+
dbname=os.getenv('DB_NAME'),
14+
user=os.getenv('DB_USER'),
15+
password=os.getenv('DB_PASSWORD'),
16+
)
17+
cur = conn.cursor()
18+
try:
19+
cur.execute('SELECT * FROM product;')
20+
rows = cur.fetchall()
21+
for row in rows:
22+
print(row)
23+
finally:
24+
cur.close()
25+
conn.close()
1826

19-
for row in rows:
20-
print(row)
2127

22-
cur.close()
23-
conn.close()
28+
if __name__ == "__main__":
29+
main()

app/seed_product_table.py

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,48 @@
11
import os
2-
from dotenv import load_dotenv
2+
33
import psycopg2
4+
from dotenv import load_dotenv
5+
6+
7+
def main() -> None:
8+
load_dotenv()
9+
10+
conn = psycopg2.connect(
11+
host=os.getenv('DB_HOST'),
12+
port=os.getenv('DB_PORT', '5432'),
13+
dbname=os.getenv('DB_NAME'),
14+
user=os.getenv('DB_USER'),
15+
password=os.getenv('DB_PASSWORD'),
16+
)
17+
cur = conn.cursor()
18+
try:
19+
# Create product table with a unique constraint on name for idempotent seeding
20+
cur.execute('''
21+
CREATE TABLE IF NOT EXISTS product (
22+
id SERIAL PRIMARY KEY,
23+
name VARCHAR(100) NOT NULL UNIQUE,
24+
description TEXT,
25+
price NUMERIC(10, 2) NOT NULL,
26+
in_stock BOOLEAN DEFAULT TRUE,
27+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
28+
);
29+
''')
30+
31+
# Insert seed data; skip rows whose name already exists
32+
cur.execute('''
33+
INSERT INTO product (name, description, price, in_stock) VALUES
34+
('Widget', 'A useful widget', 19.99, TRUE),
35+
('Gadget', 'A fancy gadget', 29.99, TRUE),
36+
('Thingamajig', 'An interesting thingamajig', 9.99, FALSE)
37+
ON CONFLICT (name) DO NOTHING;
38+
''')
39+
40+
conn.commit()
41+
print("Product table created and seeded.")
42+
finally:
43+
cur.close()
44+
conn.close()
45+
446

5-
load_dotenv()
6-
7-
# Database connection
8-
conn = psycopg2.connect(
9-
host=os.getenv('DB_HOST'),
10-
port=os.getenv('DB_PORT', '5432'),
11-
dbname=os.getenv('DB_NAME'),
12-
user=os.getenv('DB_USER'),
13-
password=os.getenv('DB_PASSWORD')
14-
)
15-
cur = conn.cursor()
16-
17-
# Create product table
18-
cur.execute('''
19-
CREATE TABLE IF NOT EXISTS product (
20-
id SERIAL PRIMARY KEY,
21-
name VARCHAR(100) NOT NULL,
22-
description TEXT,
23-
price NUMERIC(10, 2) NOT NULL,
24-
in_stock BOOLEAN DEFAULT TRUE,
25-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
26-
);
27-
''')
28-
29-
# Insert seed data
30-
cur.execute('''
31-
INSERT INTO product (name, description, price, in_stock) VALUES
32-
('Widget', 'A useful widget', 19.99, TRUE),
33-
('Gadget', 'A fancy gadget', 29.99, TRUE),
34-
('Thingamajig', 'An interesting thingamajig', 9.99, FALSE)
35-
ON CONFLICT DO NOTHING;
36-
''')
37-
38-
conn.commit()
39-
cur.close()
40-
conn.close()
41-
print("Product table created and seeded.")
47+
if __name__ == "__main__":
48+
main()

app/test_db_connection.py

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,40 @@
1-
2-
from dotenv import load_dotenv
31
import os
2+
import sys
3+
44
import psycopg2
5+
from dotenv import load_dotenv
6+
7+
8+
def main() -> int:
9+
load_dotenv()
10+
11+
db_host = os.getenv('DB_HOST')
12+
db_port = os.getenv('DB_PORT', '5432')
13+
db_name = os.getenv('DB_NAME')
14+
db_user = os.getenv('DB_USER')
15+
db_password = os.getenv('DB_PASSWORD')
16+
17+
print("Attempting connection with:")
18+
print(f"Host: {db_host}")
19+
print(f"Port: {db_port}")
20+
print(f"Database: {db_name}")
21+
print(f"User: {db_user}")
22+
23+
try:
24+
conn = psycopg2.connect(
25+
host=db_host,
26+
port=db_port,
27+
dbname=db_name,
28+
user=db_user,
29+
password=db_password,
30+
)
31+
print("Connection successful!")
32+
conn.close()
33+
return 0
34+
except Exception as e:
35+
print(f"Connection failed: {e}")
36+
return 1
37+
538

6-
# Load .env file
7-
load_dotenv()
8-
9-
# Load environment variables
10-
db_host = os.getenv('DB_HOST')
11-
db_port = os.getenv('DB_PORT', '5432')
12-
db_name = os.getenv('DB_NAME')
13-
db_user = os.getenv('DB_USER')
14-
db_password = os.getenv('DB_PASSWORD')
15-
16-
print("Attempting connection with:")
17-
print(f"Host: {db_host}")
18-
print(f"Port: {db_port}")
19-
print(f"Database: {db_name}")
20-
print(f"User: {db_user}")
21-
22-
try:
23-
conn = psycopg2.connect(
24-
host=db_host,
25-
port=db_port,
26-
dbname=db_name,
27-
user=db_user,
28-
password=db_password
29-
)
30-
print("Connection successful!")
31-
conn.close()
32-
except Exception as e:
33-
print(f"Connection failed: {e}")
39+
if __name__ == "__main__":
40+
sys.exit(main())

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ fastapi>=0.110.0
22
uvicorn[standard]>=0.29.0
33
httpx>=0.27.0
44
pytest>=8.1.0
5+
python-dotenv>=1.0.0
6+
psycopg2-binary>=2.9.0

tests/test_routes.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,63 @@
11
"""Unit and integration tests for NX AI routes."""
22

3+
from unittest.mock import MagicMock
4+
35
from fastapi.testclient import TestClient
46

7+
from app.api.routes import get_db_connection
58
from app.main import app
69

710
client = TestClient(app)
811

912

13+
<<<<<<< copilot/sub-pr-7
14+
def _mock_db_dependency(rows=None):
15+
"""Return a FastAPI dependency override that yields a mock DB connection."""
16+
if rows is None:
17+
rows = []
18+
19+
def override():
20+
mock_conn = MagicMock()
21+
mock_cursor = MagicMock()
22+
mock_conn.cursor.return_value = mock_cursor
23+
mock_cursor.fetchall.return_value = rows
24+
yield mock_conn
25+
26+
return override
27+
28+
29+
def test_root_returns_product_data() -> None:
30+
"""GET / should return meta and data with product list."""
31+
app.dependency_overrides[get_db_connection] = _mock_db_dependency(rows=[])
32+
try:
33+
response = client.get("/")
34+
assert response.status_code == 200
35+
body = response.json()
36+
assert "meta" in body
37+
assert "data" in body
38+
assert body["meta"]["severity"] == "success"
39+
assert isinstance(body["data"], list)
40+
finally:
41+
app.dependency_overrides.clear()
42+
43+
44+
def test_root_returns_products_from_db() -> None:
45+
"""GET / should include product rows returned by the database."""
46+
from datetime import datetime
47+
from decimal import Decimal
48+
mock_row = (1, "Widget", "A useful widget", Decimal("19.99"), True, datetime(2024, 1, 1, 0, 0, 0))
49+
app.dependency_overrides[get_db_connection] = _mock_db_dependency(rows=[mock_row])
50+
try:
51+
response = client.get("/")
52+
assert response.status_code == 200
53+
body = response.json()
54+
assert len(body["data"]) == 1
55+
assert body["data"][0]["name"] == "Widget"
56+
assert body["data"][0]["price"] == "19.99"
57+
assert "Returned 1 products" in body["meta"]["message"]
58+
finally:
59+
app.dependency_overrides.clear()
60+
=======
1061
def test_root_returns_welcome_message() -> None:
1162
"""GET / should return a welcome message."""
1263
response = client.get("/")
@@ -16,6 +67,7 @@ def test_root_returns_welcome_message() -> None:
1667
assert "data" in json_data
1768
assert "message" in json_data["meta"]
1869
assert "NX AI" in json_data["meta"]["message"]
70+
>>>>>>> staging
1971

2072

2173
def test_health_returns_ok() -> None:

0 commit comments

Comments
 (0)