Skip to content

Commit f0f25b5

Browse files
committed
rename tests, bump pg, add lifespan and probe, add makefile
Signed-off-by: rafsaf <rafal.safin12@gmail.com>
1 parent 8b3ab7c commit f0f25b5

22 files changed

Lines changed: 191 additions & 155 deletions

.env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@ DATABASE__HOSTNAME=localhost
66
DATABASE__USERNAME=rDGJeEDqAz
77
DATABASE__PASSWORD=XsPQhCoEfOQZueDjsILetLDUvbvSxAMnrVtgVZpmdcSssUgbvs
88
DATABASE__PORT=5455
9-
DATABASE__DB=default_db
9+
DATABASE__DB=default_db
10+
11+
PROMETHEUS__ENABLED=true
12+
PROMETHEUS__ADDR="127.0.0.1"

Dockerfile

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.13.5-slim-bookworm AS base
1+
FROM python:3.14-slim-trixie AS base
22

33
ENV PYTHONUNBUFFERED=1
44
WORKDIR /build
@@ -28,8 +28,9 @@ COPY alembic.ini .
2828
COPY pyproject.toml .
2929
COPY init.sh .
3030

31-
# Expose port
31+
# Expose port 8000 for app and optional 9090 for prometheus metrics
3232
EXPOSE 8000
33+
EXPOSE 9090
3334

3435
# Make the init script executable
3536
RUN chmod +x ./init.sh
@@ -39,4 +40,4 @@ ENTRYPOINT ["./init.sh"]
3940

4041
# Set CMD to uvicorn
4142
# /venv/bin/uvicorn is used because from entrypoint script PATH is new
42-
CMD ["/venv/bin/uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2", "--loop", "uvloop"]
43+
CMD ["/venv/bin/uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1", "--loop", "uvloop"]

Makefile

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
BIND_PORT ?= 8000
2+
BIND_HOST ?= localhost
3+
4+
.PHONY: help
5+
help: ## Print this help message
6+
grep -E '^[\.a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
7+
8+
.env: ## Ensure there is env file or create one
9+
echo "No .env file found. Want to create it from .env.example? [y/n]" && read answer && if [ $${answer:-'N'} = 'y' ]; then cp .env.example .env;fi
10+
11+
.PHONY: local-setup
12+
local-setup: ## Setup local postgres database
13+
docker compose up -d
14+
15+
.PHONY: up
16+
up: local-setup ## Run FastAPI development server
17+
uv run alembic upgrade head
18+
uv run uvicorn app.main:app --reload --host $(BIND_HOST) --port $(BIND_PORT)
19+
20+
.PHONY: run
21+
run: up ## Alias for `up`
22+
23+
.PHONY: down
24+
down: ## Stop database
25+
docker compose down
26+
27+
.PHONY: test
28+
test: local-setup ## Run unit tests
29+
uv run pytest .
30+
31+
.PHONY: lint
32+
lint: local-setup ## Run all linters
33+
uv run pre-commit run -a
34+
uv run mypy .

app/auth/tests/test_read_current_user.py

Lines changed: 0 additions & 37 deletions
This file was deleted.

app/auth/tests/test_delete_current_user.py renamed to app/auth/tests/test_view_delete_current_user.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import pytest
21
from fastapi import status
32
from httpx import AsyncClient
43
from sqlalchemy import select
@@ -8,7 +7,6 @@
87
from app.main import app
98

109

11-
@pytest.mark.asyncio(loop_scope="session")
1210
async def test_delete_current_user_status_code(
1311
client: AsyncClient,
1412
default_user_headers: dict[str, str],
@@ -21,7 +19,6 @@ async def test_delete_current_user_status_code(
2119
assert response.status_code == status.HTTP_204_NO_CONTENT
2220

2321

24-
@pytest.mark.asyncio(loop_scope="session")
2522
async def test_delete_current_user_is_deleted_in_db(
2623
client: AsyncClient,
2724
default_user_headers: dict[str, str],

app/auth/tests/test_access_token.py renamed to app/auth/tests/test_view_login_access_token.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import time
22

3-
import pytest
43
from fastapi import status
54
from freezegun import freeze_time
65
from httpx import AsyncClient
@@ -15,7 +14,6 @@
1514
from app.tests.auth import TESTS_USER_PASSWORD
1615

1716

18-
@pytest.mark.asyncio(loop_scope="session")
1917
async def test_login_access_token_has_response_status_code(
2018
client: AsyncClient,
2119
default_user: User,
@@ -31,7 +29,6 @@ async def test_login_access_token_has_response_status_code(
3129
assert response.status_code == status.HTTP_200_OK, response.text
3230

3331

34-
@pytest.mark.asyncio(loop_scope="session")
3532
async def test_login_access_token_jwt_has_valid_token_type(
3633
client: AsyncClient,
3734
default_user: User,
@@ -49,7 +46,6 @@ async def test_login_access_token_jwt_has_valid_token_type(
4946
assert token["token_type"] == "Bearer"
5047

5148

52-
@pytest.mark.asyncio(loop_scope="session")
5349
@freeze_time("2023-01-01")
5450
async def test_login_access_token_jwt_has_valid_expire_time(
5551
client: AsyncClient,
@@ -72,7 +68,6 @@ async def test_login_access_token_jwt_has_valid_expire_time(
7268
)
7369

7470

75-
@pytest.mark.asyncio(loop_scope="session")
7671
@freeze_time("2023-01-01")
7772
async def test_login_access_token_returns_valid_jwt_access_token(
7873
client: AsyncClient,
@@ -97,7 +92,6 @@ async def test_login_access_token_returns_valid_jwt_access_token(
9792
assert token_payload.exp == token["expires_at"]
9893

9994

100-
@pytest.mark.asyncio(loop_scope="session")
10195
async def test_login_access_token_refresh_token_has_valid_expire_time(
10296
client: AsyncClient,
10397
default_user: User,
@@ -120,7 +114,6 @@ async def test_login_access_token_refresh_token_has_valid_expire_time(
120114
)
121115

122116

123-
@pytest.mark.asyncio(loop_scope="session")
124117
async def test_login_access_token_refresh_token_exists_in_db(
125118
client: AsyncClient,
126119
default_user: User,
@@ -144,7 +137,6 @@ async def test_login_access_token_refresh_token_exists_in_db(
144137
assert token_db_count == 1
145138

146139

147-
@pytest.mark.asyncio(loop_scope="session")
148140
async def test_login_access_token_refresh_token_in_db_has_valid_fields(
149141
client: AsyncClient,
150142
default_user: User,
@@ -171,7 +163,6 @@ async def test_login_access_token_refresh_token_in_db_has_valid_fields(
171163
assert not refresh_token.used
172164

173165

174-
@pytest.mark.asyncio(loop_scope="session")
175166
async def test_auth_access_token_fail_for_not_existing_user_with_message(
176167
client: AsyncClient,
177168
) -> None:
@@ -188,7 +179,6 @@ async def test_auth_access_token_fail_for_not_existing_user_with_message(
188179
assert response.json() == {"detail": api_messages.PASSWORD_INVALID}
189180

190181

191-
@pytest.mark.asyncio(loop_scope="session")
192182
async def test_auth_access_token_fail_for_invalid_password_with_message(
193183
client: AsyncClient,
194184
default_user: User,

app/auth/tests/test_api_router_jwt_errors.py renamed to app/auth/tests/test_view_read_current_user.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import pytest
21
from fastapi import status
32
from freezegun import freeze_time
43
from httpx import AsyncClient
@@ -11,7 +10,35 @@
1110
from app.main import app
1211

1312

14-
@pytest.mark.asyncio(loop_scope="session")
13+
async def test_read_current_user_status_code(
14+
client: AsyncClient,
15+
default_user_headers: dict[str, str],
16+
default_user: User,
17+
) -> None:
18+
response = await client.get(
19+
app.url_path_for("read_current_user"),
20+
headers=default_user_headers,
21+
)
22+
23+
assert response.status_code == status.HTTP_200_OK
24+
25+
26+
async def test_read_current_user_response(
27+
client: AsyncClient,
28+
default_user_headers: dict[str, str],
29+
default_user: User,
30+
) -> None:
31+
response = await client.get(
32+
app.url_path_for("read_current_user"),
33+
headers=default_user_headers,
34+
)
35+
36+
assert response.json() == {
37+
"user_id": default_user.user_id,
38+
"email": default_user.email,
39+
}
40+
41+
1542
async def test_api_raise_401_on_jwt_decode_errors(
1643
client: AsyncClient,
1744
) -> None:
@@ -24,7 +51,6 @@ async def test_api_raise_401_on_jwt_decode_errors(
2451
assert response.json() == {"detail": "Token invalid: Not enough segments"}
2552

2653

27-
@pytest.mark.asyncio(loop_scope="session")
2854
async def test_api_raise_401_on_jwt_expired_token(
2955
client: AsyncClient,
3056
default_user: User,
@@ -41,7 +67,6 @@ async def test_api_raise_401_on_jwt_expired_token(
4167
assert response.json() == {"detail": "Token invalid: Signature has expired"}
4268

4369

44-
@pytest.mark.asyncio(loop_scope="session")
4570
async def test_api_raise_401_on_jwt_user_deleted(
4671
client: AsyncClient,
4772
default_user_headers: dict[str, str],
Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import time
22

3-
import pytest
43
from fastapi import status
54
from freezegun import freeze_time
65
from httpx import AsyncClient
@@ -14,7 +13,6 @@
1413
from app.main import app
1514

1615

17-
@pytest.mark.asyncio(loop_scope="session")
1816
async def test_refresh_token_fails_with_message_when_token_does_not_exist(
1917
client: AsyncClient,
2018
) -> None:
@@ -29,7 +27,6 @@ async def test_refresh_token_fails_with_message_when_token_does_not_exist(
2927
assert response.json() == {"detail": api_messages.REFRESH_TOKEN_NOT_FOUND}
3028

3129

32-
@pytest.mark.asyncio(loop_scope="session")
3330
async def test_refresh_token_fails_with_message_when_token_is_expired(
3431
client: AsyncClient,
3532
default_user: User,
@@ -54,7 +51,6 @@ async def test_refresh_token_fails_with_message_when_token_is_expired(
5451
assert response.json() == {"detail": api_messages.REFRESH_TOKEN_EXPIRED}
5552

5653

57-
@pytest.mark.asyncio(loop_scope="session")
5854
async def test_refresh_token_fails_with_message_when_token_is_used(
5955
client: AsyncClient,
6056
default_user: User,
@@ -80,7 +76,6 @@ async def test_refresh_token_fails_with_message_when_token_is_used(
8076
assert response.json() == {"detail": api_messages.REFRESH_TOKEN_ALREADY_USED}
8177

8278

83-
@pytest.mark.asyncio(loop_scope="session")
8479
async def test_refresh_token_success_response_status_code(
8580
client: AsyncClient,
8681
default_user: User,
@@ -105,7 +100,6 @@ async def test_refresh_token_success_response_status_code(
105100
assert response.status_code == status.HTTP_200_OK
106101

107102

108-
@pytest.mark.asyncio(loop_scope="session")
109103
async def test_refresh_token_success_old_token_is_used(
110104
client: AsyncClient,
111105
default_user: User,
@@ -134,7 +128,6 @@ async def test_refresh_token_success_old_token_is_used(
134128
assert used_test_refresh_token.used
135129

136130

137-
@pytest.mark.asyncio(loop_scope="session")
138131
async def test_refresh_token_success_jwt_has_valid_token_type(
139132
client: AsyncClient,
140133
default_user: User,
@@ -160,7 +153,6 @@ async def test_refresh_token_success_jwt_has_valid_token_type(
160153
assert token["token_type"] == "Bearer"
161154

162155

163-
@pytest.mark.asyncio(loop_scope="session")
164156
@freeze_time("2023-01-01")
165157
async def test_refresh_token_success_jwt_has_valid_expire_time(
166158
client: AsyncClient,
@@ -191,7 +183,6 @@ async def test_refresh_token_success_jwt_has_valid_expire_time(
191183
)
192184

193185

194-
@pytest.mark.asyncio(loop_scope="session")
195186
@freeze_time("2023-01-01")
196187
async def test_refresh_token_success_jwt_has_valid_access_token(
197188
client: AsyncClient,
@@ -223,7 +214,6 @@ async def test_refresh_token_success_jwt_has_valid_access_token(
223214
assert token_payload.exp == token["expires_at"]
224215

225216

226-
@pytest.mark.asyncio(loop_scope="session")
227217
@freeze_time("2023-01-01")
228218
async def test_refresh_token_success_refresh_token_has_valid_expire_time(
229219
client: AsyncClient,
@@ -254,7 +244,6 @@ async def test_refresh_token_success_refresh_token_has_valid_expire_time(
254244
)
255245

256246

257-
@pytest.mark.asyncio(loop_scope="session")
258247
async def test_refresh_token_success_new_refresh_token_is_in_db(
259248
client: AsyncClient,
260249
default_user: User,

app/auth/tests/test_register_new_user.py renamed to app/auth/tests/test_view_register_new_user.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import pytest
21
from fastapi import status
32
from httpx import AsyncClient
43
from sqlalchemy import func, select
@@ -9,7 +8,6 @@
98
from app.main import app
109

1110

12-
@pytest.mark.asyncio(loop_scope="session")
1311
async def test_register_new_user_status_code(
1412
client: AsyncClient,
1513
) -> None:
@@ -24,7 +22,6 @@ async def test_register_new_user_status_code(
2422
assert response.status_code == status.HTTP_201_CREATED
2523

2624

27-
@pytest.mark.asyncio(loop_scope="session")
2825
async def test_register_new_user_creates_record_in_db(
2926
client: AsyncClient,
3027
session: AsyncSession,
@@ -43,7 +40,6 @@ async def test_register_new_user_creates_record_in_db(
4340
assert user_count == 1
4441

4542

46-
@pytest.mark.asyncio(loop_scope="session")
4743
async def test_register_new_user_cannot_create_already_created_user(
4844
client: AsyncClient,
4945
session: AsyncSession,

0 commit comments

Comments
 (0)