"""Functional tests for Vector jobs endpoints.""" import hashlib import json from typing import Any, cast from unittest.mock import MagicMock import pytest from flask.testing import FlaskClient from moto import mock_aws from mypy_boto3_s3.literals import StorageClassType from owsrequest.utils import mock_request from vectororder import config from vectororder.connectors import s3 from vectororder.constants import header, jobs from vectororder.models.schemas import DeliveryType, EncodingOrderType from tests.test_utils.db import ( create_delivery_history, create_delivery_spec, create_job, create_product, create_project, create_store, create_track, create_vendor, ) @pytest.mark.parametrize( "payload, err_field, err_field_idx", ( ({"job_ids": [1, -42], "action": jobs.JOBS_ACTIONS[0]}, "job_ids", 1), ({"job_ids": [1], "action": "unknown_action"}, "action", None), ({"action": jobs.JOBS_ACTIONS[0]}, "job_ids", None), ({"job_ids": [1]}, "action", None), ), ) def test_func_jobs_status_validation_is_configured( payload: dict[str, list[int]], err_field: str, err_field_idx: list[int], fixture_client: FlaskClient, ) -> None: """Test jobs status endpoint validation is properly configured.""" # Not a comprehensive validation test, but just to cover main pitfalls. headers = { header.CONTENT_TYPE: header.CONTENT_TYPE_JSON_VAL, "Orchard-User-Id": "oa:123", } result = fixture_client.put( "/jobs/status", headers=headers, data=json.dumps(payload) ) res_data = json.loads(result.data) assert result.status_code == 400 if err_field_idx: assert res_data["message"][err_field][str(err_field_idx)] else: assert res_data["message"][err_field] def test_get_job_eligibility_complete_album( fixture_client: FlaskClient, init_db_tables: None, mock_raise_for_status: MagicMock, ) -> None: """Test get job eligibility endpoint complete_album.""" job_id = 456 upc = 199066946312 product_id = 123 store_id = 286 create_vendor() create_project() create_job( encoding_queue_detail_id=job_id, encoding_queue_detail_status="ready_to_encode", upc=upc, meta_update="N", dms_master_master_id=store_id, ) create_product( release_id=product_id, upc=upc, distribution_format_id=1, not_for_distribution="N", ) create_track( id_=789, release_id=product_id, track_type="music", offer_type="all", ) mock_request.get( "ows-carveouts", "/carveout/projection/199066946312/dms/286/distro/1", { "empty_reason": None, "territories": ["US"], }, ) create_store( customer_master_master_id=286, hd_only="Y", ) mock_request.get( "ows-carveouts", "/carveout/projection/199066946312/dms/286/distro/1", { "empty_reason": None, "territories": ["US"], }, ) mock_request.get( "ows-assets", "/validators/product/123/desired-bit-depth/24", status=200, ) result = fixture_client.get(f"/jobs/{job_id}/eligibility") assert result.status_code == 200 assert result.get_json() == { "is_eligible": True, "reason": None, } def test_get_job_eligibility_metadata_update( fixture_client: FlaskClient, init_db_tables: None, ) -> None: """Test get job eligibility endpoint metadata_update.""" job_id = 456 product_id = 123 upc = 199066946312 store_id = 286 create_vendor() create_project() create_job( encoding_queue_detail_id=job_id, encoding_queue_detail_status="ready_to_encode", upc=upc, meta_update="Y", dms_master_master_id=store_id, ) create_product( release_id=product_id, upc=upc, distribution_format_id=1, not_for_distribution="N", ) create_delivery_history( upc=upc, customer_master_master_id=store_id, ) create_delivery_spec( dms_id=store_id, order_type=EncodingOrderType.RELEASE, metadata_update="Y" ) result = fixture_client.get(f"/jobs/{job_id}/eligibility") assert result.status_code == 200 assert result.get_json() == { "is_eligible": True, "reason": None, } @pytest.mark.parametrize( "delivery_type, meta_update, expect_assets, delivery_history_required", [ (DeliveryType.COMPLETE_ALBUM, "N", True, False), (DeliveryType.METADATA_UPDATE, "Y", False, True), ], ) def test_get_product_eligibility_by_upc( fixture_client: FlaskClient, init_db_tables: None, delivery_type: DeliveryType, meta_update: str, expect_assets: bool, delivery_history_required: bool, mock_raise_for_status: MagicMock, ) -> None: """Test store delivery eligibility for various delivery types.""" upc = 199066946312 product_id = 123 store_id = 286 create_vendor() create_project() create_product( release_id=product_id, upc=upc, distribution_format_id=1, not_for_distribution="N", ) if delivery_type == DeliveryType.COMPLETE_ALBUM: create_track( id_=789, release_id=product_id, track_type="music", offer_type="all", ) if delivery_history_required: create_delivery_history( upc=upc, customer_master_master_id=store_id, ) mock_request.get( "ows-carveouts", f"/carveout/projection/{upc}/dms/{store_id}/distro/1", { "empty_reason": None, "territories": ["US"], }, ) create_store( customer_master_master_id=store_id, hd_only="Y", ) if expect_assets: mock_request.get( "ows-assets", f"/validators/product/{product_id}/desired-bit-depth/24", status=200, ) # Pass the enum's value in the query string, not the enum itself response = fixture_client.get( f"/products/{upc}/eligibility", query_string={"store_id": store_id, "delivery_type": delivery_type.value}, ) assert response.status_code == 200 assert response.get_json() == { "is_eligible": True, "reason": None, } @mock_aws def test_get_job_delivery_xml_file_exists( fixture_client: FlaskClient, init_db_tables: None, ) -> None: """Test get job delivery xml endpoint with delivery XML present.""" job_id = 456 upc = 199066946312 store_id = 286 create_job( encoding_queue_detail_id=job_id, encoding_queue_detail_status="delivered", upc=upc, meta_update="N", dms_master_master_id=store_id, ) s3client = s3.get_s3_client() s3client.create_bucket(Bucket=config.DELIVERY_XML_S3_BUCKET_NAME) delivered_xml_file_path = jobs.XML_FILE_PATH_PATTERN.format( hashlib.md5(str(job_id).encode()).hexdigest() ) s3client.put_object( Bucket=config.DELIVERY_XML_S3_BUCKET_NAME, Key=delivered_xml_file_path, Body="fake-body", ) result = fixture_client.get(f"/jobs/{job_id}/delivery-file-download") assert result.status_code == 200 presigned_link = result.get_json()["presigned_link"] assert config.DELIVERY_XML_S3_BUCKET_NAME in presigned_link assert delivered_xml_file_path in presigned_link @pytest.mark.parametrize( "test_description, job_id, pre_job_kwargs, s3_object, expected_code, expected_message", [ ( "jod does not exists", 1, None, None, "JobNotFoundError", "Job with id 1 not found", ), ( "job exists but no xml file", 456, { "encoding_queue_detail_id": 456, "encoding_queue_detail_status": "delivered", "upc": 199066946312, "meta_update": "N", "dms_master_master_id": 286, }, None, "DeliveryFileNotFoundError", "Delivery XML file does not exist", ), ( "job exists, but not complete", 456, { "encoding_queue_detail_id": 456, "encoding_queue_detail_status": "new", "upc": 199066946312, "meta_update": "N", "dms_master_master_id": 286, }, None, "DeliveryFileNotFoundError", "Job status is new. XML is available only for completed statuses", ), ( "job exists and complete, but file is in GLACIER", 456, { "encoding_queue_detail_id": 456, "encoding_queue_detail_status": "delivered", "upc": 199066946312, "meta_update": "N", "dms_master_master_id": 286, }, { "StorageClass": "GLACIER", "Body": "fake-body", }, "DeliveryFileNotAvailableError", "Delivery XML file requires restoration", ), ], ) @mock_aws def test_get_job_delivery_xml_not_found( test_description: str, fixture_client: FlaskClient, init_db_tables: None, job_id: int, pre_job_kwargs: dict[str, Any], s3_object: dict[str, str], expected_code: str, expected_message: str, ) -> None: if pre_job_kwargs: create_job(**pre_job_kwargs) s3client = s3.get_s3_client() s3client.create_bucket(Bucket=config.DELIVERY_XML_S3_BUCKET_NAME) if s3_object: xml_path = jobs.XML_FILE_PATH_PATTERN.format( hashlib.md5(str(job_id).encode()).hexdigest() ) s3client.put_object( Bucket=config.DELIVERY_XML_S3_BUCKET_NAME, Key=xml_path, StorageClass=cast(StorageClassType, s3_object["StorageClass"]), Body=s3_object["Body"], ) result = fixture_client.get(f"/jobs/{job_id}/delivery-file-download") assert result.status_code == 404 assert result.get_json() == {"code": expected_code, "message": expected_message}