diff --git a/.github/workflows/api-ci-review.yml b/.github/workflows/api-ci-review.yml index 06ae6223d..737f29816 100644 --- a/.github/workflows/api-ci-review.yml +++ b/.github/workflows/api-ci-review.yml @@ -283,3 +283,7 @@ jobs: ROUTE_53_ZONE_FOR_PUBLIC_DATA: ${{ secrets.ROUTE_53_ZONE_FOR_PUBLIC_DATA_REVIEW }} DOMAIN: ${{ secrets.DOMAIN_REVIEW }} SCORER_SERVER_SSM_ARN: ${{ secrets.SCORER_SERVER_SSM_ARN_REVIEW }} + + ETHEREUM_MODEL_ENDPOINT: ${{ secrets.ETHEREUM_MODEL_ENDPOINT_REVIEW }} + NFT_MODEL_ENDPOINT: ${{ secrets.NFT_MODEL_ENDPOINT_REVIEW }} + ZKSYNC_MODEL_ENDPOINT: ${{ secrets.ZKSYNC_MODEL_ENDPOINT_REVIEW }} diff --git a/.github/workflows/api-promote-prod.yml b/.github/workflows/api-promote-prod.yml index 5aa321537..2c5364936 100644 --- a/.github/workflows/api-promote-prod.yml +++ b/.github/workflows/api-promote-prod.yml @@ -355,3 +355,7 @@ jobs: SCORER_RDS_SECRET_ARN: ${{ secrets.SCORER_RDS_SECRET_ARN }} PAGERDUTY_INTEGRATION_ENDPOINT: ${{ secrets.PAGERDUTY_INTEGRATION_ENDPOINT }} + + ETHEREUM_MODEL_ENDPOINT: ${{ secrets.ETHEREUM_MODEL_ENDPOINT }} + NFT_MODEL_ENDPOINT: ${{ secrets.NFT_MODEL_ENDPOINT }} + ZKSYNC_MODEL_ENDPOINT: ${{ secrets.ZKSYNC_MODEL_ENDPOINT }} diff --git a/.github/workflows/api-promote-staging.yml b/.github/workflows/api-promote-staging.yml index 3636ac354..45af48725 100644 --- a/.github/workflows/api-promote-staging.yml +++ b/.github/workflows/api-promote-staging.yml @@ -360,3 +360,7 @@ jobs: REDASH_MAIL_USERNAME: ${{ secrets.REDASH_MAIL_USERNAME }} REDASH_SECRET_KEY: ${{ secrets.REDASH_SECRET_KEY }} SCORER_RDS_SECRET_ARN: ${{ secrets.SCORER_RDS_SECRET_ARN }} + + ETHEREUM_MODEL_ENDPOINT: ${{ secrets.ETHEREUM_MODEL_ENDPOINT_STAGING }} + NFT_MODEL_ENDPOINT: ${{ secrets.NFT_MODEL_ENDPOINT_STAGING }} + ZKSYNC_MODEL_ENDPOINT: ${{ secrets.ZKSYNC_MODEL_ENDPOINT_STAGING }} diff --git a/api/aws_lambdas/passport/analysis_GET.py b/api/aws_lambdas/passport/analysis_GET.py index 5a2fdd815..3fa033f85 100644 --- a/api/aws_lambdas/passport/analysis_GET.py +++ b/api/aws_lambdas/passport/analysis_GET.py @@ -2,15 +2,11 @@ This module provides a handler to manage API requests in AWS Lambda. """ -import asyncio +from aws_lambdas.utils import with_api_request_exception_handling # isort:skip from django.db import close_old_connections from passport.api import handle_get_analysis -from aws_lambdas.utils import ( - with_api_request_exception_handling, -) - @with_api_request_exception_handling def _handler(event, _context, _request, _user_account, _body): @@ -18,16 +14,10 @@ def _handler(event, _context, _request, _user_account, _body): Handles the incoming events and translates them into Django's context. """ - print(f"EVENT: \n***************\n{event}\n***************\n") address = event["path"].split("/")[-1] + model_list = event.get("queryStringParameters", {}).get("model_list", "") - loop = asyncio.get_event_loop() - # DynamoDB resource defined above is attached to this loop: - # if you use asyncio.run instead - # you will encounter "Event loop closed" exception - analysis = loop.run_until_complete(handle_get_analysis(address)) - - return analysis + return handle_get_analysis(address, model_list) def handler(*args, **kwargs): diff --git a/api/aws_lambdas/passport/tests/test_passport_analysis_lambda.py b/api/aws_lambdas/passport/tests/test_passport_analysis_lambda.py index b1ed0ac41..4af0a5c3b 100644 --- a/api/aws_lambdas/passport/tests/test_passport_analysis_lambda.py +++ b/api/aws_lambdas/passport/tests/test_passport_analysis_lambda.py @@ -1,9 +1,10 @@ # pylint: disable=no-value-for-parameter # pyright: reportGeneralTypeIssues=false import json +from unittest.mock import Mock import pytest -from passport.test.test_analysis import MockLambdaClient +from passport.api import MODEL_ENDPOINTS from aws_lambdas.scorer_api_passport.tests.helpers import MockContext @@ -14,7 +15,42 @@ address = "0x06e3c221011767FE816D0B8f5B16253E43e4Af7D" -def test_successful_analysis( +def mock_post_response(url, json, headers): + # Create a mock response object + mock_response = Mock() + mock_response.status_code = 200 + + # Define different responses based on the model (which we can infer from the URL) + responses = { + "ethereum": { + "data": {"human_probability": 75}, + "metadata": {"model_name": "ethereum_activity", "version": "1.0"}, + }, + "nft": { + "data": {"human_probability": 85}, + "metadata": {"model_name": "social_media", "version": "2.0"}, + }, + "zksync": { + "data": {"human_probability": 95}, + "metadata": {"model_name": "transaction_history", "version": "1.5"}, + }, + } + + # Determine which model is being requested + for model, endpoint in MODEL_ENDPOINTS.items(): + if endpoint in url: + response_data = responses.get(model, {"data": {"human_probability": 0}}) + break + else: + response_data = {"error": "Unknown model"} + + # Set the json method of the mock response + mock_response.json = lambda: response_data + + return mock_response + + +def test_successful_analysis_eth( scorer_api_key, mocker, ): @@ -25,47 +61,66 @@ def test_successful_analysis( event = { "headers": {"x-api-key": scorer_api_key}, "path": f"/passport/analysis/{address}", - "queryStringParameters": {}, + "queryStringParameters": {"model_list": "ethereum"}, "isBase64Encoded": False, } - mocker.patch( - "passport.api.get_lambda_client", - MockLambdaClient, - ) - response = _handler(event, MockContext()) + with mocker.patch("requests.post", side_effect=mock_post_response): + response = _handler(event, MockContext()) - # TODO: geri uncomment this - # assert response is not None - # assert response["statusCode"] == 200 + assert response is not None + assert response["statusCode"] == 200 - # body = json.loads(response["body"]) + body = json.loads(response["body"]) - # assert body["address"] == address - # assert body["details"]["models"]["ethereum_activity"]["score"] == 50 + assert body["address"] == address + assert body["details"]["models"]["ethereum"]["score"] == 75 -def test_bad_auth( +def test_successful_analysis_zksync( + scorer_api_key, mocker, ): """ Tests that analysis can be requested successfully. """ + event = { + "headers": {"x-api-key": scorer_api_key}, + "path": f"/passport/analysis/{address}", + "queryStringParameters": {"model_list": "zksync"}, + "isBase64Encoded": False, + } + with mocker.patch("requests.post", side_effect=mock_post_response): + response = _handler(event, MockContext()) + + assert response is not None + assert response["statusCode"] == 200 + + body = json.loads(response["body"]) + + assert body["address"] == address + assert body["details"]["models"]["zksync"]["score"] == 95 + + +def test_bad_auth( + mocker, +): + """ + Tests that error is thrown if auth is bad + """ + event = { "headers": {"x-api-key": "bad_auth"}, "path": f"/passport/analysis/{address}", "queryStringParameters": {}, "isBase64Encoded": False, } - mocker.patch( - "passport.api.get_lambda_client", - MockLambdaClient, - ) + response = _handler(event, MockContext()) assert response is not None - assert response["statusCode"] == 403 - assert json.loads(response["body"])["error"] == "Unauthorized" + assert response["statusCode"] == 401 + assert json.loads(response["body"])["error"] == "Invalid API Key." def test_bad_address( @@ -73,7 +128,7 @@ def test_bad_address( mocker, ): """ - Tests that analysis can be requested successfully. + Tests that error is thrown is addrss is bad """ bad_address = address[:-1] + "d" @@ -84,12 +139,34 @@ def test_bad_address( "queryStringParameters": {}, "isBase64Encoded": False, } - mocker.patch( - "passport.api.get_lambda_client", - MockLambdaClient, - ) response = _handler(event, MockContext()) assert response is not None assert response["statusCode"] == 400 - assert json.loads(response["body"])["error"] == "Invalid address" + assert json.loads(response["body"])["error"] == "Invalid address." + + +def test_bad_model( + scorer_api_key, + mocker, +): + """ + Tests that error is thrown if unsupported model is requested + """ + + model = "bad_model" + event = { + "headers": {"x-api-key": scorer_api_key}, + "path": f"/passport/analysis/{address}", + "queryStringParameters": {"model_list": model}, + "isBase64Encoded": False, + } + + response = _handler(event, MockContext()) + + assert response is not None + assert response["statusCode"] == 400 + assert ( + json.loads(response["body"])["error"] + == f"Invalid model name(s): {', '.join([model])}. Must be one of {', '.join(MODEL_ENDPOINTS.keys())}" + ) diff --git a/api/aws_lambdas/submit_passport/tests/test_submit_passport_lambda.py b/api/aws_lambdas/submit_passport/tests/test_submit_passport_lambda.py index f1363d8b8..919aba0d9 100644 --- a/api/aws_lambdas/submit_passport/tests/test_submit_passport_lambda.py +++ b/api/aws_lambdas/submit_passport/tests/test_submit_passport_lambda.py @@ -4,9 +4,10 @@ import pytest from account.models import AccountAPIKeyAnalytics +from registry.test.test_passport_submission import mock_passport + from aws_lambdas.scorer_api_passport.tests.helpers import MockContext from aws_lambdas.scorer_api_passport.utils import strip_event -from registry.test.test_passport_submission import mock_passport from ..submit_passport import _handler @@ -179,7 +180,7 @@ def test_unsucessfull_auth(scorer_account, scorer_community_with_binary_scorer): response = _handler(event, MockContext()) assert response is not None - assert response["statusCode"] == 403 + assert response["statusCode"] == 401 def test_strip_event(): @@ -362,7 +363,7 @@ def test_failed_authentication_and_analytics_logging( response = _handler(event, MockContext()) assert response is not None - assert response["statusCode"] == 403 + assert response["statusCode"] == 401 # Check for the proper analytics entry analytics_entry_count = AccountAPIKeyAnalytics.objects.order_by( @@ -423,7 +424,7 @@ def test_bad_scorer_id_and_analytics_logging( response = _handler(event, MockContext()) assert response is not None - assert response["statusCode"] == 400 + assert response["statusCode"] == 404 # Check for the proper analytics entry analytics_entry = AccountAPIKeyAnalytics.objects.order_by("-created_at")[0] assert analytics_entry.path == event["path"] diff --git a/api/aws_lambdas/tests/test_with_api_request_exception_handling.py b/api/aws_lambdas/tests/test_with_api_request_exception_handling.py index 6ae1fd6bd..3781f6b0e 100644 --- a/api/aws_lambdas/tests/test_with_api_request_exception_handling.py +++ b/api/aws_lambdas/tests/test_with_api_request_exception_handling.py @@ -1,14 +1,14 @@ import json import pytest +from registry.test.test_passport_submission import mock_passport + +from aws_lambdas.exceptions import InvalidRequest from aws_lambdas.scorer_api_passport.tests.helpers import MockContext from aws_lambdas.submit_passport.tests.test_submit_passport_lambda import ( make_test_event, ) from aws_lambdas.utils import with_api_request_exception_handling -from registry.test.test_passport_submission import mock_passport -from aws_lambdas.exceptions import InvalidRequest - pytestmark = pytest.mark.django_db @@ -31,7 +31,6 @@ def test_with_api_request_exception_handling_success( passport_holder_addresses, mocker, ): - with mocker.patch( "registry.atasks.aget_passport", return_value=mock_passport, @@ -39,7 +38,6 @@ def test_with_api_request_exception_handling_success( with mocker.patch( "registry.atasks.validate_credential", side_effect=[[], [], []] ): - wrapped_func = with_api_request_exception_handling(func_to_test) address = passport_holder_addresses[0]["address"].lower() @@ -59,7 +57,6 @@ def test_with_api_request_exception_handling_bad_api_key( passport_holder_addresses, mocker, ): - with mocker.patch( "registry.atasks.aget_passport", return_value=mock_passport, @@ -67,7 +64,6 @@ def test_with_api_request_exception_handling_bad_api_key( with mocker.patch( "registry.atasks.validate_credential", side_effect=[[], [], []] ): - wrapped_func = with_api_request_exception_handling(func_to_test) address = passport_holder_addresses[0]["address"].lower() @@ -77,8 +73,8 @@ def test_with_api_request_exception_handling_bad_api_key( ret = wrapped_func(test_event, MockContext()) - assert ret["statusCode"] == 403 - assert ret["body"] == '{"error": "Unauthorized"}' + assert ret["statusCode"] == 401 + assert ret["body"] == '{"error": "Invalid API Key."}' def test_with_api_request_exception_handling_bad_request( @@ -87,7 +83,6 @@ def test_with_api_request_exception_handling_bad_request( passport_holder_addresses, mocker, ): - with mocker.patch( "registry.atasks.aget_passport", return_value=mock_passport, @@ -95,7 +90,6 @@ def test_with_api_request_exception_handling_bad_request( with mocker.patch( "registry.atasks.validate_credential", side_effect=[[], [], []] ): - wrapped_func = with_api_request_exception_handling(func_to_test_bad_request) address = passport_holder_addresses[0]["address"].lower() @@ -115,7 +109,6 @@ def test_with_api_request_exception_handling_unexpected_error( passport_holder_addresses, mocker, ): - with mocker.patch( "registry.atasks.aget_passport", return_value=mock_passport, @@ -123,7 +116,6 @@ def test_with_api_request_exception_handling_unexpected_error( with mocker.patch( "registry.atasks.validate_credential", side_effect=[[], [], []] ): - wrapped_func = with_api_request_exception_handling( func_to_test_unexpected_error ) @@ -142,7 +134,6 @@ def test_with_api_request_exception_handling_unexpected_error( def test_with_api_request_exception_handling_bad_event( mocker, ): - with mocker.patch( "registry.atasks.aget_passport", return_value=mock_passport, @@ -150,7 +141,6 @@ def test_with_api_request_exception_handling_bad_event( with mocker.patch( "registry.atasks.validate_credential", side_effect=[[], [], []] ): - wrapped_func = with_api_request_exception_handling( func_to_test_unexpected_error ) diff --git a/api/aws_lambdas/utils.py b/api/aws_lambdas/utils.py index bbcead3b0..f15c9f52d 100644 --- a/api/aws_lambdas/utils.py +++ b/api/aws_lambdas/utils.py @@ -8,19 +8,19 @@ from functools import wraps from traceback import print_exc from typing import Any, Dict, Tuple + import boto3 from botocore.exceptions import ClientError from django.db import ( - InterfaceError, DataError, - OperationalError, IntegrityError, + InterfaceError, InternalError, - ProgrammingError, NotSupportedError, + OperationalError, + ProgrammingError, ) - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "scorer.settings") os.environ.setdefault("CERAMIC_CACHE_SCORER_ID", "1") @@ -75,11 +75,15 @@ def load_secrets(): logger = logging.getLogger(__name__) -from aws_lambdas.exceptions import InvalidRequest # noqa: E402 from django.http import HttpRequest # noqa: E402 from django_ratelimit.exceptions import Ratelimited # noqa: E402 +from ninja_extra.exceptions import APIException from ninja_jwt.exceptions import InvalidToken # noqa: E402 -from registry.api.utils import ApiKey, check_rate_limit, save_api_key_analytics # noqa: E402 +from registry.api.utils import ( + ApiKey, + check_rate_limit, + save_api_key_analytics, +) from registry.exceptions import ( # noqa: E402 InvalidAddressException, NotFoundApiException, @@ -87,6 +91,8 @@ def load_secrets(): ) from structlog.contextvars import bind_contextvars # noqa: E402 +from aws_lambdas.exceptions import InvalidRequest # noqa: E402 + RESPONSE_HEADERS = { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", @@ -150,26 +156,31 @@ def wrapper(event, context, *args): return func(event, context, *args) except Exception as e: - error_descriptions: Dict[Any, Tuple[int, str]] = { - Unauthorized: (403, "Unauthorized"), - InvalidToken: (403, "Invalid token"), - InvalidRequest: (400, "Bad request"), - Ratelimited: ( - 429, - "You have been rate limited. Please try again later.", - ), - InterfaceError: (500, "DB Error: InterfaceError"), - DataError: (500, "DB Error: DataError"), - OperationalError: (500, "DB Error: OperationalError"), - IntegrityError: (500, "DB Error: IntegrityError"), - InternalError: (500, "DB Error: InternalError"), - ProgrammingError: (500, "DB Error: ProgrammingError"), - NotSupportedError: (500, "DB Error: NotSupportedError"), - } + if isinstance(e, APIException): + status = e.status_code + message = str(e.detail) + else: + error_descriptions: Dict[Any, Tuple[int, str]] = { + Unauthorized: (403, "Unauthorized"), + InvalidToken: (403, "Invalid token"), + InvalidRequest: (400, "Bad request"), + Ratelimited: ( + 429, + "You have been rate limited. Please try again later.", + ), + InterfaceError: (500, "DB Error: InterfaceError"), + DataError: (500, "DB Error: DataError"), + OperationalError: (500, "DB Error: OperationalError"), + IntegrityError: (500, "DB Error: IntegrityError"), + InternalError: (500, "DB Error: InternalError"), + ProgrammingError: (500, "DB Error: ProgrammingError"), + NotSupportedError: (500, "DB Error: NotSupportedError"), + } + + status, message = error_descriptions.get( + type(e), (400, "An error has occurred") + ) - status, message = error_descriptions.get( - type(e), (400, "An error has occurred") - ) bind_contextvars( statusCode=status, statusCategory="4XX" if (status >= 400 and status < 500) else "5XX", @@ -232,28 +243,33 @@ def wrapper(_event, context): ) except Exception as e: error_msg = str(e) - error_descriptions: Dict[Any, Tuple[int, str]] = { - Unauthorized: (403, "Unauthorized"), - InvalidToken: (403, "Invalid token"), - InvalidRequest: (400, "Bad request"), - InvalidAddressException: (400, "Invalid address"), - NotFoundApiException: (400, "Bad request"), - Ratelimited: ( - 429, - "You have been rate limited. Please try again later.", - ), - InterfaceError: (500, "DB Error: InterfaceError"), - DataError: (500, "DB Error: DataError"), - OperationalError: (500, "DB Error: OperationalError"), - IntegrityError: (500, "DB Error: IntegrityError"), - InternalError: (500, "DB Error: InternalError"), - ProgrammingError: (500, "DB Error: ProgrammingError"), - NotSupportedError: (500, "DB Error: NotSupportedError"), - } - status, message = error_descriptions.get( - type(e), (500, "An error has occurred") - ) + if isinstance(e, APIException): + status = e.status_code + message = str(e.detail) + else: + error_descriptions: Dict[Any, Tuple[int, str]] = { + Unauthorized: (403, "Unauthorized"), + InvalidToken: (403, "Invalid token"), + InvalidRequest: (400, "Bad request"), + InvalidAddressException: (400, "Invalid address"), + NotFoundApiException: (400, "Bad request"), + Ratelimited: ( + 429, + "You have been rate limited. Please try again later.", + ), + InterfaceError: (500, "DB Error: InterfaceError"), + DataError: (500, "DB Error: DataError"), + OperationalError: (500, "DB Error: OperationalError"), + IntegrityError: (500, "DB Error: IntegrityError"), + InternalError: (500, "DB Error: InternalError"), + ProgrammingError: (500, "DB Error: ProgrammingError"), + NotSupportedError: (500, "DB Error: NotSupportedError"), + } + + status, message = error_descriptions.get( + type(e), (500, "An error has occurred") + ) bind_contextvars( statusCode=status, diff --git a/api/passport/api.py b/api/passport/api.py index f1243deb6..81d2cc3fb 100644 --- a/api/passport/api.py +++ b/api/passport/api.py @@ -1,10 +1,7 @@ -import asyncio -import json -from typing import List +from typing import Optional -import aiohttp import api_logging as logging -import boto3 +import requests from django.conf import settings from eth_utils.address import to_checksum_address from ninja import Schema @@ -26,27 +23,22 @@ ) -lambda_client = None +class EthereumModel(Schema): + score: int -def get_lambda_client(): - global lambda_client - if lambda_client is None: - lambda_client = boto3.client( - "lambda", - aws_access_key_id=settings.S3_DATA_AWS_SECRET_KEY_ID, - aws_secret_access_key=settings.S3_DATA_AWS_SECRET_ACCESS_KEY, - region_name="us-west-2", - ) - return lambda_client +class NFTModel(Schema): + score: int -class EthereumActivityModel(Schema): +class ZkSyncModel(Schema): score: int class PassportAnalysisDetailsModels(Schema): - ethereum_activity: EthereumActivityModel + ethereum: Optional[EthereumModel] + nft: Optional[NFTModel] + zksync: Optional[ZkSyncModel] class PassportAnalysisDetails(Schema): @@ -62,6 +54,11 @@ class ErrorMessageResponse(Schema): detail: str +class BadModelNameError(APIException): + status_code = 400 + default_detail = "Invalid model names" + + class PassportAnalysisError(APIException): status_code = 500 default_detail = "Error retrieving Passport analysis" @@ -79,73 +76,69 @@ class PassportAnalysisError(APIException): description="Retrieve Passport analysis for an Ethereum address, currently consisting of the ETH activity model humanity score (0-100, higher is more likely human).", tags=["Passport Analysis"], ) -async def get_analysis( - request, address: str, model_list: str -) -> PassportAnalysisResponse: - split_model_list = [model.trim for model in model_list.split(",")] - return await handle_get_analysis(address, split_model_list) +def get_analysis(request, address: str, model_list: str) -> PassportAnalysisResponse: + return handle_get_analysis(address, model_list) # TODO: this should be loaded from settings & env vars MODEL_ENDPOINTS = { - "eth-model": "http://core-alb.private.gitcoin.co/eth-stamp-v2-predict", - "nft-model": "http://core-alb.private.gitcoin.co/nft-model-predict", - "zksync-model": "http://core-alb.private.gitcoin.co/zksync-model-v2-predict", + "ethereum": settings.ETHEREUM_MODEL_ENDPOINT, + "nft": settings.NFT_MODEL_ENDPOINT, + "zksync": settings.ZKSYNC_MODEL_ENDPOINT, } -async def handle_get_analysis( - address: str, model_list: List[str] = ["eth-model", "nft-model", "zksync-model"] -) -> PassportAnalysisResponse: +def handle_get_analysis(address: str, model_list: str) -> PassportAnalysisResponse: + models = [model.strip() for model in model_list.split(",")] + if not is_valid_address(address): raise InvalidAddressException() + if len(models) > 1: + raise BadModelNameError( + detail="Currently, only one model name can be provided at a time" + ) + + if len(models) == 0 or models[0] == "": + raise BadModelNameError(detail="No model names provided") + + bad_models = set(models) - set(MODEL_ENDPOINTS.keys()) + if bad_models: + raise BadModelNameError( + detail=f"Invalid model name(s): {', '.join(bad_models)}. Must be one of {', '.join(MODEL_ENDPOINTS.keys())}" + ) + checksum_address = to_checksum_address(address) try: + scores = {} + + model = models[0] + + response = requests.post( + MODEL_ENDPOINTS[model], + json={"address": checksum_address}, + headers={"Content-Type": "application/json", "Accept": "application/json"}, + ) + + response.raise_for_status() + + response_body = response.json() + + print("Response body:", response_body) + + score = response_body.get("data", {}).get("human_probability", 0) + + scores[model] = score - async def post(session, url, data): - headers = {"Content-Type": "application/json"} - print("individual post request", url, data) - async with session.post( - url, data=json.dumps(data), headers=headers - ) as response: - return await response.text() - - async def fetch_all(requests): - async with aiohttp.ClientSession() as session: - tasks = [] - for url, data in requests: - task = asyncio.ensure_future(post(session, url, data)) - tasks.append(task) - responses = await asyncio.gather(*tasks) - return responses - - requests = [] - for model_name in model_list: - if model_name in MODEL_ENDPOINTS: - requests.append( - (f"{MODEL_ENDPOINTS[model_name]}/", {"address": checksum_address}) - ) - else: - # TODO: raise 400 cause bad model name - pass - - print("Requests:", requests) - # Run the event loop - responses = await fetch_all(requests) - print("Responses:", responses) - - # Print the responses - for response in responses: - print(response) + model_results = PassportAnalysisDetailsModels() + for model_name, score in scores.items(): + setattr(model_results, model_name, {"score": score}) return PassportAnalysisResponse( address=address, details=PassportAnalysisDetails( - models=PassportAnalysisDetailsModels( - ethereum_activity=EthereumActivityModel(score=0) - ) + models=model_results, ), ) diff --git a/api/passport/test/test_analysis.py b/api/passport/test/test_analysis.py index 399f6aa6c..78e4bb2d9 100644 --- a/api/passport/test/test_analysis.py +++ b/api/passport/test/test_analysis.py @@ -1,12 +1,16 @@ import json -from unittest.mock import patch +from unittest.mock import Mock, patch +import pytest from account.models import Account, AccountAPIKey from django.conf import settings from django.contrib.auth import get_user_model from django.test import Client, TestCase +from passport.api import MODEL_ENDPOINTS from web3 import Web3 +pytestmark = pytest.mark.django_db + web3 = Web3() web3.eth.account.enable_unaudited_hdwallet_features() my_mnemonic = settings.TEST_MNEMONIC @@ -26,17 +30,35 @@ ) -class MockPayload: - def read(self): - return mock_response_data.encode("utf-8") +def mock_post_response(url, json, headers): + # Create a mock response object + mock_response = Mock() + mock_response.status_code = 200 + + # Define different responses based on the model (which we can infer from the URL) + responses = { + "ethereum": { + "data": {"human_probability": 75}, + "metadata": {"model_name": "ethereum_activity", "version": "1.0"}, + }, + } + # Determine which model is being requested + for model, endpoint in MODEL_ENDPOINTS.items(): + if endpoint in url: + response_data = responses.get(model, {"data": {"human_probability": 0}}) + break + else: + response_data = {"error": "Unknown model"} -class MockLambdaClient: - def invoke(self, FunctionName, InvocationType, Payload): - return {"Payload": MockPayload()} + # Set the json method of the mock response + mock_response.json = lambda: response_data + return mock_response -class PassportAnalysisTestCase(TestCase): + +@pytest.mark.django_db +class TestPassportAnalysis(TestCase): def setUp(self): user = User.objects.create(username="admin", password="12345") @@ -51,52 +73,39 @@ def setUp(self): ) self.headers = {"HTTP_X-API-Key": f"{api_key}"} + self.client = Client() - def test_get_analysis_request(self): + @patch("requests.post", side_effect=mock_post_response) + def test_get_analysis_request(self, mock_post): """Test successfully requesting analysis through the API.""" - client = Client() - - # TODO: geri uncomment this - # with patch( - # "passport.api.get_lambda_client", - # MockLambdaClient, - # ): - # analysis_response = client.get( - # "/passport/analysis/0x06e3c221011767FE816D0B8f5B16253E43e4Af7D", - # content_type="application/json", - # **self.headers, - # ) - - # self.assertEqual(analysis_response.status_code, 200) - # self.assertEqual( - # analysis_response.json(), - # { - # "address": "0x06e3c221011767FE816D0B8f5B16253E43e4Af7D", - # "details": {"models": {"ethereum_activity": {"score": 50}}}, - # }, - # ) + analysis_response = self.client.get( + "/passport/analysis/0x06e3c221011767FE816D0B8f5B16253E43e4Af7D?model_list=ethereum", + content_type="application/json", + **self.headers, + ) + self.assertEqual(analysis_response.status_code, 200) + + response_data = analysis_response.json() + self.assertEqual( + response_data["address"], "0x06e3c221011767FE816D0B8f5B16253E43e4Af7D" + ) + self.assertEqual(response_data["details"]["models"]["ethereum"], {"score": 75}) def test_bad_auth(self): headers = {"HTTP_X-API-Key": "bad_auth"} - client = Client() - - # TODO: geri uncomment this - # analysis_response = client.get( - # "/passport/analysis/0x06e3c221011767FE816D0B8f5B16253E43e4Af7D", - # content_type="application/json", - # **headers, - # ) + analysis_response = self.client.get( + "/passport/analysis/0x06e3c221011767FE816D0B8f5B16253E43e4Af7D?model_list=ethereum", + content_type="application/json", + **headers, + ) - # self.assertEqual(analysis_response.status_code, 401) + self.assertEqual(analysis_response.status_code, 401) def test_bad_address(self): - client = Client() - - # TODO: geri uncomment this - # analysis_response = client.get( - # "/passport/analysis/0x06e3c221011767FE816D0B8f5B16253E43e4Af7d", - # content_type="application/json", - # **self.headers, - # ) - # self.assertEqual(analysis_response.status_code, 400) + analysis_response = self.client.get( + "/passport/analysis/0x06e3c221011767FE816D0B8f5B16253E43e4Af7d?model_list=ethereum", + content_type="application/json", + **self.headers, + ) + self.assertEqual(analysis_response.status_code, 400) diff --git a/api/scorer/settings/base.py b/api/scorer/settings/base.py index f395204ba..b23cb9281 100644 --- a/api/scorer/settings/base.py +++ b/api/scorer/settings/base.py @@ -416,6 +416,14 @@ PASSPORT_PUBLIC_URL = env("PASSPORT_PUBLIC_URL", default="http://localhost:80") +ETHEREUM_MODEL_ENDPOINT = env( + "ETHEREUM_MODEL_ENDPOINT", default="http://localhost:80/ethereum" +) +NFT_MODEL_ENDPOINT = env("NFT_MODEL_ENDPOINT", default="http://localhost:80/nft") +ZKSYNC_MODEL_ENDPOINT = env( + "ZKSYNC_MODEL_ENDPOINT", default="http://localhost:80/zksync" +) + # Deprecated in favour of TRUSTED_IAM_ISSUERS which will store a list of trusted issuers TRUSTED_IAM_ISSUER = env( "TRUSTED_IAM_ISSUER", default="did:key:GlMY_1zkc0i11O-wMBWbSiUfIkZiXzFLlAQ89pdfyBA" diff --git a/infra/aws/index.ts b/infra/aws/index.ts index e38e97ba7..e6ef5a166 100644 --- a/infra/aws/index.ts +++ b/infra/aws/index.ts @@ -64,6 +64,11 @@ const redashMailPassword = pulumi.secret( `${process.env["REDASH_MAIL_PASSWORD"]}` ); +const ethereumModelEndpoint = `${process.env["ETHEREUM_MODEL_ENDPOINT"]}`; +const nftModelEndpoint = `${process.env["NFT_MODEL_ENDPOINT"]}`; +const zksyncModelEndpoint = `${process.env["ZKSYNC_MODEL_ENDPOINT"]}`; + + const pagerDutyIntegrationEndpoint = `${process.env["PAGERDUTY_INTEGRATION_ENDPOINT"]}`; const coreInfraStack = new pulumi.StackReference(`gitcoin/core-infra/${stack}`); @@ -1485,6 +1490,21 @@ buildHttpLambdaFn( buildHttpLambdaFn( { ...lambdaSettings, + environment: [ + ...lambdaSettings.environment, + { + name: "ETHEREUM_MODEL_ENDPOINT", + value: ethereumModelEndpoint, + }, + { + name: "NFT_MODEL_ENDPOINT", + value: nftModelEndpoint, + }, + { + name: "ZKSYNC_MODEL_ENDPOINT", + value: zksyncModelEndpoint, + }, + ], name: "passport-analysis-GET-0", memorySize: 256, dockerCmd: ["aws_lambdas.passport.analysis_GET.handler"],