Skip to content

Commit ce9dac7

Browse files
test(backend): smoke and endpoint tests (#693)
* test/refactor: remove unused drone endpoints and tests * refactor: remove unused projects endpoint * test: update drone test file ensuring that the drone altitude is fetched correctly * test: add more smoke tests * test: fix conftest to return an AuthUser as expected by the endpoints * build: necessary changes to Dockerfile and compose.test.yaml to allow just command to work correctly * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * build: add healthcheck to stop race condition in CI * build: optimise backend test command * test: remove flaky test * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * revert: restore drone routes * build: use Docker Buildkit * build: restore old compose.test.yaml --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 38541e0 commit ce9dac7

File tree

10 files changed

+85
-68
lines changed

10 files changed

+85
-68
lines changed

compose.test.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# compose.test.yaml
2-
32
name: dtm-test
43

54
volumes:

src/backend/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# syntax=docker/dockerfile:1
12
# ARG for the base image
23
ARG PYTHON_IMG_TAG=3.11
34

src/backend/app/projects/project_routes.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,8 @@ async def get_uploaded_parts(
959959
)
960960

961961

962+
# Endpoint not used in production but useful to keep around just for testing the
963+
# queue
962964
@router.post("/test/arq_task")
963965
async def test(redis_pool: ArqRedis = Depends(get_redis_pool)):
964966
try:
@@ -980,16 +982,3 @@ async def test(redis_pool: ArqRedis = Depends(get_redis_pool)):
980982
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
981983
detail=f"Failed to enqueue task: {str(e)}",
982984
)
983-
984-
985-
@router.post("/projects/{project_id}/count-tasks")
986-
async def start_task_count(
987-
project_id: uuid.UUID, redis: ArqRedis = Depends(get_redis_pool)
988-
):
989-
"""Start an async task to count project tasks"""
990-
job = await redis.enqueue_job(
991-
"count_project_tasks",
992-
str(project_id),
993-
_queue_name="default_queue",
994-
)
995-
return {"job_id": job.job_id}

src/backend/tests/conftest.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,19 @@ async def db() -> AsyncConnection:
3131

3232
@pytest_asyncio.fixture(scope="function")
3333
async def auth_user(db) -> AuthUser:
34-
"""Create a test user."""
35-
db_user = await DbUser.get_or_create_user(
36-
db,
37-
AuthUser(
38-
id="101039844375937810000",
39-
email="admin@hotosm.org",
40-
name="admin",
41-
profile_img="",
42-
role=UserRole.PROJECT_CREATOR,
43-
is_superuser=True,
44-
),
34+
"""Create a test user and return the AuthUser model."""
35+
user_to_create = AuthUser(
36+
id="101039844375937810000",
37+
email="admin@hotosm.org",
38+
name="admin",
39+
profile_img="",
40+
role=UserRole.PROJECT_CREATOR.name,
41+
is_superuser=True,
4542
)
46-
db_user.is_superuser = True
47-
return db_user
43+
# Ensure the user exists in the DB for endpoints that need it
44+
await DbUser.get_or_create_user(db, user_to_create)
45+
# Return the AuthUser object, which has .role and .email, as expected by login_required
46+
return user_to_create
4847

4948

5049
@pytest_asyncio.fixture(scope="function")

src/backend/tests/test_drones_routes.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,15 @@
44

55

66
@pytest.mark.asyncio
7-
async def test_create_drone(client, drone_info):
8-
"""Create a new project."""
9-
response = await client.post("/drones/create-drone", json=drone_info)
7+
async def test_get_drone_altitude_by_country(client):
8+
"""Test getting drone altitude by country."""
9+
country = "canada"
10+
response = await client.get(f"/api/drones/drone-altitude/{country}/")
1011
assert response.status_code == HTTPStatus.OK
1112

1213
return response.json()
1314

1415

15-
@pytest.mark.asyncio
16-
async def test_read_drone(client, drone_info):
17-
"""Test retrieving a drone record."""
18-
response = await client.post("/api/drones/create-drone", json=drone_info)
19-
assert response.status_code == HTTPStatus.OK
20-
drone_id = response.json().get("drone_id")
21-
response = await client.get(f"/api/drones/{drone_id}")
22-
assert response.status_code == HTTPStatus.OK
23-
drone_data = response.json()
24-
assert drone_data.get("model") == drone_info["model"]
25-
26-
2716
if __name__ == "__main__":
2817
"""Main func if file invoked directly."""
2918
pytest.main()
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import pytest
2+
3+
from app.models.enums import HTTPStatus
4+
5+
6+
@pytest.mark.asyncio
7+
async def test_find_images_for_a_project(client, create_test_project):
8+
"""Smoke test for finding images in a project that contain a point."""
9+
project_id = create_test_project
10+
point_data = {"longitude": 0.0, "latitude": 0.0}
11+
12+
response = await client.post(
13+
f"/api/gcp/find-project-images/?project_id={project_id}",
14+
json=point_data,
15+
)
16+
17+
assert response.status_code == HTTPStatus.OK

src/backend/tests/test_projects_routes.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,31 @@ async def test_upload_project_task_boundaries(client, create_test_project):
6363
return response.json()
6464

6565

66+
@pytest.mark.asyncio
67+
async def test_read_projects(client):
68+
"""Test reading all projects."""
69+
response = await client.get("/api/projects/")
70+
assert response.status_code == 200
71+
assert "results" in response.json()
72+
73+
74+
@pytest.mark.asyncio
75+
async def test_read_project(client, create_test_project):
76+
"""Test reading a single project."""
77+
project_id = create_test_project
78+
response = await client.get(f"/api/projects/{project_id}")
79+
assert response.status_code == 200
80+
assert response.json()["id"] == project_id
81+
82+
83+
@pytest.mark.asyncio
84+
async def test_read_project_centroids(client):
85+
"""Test reading project centroids."""
86+
response = await client.get("/api/projects/centroids")
87+
assert response.status_code == 200
88+
assert isinstance(response.json(), list)
89+
90+
6691
if __name__ == "__main__":
6792
"""Main func if file invoked directly."""
6893
pytest.main()

src/backend/tests/test_tasks_routes.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import uuid
2-
31
import pytest
42

53

64
@pytest.mark.asyncio
7-
async def test_read_task(client):
8-
task_id = uuid.uuid4()
9-
response = await client.get(f"/api/tasks/{task_id}")
5+
async def test_list_tasks(client):
6+
"""Test listing tasks for the authenticated user."""
7+
response = await client.get("/api/tasks/")
108
assert response.status_code == 200
119

1210

src/backend/tests/test_users_routes.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,33 @@
1-
from datetime import datetime, timedelta
2-
3-
import jwt
41
import pytest
5-
import pytest_asyncio
62
from loguru import logger as log
73

8-
from app.config import settings
94
from app.users.user_deps import create_reset_password_token
105

116

12-
@pytest_asyncio.fixture(scope="function")
13-
def token(auth_user):
14-
"""Create a reset password token for a given user."""
15-
payload = {
16-
"sub": auth_user.email_address,
17-
"exp": datetime.utcnow()
18-
+ timedelta(minutes=settings.RESET_PASSWORD_TOKEN_EXPIRE_MINUTES),
19-
}
20-
return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
7+
@pytest.mark.asyncio
8+
async def test_my_info(client):
9+
"""Test the /my-info/ endpoint to ensure a logged-in user can fetch their data."""
10+
response = await client.get("/api/users/my-info/")
11+
assert response.status_code == 200
12+
user_info = response.json()
13+
14+
assert user_info["email_address"] == "admin@hotosm.org"
15+
16+
17+
@pytest.mark.asyncio
18+
async def test_refresh_token(client):
19+
"""Test the /refresh-token endpoint to ensure a new access token can be obtained."""
20+
response = await client.get("/api/users/refresh-token")
21+
assert response.status_code == 200
22+
token_data = response.json()
23+
assert "access_token" in token_data
24+
assert "refresh_token" in token_data
2125

2226

2327
@pytest.mark.asyncio
2428
async def test_reset_password_success(client, auth_user):
2529
"""Test successful password reset using a valid token."""
26-
token = create_reset_password_token(auth_user.email_address)
30+
token = create_reset_password_token(auth_user.email)
2731
new_password = "QPassword@12334"
2832

2933
response = await client.post(

tasks/test/Justfile

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,13 @@ default:
2727
# Test backend
2828
[no-cd]
2929
backend:
30-
just build backend
31-
32-
# Start services
3330
{{docker}} compose -f compose.test.yaml build backend
3431

35-
{{docker}} compose -f compose.test.yaml up --detach db redis minio nodeodm
32+
{{docker}} compose -f compose.test.yaml up --detach db redis minio nodeodm arq-worker
3633

34+
{{docker}} compose -f compose.test.yaml run --rm createbuckets
3735
{{docker}} compose -f compose.test.yaml run --rm migrations
3836

39-
{{docker}} compose -f compose.test.yaml up --detach
40-
4137
{{docker}} compose -f compose.test.yaml run --rm backend pytest
4238

4339
{{docker}} compose -f compose.test.yaml down

0 commit comments

Comments
 (0)