import orjson as json from datetime import datetime from http import HTTPStatus from typing import Dict, List, Tuple import boto3 import pytest from moto import mock_sqs from pytest_mock import MockerFixture from server import config from server.client.clients.apollo_api import ApolloApiClient @pytest.mark.parametrize( "body,user_id,expected_status,expected_result,empty_url", ( ({}, "user_1", HTTPStatus.BAD_REQUEST, None, False), ({"id": "track_1"}, "user_1", HTTPStatus.BAD_REQUEST, None, False), ({"type": "track"}, "user_1", HTTPStatus.BAD_REQUEST, None, False), ({"id": "track_1", "type": "track"}, "", HTTPStatus.BAD_REQUEST, None, False), ({"id": "track_1", "type": "track"}, "user_1", HTTPStatus.SERVICE_UNAVAILABLE, None, True), ( {"id": "tr_1", "type": "track"}, "u_1", HTTPStatus.ACCEPTED, {"created_at": "2022-04-10 12:15:03", "dsp_id": 1, "instance_id": "tr_1", "type_id": 1, "user_id": "u_1"}, False, ), ( {"id": "pl_5", "type": "playlist", "dsp": "spotify"}, "u_3", HTTPStatus.ACCEPTED, {"created_at": "2022-04-10 12:15:03", "dsp_id": 1, "instance_id": "pl_5", "type_id": 2, "user_id": "u_3"}, False, ), ( {"id": "pl_6", "type": "playlist", "dsp": "apple"}, "u_7", HTTPStatus.ACCEPTED, {"created_at": "2022-04-10 12:15:03", "dsp_id": 2, "instance_id": "pl_6", "type_id": 2, "user_id": "u_7"}, False, ), ), ) async def test_post_visits(body, user_id, expected_status, expected_result, empty_url, auth, client, mocker): config.USERS_VISITS_MESSAGE_QUEUE_URL = "" if empty_url else "test_sqs" mocked_datetime = mocker.patch("server.scenarios.users.visits.datetime") mocked_datetime.utcnow.return_value = datetime(2022, 4, 10, 12, 15, 3) mocked_boto = mock_sqs() mocked_boto.start() sqs_client = boto3.client("sqs", region_name=config.AWS_DEFAULT_REGION) sqs_client.create_queue(QueueName=config.USERS_VISITS_MESSAGE_QUEUE_URL) response = await client.post( "/api/users/visits/", json=body, headers={**auth, **({"X-User-Id": user_id} if user_id else {})} ) assert response.status == expected_status if expected_status == HTTPStatus.ACCEPTED: sqs_message = sqs_client.receive_message(QueueUrl=config.USERS_VISITS_MESSAGE_QUEUE_URL, VisibilityTimeout=900) sqs_message_list = [] while sqs_message.get("Messages"): sqs_message_list.append(json.loads(sqs_message["Messages"][0]["Body"])) sqs_message = sqs_client.receive_message( QueueUrl=config.USERS_VISITS_MESSAGE_QUEUE_URL, VisibilityTimeout=900 ) assert sqs_message_list == [expected_result] mocked_boto.stop() def get_visits(start: int = 1, count: int = 6) -> List[dict]: return [ {"instance_id": f"id_{i}", "count": 30 - i * 3, "dsp": "spotify", "last_visit_at": f"2022-05-03 15:1{i}:51"} for i in range(start, count + 1) ] def get_dsp_tracks(*args) -> List[dict]: return [ { "id": f"id_{i}", "vendor": "spotify", "isrc": f"isrc_{i}", "name": f"Tr{i}", "release_date": f"2022-01-0{i}", "image": f"img_{i}", "album_ids": [f"al_{i}"], **( { "artists": [ {"id": f"ar_{i}-{j}", "name": f"Ar{i}{j}", "uri": f"uri_{i}{j}"} for j in range(1, min(i, 3)) ] } if i > 2 else {} ), } for i in args ] def generate_spotify_tracks_response(track_list: List[Dict]) -> List[Dict]: return [ { "id": item.get("id"), "external_ids": {"isrc": item["isrc"]}, "name": item.get("name"), "album": { "id": item["album_ids"][0], "release_date": item["release_date"], "images": [{"width": 640, "height": 640, "url": item["image"]}], }, **({"artists": item["artists"]} if "artists" in item else {}), } for item in track_list ] def get_is_sony(*args) -> List[str]: return [f"id_{i}" for i in args] def get_playlists(*args) -> dict: country_code_list = ("us", "gb", "ca", "it", "worldwide") return { "playlists": [ { "playlist_id": f"id_{i}", "name": f"Pl{i}", "country_code": country_code_list[i % 5], "is_personalized": i % 3 == 0, "link": f"spotify:playlist:id_{i}", "image_url": f"img_{i}", "owner": { "account_id": f"ac_{i}", "account_name": f"Ac{i}", "category_name": f"Cat{i}", "category_id": i % 6, "country_code": country_code_list[i % 3], }, } for i in args ] } @pytest.mark.parametrize( ( "params,visits_data,playlist_data,track_data_tuple,expected_status,expected_result,called_count" ), ( ({}, [], {}, ([], []), HTTPStatus.BAD_REQUEST, None, (0, 0, 0)), ({"type": "track"}, [], {}, ([], []), HTTPStatus.OK, [], (1, 0, 0)), ( {"type": "track"}, get_visits(count=5), {}, (get_dsp_tracks(2, 4, 5), {"sme": (1, 2, 5), "awal": (3,)}), HTTPStatus.OK, [ { "count": 27, "data": { "artist_name": "", "artists": [], "is_sony": True, "distributed_by": "sme", "name": None, "song_id": "id_1", "image_url": None, }, "dsp": "spotify", "id": "id_1", "last_visit_at": "2022-05-03 15:11:51", }, { "count": 24, "data": { "artist_name": "", "artists": [], "image_url": "img_2", "is_sony": True, "distributed_by": "sme", "name": "Tr2", "song_id": "id_2", }, "dsp": "spotify", "id": "id_2", "last_visit_at": "2022-05-03 15:12:51", }, { "count": 21, "data": { "artist_name": "", "artists": [], "is_sony": False, "distributed_by": "awal", "song_id": "id_3", "image_url": None, "name": None, }, "dsp": "spotify", "id": "id_3", "last_visit_at": "2022-05-03 15:13:51", }, { "count": 18, "data": { "artist_name": "Ar41, Ar42", "artists": [ {"id": "ar_4-1", "name": "Ar41", "uri": "uri_41"}, {"id": "ar_4-2", "name": "Ar42", "uri": "uri_42"}, ], "image_url": "img_4", "is_sony": False, "distributed_by": None, "name": "Tr4", "song_id": "id_4", }, "dsp": "spotify", "id": "id_4", "last_visit_at": "2022-05-03 15:14:51", }, { "count": 15, "data": { "artists": [ {"id": "ar_5-1", "name": "Ar51", "uri": "uri_51"}, {"id": "ar_5-2", "name": "Ar52", "uri": "uri_52"}, ], "artist_name": "Ar51, Ar52", "image_url": "img_5", "is_sony": True, "distributed_by": "sme", "name": "Tr5", "song_id": "id_5", }, "dsp": "spotify", "id": "id_5", "last_visit_at": "2022-05-03 15:15:51", }, ], (1, 1, 1), ), ( {"type": "track", "dsp": "spotify", "limit": 3}, get_visits(count=4), {}, (get_dsp_tracks(1, 3, 4, 5), {"sme": (2, 4), "awal": (1,), "theorchard": (3,)}), HTTPStatus.OK, [ { "count": 27, "data": { "artist_name": "", "artists": [], "is_sony": False, "distributed_by": "awal", "name": "Tr1", "song_id": "id_1", "image_url": "img_1", }, "dsp": "spotify", "id": "id_1", "last_visit_at": "2022-05-03 15:11:51", }, { "count": 24, "data": { "artist_name": "", "artists": [], "image_url": None, "is_sony": True, "distributed_by": "sme", "name": None, "song_id": "id_2", }, "dsp": "spotify", "id": "id_2", "last_visit_at": "2022-05-03 15:12:51", }, { "count": 21, "data": { "artist_name": "Ar31, Ar32", "artists": [ {"id": "ar_3-1", "name": "Ar31", "uri": "uri_31"}, {"id": "ar_3-2", "name": "Ar32", "uri": "uri_32"}, ], "is_sony": False, "distributed_by": "theorchard", "song_id": "id_3", "name": "Tr3", "image_url": "img_3", }, "dsp": "spotify", "id": "id_3", "last_visit_at": "2022-05-03 15:13:51", }, { "count": 18, "data": { "artist_name": "Ar41, Ar42", "artists": [ {"id": "ar_4-1", "name": "Ar41", "uri": "uri_41"}, {"id": "ar_4-2", "name": "Ar42", "uri": "uri_42"}, ], "image_url": "img_4", "is_sony": True, "distributed_by": "sme", "name": "Tr4", "song_id": "id_4", }, "dsp": "spotify", "id": "id_4", "last_visit_at": "2022-05-03 15:14:51", }, ], (1, 1, 1), ), ( {"type": "playlist"}, get_visits(start=2), get_playlists(2, 4, 5), ([], []), HTTPStatus.OK, [ { "count": 24, "data": { "country_code": "ca", "image_url": "img_2", "is_personalized": False, "link": "spotify:playlist:id_2", "name": "Pl2", "owner": { "account_id": "ac_2", "account_name": "Ac2", "category_id": 2, "category_name": "Cat2", "country_code": "ca", }, "playlist_id": "id_2", }, "dsp": "spotify", "id": "id_2", "last_visit_at": "2022-05-03 15:12:51", }, {"count": 21, "dsp": "spotify", "id": "id_3", "last_visit_at": "2022-05-03 15:13:51"}, { "count": 18, "data": { "country_code": "worldwide", "image_url": "img_4", "is_personalized": False, "link": "spotify:playlist:id_4", "name": "Pl4", "owner": { "account_id": "ac_4", "account_name": "Ac4", "category_id": 4, "category_name": "Cat4", "country_code": "gb", }, "playlist_id": "id_4", }, "dsp": "spotify", "id": "id_4", "last_visit_at": "2022-05-03 15:14:51", }, { "count": 15, "data": { "country_code": "us", "image_url": "img_5", "is_personalized": False, "link": "spotify:playlist:id_5", "name": "Pl5", "owner": { "account_id": "ac_5", "account_name": "Ac5", "category_id": 5, "category_name": "Cat5", "country_code": "ca", }, "playlist_id": "id_5", }, "dsp": "spotify", "id": "id_5", "last_visit_at": "2022-05-03 15:15:51", }, {"count": 12, "dsp": "spotify", "id": "id_6", "last_visit_at": "2022-05-03 15:16:51"}, ], (2, 0, 0), ), ), ) async def test_get_visits( params: dict, visits_data: List[dict], playlist_data: List[dict], track_data_tuple: Tuple[List[dict], Dict[str, str]], expected_status: int, expected_result: List[dict], called_count: Tuple[int, int, int], mocker: MockerFixture, auth: dict, client, ): dsp_tracks_data, distributors_data = track_data_tuple async def apollo_send(url: str, *args, **kwargs): if url == "api/users/visits/": return visits_data elif url == "api/playlists/spotify/": return playlist_data mocked_apollo_send = mocker.patch.object(ApolloApiClient, "_send_request", side_effect=apollo_send) mocked_get_tracks = mocker.patch( "server.scenarios.users.visits.get_tracks", return_value=generate_spotify_tracks_response(dsp_tracks_data) ) mocked_check_dist = mocker.patch( "server.scenarios.users.visits.get_tracks_distributors_map", return_value={ f"id_{_id}": dist for dist, tracks in distributors_data.items() for _id in tracks } if distributors_data else {}, ) response = await client.get("/api/users/visits/", params=params, headers={"X-User-Id": "us_1", **auth}) assert response.status == expected_status assert ( mocked_apollo_send.call_count, mocked_get_tracks.call_count, mocked_check_dist.call_count ) == called_count if expected_status == HTTPStatus.OK: response = await response.json() assert response == expected_result