Skip to content

Commit

Permalink
update tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mbiannaccone committed Jul 12, 2024
1 parent b7315c8 commit 806cd87
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 107 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ repos:
rev: 6.3.0
hooks:
- id: pydocstyle
exclude: *generated
exclude: "generated/|tests.py"
additional_dependencies: [tomli]

- repo: https://github.com/pycqa/isort
Expand Down
21 changes: 13 additions & 8 deletions canvas_sdk/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@


class _BaseCommand(BaseModel):
model_config = ConfigDict(strict=True, revalidate_instances="always")
model_config = ConfigDict(strict=True, revalidate_instances="always", validate_assignment=True)

class Meta:
key = ""
Expand Down Expand Up @@ -42,18 +42,22 @@ def constantized_key(self) -> str:
command_uuid: str | None = None
user_id: int | None = None

def _create_error_detail(self, type: str, message: str, value: Any) -> InitErrorDetails:
return InitErrorDetails({"type": PydanticCustomError(type, message), "input": value})

def _get_error_details(
def _get_effect_method_required_fields(
self, method: Literal["originate", "edit", "delete", "commit", "enter_in_error"]
) -> list[InitErrorDetails]:
) -> tuple[str]:
base_required_fields: tuple = getattr(
_BaseCommand.Meta, f"{method}_required_fields", tuple()
)
command_required_fields: tuple = getattr(self.Meta, f"{method}_required_fields", tuple())
required_fields = tuple(set(base_required_fields) | set(command_required_fields))
return tuple(set(base_required_fields) | set(command_required_fields))

def _create_error_detail(self, type: str, message: str, value: Any) -> InitErrorDetails:
return InitErrorDetails({"type": PydanticCustomError(type, message), "input": value})

def _get_error_details(
self, method: Literal["originate", "edit", "delete", "commit", "enter_in_error"]
) -> list[InitErrorDetails]:
required_fields = self._get_effect_method_required_fields(method)
return [
self._create_error_detail(
"missing", f"Field '{field}' is required to {method.replace('_', ' ')} a command", v
Expand Down Expand Up @@ -98,9 +102,10 @@ def command_schema(cls) -> dict:
"""The schema of the command."""
base_properties = {"note_uuid", "command_uuid", "user_id"}
schema = cls.model_json_schema()
required_fields: tuple = getattr(cls.Meta, "originate_required_fields", tuple())
return {
definition.get("commands_api_name", name): {
"required": name in schema["required"],
"required": name in required_fields,
"type": cls._get_property_type(name),
"choices": cls._get_property_choices(name, schema),
}
Expand Down
2 changes: 1 addition & 1 deletion canvas_sdk/commands/commands/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class GoalCommand(_BaseCommand):

class Meta:
key = "goal"
originate_required_fields = ("goal_statement",)
originate_required_fields = ("goal_statement", "start_date")

class Priority(Enum):
HIGH = "high-priority"
Expand Down
2 changes: 1 addition & 1 deletion canvas_sdk/commands/commands/history_present_illness.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class Meta:
key = "hpi"
originate_required_fields = ("narrative",)

narrative: str | None
narrative: str | None = None

@property
def values(self) -> dict:
Expand Down
5 changes: 3 additions & 2 deletions canvas_sdk/commands/commands/prescribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Meta:
originate_required_fields = (
"fdb_code",
"sig",
"quantity_to_dispense",
"type_to_dispense",
"refills",
"substitutions",
Expand All @@ -33,7 +34,7 @@ class Substitutions(Enum):
quantity_to_dispense: Decimal | float | int | None = None
type_to_dispense: str | None = None
refills: int | None = None
substitutions: Substitutions = Substitutions.ALLOWED # type: ignore
substitutions: Substitutions | None = None
pharmacy: str | None = None
prescriber_id: str | None = Field(
default=None, json_schema_extra={"commands_api_name": "prescriber"}
Expand All @@ -53,7 +54,7 @@ def values(self) -> dict:
),
# "type_to_dispense": self.type_to_dispense,
"refills": self.refills,
"substitutions": self.substitutions.value,
"substitutions": self.substitutions.value if self.substitutions else None,
"pharmacy": self.pharmacy,
"prescriber_id": self.prescriber_id,
"note_to_pharmacist": self.note_to_pharmacist,
Expand Down
84 changes: 20 additions & 64 deletions canvas_sdk/commands/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,35 +77,7 @@ def fake(
return random.choice([e for e in getattr(Command, t)])


def raises_missing_error(
base: dict,
Command: (
AssessCommand
| DiagnoseCommand
| GoalCommand
| HistoryOfPresentIllnessCommand
| MedicationStatementCommand
| PlanCommand
| PrescribeCommand
| QuestionnaireCommand
| ReasonForVisitCommand
| StopMedicationCommand
),
field: str,
) -> None:
err_kwargs = base.copy()
err_kwargs.pop(field)
with pytest.raises(ValidationError) as e:
Command(**err_kwargs)
err_msg = repr(e.value)
assert (
f"1 validation error for {Command.__name__}\n{field}\n Field required [type=missing"
in err_msg
)


def raises_none_error(
base: dict,
def raises_wrong_type_error(
Command: (
AssessCommand
| DiagnoseCommand
Expand All @@ -122,21 +94,24 @@ def raises_none_error(
) -> None:
field_props = Command.model_json_schema()["properties"][field]
field_type = get_field_type(field_props)
wrong_field_type = "integer" if field_type == "string" else "string"

with pytest.raises(ValidationError) as e1:
err_kwargs = base | {field: None}
err_kwargs = {field: fake({"type": wrong_field_type}, Command)}
Command(**err_kwargs)
err_msg1 = repr(e1.value)

valid_kwargs = base | {field: fake(field_props, Command)}
valid_kwargs = {field: fake(field_props, Command)}
cmd = Command(**valid_kwargs)
err_value = fake({"type": wrong_field_type}, Command)
with pytest.raises(ValidationError) as e2:
setattr(cmd, field, None)
setattr(cmd, field, err_value)
err_msg2 = repr(e2.value)

assert f"1 validation error for {Command.__name__}\n{field}" in err_msg1
assert f"1 validation error for {Command.__name__}\n{field}" in err_msg2

field_type = "dictionary" if field_type == "Coding" else field_type
if field_type == "number":
assert f"Input should be an instance of Decimal" in err_msg1
assert f"Input should be an instance of Decimal" in err_msg2
Expand All @@ -148,8 +123,7 @@ def raises_none_error(
assert f"Input should be a valid {field_type}" in err_msg2


def raises_wrong_type_error(
base: dict,
def raises_none_error_for_effect_method(
Command: (
AssessCommand
| DiagnoseCommand
Expand All @@ -162,34 +136,16 @@ def raises_wrong_type_error(
| ReasonForVisitCommand
| StopMedicationCommand
),
field: str,
method: str,
) -> None:
field_props = Command.model_json_schema()["properties"][field]
field_type = get_field_type(field_props)
wrong_field_type = "integer" if field_type == "string" else "string"

with pytest.raises(ValidationError) as e1:
err_kwargs = base | {field: fake({"type": wrong_field_type}, Command)}
Command(**err_kwargs)
err_msg1 = repr(e1.value)

valid_kwargs = base | {field: fake(field_props, Command)}
cmd = Command(**valid_kwargs)
err_value = fake({"type": wrong_field_type}, Command)
with pytest.raises(ValidationError) as e2:
setattr(cmd, field, err_value)
err_msg2 = repr(e2.value)

assert f"1 validation error for {Command.__name__}\n{field}" in err_msg1
assert f"1 validation error for {Command.__name__}\n{field}" in err_msg2

field_type = "dictionary" if field_type == "Coding" else field_type
if field_type == "number":
assert f"Input should be an instance of Decimal" in err_msg1
assert f"Input should be an instance of Decimal" in err_msg2
elif field_type[0].isupper():
assert f"Input should be an instance of {Command.__name__}.{field_type}" in err_msg1
assert f"Input should be an instance of {Command.__name__}.{field_type}" in err_msg2
else:
assert f"Input should be a valid {field_type}" in err_msg1
assert f"Input should be a valid {field_type}" in err_msg2
cmd = Command()
method_required_fields = cmd._get_effect_method_required_fields(method)
with pytest.raises(ValidationError) as e:
getattr(cmd, method)()
e_msg = repr(e.value)
assert f"{len(method_required_fields)} validation errors for {Command.__name__}" in e_msg
for f in method_required_fields:
assert (
f"Field '{f}' is required to {method.replace('_', ' ')} a command [type=missing, input_value=None, input_type=NoneType]"
in e_msg
)
77 changes: 47 additions & 30 deletions canvas_sdk/commands/tests/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import decimal
from datetime import datetime

import pytest
Expand All @@ -22,8 +23,7 @@
from canvas_sdk.commands.tests.test_utils import (
fake,
get_field_type,
raises_missing_error,
raises_none_error,
raises_none_error_for_effect_method,
raises_wrong_type_error,
)

Expand Down Expand Up @@ -96,42 +96,38 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
),
fields_to_test: tuple[str],
) -> None:
schema = Command.model_json_schema()
schema["required"].append("note_uuid")
required_fields = {k: v for k, v in schema["properties"].items() if k in schema["required"]}
base = {field: fake(props, Command) for field, props in required_fields.items()}
for field in fields_to_test:
raises_wrong_type_error(base, Command, field)
if field in required_fields:
raises_missing_error(base, Command, field)
raises_none_error(base, Command, field)
raises_wrong_type_error(Command, field)

for method in ["originate", "edit", "delete", "commit", "enter_in_error"]:
raises_none_error_for_effect_method(Command, method)


@pytest.mark.parametrize(
"Command,err_kwargs,err_msg,valid_kwargs",
[
(
PlanCommand,
{"narrative": "yo", "user_id": 1},
"1 validation error for PlanCommand\n Value error, Command should have either a note_uuid or a command_uuid. [type=value",
{"narrative": "yo", "user_id": 5, "note_uuid": 1},
"1 validation error for PlanCommand\nnote_uuid\n Input should be a valid string [type=string_type",
{"narrative": "yo", "note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
),
(
PlanCommand,
{"narrative": "yo", "user_id": 1, "note_uuid": None},
"1 validation error for PlanCommand\n Value error, Command should have either a note_uuid or a command_uuid. [type=value",
{"narrative": "yo", "note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
{"narrative": "yo", "user_id": 5, "note_uuid": "5", "command_uuid": 5},
"1 validation error for PlanCommand\ncommand_uuid\n Input should be a valid string [type=string_type",
{"narrative": "yo", "user_id": 5, "note_uuid": "5", "command_uuid": "5"},
),
(
PlanCommand,
{"narrative": "yo", "user_id": 5, "note_uuid": 1},
"1 validation error for PlanCommand\nnote_uuid\n Input should be a valid string [type=string_type",
{"narrative": "yo", "note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
{"narrative": "yo", "note_uuid": "5", "command_uuid": "4", "user_id": "5"},
"1 validation error for PlanCommand\nuser_id\n Input should be a valid integer [type=int_type",
{"narrative": "yo", "note_uuid": "5", "command_uuid": "4", "user_id": 5},
),
(
ReasonForVisitCommand,
{"note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1, "structured": True},
"1 validation error for ReasonForVisitCommand\n Value error, Structured RFV should have a coding.",
"1 validation error for ReasonForVisitCommand\n Structured RFV should have a coding",
{
"note_uuid": "00000000-0000-0000-0000-000000000000",
"user_id": 1,
Expand Down Expand Up @@ -217,7 +213,9 @@ def test_command_raises_specific_error_when_kwarg_given_incorrect_type(
valid_kwargs: dict,
) -> None:
with pytest.raises(ValidationError) as e1:
Command(**err_kwargs)
cmd = Command(**err_kwargs)
cmd.originate()
cmd.edit()
assert err_msg in repr(e1.value)

cmd = Command(**valid_kwargs)
Expand All @@ -226,6 +224,8 @@ def test_command_raises_specific_error_when_kwarg_given_incorrect_type(
key, value = list(err_kwargs.items())[-1]
with pytest.raises(ValidationError) as e2:
setattr(cmd, key, value)
cmd.originate()
cmd.edit()
assert err_msg in repr(e2.value)


Expand Down Expand Up @@ -299,22 +299,30 @@ def test_command_allows_kwarg_with_correct_type(
fields_to_test: tuple[str],
) -> None:
schema = Command.model_json_schema()
schema["required"].append("note_uuid")
required_fields = {k: v for k, v in schema["properties"].items() if k in schema["required"]}
base = {field: fake(props, Command) for field, props in required_fields.items()}

for field in fields_to_test:
field_type = get_field_type(Command.model_json_schema()["properties"][field])
field_type = get_field_type(schema["properties"][field])

init_field_value = fake({"type": field_type}, Command)
init_kwargs = base | {field: init_field_value}
init_kwargs = {field: init_field_value}
cmd = Command(**init_kwargs)
assert getattr(cmd, field) == init_field_value

updated_field_value = fake({"type": field_type}, Command)
setattr(cmd, field, updated_field_value)
assert getattr(cmd, field) == updated_field_value

for method in ["originate", "edit", "delete", "commit", "enter_in_error"]:
required_fields = {
k: v
for k, v in schema["properties"].items()
if k in Command()._get_effect_method_required_fields(method)
}
base = {field: fake(props, Command) for field, props in required_fields.items()}
cmd = Command(**base)
effect = getattr(cmd, method)()
assert effect is not None


@pytest.fixture(scope="session")
def token() -> str:
Expand Down Expand Up @@ -357,6 +365,9 @@ def command_type_map() -> dict[str, type]:
"TextField": str,
"ChoiceField": str,
"DateField": datetime,
"ApproximateDateField": datetime,
"IntegerField": int,
"DecimalField": decimal.Decimal,
}


Expand All @@ -365,14 +376,12 @@ def command_type_map() -> dict[str, type]:
"Command",
[
(AssessCommand),
# todo: add Diagnose once it has an adapter in home-app
# (DiagnoseCommand),
(DiagnoseCommand),
(GoalCommand),
(HistoryOfPresentIllnessCommand),
(MedicationStatementCommand),
(PlanCommand),
# todo: add Prescribe once its been refactored
# (PrescribeCommand),
(PrescribeCommand),
(QuestionnaireCommand),
(ReasonForVisitCommand),
(StopMedicationCommand),
Expand Down Expand Up @@ -427,7 +436,15 @@ def test_command_schema_matches_command_api(
expected_type = expected_field["type"]
if expected_type is Coding:
expected_type = expected_type.__annotations__["code"]
assert expected_type == command_type_map.get(actual_field["type"])

actual_type = command_type_map.get(actual_field["type"])
if actual_field["type"] == "AutocompleteField" and name[-1] == "s":
# this condition initially created for Prescribe.indications,
# but could apply to other AutocompleteField fields that are lists
# making the assumption here that if the field ends in 's' (like indications), it is a list
actual_type = list[actual_type] # type: ignore

assert expected_type == actual_type

if (choices := actual_field["choices"]) is None:
assert expected_field["choices"] is None
Expand Down

0 comments on commit 806cd87

Please sign in to comment.