Skip to main content

rain_metadata/meta/query/
mod.rs

1use std::sync::Arc;
2use reqwest::Client;
3use alloy::primitives::hex::decode;
4use serde::{Deserialize, Serialize};
5use graphql_client::{GraphQLQuery, Response, QueryBody};
6use super::{
7    RainMetaDocumentV1Item, KnownMagic, types::authoring::v1::AuthoringMeta, super::error::Error,
8};
9
10type Bytes = String;
11
12#[derive(GraphQLQuery)]
13#[graphql(
14    schema_path = "src/meta/query/schema.json",
15    query_path = "src/meta/query/meta.graphql",
16    response_derives = "Debug, Serialize, Deserialize"
17)]
18pub(super) struct MetaQuery;
19
20#[derive(GraphQLQuery)]
21#[graphql(
22    schema_path = "src/meta/query/schema.json",
23    query_path = "src/meta/query/deployer.graphql",
24    response_derives = "Debug, Serialize, Deserialize"
25)]
26pub(super) struct DeployerQuery;
27
28/// response data struct for a meta
29#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
30pub struct MetaResponse {
31    #[serde(with = "serde_bytes")]
32    pub bytes: Vec<u8>,
33}
34
35/// response data struct for an ExpressionDeployer
36#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
37#[serde(rename_all = "camelCase")]
38pub struct DeployerResponse {
39    #[serde(with = "serde_bytes")]
40    pub tx_hash: Vec<u8>,
41    #[serde(with = "serde_bytes")]
42    pub bytecode_meta_hash: Vec<u8>,
43    #[serde(with = "serde_bytes")]
44    pub meta_hash: Vec<u8>,
45    #[serde(with = "serde_bytes")]
46    pub meta_bytes: Vec<u8>,
47    #[serde(with = "serde_bytes")]
48    pub bytecode: Vec<u8>,
49    #[serde(with = "serde_bytes")]
50    pub parser: Vec<u8>,
51    #[serde(with = "serde_bytes")]
52    pub store: Vec<u8>,
53    #[serde(with = "serde_bytes")]
54    pub interpreter: Vec<u8>,
55}
56
57impl DeployerResponse {
58    /// get authoring meta bytes of this deployer meta
59    pub fn get_authoring_meta(&self) -> Option<AuthoringMeta> {
60        if let Ok(meta_maps) = RainMetaDocumentV1Item::cbor_decode(&self.meta_bytes) {
61            for meta_map in &meta_maps {
62                if meta_map.magic == KnownMagic::AuthoringMetaV1 {
63                    if let Ok(v) = meta_map.unpack() {
64                        match AuthoringMeta::abi_decode_validate(&v) {
65                            Ok(am) => return Some(am),
66                            Err(_) => return None,
67                        }
68                    }
69                }
70            }
71            None
72        } else {
73            None
74        }
75    }
76}
77
78/// A graphql response carries an `errors` member independently of `data`, and
79/// serves an empty `errors` array to mean no errors at all.
80fn response_data<T>(response: Response<T>) -> Result<T, Error> {
81    if let Some(errors) = response.errors.filter(|errors| !errors.is_empty()) {
82        return Err(Error::SubgraphError(
83            errors
84                .iter()
85                .map(ToString::to_string)
86                .collect::<Vec<String>>()
87                .join("; "),
88        ));
89    }
90    response
91        .data
92        .ok_or_else(|| Error::SubgraphError("response carried neither data nor errors".to_string()))
93}
94
95/// A field of a record that was found: absent or unparsable makes the record
96/// corrupt, never absent.
97fn decode_field(field: &str, value: Option<&str>) -> Result<Vec<u8>, Error> {
98    match value {
99        Some(value) => decode(value).map_err(|e| Error::CorruptRecord(format!("{}: {}", field, e))),
100        None => Err(Error::CorruptRecord(format!("{} is missing", field))),
101    }
102}
103
104/// Process a response for a meta by resolving the record it holds, rejecting a
105/// subgraph that has no such record as `NoRecordFound` and one that cannot
106/// serve the record it has as `CorruptRecord`.
107/// This is because graphql responses are not rejected even if there was no record found for the request
108pub(super) async fn process_meta_query(
109    client: Arc<Client>,
110    request_body: &QueryBody<meta_query::Variables>,
111    url: &str,
112) -> Result<MetaResponse, Error> {
113    let raw_bytes = response_data(
114        client
115            .post(url)
116            .json(request_body)
117            .send()
118            .await
119            .map_err(Error::ReqwestError)?
120            .json::<Response<meta_query::ResponseData>>()
121            .await
122            .map_err(Error::ReqwestError)?,
123    )?
124    .meta
125    .ok_or(Error::NoRecordFound)?
126    .raw_bytes;
127
128    Ok(MetaResponse {
129        bytes: decode_field("rawBytes", Some(raw_bytes.as_str()))?,
130    })
131}
132
133/// process a response for a deployer by resolving the record it holds, rejecting
134/// a subgraph that has no such record as `NoRecordFound` and one that cannot
135/// serve the record it has as `CorruptRecord`.
136/// This is because graphql responses are not rejected even if there was no record found for the request
137pub(super) async fn process_deployer_query(
138    client: Arc<Client>,
139    request_body: &QueryBody<deployer_query::Variables>,
140    url: &str,
141) -> Result<DeployerResponse, Error> {
142    let res = response_data(
143        client
144            .post(url)
145            .json(request_body)
146            .send()
147            .await
148            .map_err(Error::ReqwestError)?
149            .json::<Response<deployer_query::ResponseData>>()
150            .await
151            .map_err(Error::ReqwestError)?,
152    )?
153    .expression_deployers;
154
155    let deployer = res.first().ok_or(Error::NoRecordFound)?;
156
157    let bytecode_meta_hash = match deployer.meta.as_slice() {
158        [meta] => decode_field("meta[0].id", Some(meta.id.as_str()))?,
159        metas => {
160            return Err(Error::CorruptRecord(format!(
161                "expected exactly one meta, got {}",
162                metas.len()
163            )))
164        }
165    };
166
167    Ok(DeployerResponse {
168        meta_hash: decode_field(
169            "constructorMetaHash",
170            Some(deployer.constructor_meta_hash.as_str()),
171        )?,
172        meta_bytes: decode_field("constructorMeta", Some(deployer.constructor_meta.as_str()))?,
173        bytecode: decode_field("bytecode", deployer.bytecode.as_deref())?,
174        parser: decode_field(
175            "parser",
176            deployer
177                .parser
178                .as_ref()
179                .map(|v| v.parser.deployed_bytecode.as_str()),
180        )?,
181        store: decode_field(
182            "store",
183            deployer
184                .store
185                .as_ref()
186                .map(|v| v.store.deployed_bytecode.as_str()),
187        )?,
188        interpreter: decode_field(
189            "interpreter",
190            deployer
191                .interpreter
192                .as_ref()
193                .map(|v| v.interpreter.deployed_bytecode.as_str()),
194        )?,
195        bytecode_meta_hash,
196        tx_hash: decode_field(
197            "deployTransaction",
198            deployer.deploy_transaction.as_ref().map(|v| v.id.as_str()),
199        )?,
200    })
201}
202
203#[cfg(all(test, not(target_family = "wasm")))]
204mod tests {
205    use super::*;
206    use crate::meta::types::authoring::v1::AuthoringMetaItem;
207    use crate::meta::{ContentEncoding, ContentLanguage, ContentType};
208    use httpmock::Method::POST;
209    use httpmock::MockServer;
210    use serde_bytes::ByteBuf;
211    use serde_json::json;
212
213    fn request_body(hash: &str) -> QueryBody<meta_query::Variables> {
214        MetaQuery::build_query(meta_query::Variables {
215            hash: Some(hash.to_string()),
216        })
217    }
218
219    const HASH: &str = "0x1111111111111111111111111111111111111111111111111111111111111111";
220
221    /// A found meta resolves to exactly the hex-decoded rawBytes of the
222    /// response, fetched with a POST carrying the query body.
223    #[tokio::test]
224    async fn test_process_meta_query_success_exact_bytes() {
225        let server = MockServer::start_async().await;
226        server.mock(|when, then| {
227            when.method(POST)
228                .path("/")
229                .json_body_partial(format!(r#"{{"variables":{{"hash":"{}"}}}}"#, HASH));
230            then.status(200)
231                .header("content-type", "application/json")
232                .body(r#"{"data":{"meta":{"__typename":"RainMetaV1","rawBytes":"0xff0a89c674ee7874deadbeef"}}}"#);
233        });
234        let result = process_meta_query(
235            Arc::new(Client::new()),
236            &request_body(HASH),
237            &server.url("/"),
238        )
239        .await
240        .unwrap();
241        assert_eq!(
242            result.bytes,
243            vec![0xff, 0x0a, 0x89, 0xc6, 0x74, 0xee, 0x78, 0x74, 0xde, 0xad, 0xbe, 0xef]
244        );
245    }
246
247    /// A response with no data member and no errors member violates the
248    /// graphql response shape: that is the subgraph failing, not absence.
249    #[tokio::test]
250    async fn test_process_meta_query_missing_data_is_subgraph_error() {
251        let server = MockServer::start_async().await;
252        server.mock(|when, then| {
253            when.method(POST).path("/");
254            then.status(200)
255                .header("content-type", "application/json")
256                .body(r#"{"data":null}"#);
257        });
258        let result = process_meta_query(
259            Arc::new(Client::new()),
260            &request_body(HASH),
261            &server.url("/"),
262        )
263        .await;
264        match result {
265            Err(Error::SubgraphError(message)) => {
266                assert_eq!(message, "response carried neither data nor errors")
267            }
268            other => panic!("expected subgraph error, got {other:?}"),
269        }
270    }
271
272    /// Top level graphql errors are the subgraph rejecting the query, and are
273    /// reported as such rather than as absence, with every message carried.
274    #[tokio::test]
275    async fn test_process_meta_query_graphql_errors_is_subgraph_error() {
276        let server = MockServer::start_async().await;
277        server.mock(|when, then| {
278            when.method(POST).path("/");
279            then.status(200)
280                .header("content-type", "application/json")
281                .body(r#"{"data":null,"errors":[{"message":"first"},{"message":"second"}]}"#);
282        });
283        let result = process_meta_query(
284            Arc::new(Client::new()),
285            &request_body(HASH),
286            &server.url("/"),
287        )
288        .await;
289        match result {
290            Err(Error::SubgraphError(message)) => {
291                assert!(message.contains("first"), "{message}");
292                assert!(message.contains("second"), "{message}");
293            }
294            other => panic!("expected subgraph error, got {other:?}"),
295        }
296    }
297
298    /// An empty errors array is the graphql wire form for "no errors": it must
299    /// not turn a served record into a failure.
300    #[tokio::test]
301    async fn test_process_meta_query_empty_errors_array_is_success() {
302        let server = MockServer::start_async().await;
303        server.mock(|when, then| {
304            when.method(POST).path("/");
305            then.status(200)
306                .header("content-type", "application/json")
307                .body(
308                    r#"{"data":{"meta":{"__typename":"RainMetaV1","rawBytes":"0x0102"}},"errors":[]}"#,
309                );
310        });
311        let result = process_meta_query(
312            Arc::new(Client::new()),
313            &request_body(HASH),
314            &server.url("/"),
315        )
316        .await
317        .unwrap();
318        assert_eq!(result.bytes, vec![0x01, 0x02]);
319    }
320
321    /// A response with data but a null meta is "no record found".
322    #[tokio::test]
323    async fn test_process_meta_query_missing_meta_is_no_record_found() {
324        let server = MockServer::start_async().await;
325        server.mock(|when, then| {
326            when.method(POST).path("/");
327            then.status(200)
328                .header("content-type", "application/json")
329                .body(r#"{"data":{"meta":null}}"#);
330        });
331        let result = process_meta_query(
332            Arc::new(Client::new()),
333            &request_body(HASH),
334            &server.url("/"),
335        )
336        .await;
337        assert!(matches!(result, Err(Error::NoRecordFound)), "{result:?}");
338    }
339
340    /// rawBytes that do not hex-decode are a record the subgraph has but
341    /// cannot serve intact: corrupt, never absent and never bytes.
342    #[tokio::test]
343    async fn test_process_meta_query_bad_hex_is_corrupt_record() {
344        let server = MockServer::start_async().await;
345        server.mock(|when, then| {
346            when.method(POST).path("/");
347            then.status(200)
348                .header("content-type", "application/json")
349                .body(r#"{"data":{"meta":{"__typename":"RainMetaV1","rawBytes":"zz-not-hex"}}}"#);
350        });
351        let result = process_meta_query(
352            Arc::new(Client::new()),
353            &request_body(HASH),
354            &server.url("/"),
355        )
356        .await;
357        match result {
358            Err(Error::CorruptRecord(message)) => {
359                assert!(message.starts_with("rawBytes: "), "{message}")
360            }
361            other => panic!("expected corrupt record, got {other:?}"),
362        }
363    }
364
365    /// A transport failure surfaces as a reqwest error, not as a
366    /// no-record-found result.
367    #[tokio::test]
368    async fn test_process_meta_query_send_error_is_reqwest_error() {
369        // Bind and immediately release a local port so the request targets a
370        // closed port.
371        let port = {
372            let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
373            listener.local_addr().unwrap().port()
374        };
375        let url = format!("http://127.0.0.1:{port}/");
376        let result = process_meta_query(Arc::new(Client::new()), &request_body(HASH), &url).await;
377        assert!(matches!(result, Err(Error::ReqwestError(_))), "{result:?}");
378    }
379
380    /// A non-JSON response body surfaces as a reqwest decode error, not as
381    /// a no-record-found result.
382    #[tokio::test]
383    async fn test_process_meta_query_non_json_is_reqwest_error() {
384        let server = MockServer::start_async().await;
385        server.mock(|when, then| {
386            when.method(POST).path("/");
387            then.status(200)
388                .header("content-type", "text/plain")
389                .body("not json");
390        });
391        let result = process_meta_query(
392            Arc::new(Client::new()),
393            &request_body(HASH),
394            &server.url("/"),
395        )
396        .await;
397        assert!(matches!(result, Err(Error::ReqwestError(_))), "{result:?}");
398    }
399
400    fn authoring_meta() -> AuthoringMeta {
401        AuthoringMeta(vec![AuthoringMetaItem {
402            word: "some-word".to_string(),
403            operand_parser_offset: 0,
404            description: "a description".to_string(),
405        }])
406    }
407
408    fn authoring_item() -> RainMetaDocumentV1Item {
409        RainMetaDocumentV1Item {
410            payload: serde_bytes::ByteBuf::from(authoring_meta().abi_encode_validate().unwrap()),
411            magic: KnownMagic::AuthoringMetaV1,
412            content_type: ContentType::None,
413            content_encoding: ContentEncoding::None,
414            content_language: ContentLanguage::None,
415            schema: None,
416        }
417    }
418
419    fn deployer_response_with(meta_bytes: Vec<u8>) -> DeployerResponse {
420        DeployerResponse {
421            tx_hash: vec![],
422            bytecode_meta_hash: vec![],
423            meta_hash: vec![],
424            meta_bytes,
425            bytecode: vec![],
426            parser: vec![],
427            store: vec![],
428            interpreter: vec![],
429        }
430    }
431
432    /// An authoring-magic item whose payload fails to unpack is skipped;
433    /// a later valid authoring meta item is still found.
434    #[test]
435    fn test_get_authoring_meta_skips_unpack_failure() {
436        // Deflate-encoded item whose payload is not valid deflate data.
437        let bad_unpack = RainMetaDocumentV1Item {
438            payload: serde_bytes::ByteBuf::from(vec![0xffu8, 0xff, 0xff, 0xff]),
439            magic: KnownMagic::AuthoringMetaV1,
440            content_type: ContentType::None,
441            content_encoding: ContentEncoding::Deflate,
442            content_language: ContentLanguage::None,
443            schema: None,
444        };
445        assert!(bad_unpack.unpack().is_err());
446        let meta_bytes = RainMetaDocumentV1Item::cbor_encode_seq(
447            &vec![bad_unpack, authoring_item()],
448            KnownMagic::RainMetaDocumentV1,
449        )
450        .unwrap();
451        assert_eq!(
452            deployer_response_with(meta_bytes).get_authoring_meta(),
453            Some(authoring_meta())
454        );
455    }
456
457    /// The scan covers every item in the document: an authoring meta in the
458    /// second position is found behind a non-authoring first item.
459    #[test]
460    fn test_get_authoring_meta_scans_beyond_first_item() {
461        let other_magic = RainMetaDocumentV1Item {
462            payload: serde_bytes::ByteBuf::from(b"_: int-add(1 2);".to_vec()),
463            magic: KnownMagic::RainlangV1,
464            content_type: ContentType::None,
465            content_encoding: ContentEncoding::None,
466            content_language: ContentLanguage::None,
467            schema: None,
468        };
469        let meta_bytes = RainMetaDocumentV1Item::cbor_encode_seq(
470            &vec![other_magic, authoring_item()],
471            KnownMagic::RainMetaDocumentV1,
472        )
473        .unwrap();
474        assert_eq!(
475            deployer_response_with(meta_bytes).get_authoring_meta(),
476            Some(authoring_meta())
477        );
478    }
479
480    /// A fully populated, valid expressionDeployers entry as the subgraph
481    /// would return it: every hex field decodes and exactly one meta.
482    fn deployer_entry() -> serde_json::Value {
483        json!({
484            "constructorMetaHash": "0x0102",
485            "constructorMeta": "0x0304",
486            "deployTransaction": { "id": "0x0506" },
487            "bytecode": "0x0708",
488            "parser": { "parser": { "deployedBytecode": "0x090a" } },
489            "store": { "store": { "deployedBytecode": "0x0b0c" } },
490            "interpreter": { "interpreter": { "deployedBytecode": "0x0d0e" } },
491            "meta": [ { "__typename": "RainMetaV1", "id": "0x0f10" } ]
492        })
493    }
494
495    async fn run_deployer_query(entry: serde_json::Value) -> Result<DeployerResponse, Error> {
496        let server = MockServer::start_async().await;
497        server.mock(|when, then| {
498            when.method(POST).path("/");
499            then.status(200)
500                .json_body(json!({ "data": { "expressionDeployers": [entry] } }));
501        });
502        let request_body = DeployerQuery::build_query(deployer_query::Variables {
503            hash: Some("0xabcd".to_string()),
504        });
505        let client = Arc::new(Client::new());
506        process_deployer_query(client, &request_body, &server.url("/")).await
507    }
508
509    #[tokio::test]
510    async fn test_process_deployer_query_success_decodes_all_fields() {
511        let res = run_deployer_query(deployer_entry()).await.unwrap();
512        assert_eq!(res.meta_hash, vec![0x01, 0x02]);
513        assert_eq!(res.meta_bytes, vec![0x03, 0x04]);
514        assert_eq!(res.tx_hash, vec![0x05, 0x06]);
515        assert_eq!(res.bytecode, vec![0x07, 0x08]);
516        assert_eq!(res.parser, vec![0x09, 0x0a]);
517        assert_eq!(res.store, vec![0x0b, 0x0c]);
518        assert_eq!(res.interpreter, vec![0x0d, 0x0e]);
519        assert_eq!(res.bytecode_meta_hash, vec![0x0f, 0x10]);
520    }
521
522    /// A deployer record that was found but cannot be served intact is
523    /// corrupt: the message names the offending field.
524    async fn deployer_corrupt_record(entry: serde_json::Value) -> String {
525        match run_deployer_query(entry).await {
526            Err(Error::CorruptRecord(message)) => message,
527            other => panic!("expected corrupt record, got {other:?}"),
528        }
529    }
530
531    #[tokio::test]
532    async fn test_process_deployer_query_null_bytecode_is_corrupt_record() {
533        let mut entry = deployer_entry();
534        entry["bytecode"] = serde_json::Value::Null;
535        assert_eq!(deployer_corrupt_record(entry).await, "bytecode is missing");
536    }
537
538    #[tokio::test]
539    async fn test_process_deployer_query_null_parser_is_corrupt_record() {
540        let mut entry = deployer_entry();
541        entry["parser"] = serde_json::Value::Null;
542        assert_eq!(deployer_corrupt_record(entry).await, "parser is missing");
543    }
544
545    #[tokio::test]
546    async fn test_process_deployer_query_null_store_is_corrupt_record() {
547        let mut entry = deployer_entry();
548        entry["store"] = serde_json::Value::Null;
549        assert_eq!(deployer_corrupt_record(entry).await, "store is missing");
550    }
551
552    #[tokio::test]
553    async fn test_process_deployer_query_null_interpreter_is_corrupt_record() {
554        let mut entry = deployer_entry();
555        entry["interpreter"] = serde_json::Value::Null;
556        assert_eq!(
557            deployer_corrupt_record(entry).await,
558            "interpreter is missing"
559        );
560    }
561
562    #[tokio::test]
563    async fn test_process_deployer_query_null_deploy_transaction_is_corrupt_record() {
564        let mut entry = deployer_entry();
565        entry["deployTransaction"] = serde_json::Value::Null;
566        assert_eq!(
567            deployer_corrupt_record(entry).await,
568            "deployTransaction is missing"
569        );
570    }
571
572    #[tokio::test]
573    async fn test_process_deployer_query_zero_metas_is_corrupt_record() {
574        let mut entry = deployer_entry();
575        entry["meta"] = json!([]);
576        assert_eq!(
577            deployer_corrupt_record(entry).await,
578            "expected exactly one meta, got 0"
579        );
580    }
581
582    #[tokio::test]
583    async fn test_process_deployer_query_two_metas_is_corrupt_record() {
584        let mut entry = deployer_entry();
585        entry["meta"] = json!([
586            { "__typename": "RainMetaV1", "id": "0x0f10" },
587            { "__typename": "RainMetaV1", "id": "0x1112" }
588        ]);
589        assert_eq!(
590            deployer_corrupt_record(entry).await,
591            "expected exactly one meta, got 2"
592        );
593    }
594
595    #[tokio::test]
596    async fn test_process_deployer_query_invalid_bytecode_hex_is_corrupt_record() {
597        let mut entry = deployer_entry();
598        entry["bytecode"] = json!("0xZZ");
599        let message = deployer_corrupt_record(entry).await;
600        assert!(message.starts_with("bytecode: "), "{message}");
601    }
602
603    #[tokio::test]
604    async fn test_process_deployer_query_invalid_parser_hex_is_corrupt_record() {
605        let mut entry = deployer_entry();
606        entry["parser"]["parser"]["deployedBytecode"] = json!("0xZZ");
607        let message = deployer_corrupt_record(entry).await;
608        assert!(message.starts_with("parser: "), "{message}");
609    }
610
611    #[tokio::test]
612    async fn test_process_deployer_query_invalid_store_hex_is_corrupt_record() {
613        let mut entry = deployer_entry();
614        entry["store"]["store"]["deployedBytecode"] = json!("0xZZ");
615        let message = deployer_corrupt_record(entry).await;
616        assert!(message.starts_with("store: "), "{message}");
617    }
618
619    #[tokio::test]
620    async fn test_process_deployer_query_invalid_interpreter_hex_is_corrupt_record() {
621        let mut entry = deployer_entry();
622        entry["interpreter"]["interpreter"]["deployedBytecode"] = json!("0xZZ");
623        let message = deployer_corrupt_record(entry).await;
624        assert!(message.starts_with("interpreter: "), "{message}");
625    }
626
627    #[tokio::test]
628    async fn test_process_deployer_query_invalid_meta_id_hex_is_corrupt_record() {
629        let mut entry = deployer_entry();
630        entry["meta"][0]["id"] = json!("0xZZ");
631        let message = deployer_corrupt_record(entry).await;
632        assert!(message.starts_with("meta[0].id: "), "{message}");
633    }
634
635    #[tokio::test]
636    async fn test_process_deployer_query_invalid_tx_id_hex_is_corrupt_record() {
637        let mut entry = deployer_entry();
638        entry["deployTransaction"]["id"] = json!("0xZZ");
639        let message = deployer_corrupt_record(entry).await;
640        assert!(message.starts_with("deployTransaction: "), "{message}");
641    }
642
643    #[tokio::test]
644    async fn test_process_deployer_query_invalid_constructor_meta_hash_hex_is_corrupt_record() {
645        let mut entry = deployer_entry();
646        entry["constructorMetaHash"] = json!("0xZZ");
647        let message = deployer_corrupt_record(entry).await;
648        assert!(message.starts_with("constructorMetaHash: "), "{message}");
649    }
650
651    #[tokio::test]
652    async fn test_process_deployer_query_invalid_constructor_meta_hex_is_corrupt_record() {
653        let mut entry = deployer_entry();
654        entry["constructorMeta"] = json!("0xZZ");
655        let message = deployer_corrupt_record(entry).await;
656        assert!(message.starts_with("constructorMeta: "), "{message}");
657    }
658
659    /// A deployer query the subgraph rejects is a subgraph error, not the
660    /// absence of a deployer record.
661    #[tokio::test]
662    async fn test_process_deployer_query_graphql_errors_is_subgraph_error() {
663        let server = MockServer::start_async().await;
664        server.mock(|when, then| {
665            when.method(POST).path("/");
666            then.status(200)
667                .json_body(json!({ "errors": [ { "message": "deployer boom" } ] }));
668        });
669        let request_body = DeployerQuery::build_query(deployer_query::Variables {
670            hash: Some("0xabcd".to_string()),
671        });
672        let result =
673            process_deployer_query(Arc::new(Client::new()), &request_body, &server.url("/")).await;
674        match result {
675            Err(Error::SubgraphError(message)) => {
676                assert!(message.contains("deployer boom"), "{message}")
677            }
678            other => panic!("expected subgraph error, got {other:?}"),
679        }
680    }
681
682    fn sample_authoring_meta() -> (AuthoringMeta, Vec<u8>) {
683        let authoring_meta: AuthoringMeta = serde_json::from_str(
684            r#"[{"word":"stack","description":"Copies an existing value from the stack.","operandParserOffset":16}]"#,
685        )
686        .unwrap();
687        let abi = authoring_meta.abi_encode_validate().unwrap();
688        (authoring_meta, abi)
689    }
690
691    fn authoring_doc(abi_payload: Vec<u8>, encoding: ContentEncoding) -> Vec<u8> {
692        let payload = encoding.encode(&abi_payload);
693        let item = RainMetaDocumentV1Item {
694            payload: ByteBuf::from(payload),
695            magic: KnownMagic::AuthoringMetaV1,
696            content_type: ContentType::Cbor,
697            content_encoding: encoding,
698            content_language: ContentLanguage::None,
699            schema: None,
700        };
701        RainMetaDocumentV1Item::cbor_encode_seq(&vec![item], KnownMagic::RainMetaDocumentV1)
702            .unwrap()
703    }
704
705    fn deployer_response(meta_bytes: Vec<u8>) -> DeployerResponse {
706        DeployerResponse {
707            tx_hash: vec![0x01],
708            bytecode_meta_hash: vec![0x02],
709            meta_hash: vec![0x03],
710            meta_bytes,
711            bytecode: vec![0x04],
712            parser: vec![0x05],
713            store: vec![0x06],
714            interpreter: vec![0x07],
715        }
716    }
717
718    /// A document carrying a valid AuthoringMetaV1 item yields the decoded
719    /// authoring meta.
720    #[test]
721    fn test_get_authoring_meta_found() {
722        let (authoring_meta, abi) = sample_authoring_meta();
723        let response = deployer_response(authoring_doc(abi, ContentEncoding::None));
724        assert_eq!(response.get_authoring_meta(), Some(authoring_meta));
725    }
726
727    /// The payload is unpacked per its content encoding before abi decoding:
728    /// a deflate encoded authoring meta still decodes.
729    #[test]
730    fn test_get_authoring_meta_deflate_unpack() {
731        let (authoring_meta, abi) = sample_authoring_meta();
732        let response = deployer_response(authoring_doc(abi, ContentEncoding::Deflate));
733        assert_eq!(response.get_authoring_meta(), Some(authoring_meta));
734    }
735
736    /// An authoring item that decodes but fails validation, or does not abi
737    /// decode at all, yields None.
738    #[test]
739    fn test_get_authoring_meta_invalid_returns_none() {
740        let invalid = AuthoringMeta(vec![AuthoringMetaItem {
741            word: "NOTKEBAB".to_string(),
742            operand_parser_offset: 0,
743            description: "some description".to_string(),
744        }]);
745        let abi = invalid.abi_encode().unwrap();
746        let response = deployer_response(authoring_doc(abi, ContentEncoding::None));
747        assert_eq!(response.get_authoring_meta(), None);
748
749        let undecodable = deployer_response(authoring_doc(
750            vec![0xde, 0xad, 0xbe, 0xef],
751            ContentEncoding::None,
752        ));
753        assert_eq!(undecodable.get_authoring_meta(), None);
754    }
755
756    /// A document without an AuthoringMetaV1 item, or meta bytes that do not
757    /// cbor decode, yield None.
758    #[test]
759    fn test_get_authoring_meta_absent_or_undecodable() {
760        let item = RainMetaDocumentV1Item {
761            payload: ByteBuf::from("some dotrain".as_bytes()),
762            magic: KnownMagic::DotrainV1,
763            content_type: ContentType::OctetStream,
764            content_encoding: ContentEncoding::None,
765            content_language: ContentLanguage::None,
766            schema: None,
767        };
768        let doc =
769            RainMetaDocumentV1Item::cbor_encode_seq(&vec![item], KnownMagic::RainMetaDocumentV1)
770                .unwrap();
771        assert_eq!(deployer_response(doc).get_authoring_meta(), None);
772
773        assert_eq!(
774            deployer_response(vec![0xff, 0x00, 0x12]).get_authoring_meta(),
775            None
776        );
777    }
778
779    /// process_meta_query separates a rejected query, a genuinely absent
780    /// record and a malformed one, and decodes a found record.
781    #[tokio::test]
782    async fn test_process_meta_query_paths() {
783        use httpmock::prelude::*;
784        let client = Arc::new(Client::builder().build().unwrap());
785        let body = MetaQuery::build_query(meta_query::Variables {
786            hash: Some("0xabc".to_string()),
787        });
788        let server = MockServer::start();
789
790        let no_data = server.mock(|when, then| {
791            when.method(POST).path("/nodata");
792            then.status(200)
793                .json_body(serde_json::json!({"errors": [{"message": "nope"}]}));
794        });
795        let result = process_meta_query(client.clone(), &body, &server.url("/nodata")).await;
796        assert!(matches!(result, Err(Error::SubgraphError(_))), "{result:?}");
797        no_data.assert();
798
799        let _no_meta = server.mock(|when, then| {
800            when.method(POST).path("/nometa");
801            then.status(200)
802                .json_body(serde_json::json!({"data": {"meta": null}}));
803        });
804        let result = process_meta_query(client.clone(), &body, &server.url("/nometa")).await;
805        assert!(matches!(result, Err(Error::NoRecordFound)), "{result:?}");
806
807        let _bad_hex = server.mock(|when, then| {
808            when.method(POST).path("/badhex");
809            then.status(200).json_body(serde_json::json!({
810                "data": {"meta": {"__typename": "RainMetaV1", "rawBytes": "0xzz"}}
811            }));
812        });
813        let result = process_meta_query(client.clone(), &body, &server.url("/badhex")).await;
814        assert!(matches!(result, Err(Error::CorruptRecord(_))), "{result:?}");
815
816        let _found = server.mock(|when, then| {
817            when.method(POST).path("/found");
818            then.status(200).json_body(serde_json::json!({
819                "data": {"meta": {"__typename": "RainMetaV1", "rawBytes": "0x0102"}}
820            }));
821        });
822        let result = process_meta_query(client.clone(), &body, &server.url("/found"))
823            .await
824            .unwrap();
825        assert_eq!(result.bytes, vec![0x01, 0x02]);
826    }
827
828    /// process_deployer_query maps an empty expressionDeployers result to
829    /// NoRecordFound.
830    #[tokio::test]
831    async fn test_process_deployer_query_empty_no_record() {
832        use httpmock::prelude::*;
833        let client = Arc::new(Client::builder().build().unwrap());
834        let body = DeployerQuery::build_query(deployer_query::Variables {
835            hash: Some("0xabc".to_string()),
836        });
837        let server = MockServer::start();
838        let _empty = server.mock(|when, then| {
839            when.method(POST);
840            then.status(200)
841                .json_body(serde_json::json!({"data": {"expressionDeployers": []}}));
842        });
843        let result = process_deployer_query(client, &body, &server.url("/")).await;
844        assert!(matches!(result, Err(Error::NoRecordFound)));
845    }
846}