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/// Process a response for a meta by resolving if a record was found or reject if nothing found or rejected with error
79/// This is because graphql responses are not rejected even if there was no record found for the request
80pub(super) async fn process_meta_query(
81    client: Arc<Client>,
82    request_body: &QueryBody<meta_query::Variables>,
83    url: &str,
84) -> Result<MetaResponse, Error> {
85    Ok(MetaResponse {
86        bytes: decode(
87            client
88                .post(url)
89                .json(request_body)
90                .send()
91                .await
92                .map_err(Error::ReqwestError)?
93                .json::<Response<meta_query::ResponseData>>()
94                .await
95                .map_err(Error::ReqwestError)?
96                .data
97                .ok_or(Error::NoRecordFound)?
98                .meta
99                .ok_or(Error::NoRecordFound)?
100                .raw_bytes,
101        )
102        .or(Err(Error::NoRecordFound))?,
103    })
104}
105
106/// process a response for a deployer by resolving if a record was found or reject if nothing found or rejected with error
107/// This is because graphql responses are not rejected even if there was no record found for the request
108pub(super) async fn process_deployer_query(
109    client: Arc<Client>,
110    request_body: &QueryBody<deployer_query::Variables>,
111    url: &str,
112) -> Result<DeployerResponse, Error> {
113    let res = client
114        .post(url)
115        .json(request_body)
116        .send()
117        .await
118        .map_err(Error::ReqwestError)?
119        .json::<Response<deployer_query::ResponseData>>()
120        .await
121        .map_err(Error::ReqwestError)?
122        .data
123        .ok_or(Error::NoRecordFound)?
124        .expression_deployers;
125
126    if !res.is_empty() {
127        let bytecode = if let Some(v) = &res[0].bytecode {
128            decode(v).or(Err(Error::NoRecordFound))?
129        } else {
130            return Err(Error::NoRecordFound);
131        };
132        let parser = if let Some(v) = &res[0].parser {
133            decode(&v.parser.deployed_bytecode).or(Err(Error::NoRecordFound))?
134        } else {
135            return Err(Error::NoRecordFound);
136        };
137        let store = if let Some(v) = &res[0].store {
138            decode(&v.store.deployed_bytecode).or(Err(Error::NoRecordFound))?
139        } else {
140            return Err(Error::NoRecordFound);
141        };
142        let interpreter = if let Some(v) = &res[0].interpreter {
143            decode(&v.interpreter.deployed_bytecode).or(Err(Error::NoRecordFound))?
144        } else {
145            return Err(Error::NoRecordFound);
146        };
147        let bytecode_meta_hash = if res[0].meta.len() == 1 {
148            decode(&res[0].meta[0].id).or(Err(Error::NoRecordFound))?
149        } else {
150            return Err(Error::NoRecordFound);
151        };
152        let tx_hash = if let Some(v) = &res[0].deploy_transaction {
153            decode(&v.id).or(Err(Error::NoRecordFound))?
154        } else {
155            return Err(Error::NoRecordFound);
156        };
157        let meta_hash = decode(&res[0].constructor_meta_hash).or(Err(Error::NoRecordFound))?;
158        let meta_bytes = decode(&res[0].constructor_meta).or(Err(Error::NoRecordFound))?;
159        Ok(DeployerResponse {
160            meta_hash,
161            meta_bytes,
162            bytecode,
163            parser,
164            store,
165            interpreter,
166            bytecode_meta_hash,
167            tx_hash,
168        })
169    } else {
170        Err(Error::NoRecordFound)
171    }
172}
173
174#[cfg(all(test, not(target_family = "wasm")))]
175mod tests {
176    use super::*;
177    use crate::meta::types::authoring::v1::AuthoringMetaItem;
178    use crate::meta::{ContentEncoding, ContentLanguage, ContentType};
179    use httpmock::Method::POST;
180    use httpmock::MockServer;
181    use serde_bytes::ByteBuf;
182    use serde_json::json;
183
184    fn request_body(hash: &str) -> QueryBody<meta_query::Variables> {
185        MetaQuery::build_query(meta_query::Variables {
186            hash: Some(hash.to_string()),
187        })
188    }
189
190    const HASH: &str = "0x1111111111111111111111111111111111111111111111111111111111111111";
191
192    /// A found meta resolves to exactly the hex-decoded rawBytes of the
193    /// response, fetched with a POST carrying the query body.
194    #[tokio::test]
195    async fn test_process_meta_query_success_exact_bytes() {
196        let server = MockServer::start_async().await;
197        server.mock(|when, then| {
198            when.method(POST)
199                .path("/")
200                .json_body_partial(format!(r#"{{"variables":{{"hash":"{}"}}}}"#, HASH));
201            then.status(200)
202                .header("content-type", "application/json")
203                .body(r#"{"data":{"meta":{"__typename":"RainMetaV1","rawBytes":"0xff0a89c674ee7874deadbeef"}}}"#);
204        });
205        let result = process_meta_query(
206            Arc::new(Client::new()),
207            &request_body(HASH),
208            &server.url("/"),
209        )
210        .await
211        .unwrap();
212        assert_eq!(
213            result.bytes,
214            vec![0xff, 0x0a, 0x89, 0xc6, 0x74, 0xee, 0x78, 0x74, 0xde, 0xad, 0xbe, 0xef]
215        );
216    }
217
218    /// A response with no data member at all is "no record found".
219    #[tokio::test]
220    async fn test_process_meta_query_missing_data_is_no_record_found() {
221        let server = MockServer::start_async().await;
222        server.mock(|when, then| {
223            when.method(POST).path("/");
224            then.status(200)
225                .header("content-type", "application/json")
226                .body(r#"{"data":null}"#);
227        });
228        let result = process_meta_query(
229            Arc::new(Client::new()),
230            &request_body(HASH),
231            &server.url("/"),
232        )
233        .await;
234        assert!(matches!(result, Err(Error::NoRecordFound)), "{result:?}");
235    }
236
237    /// A response with data but a null meta is "no record found".
238    #[tokio::test]
239    async fn test_process_meta_query_missing_meta_is_no_record_found() {
240        let server = MockServer::start_async().await;
241        server.mock(|when, then| {
242            when.method(POST).path("/");
243            then.status(200)
244                .header("content-type", "application/json")
245                .body(r#"{"data":{"meta":null}}"#);
246        });
247        let result = process_meta_query(
248            Arc::new(Client::new()),
249            &request_body(HASH),
250            &server.url("/"),
251        )
252        .await;
253        assert!(matches!(result, Err(Error::NoRecordFound)), "{result:?}");
254    }
255
256    /// rawBytes that do not hex-decode resolve to "no record found",
257    /// never to successfully-returned bytes.
258    #[tokio::test]
259    async fn test_process_meta_query_bad_hex_is_no_record_found() {
260        let server = MockServer::start_async().await;
261        server.mock(|when, then| {
262            when.method(POST).path("/");
263            then.status(200)
264                .header("content-type", "application/json")
265                .body(r#"{"data":{"meta":{"__typename":"RainMetaV1","rawBytes":"zz-not-hex"}}}"#);
266        });
267        let result = process_meta_query(
268            Arc::new(Client::new()),
269            &request_body(HASH),
270            &server.url("/"),
271        )
272        .await;
273        assert!(matches!(result, Err(Error::NoRecordFound)), "{result:?}");
274    }
275
276    /// A transport failure surfaces as a reqwest error, not as a
277    /// no-record-found result.
278    #[tokio::test]
279    async fn test_process_meta_query_send_error_is_reqwest_error() {
280        // Bind and immediately release a local port so the request targets a
281        // closed port.
282        let port = {
283            let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
284            listener.local_addr().unwrap().port()
285        };
286        let url = format!("http://127.0.0.1:{port}/");
287        let result = process_meta_query(Arc::new(Client::new()), &request_body(HASH), &url).await;
288        assert!(matches!(result, Err(Error::ReqwestError(_))), "{result:?}");
289    }
290
291    /// A non-JSON response body surfaces as a reqwest decode error, not as
292    /// a no-record-found result.
293    #[tokio::test]
294    async fn test_process_meta_query_non_json_is_reqwest_error() {
295        let server = MockServer::start_async().await;
296        server.mock(|when, then| {
297            when.method(POST).path("/");
298            then.status(200)
299                .header("content-type", "text/plain")
300                .body("not json");
301        });
302        let result = process_meta_query(
303            Arc::new(Client::new()),
304            &request_body(HASH),
305            &server.url("/"),
306        )
307        .await;
308        assert!(matches!(result, Err(Error::ReqwestError(_))), "{result:?}");
309    }
310
311    fn authoring_meta() -> AuthoringMeta {
312        AuthoringMeta(vec![AuthoringMetaItem {
313            word: "some-word".to_string(),
314            operand_parser_offset: 0,
315            description: "a description".to_string(),
316        }])
317    }
318
319    fn authoring_item() -> RainMetaDocumentV1Item {
320        RainMetaDocumentV1Item {
321            payload: serde_bytes::ByteBuf::from(authoring_meta().abi_encode_validate().unwrap()),
322            magic: KnownMagic::AuthoringMetaV1,
323            content_type: ContentType::None,
324            content_encoding: ContentEncoding::None,
325            content_language: ContentLanguage::None,
326            schema: None,
327        }
328    }
329
330    fn deployer_response_with(meta_bytes: Vec<u8>) -> DeployerResponse {
331        DeployerResponse {
332            tx_hash: vec![],
333            bytecode_meta_hash: vec![],
334            meta_hash: vec![],
335            meta_bytes,
336            bytecode: vec![],
337            parser: vec![],
338            store: vec![],
339            interpreter: vec![],
340        }
341    }
342
343    /// An authoring-magic item whose payload fails to unpack is skipped;
344    /// a later valid authoring meta item is still found.
345    #[test]
346    fn test_get_authoring_meta_skips_unpack_failure() {
347        // Deflate-encoded item whose payload is not valid deflate data.
348        let bad_unpack = RainMetaDocumentV1Item {
349            payload: serde_bytes::ByteBuf::from(vec![0xffu8, 0xff, 0xff, 0xff]),
350            magic: KnownMagic::AuthoringMetaV1,
351            content_type: ContentType::None,
352            content_encoding: ContentEncoding::Deflate,
353            content_language: ContentLanguage::None,
354            schema: None,
355        };
356        assert!(bad_unpack.unpack().is_err());
357        let meta_bytes = RainMetaDocumentV1Item::cbor_encode_seq(
358            &vec![bad_unpack, authoring_item()],
359            KnownMagic::RainMetaDocumentV1,
360        )
361        .unwrap();
362        assert_eq!(
363            deployer_response_with(meta_bytes).get_authoring_meta(),
364            Some(authoring_meta())
365        );
366    }
367
368    /// The scan covers every item in the document: an authoring meta in the
369    /// second position is found behind a non-authoring first item.
370    #[test]
371    fn test_get_authoring_meta_scans_beyond_first_item() {
372        let other_magic = RainMetaDocumentV1Item {
373            payload: serde_bytes::ByteBuf::from(b"_: int-add(1 2);".to_vec()),
374            magic: KnownMagic::RainlangV1,
375            content_type: ContentType::None,
376            content_encoding: ContentEncoding::None,
377            content_language: ContentLanguage::None,
378            schema: None,
379        };
380        let meta_bytes = RainMetaDocumentV1Item::cbor_encode_seq(
381            &vec![other_magic, authoring_item()],
382            KnownMagic::RainMetaDocumentV1,
383        )
384        .unwrap();
385        assert_eq!(
386            deployer_response_with(meta_bytes).get_authoring_meta(),
387            Some(authoring_meta())
388        );
389    }
390
391    /// A fully populated, valid expressionDeployers entry as the subgraph
392    /// would return it: every hex field decodes and exactly one meta.
393    fn deployer_entry() -> serde_json::Value {
394        json!({
395            "constructorMetaHash": "0x0102",
396            "constructorMeta": "0x0304",
397            "deployTransaction": { "id": "0x0506" },
398            "bytecode": "0x0708",
399            "parser": { "parser": { "deployedBytecode": "0x090a" } },
400            "store": { "store": { "deployedBytecode": "0x0b0c" } },
401            "interpreter": { "interpreter": { "deployedBytecode": "0x0d0e" } },
402            "meta": [ { "__typename": "RainMetaV1", "id": "0x0f10" } ]
403        })
404    }
405
406    async fn run_deployer_query(entry: serde_json::Value) -> Result<DeployerResponse, Error> {
407        let server = MockServer::start_async().await;
408        server.mock(|when, then| {
409            when.method(POST).path("/");
410            then.status(200)
411                .json_body(json!({ "data": { "expressionDeployers": [entry] } }));
412        });
413        let request_body = DeployerQuery::build_query(deployer_query::Variables {
414            hash: Some("0xabcd".to_string()),
415        });
416        let client = Arc::new(Client::new());
417        process_deployer_query(client, &request_body, &server.url("/")).await
418    }
419
420    #[tokio::test]
421    async fn test_process_deployer_query_success_decodes_all_fields() {
422        let res = run_deployer_query(deployer_entry()).await.unwrap();
423        assert_eq!(res.meta_hash, vec![0x01, 0x02]);
424        assert_eq!(res.meta_bytes, vec![0x03, 0x04]);
425        assert_eq!(res.tx_hash, vec![0x05, 0x06]);
426        assert_eq!(res.bytecode, vec![0x07, 0x08]);
427        assert_eq!(res.parser, vec![0x09, 0x0a]);
428        assert_eq!(res.store, vec![0x0b, 0x0c]);
429        assert_eq!(res.interpreter, vec![0x0d, 0x0e]);
430        assert_eq!(res.bytecode_meta_hash, vec![0x0f, 0x10]);
431    }
432
433    #[tokio::test]
434    async fn test_process_deployer_query_null_bytecode_is_no_record_found() {
435        let mut entry = deployer_entry();
436        entry["bytecode"] = serde_json::Value::Null;
437        assert!(matches!(
438            run_deployer_query(entry).await,
439            Err(Error::NoRecordFound)
440        ));
441    }
442
443    #[tokio::test]
444    async fn test_process_deployer_query_null_parser_is_no_record_found() {
445        let mut entry = deployer_entry();
446        entry["parser"] = serde_json::Value::Null;
447        assert!(matches!(
448            run_deployer_query(entry).await,
449            Err(Error::NoRecordFound)
450        ));
451    }
452
453    #[tokio::test]
454    async fn test_process_deployer_query_null_store_is_no_record_found() {
455        let mut entry = deployer_entry();
456        entry["store"] = serde_json::Value::Null;
457        assert!(matches!(
458            run_deployer_query(entry).await,
459            Err(Error::NoRecordFound)
460        ));
461    }
462
463    #[tokio::test]
464    async fn test_process_deployer_query_null_interpreter_is_no_record_found() {
465        let mut entry = deployer_entry();
466        entry["interpreter"] = serde_json::Value::Null;
467        assert!(matches!(
468            run_deployer_query(entry).await,
469            Err(Error::NoRecordFound)
470        ));
471    }
472
473    #[tokio::test]
474    async fn test_process_deployer_query_null_deploy_transaction_is_no_record_found() {
475        let mut entry = deployer_entry();
476        entry["deployTransaction"] = serde_json::Value::Null;
477        assert!(matches!(
478            run_deployer_query(entry).await,
479            Err(Error::NoRecordFound)
480        ));
481    }
482
483    #[tokio::test]
484    async fn test_process_deployer_query_zero_metas_is_no_record_found() {
485        let mut entry = deployer_entry();
486        entry["meta"] = json!([]);
487        assert!(matches!(
488            run_deployer_query(entry).await,
489            Err(Error::NoRecordFound)
490        ));
491    }
492
493    #[tokio::test]
494    async fn test_process_deployer_query_two_metas_is_no_record_found() {
495        let mut entry = deployer_entry();
496        entry["meta"] = json!([
497            { "__typename": "RainMetaV1", "id": "0x0f10" },
498            { "__typename": "RainMetaV1", "id": "0x1112" }
499        ]);
500        assert!(matches!(
501            run_deployer_query(entry).await,
502            Err(Error::NoRecordFound)
503        ));
504    }
505
506    #[tokio::test]
507    async fn test_process_deployer_query_invalid_bytecode_hex_is_no_record_found() {
508        let mut entry = deployer_entry();
509        entry["bytecode"] = json!("0xZZ");
510        assert!(matches!(
511            run_deployer_query(entry).await,
512            Err(Error::NoRecordFound)
513        ));
514    }
515
516    #[tokio::test]
517    async fn test_process_deployer_query_invalid_parser_hex_is_no_record_found() {
518        let mut entry = deployer_entry();
519        entry["parser"]["parser"]["deployedBytecode"] = json!("0xZZ");
520        assert!(matches!(
521            run_deployer_query(entry).await,
522            Err(Error::NoRecordFound)
523        ));
524    }
525
526    #[tokio::test]
527    async fn test_process_deployer_query_invalid_store_hex_is_no_record_found() {
528        let mut entry = deployer_entry();
529        entry["store"]["store"]["deployedBytecode"] = json!("0xZZ");
530        assert!(matches!(
531            run_deployer_query(entry).await,
532            Err(Error::NoRecordFound)
533        ));
534    }
535
536    #[tokio::test]
537    async fn test_process_deployer_query_invalid_interpreter_hex_is_no_record_found() {
538        let mut entry = deployer_entry();
539        entry["interpreter"]["interpreter"]["deployedBytecode"] = json!("0xZZ");
540        assert!(matches!(
541            run_deployer_query(entry).await,
542            Err(Error::NoRecordFound)
543        ));
544    }
545
546    #[tokio::test]
547    async fn test_process_deployer_query_invalid_meta_id_hex_is_no_record_found() {
548        let mut entry = deployer_entry();
549        entry["meta"][0]["id"] = json!("0xZZ");
550        assert!(matches!(
551            run_deployer_query(entry).await,
552            Err(Error::NoRecordFound)
553        ));
554    }
555
556    #[tokio::test]
557    async fn test_process_deployer_query_invalid_tx_id_hex_is_no_record_found() {
558        let mut entry = deployer_entry();
559        entry["deployTransaction"]["id"] = json!("0xZZ");
560        assert!(matches!(
561            run_deployer_query(entry).await,
562            Err(Error::NoRecordFound)
563        ));
564    }
565
566    #[tokio::test]
567    async fn test_process_deployer_query_invalid_constructor_meta_hash_hex_is_no_record_found() {
568        let mut entry = deployer_entry();
569        entry["constructorMetaHash"] = json!("0xZZ");
570        assert!(matches!(
571            run_deployer_query(entry).await,
572            Err(Error::NoRecordFound)
573        ));
574    }
575
576    #[tokio::test]
577    async fn test_process_deployer_query_invalid_constructor_meta_hex_is_no_record_found() {
578        let mut entry = deployer_entry();
579        entry["constructorMeta"] = json!("0xZZ");
580        assert!(matches!(
581            run_deployer_query(entry).await,
582            Err(Error::NoRecordFound)
583        ));
584    }
585
586    fn sample_authoring_meta() -> (AuthoringMeta, Vec<u8>) {
587        let authoring_meta: AuthoringMeta = serde_json::from_str(
588            r#"[{"word":"stack","description":"Copies an existing value from the stack.","operandParserOffset":16}]"#,
589        )
590        .unwrap();
591        let abi = authoring_meta.abi_encode_validate().unwrap();
592        (authoring_meta, abi)
593    }
594
595    fn authoring_doc(abi_payload: Vec<u8>, encoding: ContentEncoding) -> Vec<u8> {
596        let payload = encoding.encode(&abi_payload);
597        let item = RainMetaDocumentV1Item {
598            payload: ByteBuf::from(payload),
599            magic: KnownMagic::AuthoringMetaV1,
600            content_type: ContentType::Cbor,
601            content_encoding: encoding,
602            content_language: ContentLanguage::None,
603            schema: None,
604        };
605        RainMetaDocumentV1Item::cbor_encode_seq(&vec![item], KnownMagic::RainMetaDocumentV1)
606            .unwrap()
607    }
608
609    fn deployer_response(meta_bytes: Vec<u8>) -> DeployerResponse {
610        DeployerResponse {
611            tx_hash: vec![0x01],
612            bytecode_meta_hash: vec![0x02],
613            meta_hash: vec![0x03],
614            meta_bytes,
615            bytecode: vec![0x04],
616            parser: vec![0x05],
617            store: vec![0x06],
618            interpreter: vec![0x07],
619        }
620    }
621
622    /// A document carrying a valid AuthoringMetaV1 item yields the decoded
623    /// authoring meta.
624    #[test]
625    fn test_get_authoring_meta_found() {
626        let (authoring_meta, abi) = sample_authoring_meta();
627        let response = deployer_response(authoring_doc(abi, ContentEncoding::None));
628        assert_eq!(response.get_authoring_meta(), Some(authoring_meta));
629    }
630
631    /// The payload is unpacked per its content encoding before abi decoding:
632    /// a deflate encoded authoring meta still decodes.
633    #[test]
634    fn test_get_authoring_meta_deflate_unpack() {
635        let (authoring_meta, abi) = sample_authoring_meta();
636        let response = deployer_response(authoring_doc(abi, ContentEncoding::Deflate));
637        assert_eq!(response.get_authoring_meta(), Some(authoring_meta));
638    }
639
640    /// An authoring item that decodes but fails validation, or does not abi
641    /// decode at all, yields None.
642    #[test]
643    fn test_get_authoring_meta_invalid_returns_none() {
644        let invalid = AuthoringMeta(vec![AuthoringMetaItem {
645            word: "NOTKEBAB".to_string(),
646            operand_parser_offset: 0,
647            description: "some description".to_string(),
648        }]);
649        let abi = invalid.abi_encode().unwrap();
650        let response = deployer_response(authoring_doc(abi, ContentEncoding::None));
651        assert_eq!(response.get_authoring_meta(), None);
652
653        let undecodable = deployer_response(authoring_doc(
654            vec![0xde, 0xad, 0xbe, 0xef],
655            ContentEncoding::None,
656        ));
657        assert_eq!(undecodable.get_authoring_meta(), None);
658    }
659
660    /// A document without an AuthoringMetaV1 item, or meta bytes that do not
661    /// cbor decode, yield None.
662    #[test]
663    fn test_get_authoring_meta_absent_or_undecodable() {
664        let item = RainMetaDocumentV1Item {
665            payload: ByteBuf::from("some dotrain".as_bytes()),
666            magic: KnownMagic::DotrainV1,
667            content_type: ContentType::OctetStream,
668            content_encoding: ContentEncoding::None,
669            content_language: ContentLanguage::None,
670            schema: None,
671        };
672        let doc =
673            RainMetaDocumentV1Item::cbor_encode_seq(&vec![item], KnownMagic::RainMetaDocumentV1)
674                .unwrap();
675        assert_eq!(deployer_response(doc).get_authoring_meta(), None);
676
677        assert_eq!(
678            deployer_response(vec![0xff, 0x00, 0x12]).get_authoring_meta(),
679            None
680        );
681    }
682
683    /// process_meta_query maps missing data, missing meta record and
684    /// malformed hex all to NoRecordFound, and decodes a found record.
685    #[tokio::test]
686    async fn test_process_meta_query_paths() {
687        use httpmock::prelude::*;
688        let client = Arc::new(Client::builder().build().unwrap());
689        let body = MetaQuery::build_query(meta_query::Variables {
690            hash: Some("0xabc".to_string()),
691        });
692        let server = MockServer::start();
693
694        let no_data = server.mock(|when, then| {
695            when.method(POST).path("/nodata");
696            then.status(200)
697                .json_body(serde_json::json!({"errors": [{"message": "nope"}]}));
698        });
699        let result = process_meta_query(client.clone(), &body, &server.url("/nodata")).await;
700        assert!(matches!(result, Err(Error::NoRecordFound)));
701        no_data.assert();
702
703        let _no_meta = server.mock(|when, then| {
704            when.method(POST).path("/nometa");
705            then.status(200)
706                .json_body(serde_json::json!({"data": {"meta": null}}));
707        });
708        let result = process_meta_query(client.clone(), &body, &server.url("/nometa")).await;
709        assert!(matches!(result, Err(Error::NoRecordFound)));
710
711        let _bad_hex = server.mock(|when, then| {
712            when.method(POST).path("/badhex");
713            then.status(200).json_body(serde_json::json!({
714                "data": {"meta": {"__typename": "RainMetaV1", "rawBytes": "0xzz"}}
715            }));
716        });
717        let result = process_meta_query(client.clone(), &body, &server.url("/badhex")).await;
718        assert!(matches!(result, Err(Error::NoRecordFound)));
719
720        let _found = server.mock(|when, then| {
721            when.method(POST).path("/found");
722            then.status(200).json_body(serde_json::json!({
723                "data": {"meta": {"__typename": "RainMetaV1", "rawBytes": "0x0102"}}
724            }));
725        });
726        let result = process_meta_query(client.clone(), &body, &server.url("/found"))
727            .await
728            .unwrap();
729        assert_eq!(result.bytes, vec![0x01, 0x02]);
730    }
731
732    /// process_deployer_query maps an empty expressionDeployers result to
733    /// NoRecordFound.
734    #[tokio::test]
735    async fn test_process_deployer_query_empty_no_record() {
736        use httpmock::prelude::*;
737        let client = Arc::new(Client::builder().build().unwrap());
738        let body = DeployerQuery::build_query(deployer_query::Variables {
739            hash: Some("0xabc".to_string()),
740        });
741        let server = MockServer::start();
742        let _empty = server.mock(|when, then| {
743            when.method(POST);
744            then.status(200)
745                .json_body(serde_json::json!({"data": {"expressionDeployers": []}}));
746        });
747        let result = process_deployer_query(client, &body, &server.url("/")).await;
748        assert!(matches!(result, Err(Error::NoRecordFound)));
749    }
750}