// This file is part of Substrate. // Copyright (C) 2020-2022 Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use crate as example_offchain_worker; use crate::*; use codec::Decode; use frame_support::{ assert_ok, parameter_types, traits::{ConstU32, ConstU64}, }; use sp_core::{ offchain::{testing, OffchainWorkerExt, TransactionPoolExt}, sr25519::Signature, H256, }; use std::sync::Arc; use sp_keystore::{testing::KeyStore, KeystoreExt, SyncCryptoStore}; use sp_runtime::{ testing::{Header, TestXt}, traits::{BlakeTwo256, Extrinsic as ExtrinsicT, IdentifyAccount, IdentityLookup, Verify}, RuntimeAppPublic, }; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; // For testing the module, we construct a mock runtime. frame_support::construct_runtime!( pub enum Test where Block = Block, NodeBlock = Block, UncheckedExtrinsic = UncheckedExtrinsic, { System: frame_system::{Pallet, Call, Config, Storage, Event}, Example: example_offchain_worker::{Pallet, Call, Storage, Event, ValidateUnsigned}, } ); parameter_types! { pub BlockWeights: frame_system::limits::BlockWeights = frame_system::limits::BlockWeights::simple_max(frame_support::weights::Weight::from_ref_time(1024)); } impl frame_system::Config for Test { type BaseCallFilter = frame_support::traits::Everything; type BlockWeights = (); type BlockLength = (); type DbWeight = (); type RuntimeOrigin = RuntimeOrigin; type RuntimeCall = RuntimeCall; type Index = u64; type BlockNumber = u64; type Hash = H256; type Hashing = BlakeTwo256; type AccountId = sp_core::sr25519::Public; type Lookup = IdentityLookup; type Header = Header; type RuntimeEvent = RuntimeEvent; type BlockHashCount = ConstU64<250>; type Version = (); type PalletInfo = PalletInfo; type AccountData = (); type OnNewAccount = (); type OnKilledAccount = (); type SystemWeightInfo = (); type SS58Prefix = (); type OnSetCode = (); type MaxConsumers = ConstU32<16>; } type Extrinsic = TestXt; type AccountId = <::Signer as IdentifyAccount>::AccountId; impl frame_system::offchain::SigningTypes for Test { type Public = ::Signer; type Signature = Signature; } impl frame_system::offchain::SendTransactionTypes for Test where RuntimeCall: From, { type OverarchingCall = RuntimeCall; type Extrinsic = Extrinsic; } impl frame_system::offchain::CreateSignedTransaction for Test where RuntimeCall: From, { fn create_transaction>( call: RuntimeCall, _public: ::Signer, _account: AccountId, nonce: u64, ) -> Option<(RuntimeCall, ::SignaturePayload)> { Some((call, (nonce, ()))) } } parameter_types! { pub const UnsignedPriority: u64 = 1 << 20; } impl Config for Test { type RuntimeEvent = RuntimeEvent; type AuthorityId = crypto::TestAuthId; type GracePeriod = ConstU64<5>; type UnsignedInterval = ConstU64<128>; type UnsignedPriority = UnsignedPriority; type MaxPrices = ConstU32<64>; } fn test_pub() -> sp_core::sr25519::Public { sp_core::sr25519::Public::from_raw([1u8; 32]) } #[test] fn it_aggregates_the_price() { sp_io::TestExternalities::default().execute_with(|| { assert_eq!(Example::average_price(), None); assert_ok!(Example::submit_price(RuntimeOrigin::signed(test_pub()), 27)); assert_eq!(Example::average_price(), Some(27)); assert_ok!(Example::submit_price(RuntimeOrigin::signed(test_pub()), 43)); assert_eq!(Example::average_price(), Some(35)); }); } #[test] fn should_make_http_call_and_parse_result() { let (offchain, state) = testing::TestOffchainExt::new(); let mut t = sp_io::TestExternalities::default(); t.register_extension(OffchainWorkerExt::new(offchain)); price_oracle_response(&mut state.write()); t.execute_with(|| { // when let price = Example::fetch_price().unwrap(); // then assert_eq!(price, 15523); }); } #[test] fn knows_how_to_mock_several_http_calls() { let (offchain, state) = testing::TestOffchainExt::new(); let mut t = sp_io::TestExternalities::default(); t.register_extension(OffchainWorkerExt::new(offchain)); { let mut state = state.write(); state.expect_request(testing::PendingRequest { method: "GET".into(), uri: "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD".into(), response: Some(br#"{"USD": 1}"#.to_vec()), sent: true, ..Default::default() }); state.expect_request(testing::PendingRequest { method: "GET".into(), uri: "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD".into(), response: Some(br#"{"USD": 2}"#.to_vec()), sent: true, ..Default::default() }); state.expect_request(testing::PendingRequest { method: "GET".into(), uri: "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD".into(), response: Some(br#"{"USD": 3}"#.to_vec()), sent: true, ..Default::default() }); } t.execute_with(|| { let price1 = Example::fetch_price().unwrap(); let price2 = Example::fetch_price().unwrap(); let price3 = Example::fetch_price().unwrap(); assert_eq!(price1, 100); assert_eq!(price2, 200); assert_eq!(price3, 300); }) } #[test] fn should_submit_signed_transaction_on_chain() { const PHRASE: &str = "news slush supreme milk chapter athlete soap sausage put clutch what kitten"; let (offchain, offchain_state) = testing::TestOffchainExt::new(); let (pool, pool_state) = testing::TestTransactionPoolExt::new(); let keystore = KeyStore::new(); SyncCryptoStore::sr25519_generate_new( &keystore, crate::crypto::Public::ID, Some(&format!("{}/hunter1", PHRASE)), ) .unwrap(); let mut t = sp_io::TestExternalities::default(); t.register_extension(OffchainWorkerExt::new(offchain)); t.register_extension(TransactionPoolExt::new(pool)); t.register_extension(KeystoreExt(Arc::new(keystore))); price_oracle_response(&mut offchain_state.write()); t.execute_with(|| { // when Example::fetch_price_and_send_signed().unwrap(); // then let tx = pool_state.write().transactions.pop().unwrap(); assert!(pool_state.read().transactions.is_empty()); let tx = Extrinsic::decode(&mut &*tx).unwrap(); assert_eq!(tx.signature.unwrap().0, 0); assert_eq!(tx.call, RuntimeCall::Example(crate::Call::submit_price { price: 15523 })); }); } #[test] fn should_submit_unsigned_transaction_on_chain_for_any_account() { const PHRASE: &str = "news slush supreme milk chapter athlete soap sausage put clutch what kitten"; let (offchain, offchain_state) = testing::TestOffchainExt::new(); let (pool, pool_state) = testing::TestTransactionPoolExt::new(); let keystore = KeyStore::new(); SyncCryptoStore::sr25519_generate_new( &keystore, crate::crypto::Public::ID, Some(&format!("{}/hunter1", PHRASE)), ) .unwrap(); let public_key = *SyncCryptoStore::sr25519_public_keys(&keystore, crate::crypto::Public::ID) .get(0) .unwrap(); let mut t = sp_io::TestExternalities::default(); t.register_extension(OffchainWorkerExt::new(offchain)); t.register_extension(TransactionPoolExt::new(pool)); t.register_extension(KeystoreExt(Arc::new(keystore))); price_oracle_response(&mut offchain_state.write()); let price_payload = PricePayload { block_number: 1, price: 15523, public: ::Public::from(public_key), }; // let signature = price_payload.sign::().unwrap(); t.execute_with(|| { // when Example::fetch_price_and_send_unsigned_for_any_account(1).unwrap(); // then let tx = pool_state.write().transactions.pop().unwrap(); let tx = Extrinsic::decode(&mut &*tx).unwrap(); assert_eq!(tx.signature, None); if let RuntimeCall::Example(crate::Call::submit_price_unsigned_with_signed_payload { price_payload: body, signature, }) = tx.call { assert_eq!(body, price_payload); let signature_valid = ::Public, ::BlockNumber, > as SignedPayload>::verify::(&price_payload, signature); assert!(signature_valid); } }); } #[test] fn should_submit_unsigned_transaction_on_chain_for_all_accounts() { const PHRASE: &str = "news slush supreme milk chapter athlete soap sausage put clutch what kitten"; let (offchain, offchain_state) = testing::TestOffchainExt::new(); let (pool, pool_state) = testing::TestTransactionPoolExt::new(); let keystore = KeyStore::new(); SyncCryptoStore::sr25519_generate_new( &keystore, crate::crypto::Public::ID, Some(&format!("{}/hunter1", PHRASE)), ) .unwrap(); let public_key = *SyncCryptoStore::sr25519_public_keys(&keystore, crate::crypto::Public::ID) .get(0) .unwrap(); let mut t = sp_io::TestExternalities::default(); t.register_extension(OffchainWorkerExt::new(offchain)); t.register_extension(TransactionPoolExt::new(pool)); t.register_extension(KeystoreExt(Arc::new(keystore))); price_oracle_response(&mut offchain_state.write()); let price_payload = PricePayload { block_number: 1, price: 15523, public: ::Public::from(public_key), }; // let signature = price_payload.sign::().unwrap(); t.execute_with(|| { // when Example::fetch_price_and_send_unsigned_for_all_accounts(1).unwrap(); // then let tx = pool_state.write().transactions.pop().unwrap(); let tx = Extrinsic::decode(&mut &*tx).unwrap(); assert_eq!(tx.signature, None); if let RuntimeCall::Example(crate::Call::submit_price_unsigned_with_signed_payload { price_payload: body, signature, }) = tx.call { assert_eq!(body, price_payload); let signature_valid = ::Public, ::BlockNumber, > as SignedPayload>::verify::(&price_payload, signature); assert!(signature_valid); } }); } #[test] fn should_submit_raw_unsigned_transaction_on_chain() { let (offchain, offchain_state) = testing::TestOffchainExt::new(); let (pool, pool_state) = testing::TestTransactionPoolExt::new(); let keystore = KeyStore::new(); let mut t = sp_io::TestExternalities::default(); t.register_extension(OffchainWorkerExt::new(offchain)); t.register_extension(TransactionPoolExt::new(pool)); t.register_extension(KeystoreExt(Arc::new(keystore))); price_oracle_response(&mut offchain_state.write()); t.execute_with(|| { // when Example::fetch_price_and_send_raw_unsigned(1).unwrap(); // then let tx = pool_state.write().transactions.pop().unwrap(); assert!(pool_state.read().transactions.is_empty()); let tx = Extrinsic::decode(&mut &*tx).unwrap(); assert_eq!(tx.signature, None); assert_eq!( tx.call, RuntimeCall::Example(crate::Call::submit_price_unsigned { block_number: 1, price: 15523 }) ); }); } fn price_oracle_response(state: &mut testing::OffchainState) { state.expect_request(testing::PendingRequest { method: "GET".into(), uri: "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD".into(), response: Some(br#"{"USD": 155.23}"#.to_vec()), sent: true, ..Default::default() }); } #[test] fn parse_price_works() { let test_data = vec![ ("{\"USD\":6536.92}", Some(653692)), ("{\"USD\":65.92}", Some(6592)), ("{\"USD\":6536.924565}", Some(653692)), ("{\"USD\":6536}", Some(653600)), ("{\"USD2\":6536}", None), ("{\"USD\":\"6432\"}", None), ]; for (json, expected) in test_data { assert_eq!(expected, Example::parse_price(json)); } }