From 20590718ab0090ef9e419e607b4c3549da17a886 Mon Sep 17 00:00:00 2001 From: nutrina <8137830+nutrina@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:49:55 +0200 Subject: [PATCH] fix: remove '=' padding in pagination token (#741) * fix: remove '=' padding in pagination token * wip: testing v2 api * fix: handle failing tests and wrong value returned in in V2 api * fix: failing tests * fix: setting FF_V2_API to on before running the get_unmonitored_urls command * fix: add missing argument 'created_at' to http request for historic endpoint monitor * feat: adjust response time trhesholds for alarms * feat: consistent formatting of score & threshold with 5 decimal places * fix: return 404 message if score does not exist in historical endpoint * fix: adding missing file --------- Co-authored-by: Gerald Iakobinyi-Pich --- .github/workflows/ci.yml | 1 + .github/workflows/test_generic.yml | 1 + .../commands/get_unmonitored_urls_config.py | 2 +- api/registry/utils.py | 15 +- api/v2/api/__init__.py | 10 + api/v2/api/api_stamps.py | 108 ++++++---- api/v2/aws_lambdas/stamp_score_GET.py | 33 +-- .../aws_lambdas/tests/test_stamp_score_get.py | 8 +- api/v2/exceptions.py | 11 + api/v2/schema.py | 9 + api/v2/test/test_historical_score_endpoint.py | 26 +-- api/v2/test/test_passport_submission.py | 189 +++++++++++++++++- infra/aws/index.ts | 16 ++ infra/lib/scorer/loadBalancer.ts | 2 + 14 files changed, 335 insertions(+), 96 deletions(-) create mode 100644 api/v2/exceptions.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc72ed3c4..1bb69c5c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,6 +113,7 @@ jobs: REGISTRY_ADDRESS: ${{ env.REGISTRY_ADDRESS }} CERAMIC_CACHE_JWT_TOKEN: ${{ env.CERAMIC_CACHE_JWT_TOKEN }} CERAMIC_CACHE_ADDRESS: ${{ env.CERAMIC_CACHE_ADDRESS }} + FF_V2_API: on run: python manage.py get_unmonitored_urls --base-url https://api.scorer.gitcoin.co/ --base-url-xyz https://api.passport.xyz/ diff --git a/.github/workflows/test_generic.yml b/.github/workflows/test_generic.yml index bbc0935e0..a122812e3 100644 --- a/.github/workflows/test_generic.yml +++ b/.github/workflows/test_generic.yml @@ -110,6 +110,7 @@ jobs: REGISTRY_ADDRESS: ${{ env.REGISTRY_ADDRESS }} CERAMIC_CACHE_JWT_TOKEN: ${{ env.CERAMIC_CACHE_JWT_TOKEN }} CERAMIC_CACHE_ADDRESS: ${{ env.CERAMIC_CACHE_ADDRESS }} + FF_V2_API: on run: python manage.py get_unmonitored_urls --base-url https://api.scorer.gitcoin.co/ --base-url-xyz https://api.passport.xyz/ ${{ inputs.uptime-robot-monitor-dry-run }} diff --git a/api/registry/management/commands/get_unmonitored_urls_config.py b/api/registry/management/commands/get_unmonitored_urls_config.py index beccea467..2b241a020 100644 --- a/api/registry/management/commands/get_unmonitored_urls_config.py +++ b/api/registry/management/commands/get_unmonitored_urls_config.py @@ -183,7 +183,7 @@ def get_config(base_url: str, base_url_xyz: str) -> dict: "success_http_statues": [200], }, ("GET", "/v2/stamps/{scorer_id}/score/{address}/history"): { - "url": f"{base_url_xyz}v2/stamps/{REGISTRY_SCORER_ID}/score/{REGISTRY_ADDRESS}/history", + "url": f"{base_url_xyz}v2/stamps/{REGISTRY_SCORER_ID}/score/{REGISTRY_ADDRESS}/history?created_at=2024-12-01", "http_headers": {"X-API-Key": REGISTRY_API_KEY}, "success_http_statues": [200], }, diff --git a/api/registry/utils.py b/api/registry/utils.py index 07b1c1fb8..cb1df9007 100644 --- a/api/registry/utils.py +++ b/api/registry/utils.py @@ -141,13 +141,24 @@ def wrapped(request, *args, **kwargs): def encode_cursor(**kwargs) -> str: - encoded_bytes = base64.urlsafe_b64encode(json.dumps(dict(**kwargs)).encode("utf-8")) + encoded_bytes = base64.urlsafe_b64encode( + json.dumps(dict(**kwargs)).encode("utf-8") + # Remove the "=" padding ... + ).rstrip(b"=") return encoded_bytes +BASE64_PADDING = "====" + + def decode_cursor(token: str) -> dict: if token: - return json.loads(base64.urlsafe_b64decode(token).decode("utf-8")) + return json.loads( + base64.urlsafe_b64decode( + # We add back the "=" padding ... + token + BASE64_PADDING[: -(len(token) % 4)] + ).decode("utf-8") + ) return {} diff --git a/api/v2/api/__init__.py b/api/v2/api/__init__.py index c6a6041e9..101407344 100644 --- a/api/v2/api/__init__.py +++ b/api/v2/api/__init__.py @@ -2,6 +2,7 @@ from django_ratelimit.exceptions import Ratelimited from ninja_extra import NinjaExtraAPI +from ..exceptions import ScoreDoesNotExist from .api_models import * from .api_stamps import * from .router import api_router @@ -26,3 +27,12 @@ def service_unavailable(request, _): {"detail": "You have been rate limited!"}, status=429, ) + + +@api.exception_handler(ScoreDoesNotExist) +def score_not_found(request, exc): + return api.create_response( + request, + {"detail": exc.detail, "address": exc.address}, + status=exc.status_code + ) diff --git a/api/v2/api/api_stamps.py b/api/v2/api/api_stamps.py index 01dc21bbe..3cbe1af7f 100644 --- a/api/v2/api/api_stamps.py +++ b/api/v2/api/api_stamps.py @@ -1,4 +1,3 @@ -from datetime import datetime, time from decimal import Decimal from typing import Any, Dict, List from urllib.parse import urljoin @@ -13,11 +12,9 @@ from ceramic_cache.models import CeramicCache from registry.api.schema import ( CursorPaginatedStampCredentialResponse, - DetailedScoreResponse, ErrorMessageResponse, NoScoreResponse, StampDisplayResponse, - SubmitPassportPayload, ) from registry.api.utils import ( ApiKey, @@ -29,26 +26,28 @@ with_read_db, ) from registry.api.v1 import ( - ahandle_submit_passport, + aget_scorer_by_id, fetch_all_stamp_metadata, ) +from registry.atasks import ascore_passport from registry.exceptions import ( CreatedAtIsRequiredException, - CreatedAtMalFormedException, InternalServerErrorException, InvalidAddressException, InvalidAPIKeyPermissions, InvalidLimitException, api_get_object_or_404, ) -from registry.models import Event, Score +from registry.models import Event, Passport, Score from registry.utils import ( decode_cursor, encode_cursor, reverse_lazy_with_query, ) +from scorer_weighted.models import Scorer from v2.schema import V2ScoreResponse +from ..exceptions import ScoreDoesNotExist from .router import api_router METADATA_URL = urljoin(settings.PASSPORT_PUBLIC_URL, "stampMetadata.json") @@ -56,6 +55,66 @@ log = logging.getLogger(__name__) +async def handle_scoring(address: str, scorer_id: str, user_account): + address_lower = address.lower() + + if not is_valid_address(address_lower): + raise InvalidAddressException() + + # Get community object + user_community = await aget_scorer_by_id(scorer_id, user_account) + + scorer = await user_community.aget_scorer() + scorer_type = scorer.type + + # Create an empty passport instance, only needed to be able to create a pending Score + # The passport will be updated by the score_passport task + db_passport, _ = await Passport.objects.aupdate_or_create( + address=address_lower, + community=user_community, + ) + + score, _ = await Score.objects.select_related("passport").aget_or_create( + passport=db_passport, + defaults=dict(score=None, status=Score.Status.PROCESSING), + ) + + await ascore_passport(user_community, db_passport, address_lower, score) + await score.asave() + + raw_score = 0 + threshold = 20 + + if scorer_type == Scorer.Type.WEIGHTED: + raw_score = score.score + elif score.evidence and "rawScore": + raw_score = score.evidence.get("rawScore") + threshold = score.evidence.get("threshold") + + if raw_score is None: + raw_score = 0 + + if threshold is None: + threshold = 0 + + return V2ScoreResponse( + address=address_lower, + score=raw_score, + passing_score=(Decimal(raw_score) >= Decimal(threshold)), + threshold=threshold, + last_score_timestamp=( + score.last_score_timestamp.isoformat() + if score.last_score_timestamp + else None + ), + expiration_timestamp=( + score.expiration_date.isoformat() if score.expiration_date else None + ), + error=score.error, + stamp_scores=score.stamp_scores if score.stamp_scores is not None else {}, + ) + + @api_router.get( "/stamps/{scorer_id}/score/{address}", auth=aapi_key, @@ -74,35 +133,14 @@ async def a_submit_passport(request, scorer_id: int, address: str) -> V2ScoreResponse: check_rate_limit(request) try: - if not request.api_key.submit_passports: - raise InvalidAPIKeyPermissions() - - v1_score = await ahandle_submit_passport( - SubmitPassportPayload(address=address, scorer_id=str(scorer_id)), - request.auth, - ) - threshold = v1_score.evidence.threshold if v1_score.evidence else "20" - score = v1_score.evidence.rawScore if v1_score.evidence else v1_score.score - - return V2ScoreResponse( - address=v1_score.address, - score=score, - passing_score=( - Decimal(v1_score.score) >= Decimal(threshold) - if v1_score.score - else False - ), - threshold=threshold, - last_score_timestamp=v1_score.last_score_timestamp, - expiration_timestamp=v1_score.expiration_date, - error=v1_score.error, - stamp_scores=v1_score.stamp_scores, - ) + return await handle_scoring(address, str(scorer_id), request.auth) except APIException as e: raise e except Exception as e: log.exception("Error submitting passport: %s", e) - raise InternalServerErrorException("Unexpected error while submitting passport") + raise InternalServerErrorException( + "Unexpected error while submitting passport" + ) from e def extract_score_data(event_data: Dict[str, Any]) -> Dict[str, Any]: @@ -141,10 +179,10 @@ class Meta: "/stamps/{scorer_id}/score/{address}/history", auth=ApiKey(), response={ - 200: V2ScoreResponse | NoScoreResponse, + 200: V2ScoreResponse, 401: ErrorMessageResponse, 400: ErrorMessageResponse, - 404: ErrorMessageResponse, + 404: ErrorMessageResponse | NoScoreResponse, }, operation_id="v2_api_api_stamps_get_score_history", summary="Retrieve historical Stamp-based unique humanity score for a specified address", @@ -180,9 +218,7 @@ def get_score_history( score_event = filterset.qs.order_by("-created_at").first() if not score_event: - return NoScoreResponse( - address=address, status=f"No Score Found for {address} at {created_at}" - ) + raise ScoreDoesNotExist(address, f"No Score Found for {address} at {created_at}") # Extract and normalize score data from either format score_data = extract_score_data(score_event.data) diff --git a/api/v2/aws_lambdas/stamp_score_GET.py b/api/v2/aws_lambdas/stamp_score_GET.py index f06589c60..83b3a0eef 100644 --- a/api/v2/aws_lambdas/stamp_score_GET.py +++ b/api/v2/aws_lambdas/stamp_score_GET.py @@ -2,19 +2,14 @@ This module provides a handler to manage API requests in AWS Lambda. """ -from decimal import Decimal - from asgiref.sync import async_to_sync from django.db import close_old_connections from aws_lambdas.utils import ( with_api_request_exception_handling, ) -from registry.api.v1 import ( - SubmitPassportPayload, - ahandle_submit_passport, -) -from v2.schema import V2ScoreResponse + +from ..api.api_stamps import handle_scoring # Now this script or any imported module can use any part of Django it needs. # from myapp import models @@ -28,29 +23,7 @@ def _handler(event, _context, request, user_account, body): split_url = event["path"].split("/") address = split_url[-1] scorer_id = split_url[-3] - - v1_score = async_to_sync(ahandle_submit_passport)( - SubmitPassportPayload( - address=address, - scorer_id=scorer_id, - ), - user_account, - ) - - threshold = v1_score.evidence.threshold if v1_score.evidence else "20" - - return V2ScoreResponse( - address=v1_score.address, - score=v1_score.score, - passing_score=( - Decimal(v1_score.score) >= Decimal(threshold) if v1_score.score else False - ), - threshold=threshold, - last_score_timestamp=v1_score.last_score_timestamp, - expiration_timestamp=v1_score.expiration_date, - error=v1_score.error, - stamp_scores=v1_score.stamp_scores, - ) + return async_to_sync(handle_scoring)(address, scorer_id, user_account) def handler(*args, **kwargs): diff --git a/api/v2/aws_lambdas/tests/test_stamp_score_get.py b/api/v2/aws_lambdas/tests/test_stamp_score_get.py index e3b84cc7e..76015620a 100644 --- a/api/v2/aws_lambdas/tests/test_stamp_score_get.py +++ b/api/v2/aws_lambdas/tests/test_stamp_score_get.py @@ -109,9 +109,9 @@ def test_successful_authentication( body = json.loads(response["body"]) assert body["address"] == address - assert body["score"] == "0" + assert body["score"] == "0.93300" assert body["passing_score"] == False - assert body["threshold"] == "20.0" + assert body["threshold"] == "20.00000" assert body["error"] is None assert body["stamp_scores"] == {"Ens": "0.408", "Google": "0.525"} @@ -151,9 +151,9 @@ def test_successful_authentication_and_base64_encoded_body( body = json.loads(response["body"]) assert body["address"] == address - assert body["score"] == "0" + assert body["score"] == "0.93300" assert body["passing_score"] == False - assert body["threshold"] == "20.0" + assert body["threshold"] == "20.00000" assert body["error"] is None assert body["stamp_scores"] == {"Ens": "0.408", "Google": "0.525"} # We just check that something != None was recorded for the last timestamp diff --git a/api/v2/exceptions.py b/api/v2/exceptions.py new file mode 100644 index 000000000..d39f95999 --- /dev/null +++ b/api/v2/exceptions.py @@ -0,0 +1,11 @@ +from ninja_extra import status +from ninja_extra.exceptions import APIException + + +class ScoreDoesNotExist(APIException): + status_code = status.HTTP_404_NOT_FOUND + default_detail = "No score exists." + + def __init__(self, address: str, *args, **kwargs): + self.address = address + super().__init__(*args, **kwargs) diff --git a/api/v2/schema.py b/api/v2/schema.py index a0b8adf79..c82b14f4f 100644 --- a/api/v2/schema.py +++ b/api/v2/schema.py @@ -2,6 +2,7 @@ from typing import Dict, Optional from ninja import Schema +from pydantic import field_serializer class V2ScoreResponse(Schema): @@ -13,3 +14,11 @@ class V2ScoreResponse(Schema): threshold: Decimal error: Optional[str] stamp_scores: Optional[Dict[str, Decimal]] + + @field_serializer("score") + def serialize_score(self, score: Decimal, _info): + return format(score, ".5f") + + @field_serializer("threshold") + def serialize_threshold(self, threshold: Decimal, _info): + return format(threshold, ".5f") diff --git a/api/v2/test/test_historical_score_endpoint.py b/api/v2/test/test_historical_score_endpoint.py index 2302d414b..ee638d7f7 100644 --- a/api/v2/test/test_historical_score_endpoint.py +++ b/api/v2/test/test_historical_score_endpoint.py @@ -79,7 +79,7 @@ def test_get_historical_score_legacy_format( response_data = response.json() assert response.status_code == 200 - assert response_data["score"] == "309.5190000000000054014570595" + assert response_data["score"] == "309.51900" assert response_data["threshold"] == "100.00000" assert response_data["passing_score"] is True assert response_data["last_score_timestamp"] == None @@ -108,7 +108,7 @@ def test_get_historical_score_new_format( "evidence": { "type": "ThresholdScoreCheck", "success": False, - "rawScore": "5.459000000000000019095836024", + "rawScore": "5.45900", "threshold": "100.00000", }, "passport": 15, @@ -127,7 +127,7 @@ def test_get_historical_score_new_format( response_data = response.json() assert response.status_code == 200 - assert response_data["score"] == "5.459000000000000019095836024" + assert response_data["score"] == "5.45900" assert response_data["threshold"] == "100.00000" assert response_data["passing_score"] is False assert response_data["last_score_timestamp"] == "2024-10-25T19:16:14.023Z" @@ -161,8 +161,8 @@ def test_get_historical_score_missing_fields( response_data = response.json() assert response.status_code == 200 - assert response_data["score"] == "15" - assert response_data["threshold"] == "0" + assert response_data["score"] == "15.00000" + assert response_data["threshold"] == "0.00000" assert response_data["passing_score"] is True assert response_data["last_score_timestamp"] is None assert response_data["expiration_timestamp"] is None @@ -191,8 +191,8 @@ def test_get_historical_score_ne_evidence( response_data = response.json() assert response.status_code == 200 - assert response_data["score"] == "78.762" - assert response_data["threshold"] == "0" + assert response_data["score"] == "78.76200" + assert response_data["threshold"] == "0.00000" assert response_data["passing_score"] is True assert response_data["last_score_timestamp"] is None assert response_data["expiration_timestamp"] is None @@ -211,10 +211,10 @@ def test_get_historical_score_no_score_found( ) response_data = response.json() - assert response.status_code == 200 + assert response.status_code == 404 assert response_data["address"] == scorer_account.address assert ( - response_data["status"] + response_data["detail"] == f"No Score Found for {scorer_account.address} at 2023-01-01" ) @@ -230,9 +230,9 @@ def test_get_historical_score_invalid_date( HTTP_AUTHORIZATION="Token " + scorer_api_key, ) - assert response.status_code == 200 + assert response.status_code == 404 assert ( - response.json()["status"] + response.json()["detail"] == "No Score Found for 0xB81C935D01e734b3D8bb233F5c4E1D72DBC30f6c at invalid-date" ) @@ -257,7 +257,7 @@ def test_get_historical_score_valid_iso_date( HTTP_AUTHORIZATION="Token " + scorer_api_key, ) - assert response.status_code == 200 + assert response.status_code == 404 response_data = response.json() assert response_data["address"] == scorer_account.address - assert "No Score Found for" in response_data["status"] + assert "No Score Found for" in response_data["detail"] diff --git a/api/v2/test/test_passport_submission.py b/api/v2/test/test_passport_submission.py index 032e466a1..f1a4560da 100644 --- a/api/v2/test/test_passport_submission.py +++ b/api/v2/test/test_passport_submission.py @@ -14,7 +14,7 @@ from registry.models import Passport, Stamp from registry.tasks import score_passport from registry.utils import get_signing_message, verify_issuer -from scorer_weighted.models import BinaryWeightedScorer, Scorer +from scorer_weighted.models import BinaryWeightedScorer, Scorer, WeightedScorer web3 = Web3() web3.eth.account.enable_unaudited_hdwallet_features() @@ -439,9 +439,9 @@ def test_submitting_without_passport(self, aget_passport, validate_credential): assert response.status_code == 200 assert response.json() == { "address": self.account.address.lower(), - "score": None, + "score": "0.00000", "passing_score": False, - "threshold": "20", + "threshold": "20.00000", "last_score_timestamp": None, "expiration_timestamp": None, "error": "No Passport found for this address.", @@ -465,22 +465,22 @@ def test_submit_passport_multiple_times( expectedResponse = { "address": "0xb81c935d01e734b3d8bb233f5c4e1d72dbc30f6c", - "score": Decimal("0.9329999999999999960031971113"), + "score": Decimal("0.93300"), "passing_score": False, "last_score_timestamp": "2023-01-11T16:35:23.938006+00:00", "expiration_timestamp": mock_passport_expiration_date.isoformat(), - "threshold": "20", + "threshold": "20.00000", "error": None, "stamp_scores": {"Ens": "0.408", "Google": "0.525"}, } expected2ndResponse = { "address": "0xb81c935d01e734b3d8bb233f5c4e1d72dbc30f6c", - "score": Decimal("0.9329999999999999960031971113"), + "score": Decimal("0.93300"), "passing_score": False, "last_score_timestamp": "2023-01-11T16:35:23.938006+00:00", "expiration_timestamp": mock_passport_expiration_date.isoformat(), - "threshold": "20", + "threshold": "20.00000", "error": None, "stamp_scores": {"Ens": "0.408", "Google": "0.525"}, } @@ -548,12 +548,12 @@ def test_submit_passport_multiple_times( "registry.atasks.aget_passport", side_effect=[copy.deepcopy(mock_passport), copy.deepcopy(mock_passport)], ) - def test_submit_passport_with_binary_scorer( - self, _, aget_passport, validate_credential + def test_submit_passport_with_binary_scorer_above_threshold( + self, aget_passport, get_utc_time, validate_credential ): """Verify that submitting the same address multiple times only registers each stamp once, and gives back the same score""" - expected_score = "2.0" + expected_score = "2.00000" scorer = BinaryWeightedScorer.objects.create( threshold=2, @@ -564,6 +564,112 @@ def test_submit_passport_with_binary_scorer( self.community.scorer = scorer self.community.save() + expiration_date_list = [ + datetime.fromisoformat(s["credential"]["expirationDate"]) + for s in mock_passport["stamps"] + ] + + # First submission + response = self.client.get( + f"{self.base_url}/{self.community.pk}/score/{self.account.address}", + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {self.secret}", + ) + self.assertEqual(response.status_code, 200) + response_json = response.json() + self.assertEqual( + response_json, + { + "score": expected_score, + "passing_score": True, + "address": "0xb81c935d01e734b3d8bb233f5c4e1d72dbc30f6c", + "error": None, + "expiration_timestamp": min(expiration_date_list).isoformat(), + "last_score_timestamp": get_utc_time().isoformat(), + "stamp_scores": {"Ens": "1.0", "Google": "1.0"}, + "threshold": "2.00000", + }, + ) + + @patch("registry.atasks.validate_credential", side_effect=[[], [], [], []]) + @patch( + "registry.atasks.get_utc_time", + return_value=datetime.fromisoformat("2023-01-11T16:35:23.938006+00:00"), + ) + @patch( + "registry.atasks.aget_passport", + side_effect=[copy.deepcopy(mock_passport), copy.deepcopy(mock_passport)], + ) + def test_submit_passport_with_binary_scorer_below_threshold( + self, aget_passport, get_utc_time, validate_credential + ): + """Verify that submitting the same address multiple times only registers each stamp once, and gives back the same score""" + + expected_score = "2.00000" + + scorer = BinaryWeightedScorer.objects.create( + threshold=20, + weights={"FirstEthTxnProvider": 1.0, "Google": 1, "Ens": 1.0}, + type=Scorer.Type.WEIGHTED_BINARY, + ) + + self.community.scorer = scorer + self.community.save() + expiration_date_list = [ + datetime.fromisoformat(s["credential"]["expirationDate"]) + for s in mock_passport["stamps"] + ] + # First submission + response = self.client.get( + f"{self.base_url}/{self.community.pk}/score/{self.account.address}", + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {self.secret}", + ) + self.assertEqual(response.status_code, 200) + response_json = response.json() + self.assertEqual( + response_json, + { + "score": expected_score, + "passing_score": False, + "address": "0xb81c935d01e734b3d8bb233f5c4e1d72dbc30f6c", + "error": None, + "expiration_timestamp": min(expiration_date_list).isoformat(), + "last_score_timestamp": get_utc_time().isoformat(), + "stamp_scores": {"Ens": "1.0", "Google": "1.0"}, + "threshold": "20.00000", + }, + ) + + # TODO: add tests that verifies that returned threshold is from score when not resdcoring (theoretically threshold could change ...) + + @patch("registry.atasks.validate_credential", side_effect=[[], [], [], []]) + @patch( + "registry.atasks.get_utc_time", + return_value=datetime.fromisoformat("2023-01-11T16:35:23.938006+00:00"), + ) + @patch( + "registry.atasks.aget_passport", + side_effect=[copy.deepcopy(mock_passport), copy.deepcopy(mock_passport)], + ) + def test_submit_passport_with_non_binary_scorer_above_threshold( + self, aget_passport, get_utc_time, validate_credential + ): + """Verify that submitting the same address multiple times only registers each stamp once, and gives back the same score""" + + expected_score = "22.00000" + + scorer = WeightedScorer.objects.create( + weights={"FirstEthTxnProvider": 11.0, "Google": 11, "Ens": 11.0}, + type=Scorer.Type.WEIGHTED, + ) + self.community.scorer = scorer + self.community.save() + expiration_date_list = [ + datetime.fromisoformat(s["credential"]["expirationDate"]) + for s in mock_passport["stamps"] + ] + # First submission response = self.client.get( f"{self.base_url}/{self.community.pk}/score/{self.account.address}", @@ -573,6 +679,69 @@ def test_submit_passport_with_binary_scorer( self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(response_json["score"], expected_score) + self.assertEqual(response_json["passing_score"], True) + self.assertEqual( + response_json, + { + "score": expected_score, + "passing_score": True, + "address": "0xb81c935d01e734b3d8bb233f5c4e1d72dbc30f6c", + "error": None, + "expiration_timestamp": min(expiration_date_list).isoformat(), + "last_score_timestamp": get_utc_time().isoformat(), + "stamp_scores": {"Ens": "11.0", "Google": "11.0"}, + "threshold": "20.00000", + }, + ) + + @patch("registry.atasks.validate_credential", side_effect=[[], [], [], []]) + @patch( + "registry.atasks.get_utc_time", + return_value=datetime.fromisoformat("2023-01-11T16:35:23.938006+00:00"), + ) + @patch( + "registry.atasks.aget_passport", + side_effect=[copy.deepcopy(mock_passport), copy.deepcopy(mock_passport)], + ) + def test_submit_passport_with_non_binary_scorer_below_threshold( + self, aget_passport, get_utc_time, validate_credential + ): + """Verify that submitting the same address multiple times only registers each stamp once, and gives back the same score""" + + expected_score = "2.00000" + + scorer = WeightedScorer.objects.create( + weights={"FirstEthTxnProvider": 1.0, "Google": 1.0, "Ens": 1.0}, + type=Scorer.Type.WEIGHTED, + ) + self.community.scorer = scorer + self.community.save() + expiration_date_list = [ + datetime.fromisoformat(s["credential"]["expirationDate"]) + for s in mock_passport["stamps"] + ] + + # First submission + response = self.client.get( + f"{self.base_url}/{self.community.pk}/score/{self.account.address}", + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {self.secret}", + ) + self.assertEqual(response.status_code, 200) + response_json = response.json() + self.assertEqual( + response_json, + { + "score": expected_score, + "passing_score": False, + "address": "0xb81c935d01e734b3d8bb233f5c4e1d72dbc30f6c", + "error": None, + "expiration_timestamp": min(expiration_date_list).isoformat(), + "last_score_timestamp": get_utc_time().isoformat(), + "stamp_scores": {"Ens": "1.0", "Google": "1.0"}, + "threshold": "20.00000", + }, + ) def test_submit_passport_accepts_scorer_id(self): """ diff --git a/infra/aws/index.ts b/infra/aws/index.ts index 81f312d2b..2ff8425ac 100644 --- a/infra/aws/index.ts +++ b/infra/aws/index.ts @@ -222,6 +222,22 @@ const alarmConfigurations: AlarmConfigurations = { datapointsToAlarm: 7, evaluationPeriods: 10, }, + "passport-v2-stamp-score": { + percentHTTPCodeTarget4XX: 0.5, // 0..1 value for target error codes + percentHTTPCodeTarget5XX: 0.01, // 0..1 value for target error codes + targetResponseTime: 5, + period: 60, + datapointsToAlarm: 7, + evaluationPeriods: 10, + }, + "passport-v2-model-score": { + percentHTTPCodeTarget4XX: 0.5, // 0..1 value for target error codes + percentHTTPCodeTarget5XX: 0.01, // 0..1 value for target error codes + targetResponseTime: 5, + period: 60, + datapointsToAlarm: 7, + evaluationPeriods: 10, + }, }; const CERAMIC_CACHE_SCORER_ID = CERAMIC_CACHE_SCORER_ID_CONFG[stack]; diff --git a/infra/lib/scorer/loadBalancer.ts b/infra/lib/scorer/loadBalancer.ts index 06de1229d..d56fa3c10 100644 --- a/infra/lib/scorer/loadBalancer.ts +++ b/infra/lib/scorer/loadBalancer.ts @@ -25,6 +25,8 @@ export type AlarmConfigurations = { "cc-v1-st-bulk-PATCH-0": TargetGroupAlarmsConfiguration; "submit-passport-0": TargetGroupAlarmsConfiguration; "cc-v1-st-bulk-DELETE-0": TargetGroupAlarmsConfiguration; + "passport-v2-stamp-score": TargetGroupAlarmsConfiguration; + "passport-v2-model-score": TargetGroupAlarmsConfiguration; }; export function createLoadBalancerAlarms(