Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 199 additions & 1 deletion crates/cli/src/meta/query/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,207 @@ pub(super) async fn process_deployer_query(
#[cfg(all(test, not(target_family = "wasm")))]
mod tests {
use super::*;
use crate::meta::{ContentEncoding, ContentLanguage, ContentType};
use crate::meta::types::authoring::v1::AuthoringMetaItem;
use crate::meta::{ContentEncoding, ContentLanguage, ContentType};
use httpmock::Method::POST;
use httpmock::MockServer;
use serde_bytes::ByteBuf;
use serde_json::json;

/// A fully populated, valid expressionDeployers entry as the subgraph
/// would return it: every hex field decodes and exactly one meta.
fn deployer_entry() -> serde_json::Value {
json!({
"constructorMetaHash": "0x0102",
"constructorMeta": "0x0304",
"deployTransaction": { "id": "0x0506" },
"bytecode": "0x0708",
"parser": { "parser": { "deployedBytecode": "0x090a" } },
"store": { "store": { "deployedBytecode": "0x0b0c" } },
"interpreter": { "interpreter": { "deployedBytecode": "0x0d0e" } },
"meta": [ { "__typename": "RainMetaV1", "id": "0x0f10" } ]
})
}

async fn run_deployer_query(entry: serde_json::Value) -> Result<DeployerResponse, Error> {
let server = MockServer::start_async().await;
server.mock(|when, then| {
when.method(POST).path("/");
then.status(200)
.json_body(json!({ "data": { "expressionDeployers": [entry] } }));
});
let request_body = DeployerQuery::build_query(deployer_query::Variables {
hash: Some("0xabcd".to_string()),
});
let client = Arc::new(Client::new());
process_deployer_query(client, &request_body, &server.url("/")).await
}

#[tokio::test]
async fn test_process_deployer_query_success_decodes_all_fields() {
let res = run_deployer_query(deployer_entry()).await.unwrap();
assert_eq!(res.meta_hash, vec![0x01, 0x02]);
assert_eq!(res.meta_bytes, vec![0x03, 0x04]);
assert_eq!(res.tx_hash, vec![0x05, 0x06]);
assert_eq!(res.bytecode, vec![0x07, 0x08]);
assert_eq!(res.parser, vec![0x09, 0x0a]);
assert_eq!(res.store, vec![0x0b, 0x0c]);
assert_eq!(res.interpreter, vec![0x0d, 0x0e]);
assert_eq!(res.bytecode_meta_hash, vec![0x0f, 0x10]);
}

#[tokio::test]
async fn test_process_deployer_query_null_bytecode_is_no_record_found() {
let mut entry = deployer_entry();
entry["bytecode"] = serde_json::Value::Null;
assert!(matches!(
run_deployer_query(entry).await,
Err(Error::NoRecordFound)
));
}

#[tokio::test]
async fn test_process_deployer_query_null_parser_is_no_record_found() {
let mut entry = deployer_entry();
entry["parser"] = serde_json::Value::Null;
assert!(matches!(
run_deployer_query(entry).await,
Err(Error::NoRecordFound)
));
}

#[tokio::test]
async fn test_process_deployer_query_null_store_is_no_record_found() {
let mut entry = deployer_entry();
entry["store"] = serde_json::Value::Null;
assert!(matches!(
run_deployer_query(entry).await,
Err(Error::NoRecordFound)
));
}

#[tokio::test]
async fn test_process_deployer_query_null_interpreter_is_no_record_found() {
let mut entry = deployer_entry();
entry["interpreter"] = serde_json::Value::Null;
assert!(matches!(
run_deployer_query(entry).await,
Err(Error::NoRecordFound)
));
}

#[tokio::test]
async fn test_process_deployer_query_null_deploy_transaction_is_no_record_found() {
let mut entry = deployer_entry();
entry["deployTransaction"] = serde_json::Value::Null;
assert!(matches!(
run_deployer_query(entry).await,
Err(Error::NoRecordFound)
));
}

#[tokio::test]
async fn test_process_deployer_query_zero_metas_is_no_record_found() {
let mut entry = deployer_entry();
entry["meta"] = json!([]);
assert!(matches!(
run_deployer_query(entry).await,
Err(Error::NoRecordFound)
));
}

#[tokio::test]
async fn test_process_deployer_query_two_metas_is_no_record_found() {
let mut entry = deployer_entry();
entry["meta"] = json!([
{ "__typename": "RainMetaV1", "id": "0x0f10" },
{ "__typename": "RainMetaV1", "id": "0x1112" }
]);
assert!(matches!(
run_deployer_query(entry).await,
Err(Error::NoRecordFound)
));
}

#[tokio::test]
async fn test_process_deployer_query_invalid_bytecode_hex_is_no_record_found() {
let mut entry = deployer_entry();
entry["bytecode"] = json!("0xZZ");
assert!(matches!(
run_deployer_query(entry).await,
Err(Error::NoRecordFound)
));
}

#[tokio::test]
async fn test_process_deployer_query_invalid_parser_hex_is_no_record_found() {
let mut entry = deployer_entry();
entry["parser"]["parser"]["deployedBytecode"] = json!("0xZZ");
assert!(matches!(
run_deployer_query(entry).await,
Err(Error::NoRecordFound)
));
}

#[tokio::test]
async fn test_process_deployer_query_invalid_store_hex_is_no_record_found() {
let mut entry = deployer_entry();
entry["store"]["store"]["deployedBytecode"] = json!("0xZZ");
assert!(matches!(
run_deployer_query(entry).await,
Err(Error::NoRecordFound)
));
}

#[tokio::test]
async fn test_process_deployer_query_invalid_interpreter_hex_is_no_record_found() {
let mut entry = deployer_entry();
entry["interpreter"]["interpreter"]["deployedBytecode"] = json!("0xZZ");
assert!(matches!(
run_deployer_query(entry).await,
Err(Error::NoRecordFound)
));
}

#[tokio::test]
async fn test_process_deployer_query_invalid_meta_id_hex_is_no_record_found() {
let mut entry = deployer_entry();
entry["meta"][0]["id"] = json!("0xZZ");
assert!(matches!(
run_deployer_query(entry).await,
Err(Error::NoRecordFound)
));
}

#[tokio::test]
async fn test_process_deployer_query_invalid_tx_id_hex_is_no_record_found() {
let mut entry = deployer_entry();
entry["deployTransaction"]["id"] = json!("0xZZ");
assert!(matches!(
run_deployer_query(entry).await,
Err(Error::NoRecordFound)
));
}

#[tokio::test]
async fn test_process_deployer_query_invalid_constructor_meta_hash_hex_is_no_record_found() {
let mut entry = deployer_entry();
entry["constructorMetaHash"] = json!("0xZZ");
assert!(matches!(
run_deployer_query(entry).await,
Err(Error::NoRecordFound)
));
}

#[tokio::test]
async fn test_process_deployer_query_invalid_constructor_meta_hex_is_no_record_found() {
let mut entry = deployer_entry();
entry["constructorMeta"] = json!("0xZZ");
assert!(matches!(
run_deployer_query(entry).await,
Err(Error::NoRecordFound)
));
}

fn sample_authoring_meta() -> (AuthoringMeta, Vec<u8>) {
let authoring_meta: AuthoringMeta = serde_json::from_str(
Expand Down
152 changes: 152 additions & 0 deletions crates/cli/src/meta/types/authoring/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,4 +226,156 @@ mod tests {

Ok(())
}

#[test]
fn test_item_encode_decode_roundtrip_offset_and_word_bytes() -> Result<(), Error> {
let item = AuthoringMetaItem {
word: "stack".to_string(),
operand_parser_offset: 16u8,
description: "some description.".to_string(),
};
let encoded = item.abi_encode()?;
// ABI layout for a single dynamic (bytes32, uint8, string) value: one
// indirection word (offset 0x20), then the tuple body whose first
// word carries the word left-aligned and zero padded and whose second
// word carries the uint8 in its last byte.
assert_eq!(&encoded[0..31], &[0u8; 31][..]);
assert_eq!(encoded[31], 0x20u8);
assert_eq!(&encoded[32..37], &b"stack"[..]);
assert_eq!(&encoded[37..64], &[0u8; 27][..]);
assert_eq!(&encoded[64..95], &[0u8; 31][..]);
assert_eq!(encoded[95], 16u8);
let decoded = AuthoringMetaItem::abi_decode(&encoded)?;
assert_eq!(decoded, item);
Ok(())
}

#[test]
fn test_item_abi_encode_validate_rejects_invalid_word() {
let item = AuthoringMetaItem {
// printable ASCII (passes RAIN_STRING) but not lower-kebab-case
// (fails RAIN_SYMBOL), so word validation specifically must fire.
word: "Bad Word".to_string(),
operand_parser_offset: 0u8,
description: "fine description.".to_string(),
};
// encoding itself works, so any failure below is validation
assert!(item.abi_encode().is_ok());
assert!(matches!(
item.abi_encode_validate(),
Err(Error::ValidationErrors(_))
));
}

#[test]
fn test_item_abi_decode_validate_rejects_invalid_word() {
let item = AuthoringMetaItem {
word: "Bad Word".to_string(),
operand_parser_offset: 0u8,
description: "fine description.".to_string(),
};
let encoded = item.abi_encode().unwrap();
// plain decode accepts the bytes
assert_eq!(AuthoringMetaItem::abi_decode(&encoded).unwrap(), item);
// validating decode rejects them
assert!(matches!(
AuthoringMetaItem::abi_decode_validate(&encoded),
Err(Error::ValidationErrors(_))
));
}

#[test]
fn test_array_validate_rejects_and_annotates_offending_index() {
let am = AuthoringMeta(vec![
AuthoringMetaItem {
word: "stack".to_string(),
operand_parser_offset: 0u8,
description: "fine description.".to_string(),
},
AuthoringMetaItem {
word: "Bad Word".to_string(),
operand_parser_offset: 0u8,
description: "fine description.".to_string(),
},
]);
match am.abi_encode_validate() {
Err(Error::ValidationErrors(v)) => {
let errors = v.errors();
assert!(errors.contains_key("at index 1"));
assert!(!errors.contains_key("at index 0"));
}
other => panic!("expected ValidationErrors, got {:?}", other.err()),
}
}

#[test]
fn test_try_from_bytes_json_fallback() -> Result<(), Error> {
let json_bytes =
br#"[{"word":"stack","description":"a description.","operandParserOffset":16}]"#
.to_vec();
let expected = AuthoringMeta(vec![AuthoringMetaItem {
word: "stack".to_string(),
operand_parser_offset: 16u8,
description: "a description.".to_string(),
}]);
// json bytes resolve through the serde_json fallback arm
let from_vec = AuthoringMeta::try_from(json_bytes.clone())?;
assert_eq!(from_vec, expected);
let from_slice = AuthoringMeta::try_from(json_bytes.as_slice())?;
assert_eq!(from_slice, expected);
// abi encoded bytes resolve through the abi_decode arm
let encoded = expected.abi_encode()?;
assert_eq!(AuthoringMeta::try_from(encoded)?, expected);
Ok(())
}

#[test]
fn test_try_from_meta_item_unpacks_content_encoding() -> Result<(), Error> {
use crate::meta::{
ContentEncoding, ContentLanguage, ContentType, KnownMagic, RainMetaDocumentV1Item,
};
let expected = AuthoringMeta(vec![AuthoringMetaItem {
word: "stack".to_string(),
operand_parser_offset: 16u8,
description: "a description.".to_string(),
}]);
let encoded = expected.abi_encode_validate()?;
let deflated = ContentEncoding::Deflate.encode(&encoded);
assert_ne!(deflated, encoded);
let item = RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(deflated),
magic: KnownMagic::AuthoringMetaV1,
content_type: ContentType::Cbor,
content_encoding: ContentEncoding::Deflate,
content_language: ContentLanguage::None,
schema: None,
};
// TryFrom must unpack (inflate) the payload before decoding
assert_eq!(AuthoringMeta::try_from(item)?, expected);
Ok(())
}
#[test]
fn test_description_rejects_unprintable_chars() {
use validator::Validate;
// printable ASCII description passes REGEX_RAIN_STRING
let item = AuthoringMetaItem {
word: "stack".to_string(),
operand_parser_offset: 0u8,
description: "All printable ASCII is fine.".to_string(),
};
assert!(item.validate().is_ok());

// a non-printable control character is rejected, and the rejection
// surfaces through abi_encode_validate as ValidationErrors
let item = AuthoringMetaItem {
word: "stack".to_string(),
operand_parser_offset: 0u8,
description: "bell \u{7} is not printable".to_string(),
};
assert!(item.validate().is_err());
assert!(matches!(
item.abi_encode_validate(),
Err(Error::ValidationErrors(_))
));
}
}
Loading
Loading