1use crate::client::GraphqlTransport;
4use crate::error::GraphqlError;
5use bytes::Bytes;
6use serde_json::{json, Value};
7use sova_core::extend::BoxFuture;
8use std::sync::{Arc, Mutex};
9
10#[derive(Debug, Clone)]
11pub struct GraphqlCall {
12 pub query: String,
13 pub operation_name: Option<String>,
14 pub variables: Option<Value>,
15 pub endpoint: String,
16}
17
18#[derive(Default)]
19struct Inner {
20 stubs: Vec<(String, Value)>,
22 calls: Vec<GraphqlCall>,
23}
24
25#[derive(Clone, Default)]
27pub struct FakeGraphql {
28 inner: Arc<Mutex<Inner>>,
29}
30
31impl FakeGraphql {
32 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn stub(self, needle: impl Into<String>, data: Value) -> Self {
38 self.inner.lock().unwrap().stubs.push((needle.into(), data));
39 self
40 }
41
42 pub fn calls(&self) -> Vec<GraphqlCall> {
43 self.inner.lock().unwrap().calls.clone()
44 }
45
46 pub fn clear(&self) {
47 self.inner.lock().unwrap().calls.clear();
48 }
49
50 pub fn assert_called(&self) {
51 assert!(
52 !self.inner.lock().unwrap().calls.is_empty(),
53 "FakeGraphql: expected at least one GraphQL call"
54 );
55 }
56
57 pub fn assert_called_with(&self, needle: &str) {
58 let calls = self.calls();
59 assert!(
60 calls.iter().any(|c| c.query.contains(needle)),
61 "FakeGraphql: no call matched `{needle}`; calls={calls:?}"
62 );
63 }
64}
65
66impl GraphqlTransport for FakeGraphql {
67 fn post(&self, url: &str, body: Bytes) -> BoxFuture<Result<Bytes, GraphqlError>> {
68 let this = self.clone();
69 let url = url.to_string();
70 Box::pin(async move {
71 let payload: Value =
72 serde_json::from_slice(&body).map_err(|e| GraphqlError::Decode(e.to_string()))?;
73 let query = payload
74 .get("query")
75 .and_then(|v| v.as_str())
76 .unwrap_or("")
77 .to_string();
78 let operation_name = payload
79 .get("operationName")
80 .and_then(|v| v.as_str())
81 .map(str::to_string);
82 let variables = payload.get("variables").cloned();
83
84 let mut g = this.inner.lock().unwrap();
85 g.calls.push(GraphqlCall {
86 query: query.clone(),
87 operation_name,
88 variables,
89 endpoint: url,
90 });
91
92 for (needle, data) in &g.stubs {
93 if query.contains(needle) {
94 let resp = json!({ "data": data });
95 return serde_json::to_vec(&resp)
96 .map(Bytes::from)
97 .map_err(|e| GraphqlError::Decode(e.to_string()));
98 }
99 }
100
101 Err(GraphqlError::Graphql(format!(
102 "FakeGraphql: no stub matched query ({query})"
103 )))
104 })
105 }
106}