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

chore(answers): add validation of submit_id (M2-6851) #1421

Merged
merged 1 commit into from
Jun 20, 2024
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
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
Loading