diff options
Diffstat (limited to 'tests/flow')
| -rw-r--r-- | tests/flow/test_tasks.py | 511 |
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) |
