11import datetime
2- import hashlib
32import json
3+ from collections .abc import Generator
4+ from contextlib import ExitStack , contextmanager
5+ from logging import Logger
46from pathlib import Path
57from typing import Any
8+ from unittest import mock
69
710import uvicorn
811from httpx import URL
912
1013from src .chain .kvstore import register_in_kvstore
1114from src .core .config import Config
15+ from src .db import SessionLocal
1216from src .services import cloud
17+ from src .services import cvat as cvat_service
1318from src .services .cloud import BucketAccessInfo
14- from src .utils .logging import get_function_logger
19+ from src .utils .logging import format_sequence , get_function_logger
1520
1621
17- def apply_local_development_patches ():
18- """
19- Applies local development patches to avoid manual source code modification.:
20- - Overrides `EscrowUtils.get_escrow` to return local escrow data with mock values if the escrow
21- address matches a local manifest.
22- - Loads local manifest files from cloud storage into `LOCAL_MANIFEST_FILES`.
23- - Disables address validation by overriding `validate_address`.
24- - Replaces `validate_oracle_webhook_signature` with a lenient version that uses
25- partial signature parsing.
26- - Generates ECDSA keys if not already present for local JWT signing,
27- and sets the public key in `Config.human_app_config`.
28- """
29- import src .handlers .job_creation
30-
31- original_make_cvat_cloud_storage_params = (
32- src .handlers .job_creation ._make_cvat_cloud_storage_params
22+ @contextmanager
23+ def _mock_cvat_cloud_storage_params (logger : Logger ) -> Generator [None , None , None ]:
24+ from src .handlers .job_creation import (
25+ _make_cvat_cloud_storage_params as original_make_cvat_cloud_storage_params ,
3326 )
3427
3528 def patched_make_cvat_cloud_storage_params (bucket_info : BucketAccessInfo ) -> dict :
3629 original_host_url = bucket_info .host_url
3730
3831 if Config .development_config .cvat_in_docker :
3932 bucket_info .host_url = str (
40- URL (original_host_url ).copy_with (host = Config .development_config .cvat_local_host )
33+ URL (original_host_url ).copy_with (
34+ host = Config .development_config .exchange_oracle_host
35+ )
4136 )
4237 logger .info (
4338 f"DEV: Changed { original_host_url } to { bucket_info .host_url } for CVAT storage"
4439 )
40+
4541 try :
4642 return original_make_cvat_cloud_storage_params (bucket_info )
4743 finally :
4844 bucket_info .host_url = original_host_url
4945
50- src .handlers .job_creation ._make_cvat_cloud_storage_params = (
51- patched_make_cvat_cloud_storage_params
52- )
46+ with mock .patch (
47+ "src.handlers.job_creation._make_cvat_cloud_storage_params" ,
48+ patched_make_cvat_cloud_storage_params ,
49+ ):
50+ yield
5351
54- def prepare_signed_message (
55- escrow_address ,
56- chain_id ,
57- message : str | None = None ,
58- body : dict | None = None ,
59- ) -> tuple [None , str ]:
60- digest = hashlib .sha256 (
61- (escrow_address + ":" .join (map (str , (chain_id , message , body )))).encode ()
62- ).hexdigest ()
63- signature = f"{ OracleWebhookTypes .recording_oracle } :{ digest } "
64- logger .info (f"DEV: Generated patched signature { signature } " )
65- return None , signature
66-
67- src .utils .webhooks .prepare_signed_message = prepare_signed_message
6852
53+ @contextmanager
54+ def _mock_get_manifests_from_minio (logger : Logger ) -> Generator [None , None , None ]:
6955 from human_protocol_sdk .constants import ChainId
7056 from human_protocol_sdk .escrow import EscrowData , EscrowUtils
7157
72- logger = get_function_logger (apply_local_development_patches .__name__ )
73-
7458 minio_client = cloud .make_client (BucketAccessInfo .parse_obj (Config .storage_config ))
59+ original_get_escrow = EscrowUtils .get_escrow
7560
76- def get_local_escrow (chain_id : int , escrow_address : str ) -> EscrowData :
77- possible_manifest_name = escrow_address .split (":" )[0 ]
78- local_manifests = minio_client .list_files (bucket = "manifests" )
79- logger .info (f"DEV: Local manifests: { local_manifests } " )
80- if possible_manifest_name in local_manifests :
81- logger .info (f"DEV: Using local manifest { escrow_address } " )
82- return EscrowData (
83- chain_id = ChainId (chain_id ),
84- id = "test" ,
85- address = escrow_address ,
86- amount_paid = 10 ,
87- balance = 10 ,
88- count = 1 ,
89- factory_address = "" ,
90- launcher = "" ,
91- status = "Pending" ,
92- token = "HMT" , # noqa: S106
93- total_funded_amount = 10 ,
94- created_at = datetime .datetime (2023 , 1 , 1 , tzinfo = datetime .timezone .utc ),
95- manifest_url = (
96- f"http://{ Config .storage_config .endpoint_url } /manifests/{ possible_manifest_name } "
97- ),
61+ def patched_get_escrow (chain_id : int , escrow_address : str ) -> EscrowData :
62+ minio_manifests = minio_client .list_files (bucket = "manifests" )
63+ logger .debug (f"DEV: Local manifests: { format_sequence (minio_manifests )} " )
64+
65+ candidate_files = [fn for fn in minio_manifests if f"{ escrow_address } .json" in fn ]
66+ if not candidate_files :
67+ return original_get_escrow (ChainId (chain_id ), escrow_address )
68+ elif len (candidate_files ) != 1 :
69+ raise Exception (
70+ "Can't select local manifest to be used for escrow '{}'"
71+ " - several manifests math: {}" .format (
72+ escrow_address , format_sequence (candidate_files )
73+ )
9874 )
99- return original_get_escrow (ChainId (chain_id ), escrow_address )
10075
101- original_get_escrow = EscrowUtils .get_escrow
102- EscrowUtils .get_escrow = get_local_escrow
76+ manifest_file = candidate_files [0 ]
77+ escrow = EscrowData (
78+ chain_id = ChainId (chain_id ),
79+ id = "test" ,
80+ address = escrow_address ,
81+ amount_paid = 10 ,
82+ balance = 10 ,
83+ count = 1 ,
84+ factory_address = "" ,
85+ launcher = "" ,
86+ status = "Pending" ,
87+ token = "HMT" , # noqa: S106
88+ total_funded_amount = 10 ,
89+ created_at = datetime .datetime (2023 , 1 , 1 , tzinfo = datetime .timezone .utc ),
90+ manifest_url = (f"http://{ Config .storage_config .endpoint_url } /manifests/{ manifest_file } " ),
91+ )
92+
93+ logger .info (f"DEV: Using local manifest '{ manifest_file } ' for escrow '{ escrow_address } '" )
94+ return escrow
95+
96+ with mock .patch .object (EscrowUtils , "get_escrow" , patched_get_escrow ):
97+ yield
98+
99+
100+ @contextmanager
101+ def _mock_webhook_signature_checking (_ : Logger ) -> Generator [None , None , None ]:
102+ """
103+ Allows to receive webhooks from other services:
104+ - from launcher - with signature "job_launcher<number>"
105+ - from recording oracle -
106+ encoded with Config.localhost.recording_oracle_address wallet address
107+ or signature "recording_oracle<number>"
108+ - from reputation oracle -
109+ encoded with Config.localhost.reputation_oracle_address wallet address
110+ or signature "reputation_oracle<number>"
111+ """
103112
104- import src .schemas .webhook
113+ from src .chain .escrow import (
114+ get_available_webhook_types as original_get_available_webhook_types ,
115+ )
105116 from src .core .types import OracleWebhookTypes
117+ from src .validators .signature import (
118+ validate_oracle_webhook_signature as original_validate_oracle_webhook_signature ,
119+ )
106120
107- src .schemas .webhook .validate_address = lambda x : x
121+ async def patched_validate_oracle_webhook_signature (request , signature , webhook ):
122+ for webhook_type in OracleWebhookTypes :
123+ if signature .startswith (webhook_type .value .lower ()):
124+ return webhook_type
125+
126+ return await original_validate_oracle_webhook_signature (request , signature , webhook )
127+
128+ def patched_get_available_webhook_types (chain_id , escrow_address ):
129+ d = dict (original_get_available_webhook_types (chain_id , escrow_address ))
130+ d [Config .localhost .recording_oracle_address .lower ()] = OracleWebhookTypes .recording_oracle
131+ d [Config .localhost .reputation_oracle_address .lower ()] = OracleWebhookTypes .reputation_oracle
132+ return d
133+
134+ with (
135+ mock .patch ("src.schemas.webhook.validate_address" , lambda x : x ),
136+ mock .patch (
137+ "src.validators.signature.get_available_webhook_types" ,
138+ patched_get_available_webhook_types ,
139+ ),
140+ mock .patch (
141+ "src.endpoints.webhook.validate_oracle_webhook_signature" ,
142+ patched_validate_oracle_webhook_signature ,
143+ ),
144+ ):
145+ yield
146+
147+
148+ @contextmanager
149+ def _mock_endpoint_auth (logger : Logger ) -> Generator [None , None , None ]:
150+ """
151+ Allows simplified authentication:
152+ - Bearer {"wallet_address": "...", "email": "..."}
153+ - Bearer {"role": "human_app"}
154+ """
108155
109- async def lenient_validate_oracle_webhook_signature (request , signature , webhook ):
110- from src .validators .signature import validate_oracle_webhook_signature
156+ from src .endpoints .authentication import HUMAN_APP_ROLE , TokenAuthenticator
111157
112- try :
113- parsed_type = OracleWebhookTypes (signature .split (":" )[0 ])
114- logger .info (f"DEV: Recovered { parsed_type } from the signature { signature } " )
115- except (ValueError , TypeError ):
116- return await validate_oracle_webhook_signature (request , signature , webhook )
158+ original_decode_token = TokenAuthenticator ._decode_token
117159
118- import src .endpoints .webhook
160+ def decode_plain_json_token (self , token ) -> dict [str , Any ]:
161+ try :
162+ token_data = json .loads (token )
119163
120- src .endpoints .webhook .validate_oracle_webhook_signature = (
121- lenient_validate_oracle_webhook_signature
122- )
164+ if (user_wallet := token_data .get ("wallet_address" )) and not token_data .get ("email" ):
165+ with SessionLocal .begin () as session :
166+ user = cvat_service .get_user_by_id (session , user_wallet )
167+ if not user :
168+ raise Exception (f"Could not find user with wallet address '{ user_wallet } '" )
123169
124- import src . endpoints . authentication
170+ token_data [ "email" ] = user . cvat_email
125171
126- original_decode_token = src .endpoints .authentication .TokenAuthenticator ._decode_token
172+ if token_data .get ("role" ) == HUMAN_APP_ROLE :
173+ token_data ["wallet_address" ] = None
174+ token_data ["email" ] = ""
127175
128- def decode_plain_json_token (self , token ) -> dict [str , Any ]:
129- """
130- Allows Authentication: Bearer {"wallet_address": "...", "email": "..."}
131- """
132- try :
133- decoded = json .loads (token )
134- logger .info (f"DEV: Decoded plain JSON auth token: { decoded } " )
176+ logger .info (f"DEV: Decoded plain JSON auth token: { token_data } " )
177+ return token_data
135178 except (ValueError , TypeError ):
136179 return original_decode_token (self , token )
137180
138- src .endpoints .authentication .TokenAuthenticator ._decode_token = decode_plain_json_token
181+ with mock .patch .object (TokenAuthenticator , "_decode_token" , decode_plain_json_token ):
182+ yield
183+
184+
185+ @contextmanager
186+ def _mock_human_app_keys (_ : Logger ) -> Generator [None , None , None ]:
187+ "Creates or uses local Human App JWT keys"
139188
140189 from tests .api .test_exchange_api import generate_ecdsa_keys
141190
142191 # generating keys for local development
143192 repo_root = Path (__file__ ).parent
144- human_app_private_key_file , human_app_public_key_file = (
145- repo_root / "human_app_private_key.pem" ,
146- repo_root / "human_app_public_key.pem" ,
147- )
193+ dev_dir = repo_root / "dev"
194+ dev_dir .mkdir (exist_ok = True )
195+
196+ human_app_private_key_file = dev_dir / "human_app_private_key.pem"
197+ human_app_public_key_file = dev_dir / "human_app_public_key.pem"
198+
148199 if not (human_app_public_key_file .exists () and human_app_private_key_file .exists ()):
149200 private_key , public_key = generate_ecdsa_keys ()
150201 human_app_private_key_file .write_text (private_key )
@@ -154,20 +205,47 @@ def decode_plain_json_token(self, token) -> dict[str, Any]:
154205
155206 Config .human_app_config .jwt_public_key = public_key
156207
157- logger .warning ("DEV: Local development patches applied." )
208+ yield
209+
210+
211+ @contextmanager
212+ def apply_local_development_patches () -> Generator [None , None , None ]:
213+ """
214+ Applies local development patches to avoid manual source code modification
215+ """
216+
217+ logger = get_function_logger (apply_local_development_patches .__name__ )
218+
219+ logger .warning ("DEV: Applying local development patches" )
220+
221+ with ExitStack () as es :
222+ for mock_callback in (
223+ _mock_cvat_cloud_storage_params ,
224+ _mock_get_manifests_from_minio ,
225+ _mock_webhook_signature_checking ,
226+ _mock_endpoint_auth ,
227+ _mock_human_app_keys ,
228+ ):
229+ logger .warning (f"DEV: applying patch { mock_callback .__name__ } ..." )
230+ es .enter_context (mock_callback (logger ))
231+
232+ logger .warning ("DEV: Local development patches applied." )
233+
234+ yield
158235
159236
160237if __name__ == "__main__" :
161- is_dev = Config .environment == "development"
162- if is_dev :
163- apply_local_development_patches ()
164-
165- Config .validate ()
166- register_in_kvstore ()
167-
168- uvicorn .run (
169- app = "src:app" ,
170- host = "0.0.0.0" , # noqa: S104
171- port = int (Config .port ),
172- workers = Config .workers_amount ,
173- )
238+ with ExitStack () as es :
239+ is_dev = Config .environment == "development"
240+ if is_dev :
241+ es .enter_context (apply_local_development_patches ())
242+
243+ Config .validate ()
244+ register_in_kvstore ()
245+
246+ uvicorn .run (
247+ app = "src:app" ,
248+ host = "0.0.0.0" , # noqa: S104
249+ port = int (Config .port ),
250+ workers = Config .workers_amount ,
251+ )
0 commit comments