Skip to content

Commit

Permalink
add new API endpoint requests/latest
Browse files Browse the repository at this point in the history
The requests/latest endpoint will return the most recent request for a
given repo_name and git ref.

- The git repo_name and ref will be provided to the API as query
  parameters
- The git repo_name is the namespaced repository name, not the full git
  URL (e.g. release-engineering/retrodep)
- The most recent request among those with the same repo_name and ref
  is considered to be the one with the highest request_id

Signed-off-by: Taylor Madore <[email protected]>
  • Loading branch information
taylormadore committed Dec 5, 2023
1 parent 3c3beba commit 3e1f32d
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 1 deletion.
28 changes: 27 additions & 1 deletion cachito/web/api_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from flask import stream_with_context
from flask_login import current_user, login_required
from opentelemetry import trace
from sqlalchemy import and_, func
from sqlalchemy import and_, desc, func
from sqlalchemy.orm import joinedload, load_only
from werkzeug.exceptions import BadRequest, Forbidden, Gone, InternalServerError, NotFound

Expand Down Expand Up @@ -184,6 +184,32 @@ def get_request(request_id):
return flask.jsonify(json)


def get_latest_request():
"""
Retrieve the latest request for a repo/ref.
The latest request will be the one with the highest id.
:return: a Flask JSON response
:rtype: flask.Response
:raise NotFound: if the request is not found
"""
repo_name = flask.request.args.get("repo_name")
ref = flask.request.args.get("ref")

request = (
Request.query.filter(Request.ref == ref)
.filter(Request.repo.contains(repo_name))
.order_by(desc(Request.id))
.first()
)

if not request:
raise NotFound

return flask.jsonify(request.to_json())


def get_request_config_files(request_id):
"""
Retrieve the configuration files associated with the given request.
Expand Down
42 changes: 42 additions & 0 deletions cachito/web/static/api_v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,48 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/RequestUpdate'
"/requests/latest":
get:
operationId: cachito.web.api_v1.get_latest_request
summary: Get the latest request for a repo/ref
description: Return the latest request for a specified repo_name and ref
parameters:
- name: repo_name
required: true
in: query
description: The namespaced repository name (namespace/name)
schema:
type: string
maxLength: 200
pattern: '^[\w.-]+(/[\w.-]+)+$'
example: release-engineering/retrodep
- name: ref
required: true
in: query
description: The git reference
schema:
type: string
minLength: 40
maxLength: 40
pattern: '^[a-f0-9]{40}$'
example: bc9767a71ede6e0084ae4a9e01dcd8b81c30b741
responses:
"200":
description: The requested Cachito request
content:
application/json:
schema:
$ref: "#/components/schemas/Request"
"404":
description: The request wasn't found
content:
application/json:
schema:
type: object
properties:
error:
type: string
example: The requested resource was not found
"/requests/{request_id}/configuration-files":
get:
operationId: cachito.web.api_v1.get_request_config_files
Expand Down
137 changes: 137 additions & 0 deletions tests/test_api_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,143 @@ def test_datetime_validator(client, date, is_valid, expected_status):
assert rv.status_code == expected_status


@pytest.fixture()
def latest_requests_db(app, db, worker_auth_env):
"""Add requests to the db for testing the requests/latest endpoint."""
data = [
{
"repo": "https://github.com/org/foo.git",
"ref": "a50b93a32df1c9d700e3e80996845bc2e13be848",
},
{
"repo": "https://github.com/org/foo.git",
"ref": "b50b93a32df1c9d700e3e80996845bc2e13be848",
},
{
"repo": "https://github.com/org/foo.git",
"ref": "a50b93a32df1c9d700e3e80996845bc2e13be848",
},
{
"repo": "https://github.com/org/bar.git",
"ref": "a50b93a32df1c9d700e3e80996845bc2e13be848",
},
{
"repo": "https://otherforge.com/org/baz.git",
"ref": "c50b93a32df1c9d700e3e80996845bc2e13be848",
},
{
"repo": "https://github.com/org/baz.git",
"ref": "c50b93a32df1c9d700e3e80996845bc2e13be848",
},
]
for item in data:
with app.test_request_context(environ_base=worker_auth_env):
request = Request.from_json(item)
db.session.add(request)
db.session.commit()


@pytest.mark.parametrize(
"query_params, latest_request_id",
[
pytest.param(
{"repo_name": "org/foo", "ref": "a50b93a32df1c9d700e3e80996845bc2e13be848"},
3,
id="same_repo_same_ref",
),
pytest.param(
{"repo_name": "org/foo", "ref": "b50b93a32df1c9d700e3e80996845bc2e13be848"},
2,
id="same_repo_different_ref",
),
pytest.param(
{"repo_name": "org/bar", "ref": "a50b93a32df1c9d700e3e80996845bc2e13be848"},
4,
id="different_repo_same_ref",
),
pytest.param(
{"repo_name": "org/baz", "ref": "c50b93a32df1c9d700e3e80996845bc2e13be848"},
6,
id="different_forge",
),
],
)
def test_get_latest_request_peekaboo(client, latest_requests_db, query_params, latest_request_id):
rv = client.get("/api/v1/requests/latest", query_string=query_params)
assert HTTPStatus.OK == rv.status_code
response = rv.json
assert latest_request_id == response["id"]


@pytest.mark.parametrize(
"query_params",
[
pytest.param(
{"repo_name": "org/foo", "ref": "d50b93a32df1c9d700e3e80996845bc2e13be848"},
id="ref_not_found",
),
pytest.param(
{"repo_name": "org/qux", "ref": "a50b93a32df1c9d700e3e80996845bc2e13be848"},
id="repo_not_found",
),
],
)
def test_get_latest_request_not_found_peekaboo(client, latest_requests_db, query_params):
rv = client.get("/api/v1/requests/latest", query_string=query_params)
assert HTTPStatus.NOT_FOUND == rv.status_code


@pytest.mark.parametrize(
"query_params, error_str",
[
pytest.param(
{"repo_name": "org/repo", "ref": "c50b93a32df1c9d700e3e80996845bc2e13be84"},
"'c50b93a32df1c9d700e3e80996845bc2e13be84' is too short",
id="ref_too_short",
),
pytest.param(
{"repo_name": "org/repo", "ref": "c50b93a32df1c9d700e3e80996845bc2e13be8489"},
"'c50b93a32df1c9d700e3e80996845bc2e13be8489' is too long",
id="ref_too_long",
),
pytest.param(
{"repo_name": "org/repo", "ref": "c50b93a32df1c9d700e*e80996845bc2e13be848"},
"'c50b93a32df1c9d700e*e80996845bc2e13be848' does not match",
id="ref_invalid_character",
),
pytest.param(
{
"repo_name": "repo_name=org/" + ("repo" * 51),
"ref": "c50b93a32df1c9d700e3e80996845bc2e13be848",
},
"reporepo' is too long",
id="repo_name_too_long",
),
pytest.param(
{"repo_name": "git", "ref": "c50b93a32df1c9d700e3e80996845bc2e13be848"},
"'git' does not match",
id="invalid_repo_name_format",
),
pytest.param(
{"repo_name": "org/*", "ref": "c50b93a32df1c9d700e3e80996845bc2e13be848"},
"'org/*' does not match ",
id="invalid_repo_name_character",
),
pytest.param(
{"ref": "c50b93a32df1c9d700e3e80996845bc2e13be848"},
"Missing query parameter 'repo_name'",
id="missing_repo_name",
),
pytest.param({"repo_name": "org/repo"}, "Missing query parameter 'ref'", id="missing_ref"),
],
)
def test_get_latest_request_invalid_input_peekaboo(app, client, query_params, error_str):
rv = client.get("/api/v1/requests/latest", query_string=query_params)
assert rv.status_code == 400
response = rv.json
assert error_str in response["error"]


def test_fetch_paginated_requests(
app, auth_env, client, db, sample_deps_replace, sample_package, worker_auth_env, tmpdir
):
Expand Down

0 comments on commit 3e1f32d

Please sign in to comment.