Skip to content
Open
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
217 changes: 217 additions & 0 deletions crates/cli/src/meta/query/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,220 @@ pub(super) async fn process_deployer_query(
Err(Error::NoRecordFound)
}
}

#[cfg(all(test, not(target_family = "wasm")))]
mod tests {
use super::*;
use super::super::{
types::authoring::v1::AuthoringMetaItem, ContentEncoding, ContentLanguage, ContentType,
};
use httpmock::Method::POST;
use httpmock::MockServer;

fn request_body(hash: &str) -> QueryBody<meta_query::Variables> {
MetaQuery::build_query(meta_query::Variables {
hash: Some(hash.to_string()),
})
}

const HASH: &str = "0x1111111111111111111111111111111111111111111111111111111111111111";

/// A found meta resolves to exactly the hex-decoded rawBytes of the
/// response, fetched with a POST carrying the query body.
#[tokio::test]
async fn test_process_meta_query_success_exact_bytes() {
let server = MockServer::start_async().await;
server.mock(|when, then| {
when.method(POST)
.path("/")
.json_body_partial(format!(r#"{{"variables":{{"hash":"{}"}}}}"#, HASH));
then.status(200)
.header("content-type", "application/json")
.body(r#"{"data":{"meta":{"__typename":"RainMetaV1","rawBytes":"0xff0a89c674ee7874deadbeef"}}}"#);
});
let result = process_meta_query(
Arc::new(Client::new()),
&request_body(HASH),
&server.url("/"),
)
.await
.unwrap();
assert_eq!(
result.bytes,
vec![0xff, 0x0a, 0x89, 0xc6, 0x74, 0xee, 0x78, 0x74, 0xde, 0xad, 0xbe, 0xef]
);
}

/// A response with no data member at all is "no record found".
#[tokio::test]
async fn test_process_meta_query_missing_data_is_no_record_found() {
let server = MockServer::start_async().await;
server.mock(|when, then| {
when.method(POST).path("/");
then.status(200)
.header("content-type", "application/json")
.body(r#"{"data":null}"#);
});
let result = process_meta_query(
Arc::new(Client::new()),
&request_body(HASH),
&server.url("/"),
)
.await;
assert!(matches!(result, Err(Error::NoRecordFound)), "{result:?}");
}

/// A response with data but a null meta is "no record found".
#[tokio::test]
async fn test_process_meta_query_missing_meta_is_no_record_found() {
let server = MockServer::start_async().await;
server.mock(|when, then| {
when.method(POST).path("/");
then.status(200)
.header("content-type", "application/json")
.body(r#"{"data":{"meta":null}}"#);
});
let result = process_meta_query(
Arc::new(Client::new()),
&request_body(HASH),
&server.url("/"),
)
.await;
assert!(matches!(result, Err(Error::NoRecordFound)), "{result:?}");
}

/// rawBytes that do not hex-decode resolve to "no record found",
/// never to successfully-returned bytes.
#[tokio::test]
async fn test_process_meta_query_bad_hex_is_no_record_found() {
let server = MockServer::start_async().await;
server.mock(|when, then| {
when.method(POST).path("/");
then.status(200)
.header("content-type", "application/json")
.body(r#"{"data":{"meta":{"__typename":"RainMetaV1","rawBytes":"zz-not-hex"}}}"#);
});
let result = process_meta_query(
Arc::new(Client::new()),
&request_body(HASH),
&server.url("/"),
)
.await;
assert!(matches!(result, Err(Error::NoRecordFound)), "{result:?}");
}

/// A transport failure surfaces as a reqwest error, not as a
/// no-record-found result.
#[tokio::test]
async fn test_process_meta_query_send_error_is_reqwest_error() {
// Bind and immediately release a local port so the request targets a
// closed port.
let port = {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
listener.local_addr().unwrap().port()
};
let url = format!("http://127.0.0.1:{port}/");
let result = process_meta_query(Arc::new(Client::new()), &request_body(HASH), &url).await;
assert!(matches!(result, Err(Error::ReqwestError(_))), "{result:?}");
}

/// A non-JSON response body surfaces as a reqwest decode error, not as
/// a no-record-found result.
#[tokio::test]
async fn test_process_meta_query_non_json_is_reqwest_error() {
let server = MockServer::start_async().await;
server.mock(|when, then| {
when.method(POST).path("/");
then.status(200)
.header("content-type", "text/plain")
.body("not json");
});
let result = process_meta_query(
Arc::new(Client::new()),
&request_body(HASH),
&server.url("/"),
)
.await;
assert!(matches!(result, Err(Error::ReqwestError(_))), "{result:?}");
}

fn authoring_meta() -> AuthoringMeta {
AuthoringMeta(vec![AuthoringMetaItem {
word: "some-word".to_string(),
operand_parser_offset: 0,
description: "a description".to_string(),
}])
}

fn authoring_item() -> RainMetaDocumentV1Item {
RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(authoring_meta().abi_encode_validate().unwrap()),
magic: KnownMagic::AuthoringMetaV1,
content_type: ContentType::None,
content_encoding: ContentEncoding::None,
content_language: ContentLanguage::None,
schema: None,
}
}

fn deployer_response_with(meta_bytes: Vec<u8>) -> DeployerResponse {
DeployerResponse {
tx_hash: vec![],
bytecode_meta_hash: vec![],
meta_hash: vec![],
meta_bytes,
bytecode: vec![],
parser: vec![],
store: vec![],
interpreter: vec![],
}
}

/// An authoring-magic item whose payload fails to unpack is skipped;
/// a later valid authoring meta item is still found.
#[test]
fn test_get_authoring_meta_skips_unpack_failure() {
// Deflate-encoded item whose payload is not valid deflate data.
let bad_unpack = RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(vec![0xffu8, 0xff, 0xff, 0xff]),
magic: KnownMagic::AuthoringMetaV1,
content_type: ContentType::None,
content_encoding: ContentEncoding::Deflate,
content_language: ContentLanguage::None,
schema: None,
};
assert!(bad_unpack.unpack().is_err());
let meta_bytes = RainMetaDocumentV1Item::cbor_encode_seq(
&vec![bad_unpack, authoring_item()],
KnownMagic::RainMetaDocumentV1,
)
.unwrap();
assert_eq!(
deployer_response_with(meta_bytes).get_authoring_meta(),
Some(authoring_meta())
);
}

/// The scan covers every item in the document: an authoring meta in the
/// second position is found behind a non-authoring first item.
#[test]
fn test_get_authoring_meta_scans_beyond_first_item() {
let other_magic = RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(b"_: int-add(1 2);".to_vec()),
magic: KnownMagic::RainlangV1,
content_type: ContentType::None,
content_encoding: ContentEncoding::None,
content_language: ContentLanguage::None,
schema: None,
};
let meta_bytes = RainMetaDocumentV1Item::cbor_encode_seq(
&vec![other_magic, authoring_item()],
KnownMagic::RainMetaDocumentV1,
)
.unwrap();
assert_eq!(
deployer_response_with(meta_bytes).get_authoring_meta(),
Some(authoring_meta())
);
}
}
8 changes: 5 additions & 3 deletions crates/cli/src/meta/types/common/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ mod test {

// invalids
for i in [
"", "♥", "-", " ", "A", "A0", "a ", "0", "_", "0a", "0A", "\n", "\t", "\r",
"", "♥", "-", " ", "A", "A0", "a ", "0", "_", "0a", "0A", "\n", "\t", "\r", "aA",
] {
assert!(
RainSymbol {
Expand Down Expand Up @@ -137,7 +137,7 @@ mod test {
}

// invalids
for i in ["", " ", " a", "a ", "♥", "\n", "\t", "\r"] {
for i in ["", " ", " a", "a ", "♥", "\n", "\t", "\r", "\u{7f}"] {
assert!(
RainTitle {
value: i.to_string()
Expand Down Expand Up @@ -169,7 +169,7 @@ mod test {
}

// invalids
for i in ["♥", "∴"] {
for i in ["♥", "∴", "\u{7f}"] {
assert!(
RainString {
value: i.to_string()
Expand Down Expand Up @@ -249,6 +249,8 @@ mod test {
"0x78fd1edb0bdb928db6015990fecafbb964b44692e2d435693062dd4efc6254dd ",
" 0x78fd1edb0bdb928db6015990fecafbb964b44692e2d435693062dd4efc6254dd",
"0x78fd1edb0bdb928db6015990fecafbb9 64b44692e2d435693062dd4efc6254dd",
"0xgggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg",
"0X78fd1edb0bdb928db6015990fecafbb964b44692e2d435693062dd4efc6254dd",
] {
assert!(
!HASH_PATTERN.is_match(i),
Expand Down
43 changes: 43 additions & 0 deletions crates/cli/tests/magic_cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// End-to-end check of `rain-metadata magic ls` output.
#![cfg(not(target_family = "wasm"))]

use std::process::Command;

/// `magic ls` prints every known magic number, one per line, as the
/// 0x-prefixed hex value followed by the kebab-case name, in declaration
/// order, and nothing else.
#[test]
fn test_magic_ls_exact_output() {
let output = Command::new(env!("CARGO_BIN_EXE_rain-metadata"))
.args(["magic", "ls"])
.output()
.expect("failed to run rain-metadata");
assert!(output.status.success(), "exit: {:?}", output.status);
assert!(
output.stderr.is_empty(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let expected = "\
0xff0a89c674ee7874 rain-meta-document-v1
0xffe5282f43e495b4 op-meta-v1
0xffdac2f2f37be894 dotrain-v1
0xff1c198cec3b48a7 rainlang-v1
0xffe5ffb4a3ff2cde solidity-abi-v2
0xffe9e3a02ca8e235 authoring-meta-v1
0xff52fe42f1a05093 authoring-meta-v2
0xffc21bbf86cc199b interpreter-caller-meta-v1
0xffdb988a8cd04d32 expression-deployer-v2-bytecode-v1
0xff13109e41336ff2 rainlang-source-v1
0xffb2637608c09e38 address-list
0xffa15ef0fc437099 dotrain-source-v1
0xffda7b2fb167c286 order-builder-state-v1
0xff7a1507ba4419ca raindex-signed-context-oracle-v1
0xffa8e8a9b9cf4a31 oa-schema
0xff9fae3cc645f463 oa-hash-list
0xffc47a6299e8a911 oa-structure
0xff8cd2927c8c86cb oa-token-image
0xffbc38eb14ad2209 oa-token-credential-links
";
assert_eq!(String::from_utf8(output.stdout).unwrap(), expected);
}
38 changes: 38 additions & 0 deletions test/script/CopyArtifacts.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity =0.8.25;

import {Test} from "forge-std-1.16.2/src/Test.sol";
import {LibCopyArtifacts} from "script/lib/LibCopyArtifacts.sol";
import {CopyArtifacts} from "script/CopyArtifacts.sol";

contract CopyArtifactsTest is Test {
function _assertCommittedMatches(string memory contractName) internal {
Expand All @@ -18,10 +19,47 @@ contract CopyArtifactsTest is Test {
);
}

/// All committed-artifact behaviour lives in this single test function:
/// forge runs test functions in parallel, and every step below mutates or
/// reads the same on-disk committed files, so splitting the steps into
/// separate tests races them against each other.
function testArtifactsCommitted() external {
string[] memory names = LibCopyArtifacts.contracts();
for (uint256 i = 0; i < names.length; i++) {
_assertCommittedMatches(names[i]);
}

// Corrupting every committed artifact and running the script restores
// each committed file byte-exactly to its pre-corruption on-disk
// state.
bytes32[] memory preCorruption = new bytes32[](names.length);
for (uint256 i = 0; i < names.length; i++) {
preCorruption[i] = keccak256(bytes(vm.readFile(LibCopyArtifacts.committedPath(names[i]))));
vm.writeFile(LibCopyArtifacts.committedPath(names[i]), "corrupt");
}
new CopyArtifacts().run();
for (uint256 i = 0; i < names.length; i++) {
assertEq(
keccak256(bytes(vm.readFile(LibCopyArtifacts.committedPath(names[i])))),
preCorruption[i],
string.concat(names[i], ": committed artifact not restored byte-exactly")
);
}

// Deleting a committed artifact and running the script recreates it
// byte-exactly; a missing destination is written without an attempted
// removal.
string memory dst = LibCopyArtifacts.committedPath(names[0]);
bytes32 preDeletion = keccak256(bytes(vm.readFile(dst)));
//forge-lint: disable-next-line(unsafe-cheatcode)
vm.removeFile(dst);
assertFalse(vm.exists(dst));
new CopyArtifacts().run();
assertTrue(vm.exists(dst), string.concat(names[0], ": committed artifact not recreated"));
assertEq(
keccak256(bytes(vm.readFile(dst))),
preDeletion,
string.concat(names[0], ": recreated artifact does not match pre-deletion state")
);
}
}
Loading