Skip to content

Commit

Permalink
(api) test statement enrichment in put
Browse files Browse the repository at this point in the history
  • Loading branch information
Leobouloc committed Aug 8, 2023
1 parent a7720ca commit b9fb2b1
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 71 deletions.
25 changes: 14 additions & 11 deletions src/ralph/api/routers/statements.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
Request,
Response,
status,
Security
Security,
)
from pydantic import parse_obj_as
from pydantic.types import Json
Expand Down Expand Up @@ -68,12 +68,14 @@
},
}


def _enrich_statement_with_id(statement):
# id: UUID
# https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#24-statement-properties
statement["id"] = str(statement.get("id", uuid4()))
return statement["id"]


def _enrich_statement_with_stored(statement, value=None):
# stored: The time at which a Statement is stored by the LRS
# https://github.com/adlnet/xAPI-Spec/blob/1.0.3/xAPI-Data.md#248-stored
Expand All @@ -83,12 +85,14 @@ def _enrich_statement_with_stored(statement, value=None):
statement["stored"] = value
return statement["stored"]


def _enrich_statement_with_timestamp(statement):
# timestamp: If not provided, same value as stored
# https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#247-timestamp
statement["timestamp"] = statement.get("timestamp", statement.get("stored", now()))
return statement["timestamp"]


def _enrich_statement_with_authority(statement, current_user: AuthenticatedUser):
# authority: Information about whom or what has asserted that this statement is true
# https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#249-authority
Expand All @@ -101,13 +105,14 @@ def _enrich_statement_with_authority(statement, current_user: AuthenticatedUser)
status_code=status.HTTP_403_FORBIDDEN,
detail=(
"Stated `authority` does not match credentials. Change or remove"
"`authority` field from incoming statement.",
)
"`authority` field from incoming statement.",
),
)
else:
statement["authority"] = authority
pass


def _parse_agent_parameters(agent_obj: dict):
"""Parse a dict and return an AgentParameters object to use in queries."""
# Transform agent to `dict` as FastAPI cannot parse JSON (seen as string)
Expand Down Expand Up @@ -398,14 +403,12 @@ async def put(
status_code=status.HTTP_400_BAD_REQUEST,
detail="xAPI statement id does not match given statementId",
)

# Enrich statement before forwarding (NB: id is already set)
timestamp = _enrich_statement_with_timestamp(statement_as_dict)

if get_active_xapi_forwardings():
background_tasks.add_task(
forward_xapi_statements, statement_as_dict
)
background_tasks.add_task(forward_xapi_statements, statement_as_dict)

# Finish enriching statements after forwarding
_enrich_statement_with_stored(statement_as_dict, timestamp)
Expand Down Expand Up @@ -433,9 +436,7 @@ async def put(

# For valid requests, perform the bulk indexing of all incoming statements
try:
success_count = DATABASE_CLIENT.put(
[statement_as_dict], ignore_errors=False
)
success_count = DATABASE_CLIENT.put([statement_as_dict], ignore_errors=False)
except (BackendException, BadFormatException) as exc:
logger.error("Failed to index submitted statement")
raise HTTPException(
Expand Down Expand Up @@ -511,7 +512,9 @@ async def post(

# The LRS specification calls for deep comparison of duplicates. This
# is done here. If they are not exactly the same, we raise an error.
if not statements_are_equivalent(statements_dict[existing["_id"]], existing["_source"]):
if not statements_are_equivalent(
statements_dict[existing["_id"]], existing["_source"]
):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Differing statements already exist with the same ID: "
Expand Down
23 changes: 14 additions & 9 deletions src/ralph/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,12 @@ async def sem_task(task):
group.cancel()
raise exception


def statements_are_equivalent(statement_1: dict, statement_2: dict):
"""Check if statements are equivalent.
To be equivalent, they must be identical on all fields not modified on input by the
LRS and idententical on other fields, if these fields are present in both
To be equivalent, they must be identical on all fields not modified on input by the
LRS and idententical on other fields, if these fields are present in both
statements. For example, if an "authority" field is present in only one statement,
they may still be equivalent.
"""
Expand All @@ -140,17 +141,18 @@ def statements_are_equivalent(statement_1: dict, statement_2: dict):
other_fields = other_fields & statement_1.keys() & statement_2.keys()
if any(statement_1.get(field) != statement_2.get(field) for field in other_fields):
return False

return True


def _assert_statements_are_equivalent(statement_1: dict, statement_2: dict):
def _assert_statements_are_equivalent(statement_1: dict, statement_2: dict):
"""Assert that statements are identical on fields not modified by the LRS."""
assert statements_are_equivalent(statement_1, statement_2)


def assert_statement_get_responses_are_equivalent(response_1: dict, response_2: dict):
"""Check that responses to GET /statements are equivalent.
Check that all statements in response are equivalent, meaning that all
fields not modified by the LRS are equal.
"""
Expand All @@ -165,12 +167,15 @@ def _all_but_statements(response):
# Assert the statements part of the response is equivalent
assert "statements" in response_1.keys()
assert len(response_1["statements"]) == len(response_2["statements"])
for statement_1, statement_2 in zip(response_1["statements"], response_2["statements"]):
for statement_1, statement_2 in zip(
response_1["statements"], response_2["statements"]
):
_assert_statements_are_equivalent(statement_1, statement_2)


def string_is_date(string):
try:
try:
parse(string)
return True
except:
return False
return False
43 changes: 27 additions & 16 deletions tests/api/test_statements_post.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ def test_api_statements_post_single_statement_directly(
"/xAPI/statements/", headers={"Authorization": f"Basic {auth_credentials}"}
)
assert response.status_code == 200
assert_statement_get_responses_are_equivalent(response.json(), {"statements": [statement]})
assert_statement_get_responses_are_equivalent(
response.json(), {"statements": [statement]}
)


# pylint: disable=too-many-arguments
Expand All @@ -81,7 +83,9 @@ def test_api_statements_post_enriching_without_existing_values(
"""Test that statements are properly enriched when statement provides no values."""
# pylint: disable=invalid-name,unused-argument

monkeypatch.setattr("ralph.api.routers.statements.DATABASE_CLIENT", get_es_test_backend())
monkeypatch.setattr(
"ralph.api.routers.statements.DATABASE_CLIENT", get_es_test_backend()
)
statement = {
"actor": {
"account": {
Expand All @@ -108,7 +112,7 @@ def test_api_statements_post_enriching_without_existing_values(
"/xAPI/statements/", headers={"Authorization": f"Basic {auth_credentials}"}
)

statement = response.json()['statements'][0]
statement = response.json()["statements"][0]

# Test pre-processing: id
assert "id" in statement
Expand All @@ -130,7 +134,7 @@ def test_api_statements_post_enriching_without_existing_values(
@pytest.mark.parametrize(
"field,value,status",
[
("id", str(uuid4()), 200),
("id", str(uuid4()), 200),
("timestamp", "2022-06-22T08:31:38Z", 200),
("stored", "2022-06-22T08:31:38Z", 200),
("authority", {"mbox": "mailto:[email protected]"}, 200),
Expand All @@ -144,7 +148,9 @@ def test_api_statements_post_enriching_with_existing_values(
"""Test that statements are properly enriched when values are provided."""
# pylint: disable=invalid-name,unused-argument

monkeypatch.setattr("ralph.api.routers.statements.DATABASE_CLIENT", get_es_test_backend())
monkeypatch.setattr(
"ralph.api.routers.statements.DATABASE_CLIENT", get_es_test_backend()
)
statement = {
"actor": {
"account": {
Expand Down Expand Up @@ -173,7 +179,7 @@ def test_api_statements_post_enriching_with_existing_values(
response = client.get(
"/xAPI/statements/", headers={"Authorization": f"Basic {auth_credentials}"}
)
statement = response.json()['statements'][0]
statement = response.json()["statements"][0]

# Test enriching

Expand Down Expand Up @@ -261,7 +267,9 @@ def test_api_statements_post_statements_list_of_one(
"/xAPI/statements/", headers={"Authorization": f"Basic {auth_credentials}"}
)
assert response.status_code == 200
assert_statement_get_responses_are_equivalent(response.json(), {"statements": [statement]})
assert_statement_get_responses_are_equivalent(
response.json(), {"statements": [statement]}
)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -326,9 +334,9 @@ def test_api_statements_post_statements_list(
# Update statements with the generated id.
statements[1] = dict(statements[1], **{"id": generated_id})

assert_statement_get_responses_are_equivalent(get_response.json(), {"statements": statements})


assert_statement_get_responses_are_equivalent(
get_response.json(), {"statements": statements}
)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -449,8 +457,9 @@ def test_api_statements_post_statements_list_with_duplicate_of_existing_statemen
"/xAPI/statements/", headers={"Authorization": f"Basic {auth_credentials}"}
)
assert response.status_code == 200
assert_statement_get_responses_are_equivalent(response.json(), {"statements": [statement]})

assert_statement_get_responses_are_equivalent(
response.json(), {"statements": [statement]}
)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -701,8 +710,9 @@ async def test_post_statements_list_with_statement_forwarding(
headers={"Authorization": f"Basic {auth_credentials}"},
)
assert response.status_code == 200
assert_statement_get_responses_are_equivalent(response.json(), {"statements": [statement]})

assert_statement_get_responses_are_equivalent(
response.json(), {"statements": [statement]}
)

# The statement should also be stored on the receiving client
async with AsyncClient() as receiving_client:
Expand All @@ -711,8 +721,9 @@ async def test_post_statements_list_with_statement_forwarding(
headers={"Authorization": f"Basic {auth_credentials}"},
)
assert response.status_code == 200
assert_statement_get_responses_are_equivalent(response.json(), {"statements": [statement]})

assert_statement_get_responses_are_equivalent(
response.json(), {"statements": [statement]}
)

# Stop receiving LRS client
await lrs_context.__aexit__(None, None, None)
Loading

0 comments on commit b9fb2b1

Please sign in to comment.