diff --git a/api/passport/api.py b/api/passport/api.py index 86dd96cf..b6d2b74d 100644 --- a/api/passport/api.py +++ b/api/passport/api.py @@ -16,11 +16,6 @@ from registry.api.utils import aapi_key, check_rate_limit, is_valid_address from registry.exceptions import InvalidAddressException from registry.models import BatchModelScoringRequest, BatchRequestStatus -from scorer import settings -from scorer.settings import ( - BULK_MODEL_SCORE_REQUESTS_RESULTS_FOLDER, - BULK_SCORE_REQUESTS_BUCKET_NAME, -) from scorer.settings.model_config import MODEL_AGGREGATION_NAMES log = logging.getLogger(__name__) @@ -80,6 +75,62 @@ class PassportAnalysisError(APIException): default_detail = "Error retrieving Passport analysis" +class DataScienceApiKey(APIKeyHeader): + param_name = "AUTHORIZATION" + + def authenticate(self, request, key): + if key == settings.DATA_SCIENCE_API_KEY: + return key + return None + + +data_science_auth = DataScienceApiKey() + + +class BatchResponse(Schema): + created_at: str + s3_url: Optional[str] + status: BatchRequestStatus + percentage_complete: int + + +@api.get( + "/analysis/batch", + auth=data_science_auth, + response={ + 200: list[BatchResponse], + 400: ErrorMessageResponse, + 500: ErrorMessageResponse, + }, + summary="Retrieve batch scoring status and result", + description="Retrieve batch scoring status and result", + include_in_schema=False, +) +def get_batch_analysis_stats(request, limit: int = 10) -> list[BatchResponse]: + requests = BatchModelScoringRequest.objects.order_by("-created_at")[:limit] + return [ + BatchResponse( + created_at=req.created_at.isoformat(), + s3_url=( + get_s3_client().generate_presigned_url( + "get_object", + Params={ + "Bucket": settings.BULK_SCORE_REQUESTS_BUCKET_NAME, + "Key": f"{settings.BULK_MODEL_SCORE_REQUESTS_RESULTS_FOLDER}/{req.s3_filename}", + }, + # 24 hrs + ExpiresIn=60 * 60 * 24, + ) + if req.status == BatchRequestStatus.DONE + else None + ), + status=req.status, + percentage_complete=req.progress, + ) + for req in requests + ] + + @api.get( "/analysis/{address}", auth=aapi_key, @@ -297,59 +348,3 @@ async def get_model_responses(models: List[str], checksummed_address: str): payload = {"address": checksummed_address} return await fetch_all(urls, payload) - - -class DataScienceApiKey(APIKeyHeader): - param_name = "AUTHORIZATION" - - def authenticate(self, request, key): - if key == settings.DATA_SCIENCE_API_KEY: - return key - return None - - -data_science_auth = DataScienceApiKey() - - -class BatchResponse(Schema): - created_at: str - s3_url: Optional[str] - status: BatchRequestStatus - percentage_complete: int - - -@api.get( - "/analysis", - auth=data_science_auth, - response={ - 200: list[BatchResponse], - 400: ErrorMessageResponse, - 500: ErrorMessageResponse, - }, - summary="Retrieve batch scoring status and result", - description="Retrieve batch scoring status and result", - include_in_schema=False, -) -def get_batch_analysis_stats(request, limit: int = 10) -> list[BatchResponse]: - requests = BatchModelScoringRequest.objects.order_by("-created_at")[:limit] - return [ - BatchResponse( - created_at=req.created_at.isoformat(), - s3_url=( - get_s3_client().generate_presigned_url( - "get_object", - Params={ - "Bucket": BULK_SCORE_REQUESTS_BUCKET_NAME, - "Key": f"{BULK_MODEL_SCORE_REQUESTS_RESULTS_FOLDER}/{req.s3_filename}", - }, - # 24 hrs - ExpiresIn=60 * 60 * 24, - ) - if req.status == BatchRequestStatus.DONE - else None - ), - status=req.status, - percentage_complete=req.progress, - ) - for req in requests - ] diff --git a/api/passport/test/test_data_science_bulk_score.py b/api/passport/test/test_data_science_bulk_score.py index 590153e0..805cb00d 100644 --- a/api/passport/test/test_data_science_bulk_score.py +++ b/api/passport/test/test_data_science_bulk_score.py @@ -36,7 +36,7 @@ def batch_requests(): return requests -api_url = "/passport/analysis" +api_url = "/passport/analysis/batch" def test_get_batch_analysis_stats_success(client, batch_requests, mocker): diff --git a/api/registry/test/test_command_delete_user_data.py b/api/registry/test/test_command_delete_user_data.py deleted file mode 100644 index 1e6a2bce..00000000 --- a/api/registry/test/test_command_delete_user_data.py +++ /dev/null @@ -1,184 +0,0 @@ -import re -from unittest import mock - -import pytest -from django.conf import settings -from django.core.management import call_command - -from ceramic_cache.models import CeramicCache, CeramicCacheLegacy -from passport_admin.models import DismissedBanners, PassportBanner -from registry.models import Event, HashScorerLink, Passport, Score, Stamp -from registry.utils import get_utc_time - -pytestmark = pytest.mark.django_db - - -@pytest.fixture(name="user_data") -def user_data(passport_holder_addresses, scorer_community_with_binary_scorer): - address = passport_holder_addresses[0]["address"] - passport = Passport.objects.create( - address=address, - community=scorer_community_with_binary_scorer, - ) - Stamp.objects.create( - passport=passport, - provider="FirstEthTxnProvider", - hash="0x1234", - credential={ - "expirationDate": "2022-01-01T00:00:00Z", - }, - ) - Score.objects.create( - passport=passport, - score=1, - status=Score.Status.DONE, - last_score_timestamp=get_utc_time(), - error=None, - stamp_scores=[], - evidence={ - "rawScore": 10, - "type": "binary", - "success": True, - "threshold": 5, - }, - ) - CeramicCache.objects.create( - address=address, provider="Google", type=CeramicCache.StampType.V1 - ) - CeramicCacheLegacy.objects.create(address=address, provider="Google") - # No need for an event, one will be created automatically when the score is saved - # Event.objects.create( - # action=Event.Action.TRUSTALAB_SCORE, - # address=address, - # data={"some": "data"}, - # ) - HashScorerLink.objects.create( - community=scorer_community_with_binary_scorer, - hash="hash1", - address=address, - expires_at="2099-01-02 00:00:00+00:00", - ) - - banner = PassportBanner.objects.create(content="test", link="test") - DismissedBanners.objects.create(address=address, banner=banner) - - -def test_delete_user_data_dry_run(passport_holder_addresses, user_data, capsys): - """Test the delete_user_data command in dry_run mode""" - - args = [] - opts = {"eth_address": passport_holder_addresses[0]["address"], "exec": False} - - call_command("delete_user_data", *args, **opts) - - # List of phrases that lines should end with - end_phrases = ["would to be deleted"] - end_pattern = "|".join(re.escape(phrase) for phrase in end_phrases) - - captured = capsys.readouterr() - print("captured.out", captured.out) - - objects_to_be_deleted = [ - "1 CeramicCache ", - "1 CeramicCacheLegacy ", - "1 Stamp ", - "1 Score ", - "1 Passport ", - "1 Event ", - "1 HashScorerLink ", - "1 DismissedBanners ", - ] - objects_to_be_deleted = [re.escape(word) for word in objects_to_be_deleted] - - for start_word in objects_to_be_deleted: - match = re.search(rf"^{start_word}.*{end_pattern}$", captured.out, re.MULTILINE) - print(match) - - # Construct the regular expression - start_pattern = "|".join(objects_to_be_deleted) - regex_pattern = rf"^(?!({start_pattern})).*({end_pattern})$" - - # Find all matching lines - unexpected_objects_to_delete = [ - match.group(0) - for match in re.finditer(regex_pattern, captured.out, re.MULTILINE) - ] - - assert ( - unexpected_objects_to_delete == [] - ), " apparently more objects are checked for deletion than we expect ..." - - # Check that passport objects have NOT been deleted - assert Passport.objects.all().count() == 1 - assert Stamp.objects.all().count() == 1 - assert Score.objects.all().count() == 1 - assert CeramicCache.objects.all().count() == 1 - assert CeramicCacheLegacy.objects.all().count() == 1 - assert Event.objects.all().count() == 1 - assert HashScorerLink.objects.all().count() == 1 - assert DismissedBanners.objects.all().count() == 1 - - -def test_delete_user_data_exec(passport_holder_addresses, user_data, capsys): - """Test the delete_user_data command in exec mode""" - - def input_response(message): - return "yes\n" - - with mock.patch( - "registry.management.commands.delete_user_data.input", - side_effect=input_response, - ): - args = [] - opts = {"eth_address": passport_holder_addresses[0]["address"], "exec": True} - - call_command("delete_user_data", *args, **opts) - - # List of phrases that lines should end with - end_phrases = ["would to be deleted"] - end_pattern = "|".join(re.escape(phrase) for phrase in end_phrases) - - captured = capsys.readouterr() - - print("captured.out", captured.out) - objects_to_be_deleted = [ - "1 CeramicCache ", - "1 CeramicCacheLegacy ", - "1 Stamp ", - "1 Score ", - "1 Passport ", - "1 Event ", - "1 HashScorerLink ", - "1 DismissedBanners ", - ] - objects_to_be_deleted = [re.escape(word) for word in objects_to_be_deleted] - - for start_word in objects_to_be_deleted: - match = re.search( - rf"^{start_word}.*{end_pattern}$", captured.out, re.MULTILINE - ) - print(match) - - # Construct the regular expression - start_pattern = "|".join(objects_to_be_deleted) - regex_pattern = rf"^(?!({start_pattern})).*({end_pattern})$" - - # Find all matching lines - unexpected_objects_to_delete = [ - match.group(0) - for match in re.finditer(regex_pattern, captured.out, re.MULTILINE) - ] - - assert ( - unexpected_objects_to_delete == [] - ), " apparently more objects are deleted than we expect ..." - - # Check that passport objects have indeed been deleted - assert Passport.objects.all().count() == 0 - assert Stamp.objects.all().count() == 0 - assert Score.objects.all().count() == 0 - assert CeramicCache.objects.all().count() == 0 - assert CeramicCacheLegacy.objects.all().count() == 0 - assert Event.objects.all().count() == 0 - assert HashScorerLink.objects.all().count() == 0 - assert DismissedBanners.objects.all().count() == 0 diff --git a/api/registry/test/test_command_process_batch_address_upload.py b/api/registry/test/test_command_process_batch_address_upload.py deleted file mode 100644 index ab23ebb8..00000000 --- a/api/registry/test/test_command_process_batch_address_upload.py +++ /dev/null @@ -1,90 +0,0 @@ -import asyncio -from io import StringIO -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest -from django.core.management import call_command -from django.test import TransactionTestCase, override_settings - -from registry.models import BatchModelScoringRequest, BatchRequestStatus - - -class TestProcessBatchModelAddressUploads(TransactionTestCase): - def test_process_pending_requests(self): - mock_s3_client = MagicMock() - mock_s3_client.get_object.return_value = { - "ContentLength": 100, - "Body": MagicMock( - read=lambda: StringIO( - "Address\n0xd5680a051302d427efa518238fda2c848eebe714\n0xd5680a051302d427efa518238fda2c848eebe714\n0x0636F974D29d947d4946b2091d769ec6D2d415DE" - ) - .getvalue() - .encode() - ), - } - - # Create a mock for handle_get_analysis - mock_handle_get_analysis = AsyncMock(return_value={"score": 75}) - - with patch( - "registry.management.commands.process_batch_model_address_upload.get_s3_client", - return_value=mock_s3_client, - ): - with patch( - "registry.management.commands.process_batch_model_address_upload.handle_get_analysis", - mock_handle_get_analysis, - ): - good_request = BatchModelScoringRequest.objects.create( - status=BatchRequestStatus.PENDING.value, - s3_filename=f"test_file.csv", - model_list=["model1", "model2"], - ) - - call_command("process_batch_model_address_upload") - - updated_request = BatchModelScoringRequest.objects.get(id=good_request.id) - self.assertEqual( - updated_request.status, - BatchRequestStatus.DONE.value, - f"Expected status DONE, but got {good_request.status}", - ) - self.assertEqual( - updated_request.progress, - 100, - f"Expected progress 100, but got {good_request.progress}", - ) - - expected_calls = 3 # 1 files * 3 addresses each - self.assertEqual( - mock_handle_get_analysis.call_count, - expected_calls, - f"Expected {expected_calls} calls to handle_get_analysis, but got {mock_handle_get_analysis.call_count}", - ) - assert mock_s3_client.get_object.call_count > 0 - - # If you comment out the following test, the first test will fail :() - def test_handle_error_during_processing(self): - with patch( - "registry.management.commands.process_batch_model_address_upload.get_s3_client" - ) as mock_get_s3_client: - mock_get_s3_client.return_value.get_object.side_effect = Exception( - "Test error" - ) - - # Create a pending request - BatchModelScoringRequest.objects.create( - status=BatchRequestStatus.PENDING.value, - s3_filename="test_file.csv", - model_list=["model1", "model2"], - ) - - # Call the command - call_command("process_batch_model_address_upload") - - # Refresh the request from the database - request = BatchModelScoringRequest.objects.first() - self.assertEqual( - request.status, - BatchRequestStatus.ERROR.value, - f"Expected status ERROR, but got {request.status}", - ) diff --git a/api/registry/test/test_command_recalculate_scores.py b/api/registry/test/test_command_recalculate_scores.py deleted file mode 100644 index e592942f..00000000 --- a/api/registry/test/test_command_recalculate_scores.py +++ /dev/null @@ -1,531 +0,0 @@ -import json -from decimal import Decimal - -import pytest -from django.conf import settings -from django.core.management import call_command -from django.test import override_settings - -from account.models import Community -from registry.models import Passport, Score, Stamp - -pytestmark = pytest.mark.django_db - - -@pytest.fixture(name="binary_weighted_scorer_passports") -def fixture_binaty_weighted_scorer_passports( - passport_holder_addresses, scorer_community_with_binary_scorer -): - passport = Passport.objects.create( - address=passport_holder_addresses[0]["address"], - community=scorer_community_with_binary_scorer, - ) - Stamp.objects.create( - passport=passport, - provider="FirstEthTxnProvider", - hash="0x1234", - credential={ - "expirationDate": "2022-01-01T00:00:00Z", - }, - ) - - passport1 = Passport.objects.create( - address=passport_holder_addresses[1]["address"], - community=scorer_community_with_binary_scorer, - ) - - Stamp.objects.create( - passport=passport1, - provider="FirstEthTxnProvider", - hash="0x12345", - credential={ - "expirationDate": "2022-01-01T00:00:00Z", - }, - ) - Stamp.objects.create( - passport=passport1, - provider="Google", - hash="0x123456", - credential={ - "expirationDate": "2022-01-01T00:00:00Z", - }, - ) - - passport2 = Passport.objects.create( - address=passport_holder_addresses[2]["address"], - community=scorer_community_with_binary_scorer, - ) - - Stamp.objects.create( - passport=passport2, - provider="FirstEthTxnProvider", - hash="0x12345a", - credential={ - "expirationDate": "2022-01-01T00:00:00Z", - }, - ) - Stamp.objects.create( - passport=passport2, - provider="Google", - hash="0x123456ab", - credential={ - "expirationDate": "2022-01-01T00:00:00Z", - }, - ) - Stamp.objects.create( - passport=passport2, - provider="Ens", - hash="0x123456abc", - credential={ - "expirationDate": "2022-01-01T00:00:00Z", - }, - ) - - return [passport, passport1, passport2] - - -@pytest.fixture(name="weighted_scorer_passports") -def fixture_weighted_scorer_passports( - passport_holder_addresses, scorer_community_with_weighted_scorer -): - passport = Passport.objects.create( - address=passport_holder_addresses[0]["address"], - community=scorer_community_with_weighted_scorer, - ) - Stamp.objects.create( - passport=passport, - provider="FirstEthTxnProvider", - hash="0x1234", - credential={ - "expirationDate": "2022-01-01T00:00:00Z", - }, - ) - - passport1 = Passport.objects.create( - address=passport_holder_addresses[1]["address"], - community=scorer_community_with_weighted_scorer, - ) - - Stamp.objects.create( - passport=passport1, - provider="FirstEthTxnProvider", - hash="0x12345", - credential={ - "expirationDate": "2022-01-01T00:00:00Z", - }, - ) - Stamp.objects.create( - passport=passport1, - provider="Google", - hash="0x123456", - credential={ - "expirationDate": "2022-01-01T00:00:00Z", - }, - ) - - passport2 = Passport.objects.create( - address=passport_holder_addresses[2]["address"], - community=scorer_community_with_weighted_scorer, - ) - - Stamp.objects.create( - passport=passport2, - provider="FirstEthTxnProvider", - hash="0x12345a", - credential={ - "expirationDate": "2022-01-01T00:00:00Z", - }, - ) - Stamp.objects.create( - passport=passport2, - provider="Google", - hash="0x123456ab", - credential={ - "expirationDate": "2022-01-01T00:00:00Z", - }, - ) - Stamp.objects.create( - passport=passport2, - provider="Ens", - hash="0x123456abc", - credential={ - "expirationDate": "2022-01-01T00:00:00Z", - }, - ) - - return [passport, passport1, passport2] - - -class TestRecalculatScores: - def test_rescoring_binary_scorer( - self, - binary_weighted_scorer_passports, - passport_holder_addresses, - scorer_community_with_binary_scorer, - ): - """Test the recalculate_scores command for binary scorer""" - - community = scorer_community_with_binary_scorer - args = [] - opts = {} - - scores = list(Score.objects.all()) - - scorer = community.get_scorer() - - # Check the initial threshold - assert scorer.threshold == Decimal(20) - assert len(scores) == 0 - call_command("recalculate_scores", *args, **opts) - - scores = list(Score.objects.all()) - assert len(scores) == 3 - - # Expect all scores to be below threshold - for s in scores: - assert s.score == 0 - assert s.status == "DONE" - assert s.error is None - - @pytest.mark.parametrize( - "weight_config", - [ - { - "FirstEthTxnProvider": "75", - "Google": "1", - "Ens": "1", - } - ], - indirect=True, - ) - def test_rescoring_binary_scorer_w_updated_settings( - self, - binary_weighted_scorer_passports, - passport_holder_addresses, - scorer_community_with_binary_scorer, - weight_config, - ): - community = scorer_community_with_binary_scorer - args = [] - opts = {} - - scores = list(Score.objects.all()) - - scorer = community.get_scorer() - - # Check the initial threshold - assert scorer.threshold == 20 - assert len(scores) == 0 - call_command("recalculate_scores", *args, **opts) - - scores = list(Score.objects.all()) - assert len(scores) == 3 - - # Expect all scores to be above threshold - for s in scores: - assert s.score == 1 - assert s.status == "DONE" - assert s.error is None - - s1 = Score.objects.get(passport=binary_weighted_scorer_passports[0]) - assert s1.evidence["rawScore"] == "75" - assert len(s1.stamp_scores) == 1 - assert "FirstEthTxnProvider" in s1.stamp_scores - - s2 = Score.objects.get(passport=binary_weighted_scorer_passports[1]) - assert s2.evidence["rawScore"] == "76" - assert len(s2.stamp_scores) == 2 - assert "FirstEthTxnProvider" in s2.stamp_scores - assert "Google" in s2.stamp_scores - - s3 = Score.objects.get(passport=binary_weighted_scorer_passports[2]) - assert s3.evidence["rawScore"] == "77" - assert len(s3.stamp_scores) == 3 - assert "FirstEthTxnProvider" in s3.stamp_scores - assert "Google" in s3.stamp_scores - assert "Ens" in s3.stamp_scores - - @pytest.mark.parametrize( - "weight_config", - [ - { - "FirstEthTxnProvider": "1", - "Google": "1", - "Ens": "1", - } - ], - indirect=True, - ) - def test_rescoring_weighted_scorer( - self, - weighted_scorer_passports, - passport_holder_addresses, - scorer_community_with_weighted_scorer, - ): - """Test the recalculate_scores command for weighted scorer""" - - args = [] - opts = {} - - scores = list(Score.objects.all()) - - # Check the initial threshold - assert len(scores) == 0 - call_command("recalculate_scores", *args, **opts) - - scores = list(Score.objects.all()) - assert len(scores) == 3 - - for s in scores: - assert s.status == "DONE" - assert s.error is None - assert s.evidence is None - - s1 = Score.objects.get(passport=weighted_scorer_passports[0]) - assert s1.score == 1 - assert len(s1.stamp_scores) == 1 - assert "FirstEthTxnProvider" in s1.stamp_scores - - s2 = Score.objects.get(passport=weighted_scorer_passports[1]) - assert s2.score == 2 - assert len(s2.stamp_scores) == 2 - assert "FirstEthTxnProvider" in s2.stamp_scores - assert "Google" in s2.stamp_scores - - s3 = Score.objects.get(passport=weighted_scorer_passports[2]) - assert s3.score == 3 - assert len(s3.stamp_scores) == 3 - assert "FirstEthTxnProvider" in s3.stamp_scores - assert "Google" in s3.stamp_scores - assert "Ens" in s3.stamp_scores - - @pytest.mark.parametrize( - "weight_config", - [ - { - "FirstEthTxnProvider": "75", - "Google": "1", - "Ens": "1", - } - ], - indirect=True, - ) - def test_rescoring_weighted_scorer_w_updated_settings( - self, - weighted_scorer_passports, - passport_holder_addresses, - scorer_community_with_weighted_scorer, - weight_config, - ): - """Change weights and rescore ...""" - community = scorer_community_with_weighted_scorer - args = [] - opts = {} - - scores = list(Score.objects.all()) - - scorer = community.get_scorer() - - # Check the initial threshold - assert len(scores) == 0 - call_command("recalculate_scores", *args, **opts) - scorer.weights["FirstEthTxnProvider"] = 75 - scorer.save() - - call_command("recalculate_scores", *args, **opts) - - scores = list(Score.objects.all()) - assert len(scores) == 3 - - # Expect all scores to be above threshold - for s in scores: - assert s.status == "DONE" - assert s.error is None - assert s.evidence is None - - s1 = Score.objects.get(passport=weighted_scorer_passports[0]) - assert s1.score == 75 - assert len(s1.stamp_scores) == 1 - assert "FirstEthTxnProvider" in s1.stamp_scores - - s2 = Score.objects.get(passport=weighted_scorer_passports[1]) - assert s2.score == 76 - assert len(s2.stamp_scores) == 2 - assert "FirstEthTxnProvider" in s2.stamp_scores - assert "Google" in s2.stamp_scores - - s3 = Score.objects.get(passport=weighted_scorer_passports[2]) - assert s3.score == 77 - assert len(s3.stamp_scores) == 3 - assert "FirstEthTxnProvider" in s3.stamp_scores - assert "Google" in s3.stamp_scores - assert "Ens" in s3.stamp_scores - - def test_rescoring_include_filter( - self, - weighted_scorer_passports, - binary_weighted_scorer_passports, - passport_holder_addresses, - scorer_community_with_weighted_scorer, - scorer_community_with_binary_scorer, - ): - """Test the recalculate_scores command uses the include filter properly""" - - communities = list(Community.objects.all()) - # Make sure the pre-test condition is fulfilled we have 2 communities - # and each one has at least 1 passport - assert len(communities) == 2 - for c in Community.objects.all(): - assert c.passports.count() > 0 - - included_community = communities[0] - excluded_community = communities[1] - - call_command( - "recalculate_scores", - *[], - **{ - "filter_community_include": json.dumps({"id": included_community.id}), - }, - ) - - # We check for scores only in the included community - # and make sure the ones for excluded community have not been calculated - assert Score.objects.filter(passport__community=excluded_community).count() == 0 - assert Score.objects.filter( - passport__community=included_community - ).count() == len(weighted_scorer_passports) - - def test_rescoring_exclude_filter( - self, - weighted_scorer_passports, - binary_weighted_scorer_passports, - passport_holder_addresses, - scorer_community_with_weighted_scorer, - scorer_community_with_binary_scorer, - ): - """Test the recalculate_scores command uses the exclude filter properly""" - - # Make sure the pre-test condition is fulfilled we have 2 communities - # and each one has at least 1 passport - assert len(weighted_scorer_passports) > 0 - assert len(binary_weighted_scorer_passports) > 0 - communities = list(Community.objects.all()) - assert len(communities) == 2 - - included_community = scorer_community_with_weighted_scorer - excluded_community = scorer_community_with_binary_scorer - - call_command( - "recalculate_scores", - *[], - **{ - "filter_community_exclude": json.dumps({"id": excluded_community.id}), - }, - ) - - # We check for scores only in the included community - # and make sure the ones for excluded community have not been calculated - assert Score.objects.filter(passport__community=excluded_community).count() == 0 - assert Score.objects.filter( - passport__community=included_community - ).count() == len(weighted_scorer_passports) - - def test_rescoring_no_filter( - self, - weighted_scorer_passports, - binary_weighted_scorer_passports, - passport_holder_addresses, - scorer_community_with_weighted_scorer, - scorer_community_with_binary_scorer, - ): - """Test the recalculate_scores command re-calculates all scores for all communities with no include or exclude filter""" - - # Make sure the pre-test condition is fulfilled we have 2 communities - # and each one has at least 1 passport - assert len(weighted_scorer_passports) > 0 - assert len(binary_weighted_scorer_passports) > 0 - communities = list(Community.objects.all()) - assert len(communities) == 2 - - call_command( - "recalculate_scores", - *[], - **{}, - ) - - assert Score.objects.filter( - passport__community=scorer_community_with_binary_scorer - ).count() == len(binary_weighted_scorer_passports) - assert Score.objects.filter( - passport__community=scorer_community_with_weighted_scorer - ).count() == len(weighted_scorer_passports) - - def test_rescoring_excludes_communities_marked_for_exclusion( - self, - weighted_scorer_passports, - binary_weighted_scorer_passports, - passport_holder_addresses, - scorer_community_with_weighted_scorer, - scorer_community_with_binary_scorer, - capsys, - ): - """Test the recalculate_scores command excludes communities marked for exclusion""" - - # Make sure the pre-test condition is fulfilled we have 2 communities - # and each one has at least 1 passport - assert len(weighted_scorer_passports) > 0 - assert len(binary_weighted_scorer_passports) > 0 - communities = list(Community.objects.all()) - assert len(communities) == 2 - - included_community = scorer_community_with_weighted_scorer - excluded_community = scorer_community_with_binary_scorer - - scorer = excluded_community.get_scorer() - scorer.exclude_from_weight_updates = True - scorer.save() - - call_command( - "recalculate_scores", - *[], - **{}, - ) - - captured = capsys.readouterr() - print(captured.out) - assert "Updated scorers: 1" in captured.out - assert included_community.name in captured.out - assert excluded_community.name not in captured.out - assert "Recalculating scores" in captured.out - - def test_only_weights_skips_rescore( - self, - weighted_scorer_passports, - binary_weighted_scorer_passports, - passport_holder_addresses, - scorer_community_with_weighted_scorer, - scorer_community_with_binary_scorer, - capsys, - ): - """Test the recalculate_scores command excludes communities marked for exclusion""" - - # Make sure the pre-test condition is fulfilled we have 2 communities - # and each one has at least 1 passport - assert len(weighted_scorer_passports) > 0 - assert len(binary_weighted_scorer_passports) > 0 - communities = list(Community.objects.all()) - assert len(communities) == 2 - - call_command( - "recalculate_scores", - *[], - **{ - "only_weights": True, - }, - ) - - captured = capsys.readouterr() - print(captured.out) - assert "Updated scorers: 2" in captured.out - assert "Recalculating scores" not in captured.out diff --git a/api/registry/test/test_save_api_key_analytics.py b/api/registry/test/test_save_api_key_analytics.py deleted file mode 100644 index 59bcc069..00000000 --- a/api/registry/test/test_save_api_key_analytics.py +++ /dev/null @@ -1,34 +0,0 @@ -from datetime import datetime - -import pytest -from account.models import AccountAPIKey, AccountAPIKeyAnalytics -from registry.tasks import save_api_key_analytics - -path = "/test_path/" - - -@pytest.mark.django_db -class TestSaveApiKeyAnalytics: - def test_save_api_key_analytics_new_analytics(self, scorer_account): - (model, secret) = AccountAPIKey.objects.create_key( - account=scorer_account, name="Another token for user 1" - ) - - save_api_key_analytics( - model.pk, - path, - ["a", "b", "c"], - {"param1": "value1"}, - {"header": "field1"}, - {"data": "hello"}, - {"response": "reponse content"}, - response_skipped=False, - error="big problem", - ) - - obj = AccountAPIKeyAnalytics.objects.get(path=path, api_key_id=model.pk) - - created_at_day = datetime.fromisoformat(obj.created_at.isoformat()) - - assert created_at_day.day is datetime.now().day - assert created_at_day.month is datetime.now().month diff --git a/api/v2/api_models.py b/api/v2/api_models.py index 9df1bc9b..2b056223 100644 --- a/api/v2/api_models.py +++ b/api/v2/api_models.py @@ -38,8 +38,6 @@ async def get_analysis( # TODO Do we keep this here? - - @api.get( "/models/analysis", auth=data_science_auth, diff --git a/api/v2/api_stamps.py b/api/v2/api_stamps.py index 3e366079..41c5b6a3 100644 --- a/api/v2/api_stamps.py +++ b/api/v2/api_stamps.py @@ -108,6 +108,23 @@ def get_score_history( ) +@api.get( + "/stamps/metadata", + summary="Receive all Stamps available in Passport", + description="""**WARNING**: This endpoint is in beta and is subject to change.""", + auth=ApiKey(), + response={ + 200: List[StampDisplayResponse], + 500: ErrorMessageResponse, + }, + tags=["Stamp Analysis"], +) +@track_apikey_usage(track_response=False) +def stamp_display(request) -> List[StampDisplayResponse]: + check_rate_limit(request) + return fetch_all_stamp_metadata() + + @api.get( "/stamps/{str:address}", auth=ApiKey(), @@ -248,20 +265,3 @@ def fetch_stamp_metadata_for_provider(provider: str): ) return metadataByProvider.get(provider) - - -@api.get( - "/stamps/metadata", - summary="Receive all Stamps available in Passport", - description="""**WARNING**: This endpoint is in beta and is subject to change.""", - auth=ApiKey(), - response={ - 200: List[StampDisplayResponse], - 500: ErrorMessageResponse, - }, - tags=["Stamp Analysis"], -) -@track_apikey_usage(track_response=False) -def stamp_display(request) -> List[StampDisplayResponse]: - check_rate_limit(request) - return fetch_all_stamp_metadata() diff --git a/api/v2/test/generic_scorer_creation.py b/api/v2/test/generic_scorer_creation.py deleted file mode 100644 index 0eb565fb..00000000 --- a/api/v2/test/generic_scorer_creation.py +++ /dev/null @@ -1,126 +0,0 @@ -import json - -import pytest -from django.test import Client -from django.urls import reverse - -from account.models import AccountAPIKey, Community, Rules - -client = Client() - -pytestmark = pytest.mark.django_db - - -def test_create_generic_scorer_success(scorer_account): - (_, secret) = AccountAPIKey.objects.create_key( - account=scorer_account, - name="Test API key", - ) - - payload = {"name": "Test Community", "external_scorer_id": "0x0000"} - - response = client.post( - "/registry/scorer/generic", - json.dumps(payload), - content_type="application/json", - HTTP_AUTHORIZATION=f"Token {secret}", - ) - response_data = response.json() - - assert response.status_code == 200 - assert response_data["ok"] == True - assert "scorer_id" in response_data - assert "external_scorer_id" in response_data - - # Verify the community was created in the database - community = Community.objects.get(pk=response_data["scorer_id"]) - assert community.name == payload["name"] - assert community.account == scorer_account - - -def test_create_generic_scorer_no_permission(scorer_account): - (_, secret) = AccountAPIKey.objects.create_key( - account=scorer_account, name="Test API key" - ) - - payload = {"name": "Test Community", "external_scorer_id": "0x0000"} - - response = client.post( - "/registry/scorer/generic", - json.dumps(payload), - content_type="application/json", - HTTP_AUTHORIZATION=f"Token {secret}", - ) - - assert response.status_code == 403 - - -def test_create_generic_scorer_too_many_communities(scorer_account, settings): - (_, secret) = AccountAPIKey.objects.create_key( - account=scorer_account, - name="Test API key", - ) - - settings.GENERIC_COMMUNITY_CREATION_LIMIT = 1 - - Community.objects.create( - account=scorer_account, - name="Existing Community", - description="Test", - use_case="Sybil Protection", - rule=Rules.LIFO, - ) - - payload = {"name": "Test Community", "external_scorer_id": "0x0000"} - - response = client.post( - "/registry/scorer/generic", - json.dumps(payload), - content_type="application/json", - HTTP_AUTHORIZATION=f"Token {secret}", - ) - assert response.status_code == 400 - - -def test_create_generic_scorer_duplicate_name(scorer_account): - (_, secret) = AccountAPIKey.objects.create_key( - account=scorer_account, - name="Test API key", - ) - - Community.objects.create( - account=scorer_account, - name="Test Community", - description="Test", - use_case="Sybil Protection", - rule=Rules.LIFO, - ) - - payload = {"name": "Test Community", "external_scorer_id": "0x0000"} - - response = client.post( - "/registry/scorer/generic", - json.dumps(payload), - content_type="application/json", - HTTP_AUTHORIZATION=f"Token {secret}", - ) - - assert response.status_code == 400 - - -def test_create_generic_scorer_no_name(scorer_account): - (_, secret) = AccountAPIKey.objects.create_key( - account=scorer_account, - name="Test API key", - ) - - payload = {"name": "", "external_scorer_id": "0x0000"} - - response = client.post( - "/registry/scorer/generic", - json.dumps(payload), - content_type="application/json", - HTTP_AUTHORIZATION=f"Token {secret}", - ) - - assert response.status_code == 422 diff --git a/api/v2/test/test_address_validation.py b/api/v2/test/test_address_validation.py deleted file mode 100644 index 7d330a39..00000000 --- a/api/v2/test/test_address_validation.py +++ /dev/null @@ -1,35 +0,0 @@ -import pytest - -from registry.api.v1 import is_valid_address - -address = "0x71Ad3e3057Ca74967239C66ca6D3A9C2A43a58fC" - - -@pytest.mark.django_db -def test_good_checksum(): - assert is_valid_address(address) is True - - -@pytest.mark.django_db -def test_good_lowercase(): - assert is_valid_address(address.lower()) is True - - -@pytest.mark.django_db -def test_good_uppercase(): - assert is_valid_address(address.upper()) is True - - -@pytest.mark.django_db -def test_bad_checksum(): - assert is_valid_address(address.replace("A", "a")) is False - - -@pytest.mark.django_db -def test_bad_length(): - assert is_valid_address(address.lower() + "a") is False - - -@pytest.mark.django_db -def test_bad_length_checksummed(): - assert is_valid_address(address + "a") is False diff --git a/api/v2/test/test_api_analytics.py b/api/v2/test/test_api_analytics.py index ca521c27..ebec2425 100644 --- a/api/v2/test/test_api_analytics.py +++ b/api/v2/test/test_api_analytics.py @@ -17,10 +17,8 @@ @pytest.fixture( params=[ - ("get", "/registry/signing-message"), - ("post", "/registry/submit-passport"), - ("get", "/registry/score/3"), - ("get", "/registry/score/3/0x0"), + ("get", "/v2/signing-message"), + ("get", "/v2/stamps/SCORER/score/ADDRESS"), ] ) def api_path_that_requires_api_tracing(request): @@ -33,25 +31,21 @@ def test_authentication_works_with_token( """ Test that API key is accepted if it is valid token and present in the HTTP_AUTHORIZATION header """ + account = scorer_community.account + method, path = api_path_that_requires_api_tracing + path = path.replace("SCORER", str(scorer_community.id)).replace( + "ADDRESS", account.address + ) client = Client() - account = scorer_community.account - (_, secret) = AccountAPIKey.objects.create_key( account=account, name="Token for user 1" ) if method == "post": - # Now we submit a duplicate hash, and expect deduplication to happen - submission_test_payload = { - "community": scorer_community.id, - "address": account.address, - } - response = client.post( path, - json.dumps(submission_test_payload), content_type="application/json", HTTP_AUTHORIZATION="Token " + secret, ) @@ -61,7 +55,7 @@ def test_authentication_works_with_token( path, HTTP_AUTHORIZATION="Token " + secret, ) - # We should not get back any unauuthorized or forbidden errors + # We should not get back any unauthorized or forbidden errors assert response.status_code != 401 assert response.status_code != 403 diff --git a/api/v2/test/test_api_auth_required.py b/api/v2/test/test_api_auth_required.py index 23fa515e..59adeb13 100644 --- a/api/v2/test/test_api_auth_required.py +++ b/api/v2/test/test_api_auth_required.py @@ -10,22 +10,27 @@ web3 = Web3() web3.eth.account.enable_unaudited_hdwallet_features() -my_mnemonic = settings.TEST_MNEMONIC @pytest.fixture( params=[ - ("get", "/registry/signing-message"), - ("post", "/registry/submit-passport"), - ("get", "/registry/score/3"), - ("get", "/registry/score/3/0x0"), + ("get", "/v2/signing-message"), + ("get", "/v2/stamps/SCORER/score/ADDRESS"), + ("get", "/v2/stamps/ADDRESS"), + ("get", "/v2/stamps/metadata"), ] ) -def api_path_that_requires_auth(request): - return request.param +def api_path_that_requires_auth(request, scorer_community): + method, path = request.param + path = path.replace("SCORER", str(scorer_community.id)).replace( + "ADDRESS", scorer_community.account.address + ) + return (method, path) -def test_authentication_is_required_token(api_path_that_requires_auth): +def test_authentication_is_required_token( + api_path_that_requires_auth, scorer_community +): """ Test that bad api keys past in as tokens are rejected """ @@ -43,18 +48,16 @@ def test_authentication_is_required_token(api_path_that_requires_auth): assert response.status_code == 401 -def test_authentication_works_with_token(api_path_that_requires_auth, scorer_user): +def test_authentication_works_with_token( + api_path_that_requires_auth, scorer_user, scorer_community +): """ Test that API key is accepted if it is valid token and present in the HTTP_AUTHORIZATION header """ method, path = api_path_that_requires_auth client = Client() - web3_account = web3.eth.account.from_mnemonic( - my_mnemonic, account_path="m/44'/60'/0'/0/0" - ) - - account = Account.objects.create(user=scorer_user, address=web3_account.address) + account = scorer_community.account (_, secret) = AccountAPIKey.objects.create_key( account=account, name="Token for user 1" @@ -91,17 +94,15 @@ def test_authentication_is_required_api_key(api_path_that_requires_auth): assert response.status_code == 401 -def test_authentication_works_with_api_key(api_path_that_requires_auth, scorer_user): +def test_authentication_works_with_api_key( + api_path_that_requires_auth, scorer_user, scorer_community +): """ Test that API key is accepted if it is valid and present in HTTP_X-API-Key header""" method, path = api_path_that_requires_auth client = Client() - web3_account = web3.eth.account.from_mnemonic( - my_mnemonic, account_path="m/44'/60'/0'/0/0" - ) - - account = Account.objects.create(user=scorer_user, address=web3_account.address) + account = scorer_community.account (_, secret) = AccountAPIKey.objects.create_key( account=account, name="Token for user 1" diff --git a/api/v2/test/test_get_staking_results.py b/api/v2/test/test_get_staking_results.py deleted file mode 100644 index 34602b69..00000000 --- a/api/v2/test/test_get_staking_results.py +++ /dev/null @@ -1,61 +0,0 @@ -import pytest -from django.test import Client - -from registry.models import GTCStakeEvent - -pytestmark = pytest.mark.django_db - -user_address = "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955" - - -class TestGetLegacyStakingResults: - def test_successful_get_staking_results(self, scorer_api_key, gtc_staking_response): - stakes = list(GTCStakeEvent.objects.all()) - - client = Client() - response = client.get( - f"/registry/gtc-stake/{user_address}/1", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - - response_data = response.json()["results"] - assert response.status_code == 200 - - # an extra stake event was added that is below the filtered amount, hence the minus 1 - assert len(stakes) - 1 == len(response_data) - - def test_item_in_filter_condition_is_not_present( - self, scorer_api_key, gtc_staking_response - ): - client = Client() - response = client.get( - f"/registry/gtc-stake/{user_address}/1", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - - response_data = response.json()["results"] - assert response.status_code == 200 - - for item in response_data: - # ID 16 belongs to the item that does not meet the filter criteria - assert item["id"] != 16 - - def test_missing_address_error_response(self, scorer_api_key, gtc_staking_response): - client = Client() - response = client.get( - "/registry/gtc-stake//1", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - - assert response.status_code == 404 - - def test_missing_round_id_error_response( - self, scorer_api_key, gtc_staking_response - ): - client = Client() - response = client.get( - f"/registry/gtc-stake/{user_address}/", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - - assert response.status_code == 404 diff --git a/api/v2/test/test_passport_get_score.py b/api/v2/test/test_passport_get_score.py deleted file mode 100644 index b435fadb..00000000 --- a/api/v2/test/test_passport_get_score.py +++ /dev/null @@ -1,466 +0,0 @@ -import datetime - -import pytest -from django.conf import settings -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group -from django.test import Client -from web3 import Web3 - -from account.models import Account, AccountAPIKey, Community -from registry.api.v1 import get_scorer_by_id -from registry.models import Passport, Score - -User = get_user_model() -web3 = Web3() -web3.eth.account.enable_unaudited_hdwallet_features() -my_mnemonic = settings.TEST_MNEMONIC - -pytestmark = pytest.mark.django_db - - -@pytest.fixture -def paginated_scores(scorer_passport, passport_holder_addresses, scorer_community): - scores = [] - i = 0 - for holder in passport_holder_addresses: - passport = Passport.objects.create( - address=holder["address"], - community=scorer_community, - ) - - score = Score.objects.create( - status="DONE", - passport=passport, - score="1", - last_score_timestamp=datetime.datetime.now() - + datetime.timedelta(days=i + 1), - ) - - scores.append(score) - i += 1 - return scores - - -class TestPassportGetScore: - base_url = "/registry" - - def test_get_scores_with_valid_community_with_no_scores( - self, scorer_api_key, scorer_account - ): - additional_community = Community.objects.create( - name="My Community", - description="My Community description", - account=scorer_account, - ) - - client = Client() - response = client.get( - f"{self.base_url}/score/{additional_community.pk}", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - response_data = response.json() - - assert response.status_code == 200 - assert len(response_data["items"]) == 0 - - def test_get_scores_returns_first_page_scores_v1( - self, - scorer_api_key, - passport_holder_addresses, - scorer_community, - paginated_scores, - ): - offset = 0 - limit = 2 - - client = Client() - response = client.get( - f"{self.base_url}/score/{scorer_community.id}?limit={limit}&offset={offset}", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - response_data = response.json() - - assert response.status_code == 200 - - for i in range(0, 1): - assert ( - response_data["items"][i]["address"] - == passport_holder_addresses[offset + i]["address"].lower() - ) - - def test_get_scores_returns_second_page_scores_v1( - self, - scorer_api_key, - passport_holder_addresses, - scorer_community, - paginated_scores, - ): - offset = 2 - limit = 2 - - client = Client() - response = client.get( - f"/registry/score/{scorer_community.id}?limit={limit}&offset={offset}", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - response_data = response.json() - - assert response.status_code == 200 - - for i in range(0, 2): - assert ( - response_data["items"][i]["address"] - == passport_holder_addresses[offset + i]["address"].lower() - ) - - def test_get_scores_returns_paginated_and_pk_sorted_response( - self, - scorer_api_key, - passport_holder_addresses, - scorer_community, - paginated_scores, - ): - request_params = [[0, 3], [3, 5]] - - for offset, limit in request_params: - # Get the PKs of the scores we are expecting - expected_pks = [ - score.pk for score in paginated_scores[offset : offset + limit] - ] - - client = Client() - response = client.get( - f"/registry/score/{scorer_community.id}?limit={limit}&offset={offset}", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - response_data = response.json() - - assert response.status_code == 200 - returned_pks = [ - Score.objects.filter(passport__address=item["address"]).get().pk - for item in response_data["items"] - ] - - assert expected_pks == returned_pks - - def test_get_scores_returns_paginated_and_datetime_sorted_response( - self, - scorer_api_key, - passport_holder_addresses, - scorer_community, - paginated_scores, - ): - request_params = [[0, 3], [3, 5]] - - for offset, limit in request_params: - # Get the PKs of the scores we are expecting - sorted_scores = sorted( - paginated_scores, - key=lambda score: score.last_score_timestamp, - ) - expected_pks = [ - score.pk for score in sorted_scores[offset : offset + limit] - ] - - client = Client() - response = client.get( - f"/registry/score/{scorer_community.id}?limit={limit}&offset={offset}&order_by=last_score_timestamp", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - response_data = response.json() - - assert response.status_code == 200 - returned_pks = [ - Score.objects.filter(passport__address=item["address"]).get().pk - for item in response_data["items"] - ] - - assert expected_pks == returned_pks - - def test_get_scores_request_throws_400_for_invalid_community(self, scorer_api_key): - client = Client() - response = client.get( - f"{self.base_url}/score/3", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - # assert response.status_code == 404 - assert response.json() == { - "detail": "No Community matches the given query.", - } - - def test_get_single_score_for_address_in_path( - self, - scorer_api_key, - passport_holder_addresses, - scorer_community, - paginated_scores, - ): - client = Client() - response = client.get( - f"{self.base_url}/score/{scorer_community.id}/{passport_holder_addresses[0]['address']}", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - assert response.status_code == 200 - - assert ( - response.json()["address"] - == passport_holder_addresses[0]["address"].lower() - ) - - def test_cannot_get_single_score_for_address_in_path_for_other_community( - self, - passport_holder_addresses, - scorer_community, - paginated_scores, - ): - """ - Test that a user can't get scores for a community they don't belong to. - """ - # Create another user, account & api key - user = User.objects.create_user(username="anoter-test-user", password="12345") - web3_account = web3.eth.account.from_mnemonic( - my_mnemonic, account_path="m/44'/60'/0'/0/0" - ) - - account = Account.objects.create(user=user, address=web3_account.address) - (_, api_key) = AccountAPIKey.objects.create_key( - account=account, - name="Token for user 1", - ) - - client = Client() - response = client.get( - f"{self.base_url}/score/{scorer_community.id}/{passport_holder_addresses[0]['address']}", - HTTP_AUTHORIZATION="Token " + api_key, - ) - - assert response.status_code == 404 - assert response.json() == {"detail": "No Community matches the given query."} - - def test_limit_greater_than_1000_throws_an_error( - self, scorer_community, passport_holder_addresses, scorer_api_key - ): - client = Client() - response = client.get( - f"{self.base_url}/score/{scorer_community.id}?limit=1001", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - - assert response.status_code == 400 - assert response.json() == { - "detail": "Invalid limit.", - } - - def test_limit_of_1000_is_ok( - self, scorer_community, passport_holder_addresses, scorer_api_key - ): - client = Client() - response = client.get( - f"{self.base_url}/score/{scorer_community.id}?limit=1000", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - - assert response.status_code == 200 - - def test_get_single_score_for_address_without_permissions( - self, - passport_holder_addresses, - scorer_community, - scorer_api_key_no_permissions, - ): - client = Client() - response = client.get( - f"{self.base_url}/score/{scorer_community.id}/{passport_holder_addresses[0]['address']}", - HTTP_AUTHORIZATION="Token " + scorer_api_key_no_permissions, - ) - assert response.status_code == 403 - - def test_get_single_score_by_scorer_id_without_permissions( - self, - scorer_community, - scorer_api_key_no_permissions, - ): - client = Client() - response = client.get( - f"/registry/score/{scorer_community.id}", - HTTP_AUTHORIZATION="Token " + scorer_api_key_no_permissions, - ) - assert response.status_code == 403 - - def test_cannot_get_score_for_other_community(self, scorer_community): - """Test that a user can't get scores for a community they don't belong to.""" - # Create another user, account & api key - user = User.objects.create_user(username="anoter-test-user", password="12345") - web3_account = web3.eth.account.from_mnemonic( - my_mnemonic, account_path="m/44'/60'/0'/0/0" - ) - - account = Account.objects.create(user=user, address=web3_account.address) - (_, api_key) = AccountAPIKey.objects.create_key( - account=account, - name="Token for user 1", - ) - - client = Client() - response = client.get( - f"{self.base_url}/score/{scorer_community.id}?limit=1000", - HTTP_AUTHORIZATION="Token " + api_key, - ) - - assert response.status_code == 404 - assert response.json() == {"detail": "No Community matches the given query."} - - def test_get_scorer_by_id(self, scorer_account): - scorer = Community.objects.create( - name="The Scorer", - description="A great scorer", - account=scorer_account, - external_scorer_id="scorer1", - ) - - result = get_scorer_by_id(scorer.pk, scorer_account) - - assert result == scorer - - def test_get_scorer_by_external_id(self, scorer_account): - scorer = Community.objects.create( - name="The Scorer", - description="A great scorer", - account=scorer_account, - external_scorer_id="scorer1", - ) - - result = get_scorer_by_id("scorer1", scorer_account) - - assert result == scorer - - def test_get_scorer_by_id_not_found(self, scorer_account): - try: - get_scorer_by_id(999, scorer_account) - except Exception as e: - assert str(e) == "No Community matches the given query." - - try: - get_scorer_by_id("scorer1", scorer_account) - except Exception as e: - assert str(e) == "Field 'id' expected a number but got 'scorer1'." - - def test_get_scores_filter_by_last_score_timestamp__gte( - self, - scorer_api_key, - passport_holder_addresses, - scorer_community, - paginated_scores, - ): - scores = list(Score.objects.all()) - middle = len(scores) // 2 - older_scores = scores[:middle] - newer_scores = scores[middle:] - now = datetime.datetime.utcnow() - past_time_stamp = now - datetime.timedelta(days=1) - future_time_stamp = now + datetime.timedelta(days=1) - - # Make sure we have sufficient data in both queries - assert len(newer_scores) >= 2 - assert len(older_scores) >= 2 - - for score in older_scores: - score.last_score_timestamp = past_time_stamp - score.save() - - # The first score will have a last_score_timestamp equal to the value we filter by - for idx, score in enumerate(newer_scores): - if idx == 0: - score.last_score_timestamp = now - else: - score.last_score_timestamp = future_time_stamp - score.save() - - # Check the query when the filtered timestamp equals a score last_score_timestamp - client = Client() - response = client.get( - f"{self.base_url}/score/{scorer_community.id}?last_score_timestamp__gte={now.isoformat()}", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - assert response.status_code == 200 - assert len(response.json()["items"]) == len(newer_scores) - - # Check the query when the filtered timestamp does not equal a score last_score_timestamp - response = client.get( - f"{self.base_url}/score/{scorer_community.id}?last_score_timestamp__gte={(now - datetime.timedelta(milliseconds=1)).isoformat()}", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - assert response.status_code == 200 - assert len(response.json()["items"]) == len(newer_scores) - - def test_get_scores_filter_by_last_score_timestamp__gt( - self, - scorer_api_key, - passport_holder_addresses, - scorer_community, - paginated_scores, - ): - scores = list(Score.objects.all()) - middle = len(scores) // 2 - older_scores = scores[:middle] - newer_scores = scores[middle:] - now = datetime.datetime.utcnow() - past_time_stamp = now - datetime.timedelta(days=1) - future_time_stamp = now + datetime.timedelta(days=1) - - # Make sure we have sufficient data in both queries - assert len(newer_scores) >= 2 - assert len(older_scores) >= 2 - - for score in older_scores: - score.last_score_timestamp = past_time_stamp - score.save() - - # The first score will have a last_score_timestamp equal to the value we filter by - for idx, score in enumerate(newer_scores): - if idx == 0: - score.last_score_timestamp = now - else: - score.last_score_timestamp = future_time_stamp - score.save() - - # Check the query when the filtered timestamp equals a score last_score_timestamp - client = Client() - response = client.get( - f"{self.base_url}/score/{scorer_community.id}?last_score_timestamp__gt={now.isoformat()}", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - assert response.status_code == 200 - assert len(response.json()["items"]) == len(newer_scores) - 1 - - # Check the query when the filtered timestamp does not equal a score last_score_timestamp - response = client.get( - f"{self.base_url}/score/{scorer_community.id}?last_score_timestamp__gt={(now - datetime.timedelta(milliseconds=1)).isoformat()}", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - assert response.status_code == 200 - assert len(response.json()["items"]) == len(newer_scores) - - def test_backported_get_historical_scores( - self, - scorer_api_key, - passport_holder_addresses, - scorer_community, - paginated_scores, - ): - client = Client() - response = client.get( - f"{self.base_url}/score/{scorer_community.id}/history", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - - response_data = response.json() - - assert response.status_code == 200 - - items = response_data["items"] - assert len(items) == 10 - - next_url = response_data["next"] - assert "/registry/score/" in next_url diff --git a/api/v2/test/test_passport_get_stamps.py b/api/v2/test/test_passport_get_stamps.py index a98dca28..2a1c4af7 100644 --- a/api/v2/test/test_passport_get_stamps.py +++ b/api/v2/test/test_passport_get_stamps.py @@ -84,14 +84,14 @@ def paginated_stamps(scorer_community, passport_holder_addresses): class TestPassportGetStamps: - base_url = "/registry" + base_url = "/v2/stamps" def test_get_stamps_with_address_with_no_scores( self, scorer_api_key, passport_holder_addresses ): client = Client() response = client.get( - f"{self.base_url}/stamps/{passport_holder_addresses[0]['address']}", + f"{self.base_url}/{passport_holder_addresses[0]['address']}", HTTP_AUTHORIZATION="Token " + scorer_api_key, ) response_data = response.json() @@ -119,7 +119,7 @@ def test_get_stamps_only_includes_this_address( client = Client() response = client.get( - f"{self.base_url}/stamps/{passport_holder_addresses[0]['address']}?limit={limit}", + f"{self.base_url}/{passport_holder_addresses[0]['address']}?limit={limit}", HTTP_AUTHORIZATION="Token " + scorer_api_key, ) @@ -143,7 +143,7 @@ def test_include_metadata( ): client = Client() response = client.get( - f"{self.base_url}/stamps/{passport_holder_addresses[0]['address']}?include_metadata=true&limit=1", + f"{self.base_url}/{passport_holder_addresses[0]['address']}?include_metadata=true&limit=1", HTTP_AUTHORIZATION="Token " + scorer_api_key, ) response_data = response.json() @@ -174,11 +174,12 @@ def test_get_all_metadata( ): client = Client() response = client.get( - f"{self.base_url}/stamp-metadata", + f"{self.base_url}/metadata", HTTP_AUTHORIZATION="Token " + scorer_api_key, ) response_data = response.json() + print(response_data) assert response.status_code == 200 assert response_data[0]["id"] == mock_stamp_metadata[0]["id"] assert ( @@ -199,7 +200,7 @@ def test_get_stamps_returns_first_page_stamps( client = Client() response = client.get( - f"{self.base_url}/stamps/{passport_holder_addresses[0]['address']}?limit={limit}", + f"{self.base_url}/{passport_holder_addresses[0]['address']}?limit={limit}", HTTP_AUTHORIZATION="Token " + scorer_api_key, ) response_data = response.json() @@ -243,7 +244,7 @@ def test_get_stamps_returns_second_page_stamps( client = Client() page_one_response = client.get( - f"{self.base_url}/stamps/{passport_holder_addresses[0]['address']}?limit={limit}", + f"{self.base_url}/{passport_holder_addresses[0]['address']}?limit={limit}", HTTP_AUTHORIZATION="Token " + scorer_api_key, ) page_one_data = page_one_response.json() @@ -285,7 +286,7 @@ def test_get_stamps_returns_third_page_stamps( client = Client() page_one_response = client.get( - f"{self.base_url}/stamps/{passport_holder_addresses[0]['address']}?limit={limit}", + f"{self.base_url}/{passport_holder_addresses[0]['address']}?limit={limit}", HTTP_AUTHORIZATION="Token " + scorer_api_key, ) page_one_data = page_one_response.json() @@ -330,7 +331,7 @@ def test_limit_greater_than_1000_throws_an_error( ): client = Client() response = client.get( - f"{self.base_url}/stamps/{passport_holder_addresses[0]['address']}?limit=1001", + f"{self.base_url}/{passport_holder_addresses[0]['address']}?limit=1001", HTTP_AUTHORIZATION="Token " + scorer_api_key, ) @@ -342,7 +343,7 @@ def test_limit_greater_than_1000_throws_an_error( def test_limit_of_1000_is_ok(self, passport_holder_addresses, scorer_api_key): client = Client() response = client.get( - f"{self.base_url}/stamps/{passport_holder_addresses[0]['address']}?limit=1000", + f"{self.base_url}/{passport_holder_addresses[0]['address']}?limit=1000", HTTP_AUTHORIZATION="Token " + scorer_api_key, ) @@ -368,7 +369,7 @@ def test_get_last_page_stamps_by_address( # Read the 1st batch response = client.get( - f"{self.base_url}/stamps/{passport_holder_addresses[0]['address']}?limit={limit}", + f"{self.base_url}/{passport_holder_addresses[0]['address']}?limit={limit}", HTTP_AUTHORIZATION="Token " + scorer_api_key, ) response_data = response.json() diff --git a/api/v2/test/test_passport_submission.py b/api/v2/test/test_passport_submission.py index 0d49f37f..969d8a73 100644 --- a/api/v2/test/test_passport_submission.py +++ b/api/v2/test/test_passport_submission.py @@ -1,10 +1,8 @@ import copy -import json from datetime import datetime, timedelta, timezone from decimal import Decimal from unittest.mock import patch -import pytest from django.conf import settings from django.contrib.auth.models import User from django.test import Client, TransactionTestCase @@ -15,8 +13,7 @@ from ceramic_cache.models import CeramicCache from registry.models import Passport, Stamp from registry.tasks import score_passport -from registry.utils import get_signer, get_signing_message, verify_issuer -from registry.weight_models import WeightConfiguration, WeightConfigurationItem +from registry.utils import get_signing_message, verify_issuer web3 = Web3() web3.eth.account.enable_unaudited_hdwallet_features() @@ -346,15 +343,11 @@ def test_invalid_api_key(self): return_value=mock_passport, ) def test_signature_not_needed_by_default(self, aget_passport, validate_credential): - print( - "URL", f"{self.base_url}/{self.community.pk}/score/{self.account.address}" - ) 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}", ) - print("RESPONSE", response.json()) self.assertEqual(response.status_code, 200) def test_valid_issuer(self): @@ -372,10 +365,8 @@ def test_submit_passport_saves_analyitcs(self, aget_passport, validate_credentia response = self.client.get( f"{self.base_url}/{self.community.pk}/score/{self.account.address}", - headers={ - "content_type": "application/json", - "HTTP_AUTHORIZATION": f"Token {self.secret}", - }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {self.secret}", ) self.assertEqual(response.status_code, 200) @@ -393,10 +384,8 @@ def test_submit_passport(self, aget_passport, validate_credential): response = self.client.get( f"{self.base_url}/{self.community.pk}/score/{self.account.address}", - headers={ - "content_type": "application/json", - "HTTP_AUTHORIZATION": f"Token {self.secret}", - }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {self.secret}", ) self.assertEqual(response.status_code, 200) @@ -425,19 +414,10 @@ def test_submit_passport(self, aget_passport, validate_credential): def test_submitted_passport_has_lower_case_address_value( self, aget_passport, validate_credential ): - payload = { - "community": self.community.pk, - "address": self.account.address, - "signature": self.signed_message.signature.hex(), - "nonce": self.nonce, - } - response = self.client.get( f"{self.base_url}/{self.community.pk}/score/{self.account.address}", - headers={ - "content_type": "application/json", - "HTTP_AUTHORIZATION": f"Token {self.secret}", - }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {self.secret}", ) self.assertEqual(response.status_code, 200) @@ -445,32 +425,23 @@ def test_submitted_passport_has_lower_case_address_value( created_passport = Passport.objects.get(address=self.account.address.lower()) self.assertEqual(created_passport.address, self.account.address.lower()) - @patch("registry.atasks.validate_credential", side_effect=[[], []]) - @patch( - "registry.atasks.aget_passport", - return_value=mock_passport, - ) - def test_submit_passport_reused_nonce(self, aget_passport, validate_credential): - """Test that submitting a reused nonce results in rejection""" - - payload = { - "community": self.community.id, - "address": self.account.address, - "signature": self.signed_message.signature.hex(), - "nonce": self.nonce, - } + # @patch("registry.atasks.validate_credential", side_effect=[[], []]) + # @patch( + # "registry.atasks.aget_passport", + # return_value=mock_passport, + # ) + # def test_submit_passport_reused_nonce(self, aget_passport, validate_credential): + # """Test that submitting a reused nonce results in rejection""" - for _ in [1, 2]: - response = self.client.get( - f"{self.base_url}/{self.community.pk}/score/{self.account.address}", - headers={ - "content_type": "application/json", - "HTTP_AUTHORIZATION": f"Token {self.secret}", - }, - ) + # for _ in [1, 2]: + # 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.json(), {"detail": "Invalid nonce."}) - self.assertEqual(response.status_code, 400) + # self.assertEqual(response.json(), {"detail": "Invalid nonce."}) + # self.assertEqual(response.status_code, 400) @patch("registry.atasks.validate_credential", side_effect=[[], []]) @patch( @@ -480,10 +451,8 @@ def test_submit_passport_reused_nonce(self, aget_passport, validate_credential): def test_submitting_without_passport(self, aget_passport, validate_credential): response = self.client.get( f"{self.base_url}/{self.community.pk}/score/{self.account.address}", - headers={ - "content_type": "application/json", - "HTTP_AUTHORIZATION": f"Token {self.secret}", - }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {self.secret}", ) self.assertEqual(response.status_code, 200) @@ -493,11 +462,6 @@ def test_submitting_without_passport(self, aget_passport, validate_credential): self.assertEqual(len(all_passports), 1) self.assertEqual(all_passports[0].stamps.count(), 0) - response = self.client.get( - f"{self.base_url}/score/{self.community.id}/{self.account.address}", - HTTP_AUTHORIZATION=f"Token {self.secret}", - ) - assert response.status_code == 200 assert response.json() == { "address": self.account.address.lower(), @@ -525,13 +489,6 @@ def test_submit_passport_multiple_times( """Verify that submitting the same address multiple times only registers each stamp once, and gives back the same score""" # get_passport.return_value = mock_passport - payload = { - "community": self.community.id, - "address": self.account.address, - "signature": self.signed_message.signature.hex(), - "nonce": self.nonce, - } - expectedResponse = { "address": "0xb81c935d01e734b3d8bb233f5c4e1d72dbc30f6c", "evidence": None, @@ -557,10 +514,8 @@ def test_submit_passport_multiple_times( # 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}", - }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {self.secret}", ) self.assertEqual(response.status_code, 200) response_json = response.json() @@ -581,10 +536,8 @@ def test_submit_passport_multiple_times( response = self.client.get( f"{self.base_url}/{self.community.pk}/score/{self.account.address}", - headers={ - "content_type": "application/tson", - "HTTP_AUTHORIZATION": f"Token {self.secret}", - }, + content_type="application/tson", + HTTP_AUTHORIZATION=f"Token {self.secret}", ) self.assertEqual(response.status_code, 200) response_json = response.json() @@ -612,22 +565,6 @@ def test_submit_passport_multiple_times( stamp_google.hash, google_credential["credentialSubject"]["hash"] ) - def test_submit_passport_missing_community_and_scorer_id(self): - """ - Make sure that the community is required when submitting eth address - """ - - 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, 422) - - # Check if the passport data was saved to the database (data that we mock) - all_passports = list(Passport.objects.all()) - self.assertEqual(len(all_passports), 0) - def test_submit_passport_accepts_scorer_id(self): """ Make sure that the scorer_id is an acceptable parameter @@ -635,10 +572,8 @@ def test_submit_passport_accepts_scorer_id(self): response = self.client.get( f"{self.base_url}/{self.community.pk}/score/{self.account.address}", - headers={ - "content_type": "application/json", - "HTTP_AUTHORIZATION": f"Token {self.secret}", - }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {self.secret}", ) self.assertEqual(response.status_code, 200) @@ -659,10 +594,8 @@ def test_submit_passport_with_invalid_stamp( response = self.client.get( f"{self.base_url}/{self.community.pk}/score/{self.account.address}", - headers={ - "content_type": "application/json", - "HTTP_AUTHORIZATION": f"Token {self.secret}", - }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {self.secret}", ) self.assertEqual(response.status_code, 200) @@ -701,10 +634,8 @@ def test_submit_passport_with_expired_stamps( response = self.client.get( f"{self.base_url}/{self.community.pk}/score/{self.account.address}", - headers={ - "content_type": "application/json", - "HTTP_AUTHORIZATION": f"Token {self.secret}", - }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {self.secret}", ) self.assertEqual(response.status_code, 200) @@ -737,10 +668,8 @@ def test_that_community_is_associated_with_passport( response = self.client.get( f"{self.base_url}/{self.community.pk}/score/{self.account.address}", - headers={ - "content_type": "application/json", - "HTTP_AUTHORIZATION": f"Token {self.secret}", - }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {self.secret}", ) self.assertEqual(response.status_code, 200) @@ -766,10 +695,8 @@ def test_that_only_owned_communities_can_submit_passport(self): response = self.client.get( f"{self.base_url}/{self.community2.pk}/score/{self.account.address}", - headers={ - "content_type": "application/json", - "HTTP_AUTHORIZATION": f"Token {self.secret}", - }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {self.secret}", ) self.assertEqual(response.status_code, 404) diff --git a/api/v2/test/test_ratelimit.py b/api/v2/test/test_ratelimit.py index 7b02587e..d26febf7 100644 --- a/api/v2/test/test_ratelimit.py +++ b/api/v2/test/test_ratelimit.py @@ -34,7 +34,7 @@ def test_rate_limit_from_db_is_applied_for_api_key(scorer_api_key): # 125 successfull calls, and then get a 429 error for _ in range(3): response = client.get( - "/registry/signing-message", + "/v2/signing-message", # HTTP_X_API_KEY must spelled exactly as this because it # will not be converted to HTTP_X_API_KEY by Django Test Client **{"HTTP_X_API_KEY": scorer_api_key}, @@ -43,7 +43,7 @@ def test_rate_limit_from_db_is_applied_for_api_key(scorer_api_key): assert response.status_code == 200 response = client.get( - "/registry/signing-message", + "/v2/signing-message", # HTTP_X_API_KEY must spelled exactly as this because it # will not be converted to HTTP_X_API_KEY by Django Test Client **{"HTTP_X_API_KEY": scorer_api_key}, @@ -82,33 +82,28 @@ def test_rate_limit_from_db_is_applied_for_token(scorer_api_key): # The rate limit is overridden to 3 calls/30 seconds for this APIKey for _ in range(3): response = client.get( - "/registry/signing-message", + "/v2/signing-message", HTTP_AUTHORIZATION="Token " + scorer_api_key, ) assert response.status_code == 200 response = client.get( - "/registry/signing-message", + "/v2/signing-message", HTTP_AUTHORIZATION="Token " + scorer_api_key, ) assert response.status_code == 429 -# Ensure that all functions that rquire rate limiting are tested +# Ensure that all functions that require rate limiting are tested @pytest.fixture( params=[ - ("get", "/registry/signing-message", None), - ( - "post", - "/registry/submit-passport", - { - "community": "1", - "address": "0x1234", - }, - ), - ("get", "/registry/score/3", None), + ("get", "/v2/signing-message", None), + ("get", "/v2/stamps/1/score/0x1234", None), + ("get", "/v2/stamps/metadata", None), + ("get", "/v2/stamps/0x1234", None), + ("get", "/v2/models/score/0x1234", None), ] ) def api_path_that_requires_rate_limit(request): @@ -151,14 +146,14 @@ def test_no_rate_limit_for_none(unlimited_scorer_api_key): # The rate limit is overridden to 3 calls/30 seconds for this APIKey for _ in range(3): response = client.get( - "/registry/signing-message", + "/v2/signing-message", HTTP_AUTHORIZATION="Token " + unlimited_scorer_api_key, ) assert response.status_code == 200 response = client.get( - "/registry/signing-message", + "/v2/signing-message", HTTP_AUTHORIZATION="Token " + unlimited_scorer_api_key, ) diff --git a/api/v2/test/test_read_replica_usage.py b/api/v2/test/test_read_replica_usage.py deleted file mode 100644 index 14ee1ca0..00000000 --- a/api/v2/test/test_read_replica_usage.py +++ /dev/null @@ -1,87 +0,0 @@ -from unittest.mock import Mock, patch - -import pytest -from django.test import Client - -import registry.api.v1 -from account.models import Community - -pytestmark = pytest.mark.django_db - - -class TestPassportGetScore: - def test_get_scores_is_using_read_replica_in_v1( - self, scorer_api_key, scorer_community, settings - ): - settings.REGISTRY_API_READ_DB = "read_replica_0" - - with patch( - "registry.api.v1.ScoreFilter", - return_value=Mock( - qs=[ - {"passport": "1", "address": "0x1"}, - {"passport": "2", "address": "0x2"}, - {"passport": "3", "address": "0x3"}, - ], - ), - ): - with patch.object( - registry.api.v1.Community, - "objects", - using=Mock(return_value=Community.objects), - ) as mock_community_objects: - with patch.object( - registry.api.v1.Score, "objects", return_value=Mock() - ) as mock_score_objects: - client = Client() - response = client.get( - f"/registry/score/{scorer_community.id}?limit={100}&offset={0}", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - assert response.status_code == 200 - assert mock_community_objects.using.call_count == 2 - assert mock_community_objects.using.mock_calls[0].args == ( - "read_replica_0", - ) - assert mock_community_objects.using.mock_calls[1].args == ( - "read_replica_0", - ) - mock_score_objects.using.assert_called_once_with("read_replica_0") - - def test_get_scores_is_using_read_replica_in_v2( - self, scorer_api_key, scorer_community, settings - ): - settings.REGISTRY_API_READ_DB = "read_replica_0" - with patch( - "registry.api.v1.ScoreFilter", - return_value=Mock( - qs=[ - {"passport": "1", "address": "0x1"}, - {"passport": "2", "address": "0x2"}, - {"passport": "3", "address": "0x3"}, - ], - ), - ): - with patch.object( - registry.api.v1.Community, - "objects", - using=Mock(return_value=Community.objects), - ) as mock_community_objects: - with patch.object( - registry.api.v1.Score, "objects", return_value=Mock() - ) as mock_score_objects: - client = Client() - response = client.get( - f"/registry/score/{scorer_community.id}?limit={100}&offset={0}", - HTTP_AUTHORIZATION="Token " + scorer_api_key, - ) - - assert response.status_code == 200 - assert mock_community_objects.using.call_count == 2 - assert mock_community_objects.using.mock_calls[0].args == ( - "read_replica_0", - ) - assert mock_community_objects.using.mock_calls[1].args == ( - "read_replica_0", - ) - mock_score_objects.using.assert_called_once_with("read_replica_0") diff --git a/api/v2/test/test_score_passport.py b/api/v2/test/test_score_passport.py deleted file mode 100644 index fbbfc009..00000000 --- a/api/v2/test/test_score_passport.py +++ /dev/null @@ -1,489 +0,0 @@ -import copy -import json -import re -from datetime import datetime, timedelta, timezone -from decimal import Decimal -from unittest.mock import call, patch - -from django.conf import settings -from django.contrib.auth import get_user_model -from django.test import Client, TransactionTestCase -from web3 import Web3 - -from account.models import Account, AccountAPIKey, Community -from registry.api.v1 import SubmitPassportPayload, a_submit_passport, get_score -from registry.models import Event, HashScorerLink, Passport, Score, Stamp -from registry.tasks import score_passport_passport, score_registry_passport - -User = get_user_model() -my_mnemonic = settings.TEST_MNEMONIC -web3 = Web3() -web3.eth.account.enable_unaudited_hdwallet_features() - -now = datetime.now(timezone.utc) -expiration_dates = [ - now + timedelta(days=2), - now + timedelta(days=1), - now + timedelta(days=3), -] - -mock_passport_data = { - "stamps": [ - { - "provider": "Ens", - "credential": { - "type": ["VerifiableCredential"], - "credentialSubject": { - "id": settings.TRUSTED_IAM_ISSUERS[0], - "hash": "v0.0.0:1Vzw/OyM9CBUkVi/3mb+BiwFnHzsSRZhVH1gaQIyHvM=", - "provider": "Ens", - }, - "issuer": settings.TRUSTED_IAM_ISSUERS[0], - "issuanceDate": (expiration_dates[0] - timedelta(days=30)).isoformat(), - "expirationDate": expiration_dates[0].isoformat(), - }, - }, - { - "provider": "Google", - "credential": { - "type": ["VerifiableCredential"], - "credentialSubject": { - "id": settings.TRUSTED_IAM_ISSUERS[0], - "hash": "0x88888", - "provider": "Google", - }, - "issuer": settings.TRUSTED_IAM_ISSUERS[0], - "issuanceDate": (expiration_dates[1] - timedelta(days=30)).isoformat(), - "expirationDate": expiration_dates[1].isoformat(), - }, - }, - { - "provider": "Gitcoin", - "credential": { - "type": ["VerifiableCredential"], - "credentialSubject": { - "id": settings.TRUSTED_IAM_ISSUERS[0], - "hash": "0x45678", - "provider": "Gitcoin", - }, - "issuer": settings.TRUSTED_IAM_ISSUERS[0], - "issuanceDate": (expiration_dates[2] - timedelta(days=30)).isoformat(), - "expirationDate": expiration_dates[2].isoformat(), - }, - }, - ] -} - - -def mock_validate(*args, **kwargs): - return [] - - -class TestScorePassportTestCase(TransactionTestCase): - def setUp(self): - # Just create 1 user, to make sure the user id is different than account id - # This is to catch errors like the one where the user id is the same as the account id, and - # we query the account id by the user id - self.user = User.objects.create(username="admin", password="12345") - - account = web3.eth.account.from_mnemonic( - my_mnemonic, account_path="m/44'/60'/0'/0/0" - ) - account_2 = web3.eth.account.from_mnemonic( - my_mnemonic, account_path="m/44'/60'/0'/0/1" - ) - account_3 = web3.eth.account.from_mnemonic( - my_mnemonic, account_path="m/44'/60'/0'/0/2" - ) - self.account = account - self.account_2 = account_2 - self.account_3 = account_3 - - self.user_account = Account.objects.create( - user=self.user, address=account.address - ) - - AccountAPIKey.objects.create_key( - account=self.user_account, name="Token for user 1" - ) - - self.community = Community.objects.create( - name="My Community", - description="My Community description", - account=self.user_account, - ) - - self.client = Client() - - def test_no_passport(self): - with patch("registry.atasks.aget_passport", return_value=None): - score_passport_passport(self.community.pk, self.account.address) - - passport = Passport.objects.get( - address=self.account.address, community_id=self.community.pk - ) - self.assertEqual(passport.stamps.all().count(), 0) - - score = Score.objects.get(passport=passport) - self.assertEqual(score.score, None) - self.assertEqual(score.last_score_timestamp, None) - self.assertEqual(score.evidence, None) - self.assertEqual(score.status, Score.Status.ERROR) - self.assertEqual(score.error, "No Passport found for this address.") - self.assertEqual(score.expiration_date, None) - - def test_score_checksummed_address(self): - address = self.account.address - assert re.search("[A-Z]", address) is not None - - self._score_address(address) - - def test_score_nonchecksummed_address(self): - address = self.account.address.lower() - assert re.search("[A-Z]", address) is None - - self._score_address(address) - - async def _score_address(self, address): - class MockRequest: - def __init__(self, account): - self.auth = account - self.api_key = account.api_keys.all()[0] - - mock_request = MockRequest(self.user_account) - - with patch("registry.api.v1.score_passport_passport.delay", return_value=None): - await a_submit_passport( - mock_request, - SubmitPassportPayload( - address=address, - community=self.community.pk, - ), - ) - - with patch("registry.atasks.aget_passport", return_value=mock_passport_data): - with patch( - "registry.atasks.validate_credential", side_effect=mock_validate - ): - score_passport_passport(self.community.pk, address) - - Passport.objects.get(address=address, community_id=self.community.pk) - - score = get_score(mock_request, address, self.community.pk) - assert Decimal(score.score) == Decimal("3") - assert score.status == "DONE" - - def test_cleaning_stale_stamps(self): - passport, _ = Passport.objects.update_or_create( - address=self.account.address, - community_id=self.community.pk, - requires_calculation=True, - ) - - Stamp.objects.filter(passport=passport).delete() - - Stamp.objects.update_or_create( - hash="0x1234", - passport=passport, - defaults={"provider": "Gitcoin", "credential": "{}"}, - ) - - assert Stamp.objects.filter(passport=passport).count() == 1 - - with patch("registry.atasks.aget_passport", return_value=mock_passport_data): - with patch( - "registry.atasks.validate_credential", side_effect=mock_validate - ): - score_passport_passport(self.community.pk, self.account.address) - - my_stamps = Stamp.objects.filter(passport=passport) - assert len(my_stamps) == 3 - - gitcoin_stamps = my_stamps.filter(provider="Gitcoin") - assert len(gitcoin_stamps) == 1 - assert gitcoin_stamps[0].hash == "0x45678" - - def test_deduplication_of_scoring_tasks(self): - """ - Test that when multiple tasks are scheduled for the same Passport, only one of them will execute the scoring calculation, and it will also reset the requires_calculation to False - """ - passport, _ = Passport.objects.update_or_create( - address=self.account.address, - community_id=self.community.pk, - requires_calculation=True, - ) - - Stamp.objects.filter(passport=passport).delete() - - Stamp.objects.update_or_create( - hash="0x1234", - passport=passport, - defaults={"provider": "Gitcoin", "credential": "{}"}, - ) - - assert Stamp.objects.filter(passport=passport).count() == 1 - - with patch("registry.atasks.aget_passport", return_value=mock_passport_data): - with patch( - "registry.atasks.validate_credential", side_effect=mock_validate - ): - with patch("registry.tasks.log.info") as mock_log: - # Call score_passport_passport twice, but only one of them should actually execute the scoring calculation - score_passport_passport(self.community.pk, self.account.address) - score_passport_passport(self.community.pk, self.account.address) - - expected_call = call( - "Passport no passport found for address='%s', community_id='%s' that has requires_calculation=True or None", - self.account.address, - self.community.pk, - ) - assert mock_log.call_args_list.count(expected_call) == 1 - assert ( - Passport.objects.get(pk=passport.pk).requires_calculation - is False - ) - - def test_lifo_duplicate_stamp_scoring(self): - passport, _ = Passport.objects.update_or_create( - address=self.account.address, - community_id=self.community.pk, - requires_calculation=True, - ) - - passport_for_already_existing_stamp, _ = Passport.objects.update_or_create( - address=self.account_2.address, - community_id=self.community.pk, - requires_calculation=True, - ) - - passport_with_duplicates, _ = Passport.objects.update_or_create( - address=self.account_3.address, - community_id=self.community.pk, - requires_calculation=True, - ) - - already_existing_stamp = { - "provider": "POAP", - "credential": { - "type": ["VerifiableCredential"], - "credentialSubject": { - "id": settings.TRUSTED_IAM_ISSUERS[0], - "hash": "0x1111", - "provider": "Gitcoin", - }, - "issuer": settings.TRUSTED_IAM_ISSUERS[0], - "issuanceDate": "2023-02-06T23:22:58.848Z", - "expirationDate": "2099-02-06T23:22:58.848Z", - }, - } - - Stamp.objects.update_or_create( - hash=already_existing_stamp["credential"]["credentialSubject"]["hash"], - passport=passport_for_already_existing_stamp, - defaults={ - "provider": already_existing_stamp["provider"], - "credential": json.dumps(already_existing_stamp["credential"]), - }, - ) - - HashScorerLink.objects.create( - hash=already_existing_stamp["credential"]["credentialSubject"]["hash"], - address=passport_for_already_existing_stamp.address, - community=passport_for_already_existing_stamp.community, - expires_at=already_existing_stamp["credential"]["expirationDate"], - ) - - mock_passport_data_with_duplicates = { - "stamps": [ - mock_passport_data["stamps"][0], - already_existing_stamp, - { - "provider": "Google", - "credential": { - "type": ["VerifiableCredential"], - "credentialSubject": { - "id": settings.TRUSTED_IAM_ISSUERS[0], - "hash": "0x12121", - "provider": "Google", - }, - "issuer": settings.TRUSTED_IAM_ISSUERS[0], - "issuanceDate": "2023-02-06T23:22:58.848Z", - "expirationDate": "2099-02-06T23:22:58.848Z", - }, - }, - ] - } - - with patch("registry.atasks.validate_credential", side_effect=mock_validate): - # Score original passport - with patch( - "registry.atasks.aget_passport", return_value=mock_passport_data - ): - score_registry_passport(self.community.pk, passport.address) - - assert ( - Event.objects.filter(action=Event.Action.LIFO_DEDUPLICATION).count() - == 0 - ) - - # Score passport with duplicates (one duplicate from original passport, - # one duplicate from already existing stamp) - with patch( - "registry.atasks.aget_passport", - return_value=mock_passport_data_with_duplicates, - ): - score_registry_passport( - self.community.pk, passport_with_duplicates.address - ) - - original_stamps = Stamp.objects.filter(passport=passport) - assert len(original_stamps) == 3 - - assert (Score.objects.get(passport=passport).score) == Decimal( - "0.933000000" - ) - - assert ( - Event.objects.filter(action=Event.Action.LIFO_DEDUPLICATION).count() - == 2 - ) - - deduplicated_stamps = Stamp.objects.filter( - passport=passport_with_duplicates - ) - assert len(deduplicated_stamps) == 1 - - assert ( - Score.objects.get(passport=passport_with_duplicates).score - ) == Decimal("0.525000000") - - passport.requires_calculation = True - passport.save() - # Re-score original passport, just to make sure it doesn't change - with patch( - "registry.atasks.aget_passport", return_value=mock_passport_data - ): - score_registry_passport(self.community.pk, passport.address) - - assert (Score.objects.get(passport=passport).score) == Decimal( - "0.933000000" - ) - assert ( - Event.objects.filter(action=Event.Action.LIFO_DEDUPLICATION).count() - == 2 - ) - - def test_score_events(self): - count = Event.objects.filter(action=Event.Action.SCORE_UPDATE).count() - - Score.objects.create( - passport=Passport.objects.create( - address=self.account.address, community_id=self.community.pk - ), - score=1, - status="DONE", - ) - - assert ( - Event.objects.filter(action=Event.Action.SCORE_UPDATE).count() == count + 1 - ) - - def test_score_expiration_time(self): - """ - Test that the score expiration time is correctly calculated and stored - """ - passport, _ = Passport.objects.update_or_create( - address=self.account.address, - community_id=self.community.pk, - requires_calculation=True, - ) - - Stamp.objects.filter(passport=passport).delete() - - expected_score_expiration = min(expiration_dates) - - for idx, credential in enumerate(mock_passport_data["stamps"]): - Stamp.objects.update_or_create( - hash=f"0x1234{idx}", - passport=passport, - defaults={ - "provider": credential["provider"], - "credential": credential, - }, - ) - - assert Stamp.objects.filter(passport=passport).count() == len(expiration_dates) - - with patch("registry.atasks.aget_passport", return_value=mock_passport_data): - with patch( - "registry.atasks.validate_credential", side_effect=mock_validate - ): - score_passport_passport(self.community.pk, self.account.address) - - score = Score.objects.get(passport=passport) - - assert score.expiration_date == expected_score_expiration - - def test_score_expiration_time_when_all_stamps_expired(self): - """ - Test that the score expiration time is set to None when recalculating the score when all stamps are expired - """ - passport, _ = Passport.objects.update_or_create( - address=self.account.address, - community_id=self.community.pk, - requires_calculation=True, - ) - - Stamp.objects.filter(passport=passport).delete() - - expected_score_expiration = min(expiration_dates) - - ############################################################################################# - # Step 1: calculate the score as usual, with all stamps valid - ############################################################################################# - for idx, credential in enumerate(mock_passport_data["stamps"]): - Stamp.objects.update_or_create( - hash=f"0x1234{idx}", - passport=passport, - defaults={ - "provider": credential["provider"], - "credential": credential, - }, - ) - - assert Stamp.objects.filter(passport=passport).count() == len(expiration_dates) - - with patch("registry.atasks.aget_passport", return_value=mock_passport_data): - with patch( - "registry.atasks.validate_credential", side_effect=mock_validate - ): - score_passport_passport(self.community.pk, self.account.address) - - score = Score.objects.get(passport=passport) - - assert score.expiration_date == expected_score_expiration - - ############################################################################################# - # Step 2: calculate the score as usual, with a passport where all stamps are expired - ############################################################################################# - passport.requires_calculation = True - passport.save() - - mock_passport_data_expired = copy.deepcopy(mock_passport_data) - for stamp in mock_passport_data_expired["stamps"]: - stamp["credential"]["expirationDate"] = ( - now - timedelta(days=1) - ).isoformat() - - with patch( - "registry.atasks.aget_passport", return_value=mock_passport_data_expired - ): - with patch( - "registry.atasks.validate_credential", side_effect=mock_validate - ): - score_passport_passport(self.community.pk, self.account.address) - - score = Score.objects.get(passport=passport) - - assert score.score == 0 - assert score.expiration_date is None diff --git a/api/v2/test/test_signing_message.py b/api/v2/test/test_signing_message.py index cf88ed05..cc62104b 100644 --- a/api/v2/test/test_signing_message.py +++ b/api/v2/test/test_signing_message.py @@ -32,7 +32,7 @@ def setUp(self): def test_get_signing_message(self): response = self.client.get( - "/registry/signing-message", + "/v2/signing-message", content_type="application/json", HTTP_AUTHORIZATION=f"Token {self.secret}", )