Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/add landing page links #229

Merged
merged 15 commits into from
Aug 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ run-database:
run-joplin-sqlalchemy:
docker-compose run --rm loadjoplin-sqlalchemy

.PHONY: run-joplin-sqlalchemy
run-joplin-sqlalchemy:
docker-compose run --rm loadjoplin-sqlalchemy

.PHONY: test
test: test-sqlalchemy test-pgstac

Expand Down
6 changes: 5 additions & 1 deletion stac_fastapi/api/stac_fastapi/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ class StacApi:
exceptions: Dict[Type[Exception], int] = attr.ib(
default=attr.Factory(lambda: DEFAULT_STATUS_CODES)
)
app: FastAPI = attr.ib(default=attr.Factory(FastAPI))
app: FastAPI = attr.ib(
default=attr.Factory(
lambda self: FastAPI(openapi_url=self.settings.openapi_url), takes_self=True
)
)
router: APIRouter = attr.ib(default=attr.Factory(APIRouter))
title: str = attr.ib(default="stac-fastapi")
api_version: str = attr.ib(default="0.1")
Expand Down
4 changes: 2 additions & 2 deletions stac_fastapi/pgstac/tests/clients/test_postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ async def test_update_item(app_client, load_test_collection, load_test_item):

item.properties.description = "Update Test"

resp = await app_client.put(f"/collections/{coll.id}/items", data=item.json())
resp = await app_client.put(f"/collections/{coll.id}/items", content=item.json())
assert resp.status_code == 200

resp = await app_client.get(f"/collections/{coll.id}/items/{item.id}")
Expand Down Expand Up @@ -113,7 +113,7 @@ async def test_get_collection_items(app_client, load_test_collection, load_test_
item.id = str(uuid.uuid4())
resp = await app_client.post(
f"/collections/{coll.id}/items",
data=item.json(),
content=item.json(),
)
assert resp.status_code == 200

Expand Down
2 changes: 1 addition & 1 deletion stac_fastapi/pgstac/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ async def app(api_client):


@pytest.mark.asyncio
@pytest.fixture()
@pytest.fixture(scope="session")
async def app_client(app):
async with AsyncClient(app=app, base_url="http://test") as c:
yield c
Expand Down
76 changes: 62 additions & 14 deletions stac_fastapi/pgstac/tests/resources/test_conformance.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,69 @@
import urllib.parse

import pytest


@pytest.mark.asyncio
async def test_landing_page(app_client):
@pytest.fixture(scope="module")
async def response(app_client):
return await app_client.get("/")


@pytest.fixture(scope="module")
async def response_json(response):
return response.json()


def get_link(landing_page, rel_type):
return next(
filter(lambda link: link["rel"] == rel_type, landing_page["links"]), None
)


def test_landing_page_health(response):
"""Test landing page"""
resp = await app_client.get("/")
assert resp.status_code == 200
resp_json = resp.json()
assert response.status_code == 200
assert response.headers["content-type"] == "application/json"

# Make sure OpenAPI docs are linked
docs = next(filter(lambda link: link["rel"] == "docs", resp_json["links"]))["href"]
resp = await app_client.get(docs)
assert resp.status_code == 200

# Make sure conformance classes are linked
conf = next(filter(lambda link: link["rel"] == "conformance", resp_json["links"]))[
"href"
]
resp = await app_client.get(conf)
# Parameters for test_landing_page_links test below.
# Each tuple has the following values (in this order):
# - Rel type of link to test
# - Expected MIME/Media Type
# - Expected relative path
link_tests = [
("root", "application/json", "/"),
("conformance", "application/json", "/conformance"),
("docs", "application/json", "/docs"),
("service-desc", "application/vnd.oai.openapi+json;version=3.0", "/api"),
]


@pytest.mark.asyncio
@pytest.mark.parametrize("rel_type,expected_media_type,expected_path", link_tests)
async def test_landing_page_links(
response_json, app_client, rel_type, expected_media_type, expected_path
):
link = get_link(response_json, rel_type)

assert link is not None, f"Missing {rel_type} link in landing page"
assert link.get("type") == expected_media_type

link_path = urllib.parse.urlsplit(link.get("href")).path
assert link_path == expected_path

resp = await app_client.get(link_path)
assert resp.status_code == 200


# This endpoint currently returns a 404 for empty result sets, but testing for this response
# code here seems meaningless since it would be the same as if the endpoint did not exist. Once
# https://github.com/stac-utils/stac-fastapi/pull/227 has been merged we can add this to the
# parameterized tests above.
def test_search_link(response_json):
search_link = get_link(response_json, "search")

assert search_link is not None
assert search_link.get("type") == "application/geo+json"

search_path = urllib.parse.urlsplit(search_link.get("href")).path
assert search_path == "/search"
8 changes: 4 additions & 4 deletions stac_fastapi/pgstac/tests/resources/test_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ async def test_update_item(

item.properties.description = "Update Test"

resp = await app_client.put(f"/collections/{coll.id}/items", data=item.json())
resp = await app_client.put(f"/collections/{coll.id}/items", content=item.json())
assert resp.status_code == 200

resp = await app_client.get(f"/collections/{coll.id}/items/{item.id}")
Expand Down Expand Up @@ -155,7 +155,7 @@ async def test_get_collection_items(app_client, load_test_collection, load_test_
item.id = str(uuid.uuid4())
resp = await app_client.post(
f"/collections/{coll.id}/items",
data=item.json(),
content=item.json(),
)
assert resp.status_code == 200

Expand Down Expand Up @@ -225,7 +225,7 @@ async def test_update_new_item(
item = load_test_item
item.id = "test-updatenewitem"

resp = await app_client.put(f"/collections/{coll.id}/items", data=item.json())
resp = await app_client.put(f"/collections/{coll.id}/items", content=item.json())
assert resp.status_code == 404


Expand All @@ -237,7 +237,7 @@ async def test_update_item_missing_collection(
item = load_test_item
item.collection = None

resp = await app_client.put(f"/collections/{coll.id}/items", data=item.json())
resp = await app_client.put(f"/collections/{coll.id}/items", content=item.json())
assert resp.status_code == 424


Expand Down
7 changes: 6 additions & 1 deletion stac_fastapi/sqlalchemy/tests/clients/test_postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from stac_pydantic import Collection, Item
from tests.conftest import MockStarletteRequest

from stac_fastapi.api.app import StacApi
from stac_fastapi.sqlalchemy.core import CoreCrudClient
from stac_fastapi.sqlalchemy.transactions import (
BulkTransactionsClient,
Expand Down Expand Up @@ -256,12 +257,16 @@ def test_landing_page_no_collection_title(
postgres_core: CoreCrudClient,
postgres_transactions: TransactionsClient,
load_test_data: Callable,
api_client: StacApi,
):
class MockStarletteRequestWithApp(MockStarletteRequest):
app = api_client.app

coll = load_test_data("test_collection.json")
del coll["title"]
postgres_transactions.create_collection(coll, request=MockStarletteRequest)

landing_page = postgres_core.landing_page(request=MockStarletteRequest)
landing_page = postgres_core.landing_page(request=MockStarletteRequestWithApp)
for link in landing_page["links"]:
if link["href"].split("/")[-1] == coll["id"]:
assert not link["title"]
77 changes: 64 additions & 13 deletions stac_fastapi/sqlalchemy/tests/resources/test_conformance.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,68 @@
def test_landing_page(app_client):
import urllib.parse

import pytest


@pytest.fixture
def response(app_client):
return app_client.get("/")


@pytest.fixture
def response_json(response):
return response.json()


def get_link(landing_page, rel_type):
return next(
filter(lambda link: link["rel"] == rel_type, landing_page["links"]), None
)


def test_landing_page_health(response):
"""Test landing page"""
resp = app_client.get("/")
assert resp.status_code == 200
resp_json = resp.json()
assert response.status_code == 200
assert response.headers["content-type"] == "application/json"

# Make sure OpenAPI docs are linked
docs = next(filter(lambda link: link["rel"] == "docs", resp_json["links"]))["href"]
resp = app_client.get(docs)
assert resp.status_code == 200

# Make sure conformance classes are linked
conf = next(filter(lambda link: link["rel"] == "conformance", resp_json["links"]))[
"href"
]
resp = app_client.get(conf)
# Parameters for test_landing_page_links test below.
# Each tuple has the following values (in this order):
# - Rel type of link to test
# - Expected MIME/Media Type
# - Expected relative path
link_tests = [
("root", "application/json", "/"),
("conformance", "application/json", "/conformance"),
("docs", "application/json", "/docs"),
("service-desc", "application/vnd.oai.openapi+json;version=3.0", "/api"),
]


@pytest.mark.parametrize("rel_type,expected_media_type,expected_path", link_tests)
def test_landing_page_links(
response_json, app_client, rel_type, expected_media_type, expected_path
):
link = get_link(response_json, rel_type)

assert link is not None, f"Missing {rel_type} link in landing page"
assert link.get("type") == expected_media_type

link_path = urllib.parse.urlsplit(link.get("href")).path
assert link_path == expected_path

resp = app_client.get(link_path)
assert resp.status_code == 200


# This endpoint currently returns a 404 for empty result sets, but testing for this response
# code here seems meaningless since it would be the same as if the endpoint did not exist. Once
# https://github.com/stac-utils/stac-fastapi/pull/227 has been merged we can add this to the
# parameterized tests above.
def test_search_link(response_json):
search_link = get_link(response_json, "search")

assert search_link is not None
assert search_link.get("type") == "application/geo+json"

search_path = urllib.parse.urlsplit(search_link.get("href")).path
assert search_path == "/search"
2 changes: 2 additions & 0 deletions stac_fastapi/types/stac_fastapi/types/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class ApiSettings(BaseSettings):
reload: bool = True
enable_response_models: bool = False

openapi_url: str = "/api"

class Config:
"""model config (https://pydantic-docs.helpmanual.io/usage/model_config/)."""

Expand Down
37 changes: 34 additions & 3 deletions stac_fastapi/types/stac_fastapi/types/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from urllib.parse import urljoin

import attr
from fastapi import Request
from stac_pydantic.api import Search
from stac_pydantic.links import Relations
from stac_pydantic.shared import MimeTypes
Expand Down Expand Up @@ -247,6 +248,11 @@ def _landing_page(
"type": MimeTypes.json,
"href": base_url,
},
{
"rel": Relations.root.value,
"type": MimeTypes.json,
"href": base_url,
},
{
"rel": "data",
"type": MimeTypes.json,
Expand All @@ -266,7 +272,7 @@ def _landing_page(
},
{
"rel": Relations.search.value,
"type": MimeTypes.json,
"type": MimeTypes.geojson,
"title": "STAC search",
"href": urljoin(base_url, "search"),
},
Expand Down Expand Up @@ -318,10 +324,13 @@ def landing_page(self, **kwargs) -> stac_types.LandingPage:
Returns:
API landing page, serving as an entry point to the API.
"""
base_url = str(kwargs["request"].base_url)
request: Request = kwargs["request"]
base_url = str(request.base_url)
landing_page = self._landing_page(
base_url=base_url, conformance_classes=self.conformance_classes()
)

# Add Collections links
collections = self.all_collections(request=kwargs["request"])
for collection in collections:
landing_page["links"].append(
Expand All @@ -332,6 +341,16 @@ def landing_page(self, **kwargs) -> stac_types.LandingPage:
"href": urljoin(base_url, f"collections/{collection['id']}"),
}
)

# Add OpenAPI URL
landing_page["links"].append(
{
"rel": "service-desc",
"type": "application/vnd.oai.openapi+json;version=3.0",
"title": "OpenAPI service description",
"href": urljoin(base_url, request.app.openapi_url.lstrip("/")),
}
)
return landing_page

def conformance(self, **kwargs) -> stac_types.Conformance:
Expand Down Expand Up @@ -484,7 +503,8 @@ async def landing_page(self, **kwargs) -> stac_types.LandingPage:
Returns:
API landing page, serving as an entry point to the API.
"""
base_url = str(kwargs["request"].base_url)
request: Request = kwargs["request"]
base_url = str(request.base_url)
landing_page = self._landing_page(
base_url=base_url, conformance_classes=self.conformance_classes()
)
Expand All @@ -498,6 +518,17 @@ async def landing_page(self, **kwargs) -> stac_types.LandingPage:
"href": urljoin(base_url, f"collections/{collection['id']}"),
}
)

# Add OpenAPI URL
landing_page["links"].append(
{
"rel": "service-desc",
"type": "application/vnd.oai.openapi+json;version=3.0",
"title": "OpenAPI service description",
"href": urljoin(base_url, request.app.openapi_url.lstrip("/")),
}
)

return landing_page

async def conformance(self, **kwargs) -> stac_types.Conformance:
Expand Down