aboutsummaryrefslogtreecommitdiff
path: root/tests/flow
diff options
context:
space:
mode:
Diffstat (limited to 'tests/flow')
-rw-r--r--tests/flow/test_tasks.py511
1 files changed, 137 insertions, 374 deletions
diff --git a/tests/flow/test_tasks.py b/tests/flow/test_tasks.py
index b708cf9..82d4912 100644
--- a/tests/flow/test_tasks.py
+++ b/tests/flow/test_tasks.py
@@ -1,21 +1,10 @@
import logging
from contextlib import contextmanager
-from datetime import timezone, datetime
-from typing import Dict
-from uuid import uuid4
+from typing import Callable, Dict, Any
import pytest
-import requests
from botocore.stub import Stubber
-from generalresearchutils.models.thl.payout import UserPayoutEvent
-from generalresearchutils.models.thl.wallet import PayoutType
-from generalresearchutils.models.thl.wallet.cashout_method import (
- CashoutRequestResponse,
- CashoutRequestInfo,
-)
-
-from jb.config import settings
-from jb.decorators import AMT_CLIENT, AM, BM
+from jb.decorators import AMT_CLIENT
from jb.flow.assignment_tasks import process_assignment_submitted
from jb.managers.amt import (
AMTManager,
@@ -23,13 +12,19 @@ from jb.managers.amt import (
REJECT_MESSAGE_BADDIE,
REJECT_MESSAGE_UNKNOWN_ASSIGNMENT,
REJECT_MESSAGE_NO_WORK,
- BONUS_MESSAGE,
NO_WORK_APPROVAL_MESSAGE,
)
+from mypy_boto3_mturk.type_defs import (
+ GetAssignmentResponseTypeDef,
+)
from generalresearchutils.currency import USDCent
+from jb.managers.assignment import AssignmentManager
+from jb.managers.bonus import BonusManager
+from jb.managers.hit import HitManager
from jb.models.definitions import AssignmentStatus
-from generalresearchutils.models.thl.definitions import PayoutStatus
from jb.models.event import MTurkEvent
+from jb.models.hit import Hit
+from jb.models.assignment import Assignment, AssignmentStub
@contextmanager
@@ -45,303 +40,52 @@ def amt_stub_context(responses):
yield stub
-@pytest.fixture
-def approved_assignment_stubs(
- get_assignment_response,
- get_assignment_response_approved,
- amt_assignment_id,
- amt_hit_id,
- get_hit_response_reviewing,
-):
- # These are the AMT_CLIENT stubs/mocks that need to be set when running
- # process_assignment_submitted() which will result in an approved
- # assignment and sent bonus
- def _approved_assignment_stubs(
- feedback: str = APPROVAL_MESSAGE,
- override_response=None,
- override_approve_response=None,
- ):
- response = override_response or get_assignment_response
- approve_response = (
- override_approve_response or get_assignment_response_approved(feedback)
- )
- return [
- {
- "operation": "get_assignment",
- "response": response,
- "expected_params": {"AssignmentId": amt_assignment_id},
- },
- {
- "operation": "approve_assignment",
- "response": {},
- "expected_params": {
- "AssignmentId": amt_assignment_id,
- "RequesterFeedback": feedback,
- "OverrideRejection": False,
- },
- },
- {
- "operation": "get_assignment",
- "response": approve_response,
- "expected_params": {"AssignmentId": amt_assignment_id},
- },
- {
- "operation": "update_hit_review_status",
- "response": {},
- "expected_params": {"HITId": amt_hit_id, "Revert": False},
- },
- {
- "operation": "get_hit",
- "response": get_hit_response_reviewing,
- "expected_params": {"HITId": amt_hit_id},
- },
- ]
-
- return _approved_assignment_stubs
-
-
-@pytest.fixture
-def approved_assignment_stubs_w_bonus(
- approved_assignment_stubs, amt_worker_id, amt_assignment_id, pe_id
-):
- now = datetime.now(tz=timezone.utc)
- stubs = approved_assignment_stubs().copy()
- stubs.append(
- {
- "operation": "send_bonus",
- "response": {},
- "expected_params": {
- "WorkerId": amt_worker_id,
- "BonusAmount": "0.07",
- "AssignmentId": amt_assignment_id,
- "Reason": BONUS_MESSAGE,
- "UniqueRequestToken": pe_id,
- },
- }
- )
- stubs.append(
- {
- "operation": "list_bonus_payments",
- "response": {
- "BonusPayments": [
- {
- "WorkerId": amt_worker_id,
- "BonusAmount": "0.07",
- "AssignmentId": amt_assignment_id,
- "Reason": BONUS_MESSAGE,
- "GrantTime": now,
- }
- ]
- },
- "expected_params": {"AssignmentId": amt_assignment_id},
- }
- )
- return stubs
-
-
-@pytest.fixture
-def rejected_assignment_stubs(
- get_assignment_response,
- get_assignment_response_rejected,
- amt_assignment_id,
- amt_hit_id,
- get_hit_response_reviewing,
-):
- # These are the AMT_CLIENT stubs/mocks that need to be set when running
- # process_assignment_submitted() which will result in a rejected
- # assignment
- def _rejected_assignment_stubs(
- reject_reason: str, override_response=None, override_reject_response=None
- ):
- response = override_response or get_assignment_response
- reject_response = override_reject_response or get_assignment_response_rejected(
- reject_reason
- )
- return [
- {
- "operation": "get_assignment",
- "response": response,
- "expected_params": {"AssignmentId": amt_assignment_id},
- },
- {
- "operation": "reject_assignment",
- "response": {},
- "expected_params": {
- "AssignmentId": amt_assignment_id,
- "RequesterFeedback": reject_reason,
- },
- },
- {
- "operation": "get_assignment",
- "response": reject_response,
- "expected_params": {"AssignmentId": amt_assignment_id},
- },
- {
- "operation": "update_hit_review_status",
- "response": {},
- "expected_params": {"HITId": amt_hit_id, "Revert": False},
- },
- {
- "operation": "get_hit",
- "response": get_hit_response_reviewing,
- "expected_params": {"HITId": amt_hit_id},
- },
- ]
-
- return _rejected_assignment_stubs
-
-
-@pytest.fixture
-def mock_thl_responses(monkeypatch, amt_worker_id, tsid, pe_id):
- original_get = requests.get
- original_post = requests.post
-
- class MockThlCashoutRequestResponse:
- def json(self):
- return CashoutRequestResponse(
- status="success",
- cashout=CashoutRequestInfo(
- id=pe_id,
- description="amt something",
- status=PayoutStatus.PENDING,
- ),
- ).model_dump(mode="json")
-
- def _apply_mock(
- user_blocked: bool = False,
- status_finished: bool = True,
- status_complete: bool = False,
- wallet_redeemable_amount: int = 10,
- ):
-
- def mock_get(url, *args, **kwargs):
- profile_url = f"{settings.fsb_host}{settings.product_id}/user/{amt_worker_id}/profile/"
- status_url = f"{settings.fsb_host}{settings.product_id}/status/{tsid}/"
- cashout_request_url = f"{settings.fsb_host}{settings.product_id}/cashout/"
- wallet_url = f"{settings.fsb_host}{settings.product_id}/wallet/"
- if url == profile_url:
-
- class MockThlProfileResponse:
- def json(self):
- return {"user-profile": {"user": {"blocked": user_blocked}}}
-
- return MockThlProfileResponse()
-
- elif url == wallet_url:
-
- class MockThlWalletResponse:
- def json(self):
- return {
- "wallet": {"redeemable_amount": wallet_redeemable_amount}
- }
-
- return MockThlWalletResponse()
-
- elif url == status_url:
-
- class MockThlStatusResponse:
- def json(self):
- return {
- "tsid": tsid,
- "product_id": str(settings.product_id),
- "product_user_id": amt_worker_id,
- "started": "2020-06-02T00:30:35.036398Z",
- "finished": (
- "2020-06-02T00:31:35.036398Z"
- if status_finished
- else None
- ),
- "status": 3 if status_complete else 2,
- "payout": 10 if status_complete else 0,
- "user_payout": 10 if status_complete else 0,
- "status_code_1": "BUYER_FAIL",
- "status_code_2": None,
- }
-
- return MockThlStatusResponse()
- elif url == cashout_request_url:
- return MockThlCashoutRequestResponse()
- else:
- raise ValueError(f"unhandled call: {url=} {args=} {kwargs=}")
- return original_get(url, *args, **kwargs)
-
- def mock_post(url, *args, **kwargs):
- cashout_request_url = f"{settings.fsb_host}{settings.product_id}/cashout/"
- manage_cashout_request_url = f"{settings.fsb_host}{settings.fsb_host_private_route}/thl/manage_cashout/"
-
- if url == cashout_request_url:
- return MockThlCashoutRequestResponse()
-
- elif url == manage_cashout_request_url:
- json = kwargs["json"]
- print(json)
- payout_id = json["payout_id"]
- new_status = json["new_status"]
-
- class MockThlManageCashoutResponse:
- def json(self):
- return UserPayoutEvent(
- uuid=payout_id,
- status=new_status,
- amount=USDCent(5),
- debit_account_uuid=uuid4().hex,
- cashout_method_uuid=uuid4().hex,
- payout_type=PayoutType.AMT,
- ).model_dump(mode="json")
-
- return MockThlManageCashoutResponse()
- return original_post(url, *args, **kwargs)
-
- monkeypatch.setattr(requests, "get", mock_get)
- monkeypatch.setattr(requests, "post", mock_post)
-
- return _apply_mock
-
-
-@pytest.fixture
-def mturk_event(amt_assignment_id, amt_hit_id, amt_hit_type_id):
- now = datetime.now(tz=timezone.utc)
- return MTurkEvent(
- event_type="AssignmentSubmitted",
- event_timestamp=now,
- amt_assignment_id=amt_assignment_id,
- amt_hit_type_id=amt_hit_type_id,
- amt_hit_id=amt_hit_id,
- )
-
-
-def test_fake_get_assignment(
- amt_assignment_id, amt_worker_id, get_assignment_response: Dict
-):
- # Testing just that this boto stubber works (we fake a response using the real boto client)
- fake_response = get_assignment_response.copy()
- with Stubber(AMT_CLIENT) as stub:
- expected_params = {"AssignmentId": amt_assignment_id}
- stub.add_response("get_assignment", fake_response, expected_params)
+class TestHITTasks:
- assignment = AMTManager.get_assignment_if_exists(
- amt_assignment_id=amt_assignment_id
- )
- assert assignment.amt_assignment_id == amt_assignment_id
- assert assignment.amt_worker_id == amt_worker_id
+ def test_fake_get_assignment(
+ self,
+ amt_assignment_id: str,
+ amt_worker_id: str,
+ assignment_response: GetAssignmentResponseTypeDef,
+ ):
+ # Testing just that this boto stubber works (we fake a response
+ # using the real boto client)
+ fake_response = assignment_response.copy()
+ with Stubber(AMT_CLIENT) as stub:
+ expected_params = {"AssignmentId": amt_assignment_id}
+ stub.add_response("get_assignment", fake_response, expected_params)
+
+ assignment = AMTManager.get_assignment_if_exists(
+ amt_assignment_id=amt_assignment_id
+ )
+ assert assignment is not None
+ assert assignment.amt_assignment_id == amt_assignment_id
+ assert assignment.amt_worker_id == amt_worker_id
- # Optionally, ensure all queued responses were used:
- stub.assert_no_pending_responses()
+ # Optionally, ensure all queued responses were used:
+ stub.assert_no_pending_responses()
class TestProcessAssignmentSubmitted:
def test_no_assignment_in_db(
self,
- mturk_event,
- amt_assignment_id,
- get_assignment_response: Dict,
- caplog,
- hit_record,
- rejected_assignment_stubs,
+ am: AssignmentManager,
+ hit_record: Hit,
+ mturk_event: MTurkEvent,
+ amt_assignment_id: str,
+ caplog: pytest.LogCaptureFixture,
+ rejected_assignment_stubs: Callable[..., list[Dict[str, Any]]],
):
- # An assignment is submitted. The hit exists in the DB. The amt assignment id is valid,
- # but the assignment stub is not in our db. Reject it and write the assignment to the db.
+
+ # These records are auto cleaned up, so we need to explicitly create
+ # a HIT record in the DB so the process_assignment_submitted task
+ # doesn't error when we try to process the Request
+ _ = hit_record
+
+ # An assignment is submitted. The hit exists in the DB. The amt
+ # assignment id is valid, but the assignment stub is not in our
+ # db. Reject it and write the assignment to the db.
amt_stubs = rejected_assignment_stubs(
reject_reason=REJECT_MESSAGE_UNKNOWN_ASSIGNMENT
@@ -356,65 +100,73 @@ class TestProcessAssignmentSubmitted:
assert f"Rejected assignment: " in caplog.text
stub.assert_no_pending_responses()
- ass = AM.get(amt_assignment_id=amt_assignment_id)
+ ass = am.get(amt_assignment_id=amt_assignment_id)
assert ass.status == AssignmentStatus.Rejected
assert ass.requester_feedback == REJECT_MESSAGE_UNKNOWN_ASSIGNMENT
def test_assignment_in_db_user_doesnt_exist(
self,
- mturk_event,
- amt_assignment_id,
- assignment_stub_in_db,
- caplog,
- mock_thl_responses,
- rejected_assignment_stubs,
+ am: AssignmentManager,
+ hm: HitManager,
+ bm: BonusManager,
+ mturk_event: MTurkEvent,
+ amt_assignment_id: str,
+ assignment_stub_record: AssignmentStub,
+ caplog: pytest.LogCaptureFixture,
+ mock_thl_responses: Callable[..., None],
+ rejected_assignment_stubs: Callable[..., list[Dict[str, Any]]],
):
- # An assignment is submitted. The hit and assignment stub exist in the DB. We think we're going to
- # approve the assignment, but the user-profile / check blocked call on THL shows the user doesn't
- # exist (same thing would happen if the user does exist and is blocked). So we reject.
+ # An assignment is submitted. The hit and AssignmentStub exist in the
+ # DB. We think we're going to approve the Assignment, but the
+ # user-profile / check blocked call on THL shows the user doesn't
+ # exist (same thing would happen if the user does exist and is
+ # blocked). So we reject.
- _ = assignment_stub_in_db # we need this to make the assignment stub in the db
+ # We need this to make the assignment stub in the db
+ _ = assignment_stub_record
amt_stubs = rejected_assignment_stubs(reject_reason=REJECT_MESSAGE_BADDIE)
mock_thl_responses(user_blocked=True)
with amt_stub_context(amt_stubs) as stub, caplog.at_level(logging.WARNING):
- process_assignment_submitted(mturk_event)
+ process_assignment_submitted(am=am, hm=hm, bm=bm, event=mturk_event)
stub.assert_no_pending_responses()
assert f"No assignment found in DB: {amt_assignment_id}" not in caplog.text
assert f"blocked or not exists" in caplog.text
assert f"Rejected assignment: " in caplog.text
- ass = AM.get(amt_assignment_id=amt_assignment_id)
+ ass = am.get(amt_assignment_id=amt_assignment_id)
assert ass.status == AssignmentStatus.Rejected
assert ass.requester_feedback == REJECT_MESSAGE_BADDIE
def test_no_work_w_warning(
self,
- mturk_event,
- amt_assignment_id,
- assignment_stub_in_db,
- caplog,
- mock_thl_responses,
- approved_assignment_stubs,
- get_assignment_response_approved_no_tsid,
- get_assignment_response_no_tsid,
+ am: AssignmentManager,
+ mturk_event: MTurkEvent,
+ amt_assignment_id: str,
+ assignment_stub_record: AssignmentStub,
+ caplog: pytest.LogCaptureFixture,
+ mock_thl_responses: Callable[..., None],
+ approved_assignment_stubs: Callable[..., list[Dict[str, Any]]],
+ assignment_response_approved_no_tsid: GetAssignmentResponseTypeDef,
+ assignment_response_no_tsid: GetAssignmentResponseTypeDef,
):
- # An assignment is submitted. The hit and assignment stub exist in the DB.
- # The assignment has no tsid.
- # We APPROVE this assignment b/c we are very nice and give users a couple
- # chances, with an explanation, before rejecting.
+ # An Assignment is submitted. The hit and AssignmentStub exist in
+ # the DB. The assignment has no tsid.
+ # We APPROVE this assignment b/c we are very nice and give users a
+ # couple chances, with an explanation, before rejecting.
- _ = assignment_stub_in_db # we need this to make the assignment stub in the db
+ # We need this to make the assignment stub in the db
+ _ = assignment_stub_record
- # Simulate that the AMT.get_assignment call returns the assignment, but the answers xml
- # has no tsid.
+ # Simulate that the AMT.get_assignment call returns the assignment,
+ # but the answers XML has no tsid.
amt_stubs = approved_assignment_stubs(
feedback=NO_WORK_APPROVAL_MESSAGE,
- override_response=get_assignment_response_no_tsid,
- override_approve_response=get_assignment_response_approved_no_tsid,
+ override_response=assignment_response_no_tsid,
+ override_approve_response=assignment_response_approved_no_tsid,
)
mock_thl_responses(user_blocked=False)
@@ -427,24 +179,26 @@ class TestProcessAssignmentSubmitted:
assert f"Assignment submitted with no tsid" in caplog.text
assert f"Approved assignment: " in caplog.text
- ass = AM.get(amt_assignment_id=amt_assignment_id)
+ ass = am.get(amt_assignment_id=amt_assignment_id)
assert ass.status == AssignmentStatus.Approved
assert ass.requester_feedback == NO_WORK_APPROVAL_MESSAGE
- assert AM.missing_tsid_count(amt_worker_id=ass.amt_worker_id) == 1
+ assert am.missing_tsid_count(amt_worker_id=ass.amt_worker_id) == 1
def test_no_work_no_warning(
self,
- mturk_event,
- amt_assignment_id,
- assignment_stub_in_db,
- caplog,
- mock_thl_responses,
- rejected_assignment_stubs,
- get_assignment_response_rejected_no_tsid,
- get_assignment_response_no_tsid,
- assignment_in_db_factory,
- hit_record,
- amt_worker_id,
+ am: AssignmentManager,
+ mturk_event: MTurkEvent,
+ amt_assignment_id: str,
+ assignment_stub_record: AssignmentStub,
+ caplog: pytest.LogCaptureFixture,
+ mock_thl_responses: Callable[..., None],
+ rejected_assignment_stubs: Callable[..., list[Dict[str, Any]]],
+ assignment_response_rejected_no_tsid: GetAssignmentResponseTypeDef,
+ assignment_response_no_tsid: GetAssignmentResponseTypeDef,
+ assignment_factory: Assignment,
+ assignment_stub_factory: Assignment,
+ hit_record: Hit,
+ amt_worker_id: str,
):
# An assignment is submitted. The hit and assignment stub exist in the DB.
# The assignment has no tsid.
@@ -452,20 +206,20 @@ class TestProcessAssignmentSubmitted:
# Going to create and submit 3 assignments w no work
# (all on the same hit, which we don't do in JB for real,
# but doesn't matter here)
- a1 = assignment_in_db_factory(hit_id=hit_record.id, amt_worker_id=amt_worker_id)
- a2 = assignment_in_db_factory(hit_id=hit_record.id, amt_worker_id=amt_worker_id)
- a3 = assignment_in_db_factory(hit_id=hit_record.id, amt_worker_id=amt_worker_id)
- assert AM.missing_tsid_count(amt_worker_id=amt_worker_id) == 3
+ _a1 = assignment_factory(hit_id=hit_record.id, amt_worker_id=amt_worker_id)
+ _a2 = assignment_factory(hit_id=hit_record.id, amt_worker_id=amt_worker_id)
+ _a3 = assignment_factory(hit_id=hit_record.id, amt_worker_id=amt_worker_id)
+ assert am.missing_tsid_count(amt_worker_id=amt_worker_id) == 3
# So now, we'll reject, b/c they've already gotten 3 warnings
- _ = assignment_stub_in_db # we need this to make the assignment stub in the db
+ _ = assignment_stub_factory # we need this to make the assignment stub in the db
# Simulate that the AMT.get_assignment call returns the assignment, but the answers xml
# has no tsid.
amt_stubs = rejected_assignment_stubs(
reject_reason=REJECT_MESSAGE_NO_WORK,
- override_response=get_assignment_response_no_tsid,
- override_reject_response=get_assignment_response_rejected_no_tsid(
+ override_response=assignment_response_no_tsid,
+ override_reject_response=assignment_response_rejected_no_tsid(
REJECT_MESSAGE_NO_WORK
),
)
@@ -481,24 +235,31 @@ class TestProcessAssignmentSubmitted:
assert f"Rejected assignment: " in caplog.text
# It will exist in the db since we can validate the model.
- ass = AM.get(amt_assignment_id=amt_assignment_id)
+ ass = am.get(amt_assignment_id=amt_assignment_id)
assert ass.status == AssignmentStatus.Rejected
assert ass.requester_feedback == REJECT_MESSAGE_NO_WORK
def test_assignment_submitted_no_bonus(
self,
- mturk_event,
- amt_assignment_id,
- assignment_stub_in_db,
- caplog,
- mock_thl_responses,
- approved_assignment_stubs,
+ am: AssignmentManager,
+ mturk_event: MTurkEvent,
+ amt_assignment_id: str,
+ assignment_stub_factory: Assignment,
+ caplog: pytest.LogCaptureFixture,
+ mock_thl_responses: Callable[..., None],
+ approved_assignment_stubs: Callable[..., list[Dict[str, Any]]],
):
- _ = assignment_stub_in_db # we need this to make the assignment stub in the db
- # The "send bonus" stuff will still run, even if the user didn't get a complete,
- # because all we do is check the user's wallet balance (if an assignment is approved)
- # and they may have money in their wallet from a prev event or bribe
+ _ = (
+ assignment_stub_factory()
+ ) # we need this to make the assignment stub in the db
+
+ # The "send bonus" stuff will still run, even if the user didn't get
+ # a complete, because all we do is check the user's wallet balance (if
+ # an assignment is approved) and they may have money in their wallet
+ # from a prev event or bribe
+ #
# So mock the wallet balance as 1cent, so no bonus will be triggered
+
mock_thl_responses(status_complete=False, wallet_redeemable_amount=1)
with amt_stub_context(approved_assignment_stubs()) as stub, caplog.at_level(
logging.WARNING
@@ -506,20 +267,22 @@ class TestProcessAssignmentSubmitted:
process_assignment_submitted(mturk_event)
stub.assert_no_pending_responses()
- ass = AM.get(amt_assignment_id=amt_assignment_id)
+ ass = am.get(amt_assignment_id=amt_assignment_id)
assert ass.status == AssignmentStatus.Approved
assert ass.requester_feedback == APPROVAL_MESSAGE
def test_assignment_submitted_w_bonus(
self,
- mturk_event,
- amt_assignment_id,
- assignment_stub_in_db,
- caplog,
- mock_thl_responses,
- approved_assignment_stubs_w_bonus,
+ am: AssignmentManager,
+ bm: BonusManager,
+ mturk_event: MTurkEvent,
+ amt_assignment_id: str,
+ assignment_stub_factory: Assignment,
+ caplog: pytest.LogCaptureFixture,
+ mock_thl_responses: Callable[..., None],
+ approved_assignment_stubs_w_bonus: list[Dict[str, Any]],
):
- _ = assignment_stub_in_db # we need this to make the assignment stub in the db
+ _ = assignment_stub_factory # we need this to make the assignment stub in the db
mock_thl_responses(status_complete=True, wallet_redeemable_amount=10)
with amt_stub_context(
approved_assignment_stubs_w_bonus
@@ -527,9 +290,9 @@ class TestProcessAssignmentSubmitted:
process_assignment_submitted(mturk_event)
stub.assert_no_pending_responses()
- ass = AM.get(amt_assignment_id=amt_assignment_id)
+ ass = am.get(amt_assignment_id=amt_assignment_id)
assert ass.status == AssignmentStatus.Approved
assert ass.requester_feedback == APPROVAL_MESSAGE
- bonus = BM.filter(amt_assignment_id=amt_assignment_id)[0]
+ bonus = bm.filter(amt_assignment_id=amt_assignment_id)[0]
assert bonus.amount == USDCent(7)