"""Tests for POST /transferwise/webhook.""" import base64 import datetime import json from unittest.mock import patch from Crypto.Hash import SHA from Crypto.PublicKey import RSA from Crypto.Signature import pkcs1_15 from oto import status from collaborator import config from collaborator.constants import transferwise_api from collaborator.constants import transferwise_transfer as transfer from collaborator.models.rds.transaction_persister import TransactionPersister from tests.testutils import db _TEST_KEY = RSA.generate(2048) _TEST_PUBLIC_KEY = _TEST_KEY.publickey().export_key() def _make_webhook_payload(previous_status, current_status, occurred_at): return json.dumps( { "data": { "resource": { "id": 1, # transfer ID "profile_id": 4497, "account_id": 148309238, "type": "transfer", }, "current_state": current_status, "previous_state": previous_status, "occurred_at": occurred_at, }, "subscription_id": "a75e49ff-7478-4824-9918-e33387c7670a", "event_type": "transfers#state-change", "schema_version": "2.0.0", "sent_at": occurred_at, } ) @patch.object(config, "TRANSFERWISE_PUBLIC_KEY", _TEST_PUBLIC_KEY) @db.test_schema_default_seed def test_webhook_verification_state_change(test_client): """Test handling a verification state change event.""" # Payload taken from: # https://transferwise.github.io/api-docs-partners/#webhook-events-verification-state-change-event payload = json.dumps( { "data": { "current_state": "verified", "occurred_at": "2019-12-23T15:04:54Z", "resource": {"id": 12341234, "type": "profile"}, }, "subscription_id": "1d98f73f-360e-41db-97df-4c63a1f7b2be", "event_type": "profiles#verification-state-change", "schema_version": "2.0.0", "sent_at": "2019-12-23T15:04:54Z", } ) signature = pkcs1_15.new(_TEST_KEY).sign(SHA.new(payload.encode())) result = test_client.post( "/transferwise/webhook", data=payload, headers={ "Content-Type": "application/json", transferwise_api.WEBHOOK_SIGNATURE_HEADER: base64.b64encode( signature ).decode("utf-8"), }, ) profiles = db.get_transferwise_profiles(12341234) assert result.status_code == status.OK for profile in profiles: assert profile["status"] == "VERIFIED" @patch.object(config, "TRANSFERWISE_PUBLIC_KEY", _TEST_PUBLIC_KEY) def test_webhook_test_event(test_client): """Test receiving a test event.""" payload = "test" signature = pkcs1_15.new(_TEST_KEY).sign(SHA.new(payload.encode())) result = test_client.post( "/transferwise/webhook", data=payload, headers={ transferwise_api.WEBHOOK_TEST_HEADER: 1, transferwise_api.WEBHOOK_SIGNATURE_HEADER: base64.b64encode( signature ).decode("utf-8"), }, ) assert result.status_code == status.OK @patch.object(config, "TRANSFERWISE_PUBLIC_KEY", _TEST_PUBLIC_KEY) @db.test_schema_default_seed def test_webhook_unhandled_event(test_client): """Test receiving an unhandled event.""" payload = json.dumps( { "data": { "test": "lol", }, "subscription_id": "1d98f73f-360e-41db-97df-4c63a1f7b2be", "event_type": "profiles#something-different", "schema_version": "2.0.0", "sent_at": "2019-12-23T15:04:54Z", } ) signature = pkcs1_15.new(_TEST_KEY).sign(SHA.new(payload.encode())) result = test_client.post( "/transferwise/webhook", data=payload, headers={ "Content-Type": "application/json", transferwise_api.WEBHOOK_SIGNATURE_HEADER: base64.b64encode( signature ).decode("utf-8"), }, ) assert result.status_code == status.INTERNAL_ERROR @patch.object(config, "TRANSFERWISE_PUBLIC_KEY", _TEST_PUBLIC_KEY) @db.test_schema_default_seed def test_webhook_incoming_to_funds_refunded(test_client): """Test payment transfer process from start to funds refunded.""" statuses = [ transfer.STATUS_INCOMING_PAYMENT_WAITING, transfer.STATUS_PROCESSING, transfer.STATUS_FUNDS_CONVERTED, transfer.STATUS_OUTGOING_PAYMENT_SENT, transfer.STATUS_BOUNCED_BACK, transfer.STATUS_FUNDS_REFUNDED, ] start = datetime.datetime.utcnow() for i in range(len(statuses) - 1): occurred_at = start + datetime.timedelta(0, i) payload = _make_webhook_payload( previous_status=statuses[i], current_status=statuses[i + 1], occurred_at=occurred_at.strftime("%Y-%m-%dT%H:%M:%SZ"), ) signature = pkcs1_15.new(_TEST_KEY).sign(SHA.new(payload.encode())) test_client.post( "/transferwise/webhook", data=payload, headers={ "Content-Type": "application/json", transferwise_api.WEBHOOK_SIGNATURE_HEADER: ( base64.b64encode(signature).decode("utf-8") ), }, ) assert TransactionPersister.is_transaction_credited(payment_id=1) @patch.object(config, "TRANSFERWISE_PUBLIC_KEY", _TEST_PUBLIC_KEY) @db.test_schema_default_seed def test_webhook_duplicate_cancelled_messages(test_client): """Test duplicate cancelled messages.""" statuses = [ transfer.STATUS_INCOMING_PAYMENT_WAITING, transfer.STATUS_CANCELLED, transfer.STATUS_CANCELLED, ] results = [] start = datetime.datetime.utcnow() for i in range(len(statuses) - 1): occurred_at = start + datetime.timedelta(0, i) payload = _make_webhook_payload( previous_status=statuses[i], current_status=statuses[i + 1], occurred_at=occurred_at.strftime("%Y-%m-%dT%H:%M:%SZ"), ) signature = pkcs1_15.new(_TEST_KEY).sign(SHA.new(payload.encode())) result = test_client.post( "/transferwise/webhook", data=payload, headers={ "Content-Type": "application/json", transferwise_api.WEBHOOK_SIGNATURE_HEADER: ( base64.b64encode(signature).decode("utf-8") ), }, ) results.append(result) assert [result.status_code for result in results] == [200, 200]