import gzip import json import unittest from datetime import datetime, timedelta from http import HTTPStatus from unittest.mock import Mock import pytest from requests import Response from slz_appreciationengine_scrapper.const import ( AE_RATE_LIMIT_MESSAGE, RATE_LIMIT_HOUR_HEADER, RATE_LIMIT_MINUTE_HEADER, RATE_LIMIT_RESET_HEADER, ) from slz_appreciationengine_scrapper.content_status_service import ContentStatusService from slz_appreciationengine_scrapper.dsp.appreciationengine.clients import PaginateClientPartial from slz_appreciationengine_scrapper.dsp.util import wait_for_refresh from slz_appreciationengine_scrapper.entities import Job @pytest.mark.parametrize( 'status_code, headers, result, sleep_called, response_content', [ (HTTPStatus.OK, {}, False, 0, {}), (HTTPStatus.BAD_REQUEST, {}, False, 0, {}), (HTTPStatus.TOO_MANY_REQUESTS, {}, False, 0, {}), ( HTTPStatus.TOO_MANY_REQUESTS, { RATE_LIMIT_HOUR_HEADER: 500, RATE_LIMIT_MINUTE_HEADER: 0, RATE_LIMIT_RESET_HEADER: 15, }, False, 0, {} ), ( HTTPStatus.TOO_MANY_REQUESTS, { RATE_LIMIT_HOUR_HEADER: 0, RATE_LIMIT_MINUTE_HEADER: 0, RATE_LIMIT_RESET_HEADER: 15, }, False, 0, {} ), ( HTTPStatus.TOO_MANY_REQUESTS, { RATE_LIMIT_HOUR_HEADER: 500, RATE_LIMIT_MINUTE_HEADER: 0, RATE_LIMIT_RESET_HEADER: 4, }, True, 1, {} ), ( HTTPStatus.FORBIDDEN, { RATE_LIMIT_HOUR_HEADER: 500, RATE_LIMIT_MINUTE_HEADER: 0, RATE_LIMIT_RESET_HEADER: 15, }, True, 1, { 'error': AE_RATE_LIMIT_MESSAGE } ), ( HTTPStatus.BAD_REQUEST, { RATE_LIMIT_HOUR_HEADER: 500, RATE_LIMIT_MINUTE_HEADER: 0, RATE_LIMIT_RESET_HEADER: 15, }, False, 0, { 'error': AE_RATE_LIMIT_MESSAGE } ), ( HTTPStatus.FORBIDDEN, { RATE_LIMIT_HOUR_HEADER: 500, RATE_LIMIT_MINUTE_HEADER: 0, RATE_LIMIT_RESET_HEADER: 15, }, True, 1, { 'message': 'test message', 'error': AE_RATE_LIMIT_MESSAGE } ), ] ) @unittest.mock.patch('time.sleep') def test_check_should_wait_and_retry( sleep_mock, status_code, headers, result, sleep_called, response_content, logger_test, ): resp_mock = unittest.mock.MagicMock(spec=Response) resp_mock.status_code = status_code resp_mock.headers = headers resp_mock.content = bytes(json.dumps(response_content), encoding='utf8') resp_mock.url = 'http://test.url' actual = wait_for_refresh( logger_test, resp_mock, max_waiting_seconds=5, ) assert actual == result assert sleep_mock.call_count == sleep_called def resp_too_many_requests(): resp_mock = unittest.mock.MagicMock(spec=Response) resp_mock.ok = False resp_mock.url = '' resp_mock.content = '' resp_mock.headers = { RATE_LIMIT_HOUR_HEADER: 1000, RATE_LIMIT_MINUTE_HEADER: 0, RATE_LIMIT_RESET_HEADER: 15, } resp_mock.status_code = HTTPStatus.TOO_MANY_REQUESTS return resp_mock def resp_mock_membersextended(): data = [ { 'totalSize': 1000, 'items': [ { 'Extended': { 'lastUpdated': '2021-03-08 17:00:09' } }, { 'Extended': { 'lastUpdated': '2021-03-08 19:00:08' } }, ] }, { 'totalSize': 1000, 'items': [ { 'Extended': { 'lastUpdated': '2021-03-08 17:00:09' } }, { 'Extended': { 'lastUpdated': '2021-03-08 19:00:08' } }, ] }, { 'totalSize': 1000, 'items': [ { 'Extended': { 'lastUpdated': '2021-03-08 20:00:07' } }, { 'Extended': { 'lastUpdated': '2021-03-08 21:51:17' } }, ] }, { 'totalSize': 1, 'items': [ { 'Extended': { 'lastUpdated': '2021-03-08 21:51:17' } }, { 'Extended': { 'lastUpdated': '2021-03-08 21:51:18' } }, ] }, ] for dt in data: resp_mock = unittest.mock.MagicMock(spec=Response) resp_mock.ok = True resp_mock.url = '' resp_mock.content = bytes(json.dumps(dt), encoding='utf8') resp_mock.headers = {} resp_mock.status_code = 200 yield resp_mock @pytest.mark.integration @unittest.mock.patch('slz_appreciationengine_scrapper.dsp.appreciationengine.clients.get_secret') @unittest.mock.patch('requests.get') @unittest.mock.patch( 'slz_appreciationengine_scrapper.dsp.appreciationengine.clients.datetime', wraps=datetime, now=lambda *args, **kwargs: datetime(year=2021, month=6, day=5, hour=1, minute=1, second=1) ) @unittest.mock.patch('time.sleep') def test_downloading_paginate_retry_when_limit_reached( sleep_mock, dt_mock, requests_get, get_secret, db, params, ae_mapping, s3_client ): for bucket in ['bucket-archive-quarantine', 'bucket-decompressed-quarantine']: s3_client.create_bucket(Bucket=bucket) uow_dict = { 'uow_id': 'appreciationengine-20201104-sme-membersextended-v1', 'dsp': 'appreciationengine', 'report_type': 'membersextended', 'version': 'v1', 'report_date': '2020-11-04', 'licensor': 'sme', 'extension': 'json', 'context': 'CenturyMediaRecords', } # Client initialization logger = unittest.mock.Mock() timer = unittest.mock.Mock() client = PaginateClientPartial(logger=logger) # Mock secrets get_secret.return_value = json.dumps({ "Sony Music - Century Media Records": "token72739d3", }) params.scrapper.max_retries = 3 client.configure(params) job = Job.from_dict(uow_dict) # Mock response from source API expected_file_content = ''.join( [ '{"Extended": {"lastUpdated": "2021-03-08 17:00:09"}}\n', '{"Extended": {"lastUpdated": "2021-03-08 19:00:08"}}\n', '{"Extended": {"lastUpdated": "2021-03-08 20:00:07"}}\n', '{"Extended": {"lastUpdated": "2021-03-08 21:51:17"}}\n', '{"Extended": {"lastUpdated": "2021-03-08 21:51:17"}}\n', '{"Extended": {"lastUpdated": "2021-03-08 21:51:18"}}\n', ] ) mocks = [*resp_mock_membersextended()] # adding some 429s in between of requests mocks.insert(1, resp_too_many_requests()) mocks.insert(3, resp_too_many_requests()) requests_get.return_value.__enter__.side_effect = mocks with unittest.mock.patch( 'slz_appreciationengine_scrapper.dsp.appreciationengine.ae_io.AEPaginateStringIOPartial.CHUNK_TIMEDELTA', timedelta(hours=24) ): content_status_service = ContentStatusService(logger=logger, db_conn=db) client.download( job, chunk_size=1, timer=timer, timeslot=None, content_status_service=content_status_service ) # read data from mocked S3 buckets path = 'appreciationengine/membersextended/v1/report_date=2020-11-04/report_licensor=sme' actual_decompressed = s3_client.get_object( Bucket='bucket-decompressed-quarantine', Key=f'{path}/CenturyMediaRecords_20201104_20210605010101.json', )['Body'].read() actual_compressed = s3_client.get_object( Bucket='bucket-archive-quarantine', Key=f'{path}/CenturyMediaRecords_20201104_20210605010101.json.gz', )['Body'].read() assert expected_file_content == actual_decompressed.decode('utf8') assert expected_file_content == gzip.decompress(actual_compressed).decode('utf8') # number of retries assert sleep_mock.call_count == 7