import copy from datetime import datetime, timezone, timedelta from typing import Optional from uuid import uuid4 import pytest from dateutil.tz import tzlocal from mypy_boto3_mturk.type_defs import ( GetHITResponseTypeDef, CreateHITTypeResponseTypeDef, CreateHITWithHITTypeResponseTypeDef, GetAssignmentResponseTypeDef, ) from jb.decorators import HQM, HTM, HM, AM from jb.managers.amt import AMTManager, APPROVAL_MESSAGE, NO_WORK_APPROVAL_MESSAGE from jb.models.assignment import AssignmentStub, Assignment from jb.models.currency import USDCent from jb.models.definitions import HitStatus, HitReviewStatus, AssignmentStatus from jb.models.hit import HitType, HitQuestion, Hit from tests import generate_amt_id @pytest.fixture def amt_hit_type_id(): return generate_amt_id() @pytest.fixture def amt_hit_id(): return generate_amt_id() @pytest.fixture def amt_assignment_id(): return generate_amt_id() @pytest.fixture def amt_worker_id(): return generate_amt_id(length=21) @pytest.fixture def amt_group_id(): return generate_amt_id() @pytest.fixture def tsid(): return uuid4().hex @pytest.fixture def tsid1(): return uuid4().hex @pytest.fixture def tsid2(): return uuid4().hex @pytest.fixture def pe_id(): # payout event / cashout request UUID return uuid4().hex @pytest.fixture def hit_type() -> HitType: return HitType( title="Awesome Surveys!", description="Give us your opinion", reward=USDCent(5), keywords="market,research,amazing", min_active=10, ) from jb.models.hit import HitType @pytest.fixture def hit_type_with_amt_id(hit_type: HitType) -> HitType: # This is a real hit type I've previously registered with amt (sandbox). # It will always exist hit_type.amt_hit_type_id = "3217B3DC4P5YW9DRV9R3X8O56V041J" # Get or create our db HTM.get_or_create(hit_type) # this call adds the pk int id ---^ return hit_type @pytest.fixture def question(): return HitQuestion(url="https://jamesbillings67.com/work/", height=1200) @pytest.fixture def hit_in_amt(hit_type_with_amt_id: HitType, question: HitQuestion) -> Hit: # Actually create a new HIT in amt (sandbox) question = HQM.get_or_create(question) hit = AMTManager.create_hit_with_hit_type( hit_type=hit_type_with_amt_id, question=question ) # Create it in the DB HM.create(hit) return hit @pytest.fixture def hit(amt_hit_id, amt_hit_type_id, amt_group_id, question): now = datetime.now(tz=timezone.utc) return Hit.model_validate( dict( amt_hit_id=amt_hit_id, amt_hit_type_id=amt_hit_type_id, amt_group_id=amt_group_id, status=HitStatus.Assignable, review_status=HitReviewStatus.NotReviewed, creation_time=now, expiration=now + timedelta(days=3), hit_question_xml=question.xml, qualification_requirements=[], max_assignments=1, assignment_pending_count=0, assignment_available_count=1, assignment_completed_count=0, description="Description", keywords="Keywords", reward=USDCent(5), title="Title", question_id=question.id, hit_type_id=None, ) ) @pytest.fixture def hit_in_db( hit_type: HitType, amt_hit_type_id, amt_hit_id, question: HitQuestion, hit: Hit ) -> Hit: """ Returns a hit that exists in our db, but does not in amazon (the amt ids are random). The mtwerk_hittype and mtwerk_question records will also exist (in the db) """ question = HQM.get_or_create(question) hit_type.amt_hit_type_id = amt_hit_type_id HTM.create(hit_type) hit.hit_type_id = hit_type.id hit.amt_hit_id = amt_hit_id hit.question_id = question.id HM.create(hit) return hit @pytest.fixture def assignment_stub(hit: Hit, amt_assignment_id, amt_worker_id): now = datetime.now(tz=timezone.utc) return AssignmentStub( amt_assignment_id=amt_assignment_id, amt_hit_id=hit.amt_hit_id, amt_worker_id=amt_worker_id, status=AssignmentStatus.Submitted, modified_at=now, created_at=now, ) @pytest.fixture def assignment_factory(hit: Hit): def inner(amt_worker_id: str = None): now = datetime.now(tz=timezone.utc) amt_assignment_id = generate_amt_id() amt_worker_id = amt_worker_id or generate_amt_id() return Assignment( amt_assignment_id=amt_assignment_id, amt_hit_id=hit.amt_hit_id, amt_worker_id=amt_worker_id, status=AssignmentStatus.Submitted, modified_at=now, created_at=now, accept_time=now, auto_approval_time=now, submit_time=now, ) return inner @pytest.fixture def assignment_in_db_factory(assignment_factory): def inner(hit_id: int, amt_worker_id: Optional[str] = None): a = assignment_factory(amt_worker_id=amt_worker_id) a.hit_id = hit_id AM.create_stub(a) AM.update_answer(a) return a return inner @pytest.fixture def assignment_stub_in_db(hit_in_db, assignment_stub) -> AssignmentStub: """ Returns an AssignmentStub that exists in our db, but does not in amazon (the amt ids are random). The mtwerk_hit, mtwerk_hittype, and mtwerk_question records will also exist (in the db) """ assignment_stub.hit_id = hit_in_db.id AM.create_stub(assignment_stub) return assignment_stub @pytest.fixture def amt_response_metadata(): req_id = str(uuid4()) return { "RequestId": req_id, "HTTPStatusCode": 200, "HTTPHeaders": { "x-amzn-requestid": req_id, "content-type": "application/x-amz-json-1.1", "content-length": "46", "date": "Wed, 15 Oct 2025 02:16:16 GMT", }, "RetryAttempts": 0, } @pytest.fixture def create_hit_type_response( amt_hit_type_id, amt_response_metadata ) -> CreateHITTypeResponseTypeDef: return { "HITTypeId": amt_hit_type_id, "ResponseMetadata": amt_response_metadata, } @pytest.fixture def create_hit_with_hit_type_response( amt_hit_type_id, amt_hit_id, amt_response_metadata ) -> CreateHITWithHITTypeResponseTypeDef: amt_group_id = generate_amt_id(length=30) return { "HIT": { "HITId": amt_hit_id, "HITTypeId": amt_hit_type_id, "HITGroupId": amt_group_id, "CreationTime": datetime(2025, 10, 14, 20, 22, tzinfo=tzlocal()), "Title": "Test", "Description": "test", "Question": '\n\n https://jamesbillings67.com/work/\n 1200\n ', "HITStatus": "Assignable", "MaxAssignments": 1, "Reward": "0.05", "AutoApprovalDelayInSeconds": 2_592_000, "Expiration": datetime(2025, 10, 14, 20, 24, 3, tzinfo=tzlocal()), "AssignmentDurationInSeconds": 123, "QualificationRequirements": [], "HITReviewStatus": "NotReviewed", "NumberOfAssignmentsPending": 0, "NumberOfAssignmentsAvailable": 1, "NumberOfAssignmentsCompleted": 0, }, "ResponseMetadata": amt_response_metadata, } @pytest.fixture def get_hit_response( amt_hit_type_id, amt_hit_id, amt_response_metadata ) -> GetHITResponseTypeDef: amt_group_id = generate_amt_id(length=30) return { "HIT": { "HITId": amt_hit_id, "HITTypeId": amt_hit_type_id, "HITGroupId": amt_group_id, "CreationTime": datetime(2025, 10, 13, 23, 0, 3, tzinfo=tzlocal()), "Title": "Awesome Surveys!", "Description": "Give us your opinion", "Question": '\n\n https://jamesbillings67.com/work/\n 1200\n ', "Keywords": "market,research,amazing", "HITStatus": "Assignable", "MaxAssignments": 1, "Reward": "0.05", "AutoApprovalDelayInSeconds": 604_800, "Expiration": datetime(2025, 10, 27, 23, 0, 3, tzinfo=tzlocal()), "AssignmentDurationInSeconds": 5_400, "QualificationRequirements": [], "HITReviewStatus": "NotReviewed", "NumberOfAssignmentsPending": 0, "NumberOfAssignmentsAvailable": 1, "NumberOfAssignmentsCompleted": 0, }, "ResponseMetadata": amt_response_metadata, } @pytest.fixture def get_hit_response_reviewing(get_hit_response): res = copy.deepcopy(get_hit_response) res["HIT"]["NumberOfAssignmentsAvailable"] = 0 res["HIT"]["NumberOfAssignmentsCompleted"] = 1 res["HIT"]["HITStatus"] = "Reviewing" return res @pytest.fixture def get_assignment_response( amt_hit_type_id, amt_hit_id, amt_assignment_id, amt_worker_id, get_hit_response, amt_response_metadata, tsid, ) -> GetAssignmentResponseTypeDef: hit_response = get_hit_response["HIT"] local_now = datetime.now(tz=tzlocal()) return { "Assignment": { "AssignmentId": amt_assignment_id, "WorkerId": amt_worker_id, "HITId": amt_hit_id, "AssignmentStatus": "Submitted", "AutoApprovalTime": local_now + timedelta(days=7), "AcceptTime": local_now - timedelta(minutes=10), "SubmitTime": local_now, "Deadline": local_now + timedelta(minutes=90), "Answer": '\n' '\n ' "\n amt_worker_id\n " f" {amt_worker_id}\n \n \n " " amt_assignment_id\n " f" {amt_assignment_id}\n \n \n " f" tsid\n {tsid}\n " " \n", "RequesterFeedback": "Good work", }, "HIT": hit_response, "ResponseMetadata": amt_response_metadata, } @pytest.fixture def get_assignment_response_no_tsid( get_assignment_response, amt_worker_id, amt_assignment_id ): res = copy.deepcopy(get_assignment_response) res["Assignment"]["Answer"] = ( '\n' '\n ' "\n amt_worker_id\n " f" {amt_worker_id}\n \n \n " " amt_assignment_id\n " f" {amt_assignment_id}\n \n " # f"\n tsid\n {tsid}\n \n" f"" ) return res @pytest.fixture def get_assignment_response_approved( get_assignment_response: GetAssignmentResponseTypeDef, ): def inner(feedback: str = APPROVAL_MESSAGE) -> GetAssignmentResponseTypeDef: res = copy.deepcopy(get_assignment_response) res["Assignment"]["AssignmentStatus"] = "Approved" res["Assignment"]["RequesterFeedback"] = feedback res["Assignment"]["ApprovalTime"] = res["Assignment"]["SubmitTime"] return res return inner @pytest.fixture def get_assignment_response_rejected( get_assignment_response: GetAssignmentResponseTypeDef, ): def inner(reject_reason: str = "reject reason") -> GetAssignmentResponseTypeDef: res = copy.deepcopy(get_assignment_response) res["Assignment"]["AssignmentStatus"] = "Rejected" res["Assignment"]["RequesterFeedback"] = reject_reason res["Assignment"]["RejectionTime"] = res["Assignment"]["SubmitTime"] return res return inner @pytest.fixture def get_assignment_response_rejected_no_tsid( get_assignment_response_no_tsid: GetAssignmentResponseTypeDef, ): def inner(reject_reason: str = "reject reason") -> GetAssignmentResponseTypeDef: res = copy.deepcopy(get_assignment_response_no_tsid) res["Assignment"]["AssignmentStatus"] = "Rejected" res["Assignment"]["RequesterFeedback"] = reject_reason res["Assignment"]["RejectionTime"] = res["Assignment"]["SubmitTime"] return res return inner @pytest.fixture def get_assignment_response_approved_no_tsid( get_assignment_response_no_tsid: GetAssignmentResponseTypeDef, ): res = copy.deepcopy(get_assignment_response_no_tsid) res["Assignment"]["AssignmentStatus"] = "Approved" res["Assignment"]["RequesterFeedback"] = NO_WORK_APPROVAL_MESSAGE res["Assignment"]["ApprovalTime"] = res["Assignment"]["SubmitTime"] return res