Skip to content

Commit

Permalink
chore(answers): add validation of submit_id (M2-6851)
Browse files Browse the repository at this point in the history
  • Loading branch information
vshvechko committed Jun 19, 2024
1 parent a9284d4 commit 81a833d
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 15 deletions.
15 changes: 9 additions & 6 deletions src/apps/activity_flows/crud/flow_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,20 @@ async def get_by_id_versions(self, id_versions: list[str]) -> list[ActivityFlowH
result = await self._execute(query)
return result.scalars().all()

async def load_full(self, id_versions: list[str]) -> list[FlowHistoryWithActivityFull]:
async def load_full(
self, id_versions: list[str], *, load_activities: bool = True, load_activity_items: bool = True
) -> list[FlowHistoryWithActivityFull]:
if not id_versions:
return []
opts = joinedload(ActivityFlowHistoriesSchema.items, innerjoin=True)
if load_activities:
opts = opts.joinedload(ActivityFlowItemHistorySchema.activity, innerjoin=True)
if load_activity_items:
opts = opts.joinedload(ActivityHistorySchema.items, innerjoin=True)

query = (
select(ActivityFlowHistoriesSchema)
.options(
joinedload(ActivityFlowHistoriesSchema.items, innerjoin=True)
.joinedload(ActivityFlowItemHistorySchema.activity, innerjoin=True)
.joinedload(ActivityHistorySchema.items, innerjoin=True)
)
.options(opts)
.where(ActivityFlowHistoriesSchema.id_version.in_(id_versions))
)
res = await self._execute(query)
Expand Down
2 changes: 1 addition & 1 deletion src/apps/activity_flows/domain/flow_full.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class FlowItemHistoryFull(InternalModel):


class FlowItemHistoryWithActivityFull(FlowItemHistoryFull):
activity: ActivityHistoryFull
activity: ActivityHistoryFull | None = None


class FlowFull(FlowBase, InternalModel):
Expand Down
48 changes: 44 additions & 4 deletions src/apps/answers/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,35 @@ async def _validate_anonymous_answer(self, activity_answer: AppletAnswerCreate)
await self._validate_applet_for_anonymous_response(activity_answer.applet_id, activity_answer.version)
await self._validate_answer(activity_answer)

async def _validate_answer(self, applet_answer: AppletAnswerCreate) -> None:
existed_answers = await AnswersCRUD(self.session).get_by_submit_id(applet_answer.submit_id)
async def _validate_answer(self, applet_answer: AppletAnswerCreate) -> None: # noqa: C901
pk = self._generate_history_id(applet_answer.version)
existed_answers = await AnswersCRUD(self.answer_session).get_by_submit_id(applet_answer.submit_id)

activity_history_id = pk(applet_answer.activity_id)
flow_history_id = pk(applet_answer.flow_id) if applet_answer.flow_id else None

activity_index = None
if flow_history_id:
flow_histories = await FlowsHistoryCRUD(self.session).load_full(
[pk(applet_answer.flow_id)], load_activities=False
)
if not flow_histories:
raise ValidationError("Flow not found")
flow_history = next(iter(flow_histories))

# check activity in the flow
for i, item in enumerate(flow_history.items):
if item.activity_id == activity_history_id:
activity_index = i
break
if activity_index is None:
raise ValidationError("Activity not found in the flow")

if existed_answers:
# check uniqueness for activities (duplicated for flow submission only)
if not flow_history_id:
raise ValidationError("Submit id duplicate error")

existed_answer = existed_answers[0]
if existed_answer.applet_id != applet_answer.applet_id:
raise WrongAnswerGroupAppletId()
Expand All @@ -145,8 +170,23 @@ async def _validate_answer(self, applet_answer: AppletAnswerCreate) -> None:
elif existed_answer.respondent_id != self.user_id:
raise WrongRespondentForAnswerGroup()

pk = self._generate_history_id(applet_answer.version)
activity_history = await ActivityHistoriesCRUD(self.session).get_by_id(pk(applet_answer.activity_id))
# check uniqueness in flow submisssions
if flow_history_id != existed_answer.flow_history_id:
raise ValidationError("Submit id duplicate error")

# check current answer is provided in right order in the flow, so prev activities already answered
prev_answers_count = len(existed_answers)
if prev_answers_count != activity_index:
assert activity_index is not None
if prev_answers_count < activity_index:
raise ValidationError("Wrong activity order in the flow")
raise ValidationError("Wrong activity order in the flow: previous activity answer missed")

elif flow_history_id and activity_index != 0:
# check first flow answer
raise ValidationError("Wrong activity order in the flow")

activity_history = await ActivityHistoriesCRUD(self.session).get_by_id(activity_history_id)

if not activity_history.applet_id.startswith(f"{applet_answer.applet_id}"):
raise ActivityHistoryDoeNotExist()
Expand Down
1 change: 1 addition & 0 deletions src/apps/answers/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ def answer_reviewable_activity_with_tz_offset_create(
answer_reviewable_activity_create: AppletAnswerCreate,
) -> AppletAnswerCreate:
data = answer_reviewable_activity_create.copy(deep=True)
data.submit_id = uuid.uuid4()
# US/Pacific
tz_offset = -420
# To minutes like in api
Expand Down
12 changes: 8 additions & 4 deletions src/apps/answers/tests/test_answer_for_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,20 @@ async def applet_data_with_flow(applet_minimal_data: AppletCreate) -> AppletCrea
data = applet_minimal_data.copy(deep=True)
data.display_name = "schedule"
# By some reasons for test need ActivityFlow
second_activity = data.activities[0].copy(deep=True)
second_activity.name = data.activities[0].name + " second"
second_activity.key = uuid.uuid4()
data.activities.append(second_activity)
data.activity_flows = [
FlowCreate(
name="flow",
description={Language.ENGLISH: "description"},
items=[FlowItemCreate(activity_key=data.activities[0].key)],
items=[
FlowItemCreate(activity_key=data.activities[0].key),
FlowItemCreate(activity_key=data.activities[1].key),
],
)
]
second_activity = data.activities[0].copy(deep=True)
second_activity.name = data.activities[0].name + " second"
data.activities.append(second_activity)
return AppletCreate(**data.dict())


Expand Down
114 changes: 114 additions & 0 deletions src/apps/answers/tests/test_answers.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,120 @@ async def test_create_answer__wrong_applet_version(
assert response.status_code == http.HTTPStatus.BAD_REQUEST
assert response.json()["result"][0]["message"] == InvalidVersionError.message

async def test_create_activity_answers__submit_duplicate(
self,
client: TestClient,
tom: User,
answer_create: AppletAnswerCreate,
):
client.login(tom)
data = answer_create.copy(deep=True)
response = await client.post(self.answer_url, data=data)
assert response.status_code == http.HTTPStatus.CREATED

response = await client.post(self.answer_url, data=data)
assert response.status_code == http.HTTPStatus.BAD_REQUEST

data.submit_id = uuid.uuid4()
response = await client.post(self.answer_url, data=data)
assert response.status_code == http.HTTPStatus.CREATED

async def test_create_activity_answer_flow_answer__submit_duplicate(
self,
client: TestClient,
tom: User,
answer_create: AppletAnswerCreate,
applet_with_flow: AppletFull,
):
client.login(tom)
data: AppletAnswerCreate = answer_create.copy(deep=True)
data.submit_id = uuid.uuid4()
data.applet_id = applet_with_flow.id
data.activity_id = applet_with_flow.activities[0].id
data.flow_id = None

response = await client.post(self.answer_url, data=data)
assert response.status_code == http.HTTPStatus.CREATED

data.flow_id = applet_with_flow.activity_flows[0].id
response = await client.post(self.answer_url, data=data)
assert response.status_code == http.HTTPStatus.BAD_REQUEST

data.submit_id = uuid.uuid4()
response = await client.post(self.answer_url, data=data)
assert response.status_code == http.HTTPStatus.CREATED

async def test_create_flow_answer__submit_duplicate(
self,
client: TestClient,
tom: User,
answer_create: AppletAnswerCreate,
applet_with_flow: AppletFull,
):
client.login(tom)
data: AppletAnswerCreate = answer_create.copy(deep=True)
data.submit_id = uuid.uuid4()
data.applet_id = applet_with_flow.id
data.flow_id = applet_with_flow.activity_flows[0].id
data.activity_id = applet_with_flow.activity_flows[0].items[0].activity_id

response = await client.post(self.answer_url, data=data)
assert response.status_code == http.HTTPStatus.CREATED

response = await client.post(self.answer_url, data=data)
assert response.status_code == http.HTTPStatus.BAD_REQUEST

async def test_create_flow_answer__correct_order(
self,
client: TestClient,
tom: User,
answer_create: AppletAnswerCreate,
applet_with_flow: AppletFull,
):
client.login(tom)
data: AppletAnswerCreate = answer_create.copy(deep=True)
data.submit_id = uuid.uuid4()
data.applet_id = applet_with_flow.id
data.flow_id = applet_with_flow.activity_flows[1].id
data.activity_id = applet_with_flow.activity_flows[1].items[0].activity_id

response = await client.post(self.answer_url, data=data)
assert response.status_code == http.HTTPStatus.CREATED

data.activity_id = applet_with_flow.activity_flows[1].items[1].activity_id

response = await client.post(self.answer_url, data=data)
assert response.status_code == http.HTTPStatus.CREATED

async def test_create_flow_answer__wrong_order(
self,
client: TestClient,
tom: User,
answer_create: AppletAnswerCreate,
applet_with_flow: AppletFull,
):
client.login(tom)
data: AppletAnswerCreate = answer_create.copy(deep=True)
data.submit_id = uuid.uuid4()
data.applet_id = applet_with_flow.id
data.flow_id = applet_with_flow.activity_flows[1].id
data.activity_id = applet_with_flow.activity_flows[1].items[1].activity_id

response = await client.post(self.answer_url, data=data)
assert response.status_code == http.HTTPStatus.BAD_REQUEST

# different submit_id for second activity
data.activity_id = applet_with_flow.activity_flows[1].items[0].activity_id

response = await client.post(self.answer_url, data=data)
assert response.status_code == http.HTTPStatus.CREATED

data.submit_id = uuid.uuid4()
data.activity_id = applet_with_flow.activity_flows[1].items[1].activity_id

response = await client.post(self.answer_url, data=data)
assert response.status_code == http.HTTPStatus.BAD_REQUEST

@pytest.mark.usefixtures("mock_report_server_response", "answer")
async def test_get_latest_summary(
self, client: TestClient, tom: User, applet: AppletFull, tom_applet_subject: Subject
Expand Down

0 comments on commit 81a833d

Please sign in to comment.