diff --git a/birdxplorer/routers/data.py b/birdxplorer/routers/data.py index 20327a2..c375814 100644 --- a/birdxplorer/routers/data.py +++ b/birdxplorer/routers/data.py @@ -3,6 +3,7 @@ from dateutil.parser import parse as dateutil_parse from fastapi import APIRouter, HTTPException, Query +from pydantic import AnyHttpUrl, Field from ..models import ( BaseModel, @@ -16,12 +17,18 @@ from ..storage import Storage +class ResponseMeta(BaseModel): + next: Union[AnyHttpUrl, None] = None + prev: Union[AnyHttpUrl, None] = None + + class TopicListResponse(BaseModel): data: List[Topic] class PostListResponse(BaseModel): data: List[Post] + meta: ResponseMeta = Field(default=ResponseMeta()) def str_to_twitter_timestamp(s: str) -> TwitterTimestamp: diff --git a/tests/routers/test_data.py b/tests/routers/test_data.py index 21ef02e..639642a 100644 --- a/tests/routers/test_data.py +++ b/tests/routers/test_data.py @@ -24,7 +24,10 @@ def test_posts_get(client: TestClient, post_samples: List[Post]) -> None: response = client.get("/api/v1/data/posts") assert response.status_code == 200 res_json = response.json() - assert res_json == {"data": [json.loads(d.model_dump_json()) for d in post_samples]} + assert res_json == { + "data": [json.loads(d.model_dump_json()) for d in post_samples], + "meta": {"next": None, "prev": None}, + } def test_posts_get_has_post_id_filter(client: TestClient, post_samples: List[Post]) -> None: @@ -32,7 +35,8 @@ def test_posts_get_has_post_id_filter(client: TestClient, post_samples: List[Pos assert response.status_code == 200 res_json = response.json() assert res_json == { - "data": [json.loads(post_samples[0].model_dump_json()), json.loads(post_samples[2].model_dump_json())] + "data": [json.loads(post_samples[0].model_dump_json()), json.loads(post_samples[2].model_dump_json())], + "meta": {"next": None, "prev": None}, } @@ -40,44 +44,72 @@ def test_posts_get_has_created_at_filter_start_and_end(client: TestClient, post_ response = client.get("/api/v1/data/posts/?createdAtStart=2006-7-25 00:00:00&createdAtEnd=2006-7-30 23:59:59") assert response.status_code == 200 res_json = response.json() - assert res_json == {"data": [json.loads(post_samples[1].model_dump_json())]} + assert res_json == { + "data": [json.loads(post_samples[1].model_dump_json())], + "meta": {"next": None, "prev": None}, + } def test_posts_get_has_created_at_filter_start(client: TestClient, post_samples: List[Post]) -> None: response = client.get("/api/v1/data/posts/?createdAtStart=2006-7-25 00:00:00") assert response.status_code == 200 res_json = response.json() - assert res_json == {"data": [json.loads(post_samples[i].model_dump_json()) for i in (1, 2)]} + assert res_json == { + "data": [json.loads(post_samples[i].model_dump_json()) for i in (1, 2)], + "meta": {"next": None, "prev": None}, + } def test_posts_get_has_created_at_filter_end(client: TestClient, post_samples: List[Post]) -> None: response = client.get("/api/v1/data/posts/?createdAtEnd=2006-7-30 00:00:00") assert response.status_code == 200 res_json = response.json() - assert res_json == {"data": [json.loads(post_samples[i].model_dump_json()) for i in (0, 1)]} + assert res_json == { + "data": [json.loads(post_samples[i].model_dump_json()) for i in (0, 1)], + "meta": {"next": None, "prev": None}, + } def test_posts_get_created_at_range_filter_accepts_integer(client: TestClient, post_samples: List[Post]) -> None: response = client.get("/api/v1/data/posts/?createdAtStart=1153921700000&createdAtEnd=1154921800000") assert response.status_code == 200 res_json = response.json() - assert res_json == {"data": [json.loads(post_samples[1].model_dump_json())]} + assert res_json == { + "data": [json.loads(post_samples[1].model_dump_json())], + "meta": {"next": None, "prev": None}, + } def test_posts_get_created_at_start_filter_accepts_integer(client: TestClient, post_samples: List[Post]) -> None: response = client.get("/api/v1/data/posts/?createdAtStart=1153921700000") assert response.status_code == 200 res_json = response.json() - assert res_json == {"data": [json.loads(post_samples[i].model_dump_json()) for i in (1, 2)]} + assert res_json == { + "data": [json.loads(post_samples[i].model_dump_json()) for i in (1, 2)], + "meta": {"next": None, "prev": None}, + } def test_posts_get_created_at_end_filter_accepts_integer(client: TestClient, post_samples: List[Post]) -> None: response = client.get("/api/v1/data/posts/?createdAtEnd=1154921800000") assert response.status_code == 200 res_json = response.json() - assert res_json == {"data": [json.loads(post_samples[i].model_dump_json()) for i in (0, 1)]} + assert res_json == { + "data": [json.loads(post_samples[i].model_dump_json()) for i in (0, 1)], + "meta": {"next": None, "prev": None}, + } def test_posts_get_timestamp_out_of_range(client: TestClient, post_samples: List[Post]) -> None: response = client.get("/api/v1/data/posts/?createdAtStart=1153921700&createdAtEnd=1153921700") assert response.status_code == 422 + + +def test_posts_get_limit_two_and_paginate(client: TestClient, post_samples: List[Post]) -> None: + response = client.get("/api/v1/data/posts/?limit=2") + assert response.status_code == 200 + res_json = response.json() + assert res_json == { + "data": [json.loads(post_samples[0].model_dump_json()), json.loads(post_samples[1].model_dump_json())], + "meta": {"next": None, "prev": None}, + }