Skip to main content

quicknode_sdk/kvstore/
mod.rs

1use std::collections::HashMap;
2
3#[cfg(feature = "rust")]
4use bon::Builder;
5#[cfg(feature = "node")]
6use napi_derive::napi;
7#[cfg(feature = "python")]
8use pyo3::{pyclass, pymethods};
9#[cfg(feature = "python")]
10use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
11use serde::{Deserialize, Deserializer, Serialize};
12
13use crate::{config::KvStoreConfig, errors::SdkError, SdkConfig};
14
15// The KV index endpoints return `data: null` when the store has no entries.
16// Map that to the default value so consumers get `[]` (or an empty struct) instead of a decode error.
17fn null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
18where
19    D: Deserializer<'de>,
20    T: Deserialize<'de> + Default,
21{
22    Option::<T>::deserialize(deserializer).map(Option::unwrap_or_default)
23}
24
25const KV_STORE_BASE_URL: &str = "https://api.quicknode.com/kv/rest/v1/";
26
27// ── Resolved config ────────────────────────────────────────────────────────
28
29pub(crate) struct ResolvedKvStoreConfig {
30    pub(crate) base_url: reqwest::Url,
31}
32
33impl ResolvedKvStoreConfig {
34    pub(crate) fn from_config(config: Option<&KvStoreConfig>) -> Result<Self, SdkError> {
35        let url_str = config
36            .and_then(|s| s.base_url.as_deref())
37            .unwrap_or(KV_STORE_BASE_URL);
38        let mut base_url =
39            reqwest::Url::parse(url_str).map_err(|e| SdkError::Config(e.to_string()))?;
40        if !base_url.path().ends_with('/') {
41            base_url.set_path(&format!("{}/", base_url.path()));
42        }
43        Ok(Self { base_url })
44    }
45}
46
47// ── Request types ──────────────────────────────────────────────────────────
48
49/// Parameters for `create_set`.
50#[cfg_attr(feature = "rust", derive(Builder))]
51#[cfg_attr(feature = "node", napi(object))]
52#[cfg_attr(not(feature = "node"), derive(Clone))]
53#[derive(Debug, Serialize, Deserialize)]
54pub struct CreateSetParams {
55    /// Unique key identifying the set.
56    pub key: String,
57    /// String value stored under the key.
58    pub value: String,
59}
60
61/// Parameters for `get_sets`.
62#[cfg_attr(feature = "rust", derive(Builder))]
63#[cfg_attr(feature = "node", napi(object))]
64#[cfg_attr(not(feature = "node"), derive(Clone))]
65#[derive(Debug, Default, Serialize, Deserialize)]
66pub struct GetSetsParams {
67    /// Maximum number of entries returned.
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub limit: Option<i64>,
70    /// Cursor returned by a previous page; pass to fetch the next page.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub cursor: Option<String>,
73}
74
75/// Parameters for `bulk_sets`. Either or both fields may be supplied.
76#[cfg_attr(feature = "rust", derive(Builder))]
77#[cfg_attr(feature = "node", napi(object))]
78#[cfg_attr(not(feature = "node"), derive(Clone))]
79#[derive(Debug, Serialize, Deserialize)]
80pub struct BulkSetsParams {
81    /// Key/value pairs to add.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub add_sets: Option<HashMap<String, String>>,
84    /// Keys to delete.
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub delete_sets: Option<Vec<String>>,
87}
88
89/// Parameters for `create_list`.
90#[cfg_attr(feature = "rust", derive(Builder))]
91#[cfg_attr(feature = "node", napi(object))]
92#[cfg_attr(not(feature = "node"), derive(Clone))]
93#[derive(Debug, Serialize, Deserialize)]
94pub struct CreateListParams {
95    /// Unique key identifying the list.
96    pub key: String,
97    /// Initial items inserted into the list.
98    pub items: Vec<String>,
99}
100
101/// Parameters for `get_lists`.
102#[cfg_attr(feature = "rust", derive(Builder))]
103#[cfg_attr(feature = "node", napi(object))]
104#[cfg_attr(not(feature = "node"), derive(Clone))]
105#[derive(Debug, Default, Serialize, Deserialize)]
106pub struct GetListsParams {
107    /// Maximum number of list keys returned.
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub limit: Option<i64>,
110    /// Cursor returned by a previous page; pass to fetch the next page.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub cursor: Option<String>,
113}
114
115/// Parameters for `get_list`.
116#[cfg_attr(feature = "rust", derive(Builder))]
117#[cfg_attr(feature = "node", napi(object))]
118#[cfg_attr(not(feature = "node"), derive(Clone))]
119#[derive(Debug, Default, Serialize, Deserialize)]
120pub struct GetListParams {
121    /// Maximum number of items returned.
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub limit: Option<i64>,
124    /// Cursor returned by a previous page; pass to fetch the next page.
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub cursor: Option<String>,
127}
128
129/// Parameters for `update_list`. Either or both fields may be supplied.
130#[cfg_attr(feature = "rust", derive(Builder))]
131#[cfg_attr(feature = "node", napi(object))]
132#[cfg_attr(not(feature = "node"), derive(Clone))]
133#[derive(Debug, Default, Serialize, Deserialize)]
134pub struct UpdateListParams {
135    /// Items to add to the list.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub add_items: Option<Vec<String>>,
138    /// Items to remove from the list.
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub remove_items: Option<Vec<String>>,
141}
142
143/// Parameters for `add_list_item`.
144#[cfg_attr(feature = "rust", derive(Builder))]
145#[cfg_attr(feature = "node", napi(object))]
146#[cfg_attr(not(feature = "node"), derive(Clone))]
147#[derive(Debug, Serialize, Deserialize)]
148pub struct AddListItemParams {
149    /// Item to append to the list.
150    pub item: String,
151}
152
153// ── Response types ─────────────────────────────────────────────────────────
154
155/// A single key/value entry returned by `get_sets`.
156#[cfg_attr(feature = "python", gen_stub_pyclass)]
157#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
158#[cfg_attr(feature = "node", napi(object))]
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct KvSetEntry {
161    /// Key identifying the set.
162    pub key: String,
163    /// Stored string value.
164    pub value: String,
165}
166
167#[cfg(feature = "python")]
168#[gen_stub_pymethods]
169#[pymethods]
170impl KvSetEntry {
171    #[new]
172    pub fn new(key: String, value: String) -> Self {
173        Self { key, value }
174    }
175}
176
177/// Response from `get_sets`.
178// GET /sets → {"data": [{key, value}], "cursor": ""}
179#[cfg_attr(feature = "python", gen_stub_pyclass)]
180#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
181#[cfg_attr(feature = "node", napi(object))]
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct GetSetsResponse {
184    /// Key/value entries on the current page.
185    #[serde(default, deserialize_with = "null_as_default")]
186    pub data: Vec<KvSetEntry>,
187    /// Cursor for the next page; empty string when there are no more pages.
188    pub cursor: String,
189}
190
191#[cfg(feature = "python")]
192#[gen_stub_pymethods]
193#[pymethods]
194impl GetSetsResponse {
195    #[new]
196    pub fn new(data: Vec<KvSetEntry>, cursor: String) -> Self {
197        Self { data, cursor }
198    }
199}
200
201/// Response from `get_set`.
202// GET /sets/{key} → {"data": {"key": "...", "value": "..."}}
203#[cfg_attr(feature = "python", gen_stub_pyclass)]
204#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
205#[cfg_attr(feature = "node", napi(object))]
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct GetSetResponse {
208    /// Stored string value.
209    pub value: String,
210}
211
212#[cfg(feature = "python")]
213#[gen_stub_pymethods]
214#[pymethods]
215impl GetSetResponse {
216    #[new]
217    pub fn new(value: String) -> Self {
218        Self { value }
219    }
220}
221
222/// Inner data for `get_lists` responses.
223// Inner data for GET /lists → data.keys
224#[cfg_attr(feature = "python", gen_stub_pyclass)]
225#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
226#[cfg_attr(feature = "node", napi(object))]
227#[derive(Debug, Clone, Default, Serialize, Deserialize)]
228pub struct GetListsData {
229    /// List keys on the current page.
230    pub keys: Vec<String>,
231}
232
233#[cfg(feature = "python")]
234#[gen_stub_pymethods]
235#[pymethods]
236impl GetListsData {
237    #[new]
238    pub fn new(keys: Vec<String>) -> Self {
239        Self { keys }
240    }
241}
242
243/// Response from `get_lists`.
244// GET /lists → {"data": {"keys": [...]}, "cursor": ""}
245#[cfg_attr(feature = "python", gen_stub_pyclass)]
246#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
247#[cfg_attr(feature = "node", napi(object))]
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct GetListsResponse {
250    /// List keys on the current page.
251    #[serde(default, deserialize_with = "null_as_default")]
252    pub data: GetListsData,
253    /// Cursor for the next page; empty string when there are no more pages.
254    pub cursor: String,
255}
256
257#[cfg(feature = "python")]
258#[gen_stub_pymethods]
259#[pymethods]
260impl GetListsResponse {
261    #[new]
262    pub fn new(data: GetListsData, cursor: String) -> Self {
263        Self { data, cursor }
264    }
265}
266
267/// Inner data for `get_list` responses.
268// Inner data for GET /lists/{key} → data.items
269#[cfg_attr(feature = "python", gen_stub_pyclass)]
270#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
271#[cfg_attr(feature = "node", napi(object))]
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct GetListData {
274    /// Items in the list on the current page.
275    pub items: Vec<String>,
276}
277
278#[cfg(feature = "python")]
279#[gen_stub_pymethods]
280#[pymethods]
281impl GetListData {
282    #[new]
283    pub fn new(items: Vec<String>) -> Self {
284        Self { items }
285    }
286}
287
288/// Response from `get_list`.
289// GET /lists/{key} → {"data": {"items": [...]}, "cursor": ""}
290#[cfg_attr(feature = "python", gen_stub_pyclass)]
291#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
292#[cfg_attr(feature = "node", napi(object))]
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct GetListResponse {
295    /// Items for the list on the current page.
296    pub data: GetListData,
297    /// Cursor for the next page; empty string when there are no more pages.
298    pub cursor: String,
299}
300
301#[cfg(feature = "python")]
302#[gen_stub_pymethods]
303#[pymethods]
304impl GetListResponse {
305    #[new]
306    pub fn new(data: GetListData, cursor: String) -> Self {
307        Self { data, cursor }
308    }
309}
310
311/// Response from `list_contains_item`.
312#[cfg_attr(feature = "python", gen_stub_pyclass)]
313#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
314#[cfg_attr(feature = "node", napi(object))]
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct ListContainsItemResponse {
317    /// `true` when the item is present in the list.
318    pub exists: bool,
319}
320
321#[cfg(feature = "python")]
322#[gen_stub_pymethods]
323#[pymethods]
324impl ListContainsItemResponse {
325    #[new]
326    pub fn new(exists: bool) -> Self {
327        Self { exists }
328    }
329}
330
331// ── API response wrapper ───────────────────────────────────────────────────
332
333#[derive(Debug, Serialize, Deserialize)]
334struct ApiResponse<T> {
335    data: T,
336}
337
338// ── Client ─────────────────────────────────────────────────────────────────
339
340/// Client for the Quicknode Key-Value Store. Supports two primitives: *sets*
341/// (single string values under a key) and *lists* (ordered collections of
342/// strings under a key).
343#[derive(Debug, Clone)]
344pub struct KvStoreApiClient {
345    config: SdkConfig,
346}
347
348impl KvStoreApiClient {
349    pub fn new(config: SdkConfig) -> Self {
350        Self { config }
351    }
352
353    // ── Sets ────────────────────────────────────────────────────────────────
354
355    /// Creates a new set, storing a single string value under the given key.
356    pub async fn create_set(&self, params: &CreateSetParams) -> Result<(), SdkError> {
357        let url = self.config.kvstore().base_url.join("sets")?;
358        let resp = self
359            .config
360            .http_client()
361            .post(url)
362            .json(params)
363            .send()
364            .await
365            .map_err(SdkError::Http)?;
366        let status = resp.status();
367        if !status.is_success() {
368            let body = resp.text().await.map_err(SdkError::Http)?;
369            return Err(SdkError::Api { status, body });
370        }
371        Ok(())
372    }
373
374    /// Returns a paginated page of key/value entries from the store. Use the
375    /// response `cursor` to fetch subsequent pages.
376    pub async fn get_sets(&self, params: &GetSetsParams) -> Result<GetSetsResponse, SdkError> {
377        let mut url = self.config.kvstore().base_url.join("sets")?;
378        {
379            let mut pairs = url.query_pairs_mut();
380            if let Some(v) = params.limit {
381                pairs.append_pair("limit", &v.to_string());
382            }
383            if let Some(v) = &params.cursor {
384                pairs.append_pair("cursor", v);
385            }
386        }
387        let resp = self
388            .config
389            .http_client()
390            .get(url)
391            .send()
392            .await
393            .map_err(SdkError::Http)?;
394        let status = resp.status();
395        let body = resp.text().await.map_err(SdkError::Http)?;
396        if !status.is_success() {
397            return Err(SdkError::Api { status, body });
398        }
399        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
400    }
401
402    /// Returns the string value stored for a single set by key.
403    pub async fn get_set(&self, key: &str) -> Result<GetSetResponse, SdkError> {
404        let url = self
405            .config
406            .kvstore()
407            .base_url
408            .join(&format!("sets/{key}"))?;
409        let resp = self
410            .config
411            .http_client()
412            .get(url)
413            .send()
414            .await
415            .map_err(SdkError::Http)?;
416        let status = resp.status();
417        let body = resp.text().await.map_err(SdkError::Http)?;
418        if !status.is_success() {
419            return Err(SdkError::Api { status, body });
420        }
421        let wrapper: ApiResponse<GetSetResponse> =
422            serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })?;
423        Ok(wrapper.data)
424    }
425
426    /// Adds and removes multiple sets in a single request. Either `add_sets`,
427    /// `delete_sets`, or both may be supplied.
428    pub async fn bulk_sets(&self, params: &BulkSetsParams) -> Result<(), SdkError> {
429        let url = self.config.kvstore().base_url.join("sets/bulk")?;
430        let resp = self
431            .config
432            .http_client()
433            .post(url)
434            .json(params)
435            .send()
436            .await
437            .map_err(SdkError::Http)?;
438        let status = resp.status();
439        if !status.is_success() {
440            let body = resp.text().await.map_err(SdkError::Http)?;
441            return Err(SdkError::Api { status, body });
442        }
443        Ok(())
444    }
445
446    /// Removes a single set by key.
447    pub async fn delete_set(&self, key: &str) -> Result<(), SdkError> {
448        let url = self
449            .config
450            .kvstore()
451            .base_url
452            .join(&format!("sets/{key}"))?;
453        let resp = self
454            .config
455            .http_client()
456            .delete(url)
457            .send()
458            .await
459            .map_err(SdkError::Http)?;
460        let status = resp.status();
461        if !status.is_success() {
462            let body = resp.text().await.map_err(SdkError::Http)?;
463            return Err(SdkError::Api { status, body });
464        }
465        Ok(())
466    }
467
468    // ── Lists ───────────────────────────────────────────────────────────────
469
470    /// Creates a new list under the given key, seeded with the provided items.
471    pub async fn create_list(&self, params: &CreateListParams) -> Result<(), SdkError> {
472        let url = self.config.kvstore().base_url.join("lists")?;
473        let resp = self
474            .config
475            .http_client()
476            .post(url)
477            .json(params)
478            .send()
479            .await
480            .map_err(SdkError::Http)?;
481        let status = resp.status();
482        if !status.is_success() {
483            let body = resp.text().await.map_err(SdkError::Http)?;
484            return Err(SdkError::Api { status, body });
485        }
486        Ok(())
487    }
488
489    /// Returns a paginated page of list keys from the store. Use the response
490    /// `cursor` to fetch subsequent pages.
491    pub async fn get_lists(&self, params: &GetListsParams) -> Result<GetListsResponse, SdkError> {
492        let mut url = self.config.kvstore().base_url.join("lists")?;
493        {
494            let mut pairs = url.query_pairs_mut();
495            if let Some(v) = params.limit {
496                pairs.append_pair("limit", &v.to_string());
497            }
498            if let Some(v) = &params.cursor {
499                pairs.append_pair("cursor", v);
500            }
501        }
502        let resp = self
503            .config
504            .http_client()
505            .get(url)
506            .send()
507            .await
508            .map_err(SdkError::Http)?;
509        let status = resp.status();
510        let body = resp.text().await.map_err(SdkError::Http)?;
511        if !status.is_success() {
512            return Err(SdkError::Api { status, body });
513        }
514        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
515    }
516
517    /// Returns a paginated page of items from the list identified by `key`.
518    /// Use the response `cursor` to fetch subsequent pages.
519    pub async fn get_list(
520        &self,
521        key: &str,
522        params: &GetListParams,
523    ) -> Result<GetListResponse, SdkError> {
524        let mut url = self
525            .config
526            .kvstore()
527            .base_url
528            .join(&format!("lists/{key}"))?;
529        {
530            let mut pairs = url.query_pairs_mut();
531            if let Some(v) = params.limit {
532                pairs.append_pair("limit", &v.to_string());
533            }
534            if let Some(v) = &params.cursor {
535                pairs.append_pair("cursor", v);
536            }
537        }
538        let resp = self
539            .config
540            .http_client()
541            .get(url)
542            .send()
543            .await
544            .map_err(SdkError::Http)?;
545        let status = resp.status();
546        let body = resp.text().await.map_err(SdkError::Http)?;
547        if !status.is_success() {
548            return Err(SdkError::Api { status, body });
549        }
550        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
551    }
552
553    /// Updates an existing list by adding and/or removing items in a single
554    /// operation. Either `add_items`, `remove_items`, or both may be supplied.
555    pub async fn update_list(&self, key: &str, params: &UpdateListParams) -> Result<(), SdkError> {
556        let url = self
557            .config
558            .kvstore()
559            .base_url
560            .join(&format!("lists/{key}"))?;
561        let resp = self
562            .config
563            .http_client()
564            .patch(url)
565            .json(params)
566            .send()
567            .await
568            .map_err(SdkError::Http)?;
569        let status = resp.status();
570        if !status.is_success() {
571            let body = resp.text().await.map_err(SdkError::Http)?;
572            return Err(SdkError::Api { status, body });
573        }
574        Ok(())
575    }
576
577    /// Appends a single item to the list identified by `key`.
578    pub async fn add_list_item(
579        &self,
580        key: &str,
581        params: &AddListItemParams,
582    ) -> Result<(), SdkError> {
583        let url = self
584            .config
585            .kvstore()
586            .base_url
587            .join(&format!("lists/{key}/items"))?;
588        let resp = self
589            .config
590            .http_client()
591            .post(url)
592            .json(params)
593            .send()
594            .await
595            .map_err(SdkError::Http)?;
596        let status = resp.status();
597        if !status.is_success() {
598            let body = resp.text().await.map_err(SdkError::Http)?;
599            return Err(SdkError::Api { status, body });
600        }
601        Ok(())
602    }
603
604    /// Checks whether the specified list contains the given item.
605    pub async fn list_contains_item(
606        &self,
607        key: &str,
608        item: &str,
609    ) -> Result<ListContainsItemResponse, SdkError> {
610        let url = self
611            .config
612            .kvstore()
613            .base_url
614            .join(&format!("lists/{key}/contains/{item}"))?;
615        let resp = self
616            .config
617            .http_client()
618            .get(url)
619            .send()
620            .await
621            .map_err(SdkError::Http)?;
622        let status = resp.status();
623        let body = resp.text().await.map_err(SdkError::Http)?;
624        if !status.is_success() {
625            return Err(SdkError::Api { status, body });
626        }
627        let wrapper: ApiResponse<ListContainsItemResponse> =
628            serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })?;
629        Ok(wrapper.data)
630    }
631
632    /// Removes a specific item from the list identified by `key`.
633    pub async fn delete_list_item(&self, key: &str, item: &str) -> Result<(), SdkError> {
634        let url = self
635            .config
636            .kvstore()
637            .base_url
638            .join(&format!("lists/{key}/items/{item}"))?;
639        let resp = self
640            .config
641            .http_client()
642            .delete(url)
643            .send()
644            .await
645            .map_err(SdkError::Http)?;
646        let status = resp.status();
647        if !status.is_success() {
648            let body = resp.text().await.map_err(SdkError::Http)?;
649            return Err(SdkError::Api { status, body });
650        }
651        Ok(())
652    }
653
654    /// Removes a list and all of its items by key.
655    pub async fn delete_list(&self, key: &str) -> Result<(), SdkError> {
656        let url = self
657            .config
658            .kvstore()
659            .base_url
660            .join(&format!("lists/{key}"))?;
661        let resp = self
662            .config
663            .http_client()
664            .delete(url)
665            .send()
666            .await
667            .map_err(SdkError::Http)?;
668        let status = resp.status();
669        if !status.is_success() {
670            let body = resp.text().await.map_err(SdkError::Http)?;
671            return Err(SdkError::Api { status, body });
672        }
673        Ok(())
674    }
675}
676
677// ── Tests ──────────────────────────────────────────────────────────────────
678
679#[cfg(test)]
680#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
681mod tests {
682    use super::*;
683    use crate::{KvStoreConfig, QuicknodeSdk, SdkFullConfig};
684    use wiremock::matchers::{method, path};
685    use wiremock::{Mock, MockServer, ResponseTemplate};
686
687    fn make_sdk(base_url: String) -> QuicknodeSdk {
688        QuicknodeSdk::new(&SdkFullConfig {
689            api_key: "test-key".to_string(),
690            http: None,
691            admin: None,
692            streams: None,
693            webhooks: None,
694            kvstore: Some(KvStoreConfig {
695                base_url: Some(base_url),
696            }),
697        })
698        .unwrap()
699    }
700
701    // ── Sets ─────────────────────────────────────────────────────────────────
702
703    #[tokio::test]
704    async fn create_set_success() {
705        let server = MockServer::start().await;
706        Mock::given(method("POST"))
707            .and(path("/sets"))
708            .respond_with(
709                ResponseTemplate::new(201)
710                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
711            )
712            .mount(&server)
713            .await;
714        let sdk = make_sdk(format!("{}/", server.uri()));
715        sdk.kvstore
716            .create_set(&CreateSetParams {
717                key: "k".to_string(),
718                value: "v".to_string(),
719            })
720            .await
721            .unwrap();
722    }
723
724    #[tokio::test]
725    async fn create_set_api_error() {
726        let server = MockServer::start().await;
727        Mock::given(method("POST"))
728            .and(path("/sets"))
729            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
730            .mount(&server)
731            .await;
732        let sdk = make_sdk(format!("{}/", server.uri()));
733        let err = sdk
734            .kvstore
735            .create_set(&CreateSetParams {
736                key: "k".to_string(),
737                value: "v".to_string(),
738            })
739            .await
740            .unwrap_err();
741        assert!(matches!(err, SdkError::Api { .. }));
742    }
743
744    #[tokio::test]
745    async fn create_set_server_error() {
746        let server = MockServer::start().await;
747        Mock::given(method("POST"))
748            .and(path("/sets"))
749            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
750            .mount(&server)
751            .await;
752        let sdk = make_sdk(format!("{}/", server.uri()));
753        let err = sdk
754            .kvstore
755            .create_set(&CreateSetParams {
756                key: "k".to_string(),
757                value: "v".to_string(),
758            })
759            .await
760            .unwrap_err();
761        assert!(matches!(err, SdkError::Api { .. }));
762    }
763
764    #[tokio::test]
765    async fn get_sets_success() {
766        let server = MockServer::start().await;
767        Mock::given(method("GET"))
768            .and(path("/sets"))
769            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"data": [{"key": "k1", "value": "v1"}, {"key": "k2", "value": "v2"}], "cursor": ""})))
770            .mount(&server)
771            .await;
772        let sdk = make_sdk(format!("{}/", server.uri()));
773        let resp = sdk
774            .kvstore
775            .get_sets(&GetSetsParams::default())
776            .await
777            .unwrap();
778        assert_eq!(resp.data.len(), 2);
779        assert_eq!(resp.data[0].key, "k1");
780    }
781
782    #[tokio::test]
783    async fn get_sets_null_data_empty_store() {
784        let server = MockServer::start().await;
785        Mock::given(method("GET"))
786            .and(path("/sets"))
787            .respond_with(ResponseTemplate::new(200).set_body_json(
788                serde_json::json!({"code": 200, "msg": "", "data": null, "cursor": ""}),
789            ))
790            .mount(&server)
791            .await;
792        let sdk = make_sdk(format!("{}/", server.uri()));
793        let resp = sdk
794            .kvstore
795            .get_sets(&GetSetsParams::default())
796            .await
797            .unwrap();
798        assert!(resp.data.is_empty());
799        assert_eq!(resp.cursor, "");
800    }
801
802    #[tokio::test]
803    async fn get_sets_api_error() {
804        let server = MockServer::start().await;
805        Mock::given(method("GET"))
806            .and(path("/sets"))
807            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
808            .mount(&server)
809            .await;
810        let sdk = make_sdk(format!("{}/", server.uri()));
811        let err = sdk
812            .kvstore
813            .get_sets(&GetSetsParams::default())
814            .await
815            .unwrap_err();
816        assert!(matches!(err, SdkError::Api { .. }));
817    }
818
819    #[tokio::test]
820    async fn get_sets_server_error() {
821        let server = MockServer::start().await;
822        Mock::given(method("GET"))
823            .and(path("/sets"))
824            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
825            .mount(&server)
826            .await;
827        let sdk = make_sdk(format!("{}/", server.uri()));
828        let err = sdk
829            .kvstore
830            .get_sets(&GetSetsParams::default())
831            .await
832            .unwrap_err();
833        assert!(matches!(err, SdkError::Api { .. }));
834    }
835
836    #[tokio::test]
837    async fn get_set_success() {
838        let server = MockServer::start().await;
839        Mock::given(method("GET"))
840            .and(path("/sets/my-key"))
841            .respond_with(
842                ResponseTemplate::new(200)
843                    .set_body_json(serde_json::json!({"data": {"value": "my-value"}})),
844            )
845            .mount(&server)
846            .await;
847        let sdk = make_sdk(format!("{}/", server.uri()));
848        let resp = sdk.kvstore.get_set("my-key").await.unwrap();
849        assert_eq!(resp.value, "my-value");
850    }
851
852    #[tokio::test]
853    async fn get_set_not_found() {
854        let server = MockServer::start().await;
855        Mock::given(method("GET"))
856            .and(path("/sets/my-key"))
857            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
858            .mount(&server)
859            .await;
860        let sdk = make_sdk(format!("{}/", server.uri()));
861        let err = sdk.kvstore.get_set("my-key").await.unwrap_err();
862        assert!(matches!(err, SdkError::Api { .. }));
863    }
864
865    #[tokio::test]
866    async fn get_set_server_error() {
867        let server = MockServer::start().await;
868        Mock::given(method("GET"))
869            .and(path("/sets/my-key"))
870            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
871            .mount(&server)
872            .await;
873        let sdk = make_sdk(format!("{}/", server.uri()));
874        let err = sdk.kvstore.get_set("my-key").await.unwrap_err();
875        assert!(matches!(err, SdkError::Api { .. }));
876    }
877
878    #[tokio::test]
879    async fn bulk_sets_success() {
880        let server = MockServer::start().await;
881        Mock::given(method("POST"))
882            .and(path("/sets/bulk"))
883            .respond_with(
884                ResponseTemplate::new(201)
885                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
886            )
887            .mount(&server)
888            .await;
889        let sdk = make_sdk(format!("{}/", server.uri()));
890        let mut add = HashMap::new();
891        add.insert("k1".to_string(), "v1".to_string());
892        sdk.kvstore
893            .bulk_sets(&BulkSetsParams {
894                add_sets: Some(add),
895                delete_sets: None,
896            })
897            .await
898            .unwrap();
899    }
900
901    #[tokio::test]
902    async fn bulk_sets_api_error() {
903        let server = MockServer::start().await;
904        Mock::given(method("POST"))
905            .and(path("/sets/bulk"))
906            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
907            .mount(&server)
908            .await;
909        let sdk = make_sdk(format!("{}/", server.uri()));
910        let err = sdk
911            .kvstore
912            .bulk_sets(&BulkSetsParams {
913                add_sets: None,
914                delete_sets: None,
915            })
916            .await
917            .unwrap_err();
918        assert!(matches!(err, SdkError::Api { .. }));
919    }
920
921    #[tokio::test]
922    async fn bulk_sets_server_error() {
923        let server = MockServer::start().await;
924        Mock::given(method("POST"))
925            .and(path("/sets/bulk"))
926            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
927            .mount(&server)
928            .await;
929        let sdk = make_sdk(format!("{}/", server.uri()));
930        let err = sdk
931            .kvstore
932            .bulk_sets(&BulkSetsParams {
933                add_sets: None,
934                delete_sets: None,
935            })
936            .await
937            .unwrap_err();
938        assert!(matches!(err, SdkError::Api { .. }));
939    }
940
941    #[tokio::test]
942    async fn delete_set_success() {
943        let server = MockServer::start().await;
944        Mock::given(method("DELETE"))
945            .and(path("/sets/my-key"))
946            .respond_with(
947                ResponseTemplate::new(200)
948                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
949            )
950            .mount(&server)
951            .await;
952        let sdk = make_sdk(format!("{}/", server.uri()));
953        sdk.kvstore.delete_set("my-key").await.unwrap();
954    }
955
956    #[tokio::test]
957    async fn delete_set_not_found() {
958        let server = MockServer::start().await;
959        Mock::given(method("DELETE"))
960            .and(path("/sets/my-key"))
961            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
962            .mount(&server)
963            .await;
964        let sdk = make_sdk(format!("{}/", server.uri()));
965        let err = sdk.kvstore.delete_set("my-key").await.unwrap_err();
966        assert!(matches!(err, SdkError::Api { .. }));
967    }
968
969    #[tokio::test]
970    async fn delete_set_server_error() {
971        let server = MockServer::start().await;
972        Mock::given(method("DELETE"))
973            .and(path("/sets/my-key"))
974            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
975            .mount(&server)
976            .await;
977        let sdk = make_sdk(format!("{}/", server.uri()));
978        let err = sdk.kvstore.delete_set("my-key").await.unwrap_err();
979        assert!(matches!(err, SdkError::Api { .. }));
980    }
981
982    // ── Lists ─────────────────────────────────────────────────────────────────
983
984    #[tokio::test]
985    async fn create_list_success() {
986        let server = MockServer::start().await;
987        Mock::given(method("POST"))
988            .and(path("/lists"))
989            .respond_with(
990                ResponseTemplate::new(201)
991                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
992            )
993            .mount(&server)
994            .await;
995        let sdk = make_sdk(format!("{}/", server.uri()));
996        sdk.kvstore
997            .create_list(&CreateListParams {
998                key: "my-list".to_string(),
999                items: vec!["item1".to_string()],
1000            })
1001            .await
1002            .unwrap();
1003    }
1004
1005    #[tokio::test]
1006    async fn create_list_api_error() {
1007        let server = MockServer::start().await;
1008        Mock::given(method("POST"))
1009            .and(path("/lists"))
1010            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
1011            .mount(&server)
1012            .await;
1013        let sdk = make_sdk(format!("{}/", server.uri()));
1014        let err = sdk
1015            .kvstore
1016            .create_list(&CreateListParams {
1017                key: "my-list".to_string(),
1018                items: vec![],
1019            })
1020            .await
1021            .unwrap_err();
1022        assert!(matches!(err, SdkError::Api { .. }));
1023    }
1024
1025    #[tokio::test]
1026    async fn create_list_server_error() {
1027        let server = MockServer::start().await;
1028        Mock::given(method("POST"))
1029            .and(path("/lists"))
1030            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1031            .mount(&server)
1032            .await;
1033        let sdk = make_sdk(format!("{}/", server.uri()));
1034        let err = sdk
1035            .kvstore
1036            .create_list(&CreateListParams {
1037                key: "my-list".to_string(),
1038                items: vec![],
1039            })
1040            .await
1041            .unwrap_err();
1042        assert!(matches!(err, SdkError::Api { .. }));
1043    }
1044
1045    #[tokio::test]
1046    async fn get_lists_success() {
1047        let server = MockServer::start().await;
1048        Mock::given(method("GET"))
1049            .and(path("/lists"))
1050            .respond_with(ResponseTemplate::new(200).set_body_json(
1051                serde_json::json!({"data": {"keys": ["list1", "list2"]}, "cursor": ""}),
1052            ))
1053            .mount(&server)
1054            .await;
1055        let sdk = make_sdk(format!("{}/", server.uri()));
1056        let resp = sdk
1057            .kvstore
1058            .get_lists(&GetListsParams::default())
1059            .await
1060            .unwrap();
1061        assert_eq!(resp.data.keys, vec!["list1", "list2"]);
1062    }
1063
1064    #[tokio::test]
1065    async fn get_lists_null_data_empty_store() {
1066        let server = MockServer::start().await;
1067        Mock::given(method("GET"))
1068            .and(path("/lists"))
1069            .respond_with(ResponseTemplate::new(200).set_body_json(
1070                serde_json::json!({"code": 200, "msg": "", "data": null, "cursor": ""}),
1071            ))
1072            .mount(&server)
1073            .await;
1074        let sdk = make_sdk(format!("{}/", server.uri()));
1075        let resp = sdk
1076            .kvstore
1077            .get_lists(&GetListsParams::default())
1078            .await
1079            .unwrap();
1080        assert!(resp.data.keys.is_empty());
1081        assert_eq!(resp.cursor, "");
1082    }
1083
1084    #[tokio::test]
1085    async fn get_lists_api_error() {
1086        let server = MockServer::start().await;
1087        Mock::given(method("GET"))
1088            .and(path("/lists"))
1089            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
1090            .mount(&server)
1091            .await;
1092        let sdk = make_sdk(format!("{}/", server.uri()));
1093        let err = sdk
1094            .kvstore
1095            .get_lists(&GetListsParams::default())
1096            .await
1097            .unwrap_err();
1098        assert!(matches!(err, SdkError::Api { .. }));
1099    }
1100
1101    #[tokio::test]
1102    async fn get_lists_server_error() {
1103        let server = MockServer::start().await;
1104        Mock::given(method("GET"))
1105            .and(path("/lists"))
1106            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1107            .mount(&server)
1108            .await;
1109        let sdk = make_sdk(format!("{}/", server.uri()));
1110        let err = sdk
1111            .kvstore
1112            .get_lists(&GetListsParams::default())
1113            .await
1114            .unwrap_err();
1115        assert!(matches!(err, SdkError::Api { .. }));
1116    }
1117
1118    #[tokio::test]
1119    async fn get_list_success() {
1120        let server = MockServer::start().await;
1121        Mock::given(method("GET"))
1122            .and(path("/lists/my-list"))
1123            .respond_with(ResponseTemplate::new(200).set_body_json(
1124                serde_json::json!({"data": {"items": ["item1", "item2"]}, "cursor": ""}),
1125            ))
1126            .mount(&server)
1127            .await;
1128        let sdk = make_sdk(format!("{}/", server.uri()));
1129        let resp = sdk
1130            .kvstore
1131            .get_list("my-list", &GetListParams::default())
1132            .await
1133            .unwrap();
1134        assert_eq!(resp.data.items, vec!["item1", "item2"]);
1135    }
1136
1137    #[tokio::test]
1138    async fn get_list_not_found() {
1139        let server = MockServer::start().await;
1140        Mock::given(method("GET"))
1141            .and(path("/lists/my-list"))
1142            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1143            .mount(&server)
1144            .await;
1145        let sdk = make_sdk(format!("{}/", server.uri()));
1146        let err = sdk
1147            .kvstore
1148            .get_list("my-list", &GetListParams::default())
1149            .await
1150            .unwrap_err();
1151        assert!(matches!(err, SdkError::Api { .. }));
1152    }
1153
1154    #[tokio::test]
1155    async fn get_list_server_error() {
1156        let server = MockServer::start().await;
1157        Mock::given(method("GET"))
1158            .and(path("/lists/my-list"))
1159            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1160            .mount(&server)
1161            .await;
1162        let sdk = make_sdk(format!("{}/", server.uri()));
1163        let err = sdk
1164            .kvstore
1165            .get_list("my-list", &GetListParams::default())
1166            .await
1167            .unwrap_err();
1168        assert!(matches!(err, SdkError::Api { .. }));
1169    }
1170
1171    #[tokio::test]
1172    async fn update_list_success() {
1173        let server = MockServer::start().await;
1174        Mock::given(method("PATCH"))
1175            .and(path("/lists/my-list"))
1176            .respond_with(
1177                ResponseTemplate::new(200)
1178                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1179            )
1180            .mount(&server)
1181            .await;
1182        let sdk = make_sdk(format!("{}/", server.uri()));
1183        sdk.kvstore
1184            .update_list(
1185                "my-list",
1186                &UpdateListParams {
1187                    add_items: Some(vec!["item3".to_string()]),
1188                    remove_items: None,
1189                },
1190            )
1191            .await
1192            .unwrap();
1193    }
1194
1195    #[tokio::test]
1196    async fn update_list_api_error() {
1197        let server = MockServer::start().await;
1198        Mock::given(method("PATCH"))
1199            .and(path("/lists/my-list"))
1200            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
1201            .mount(&server)
1202            .await;
1203        let sdk = make_sdk(format!("{}/", server.uri()));
1204        let err = sdk
1205            .kvstore
1206            .update_list("my-list", &UpdateListParams::default())
1207            .await
1208            .unwrap_err();
1209        assert!(matches!(err, SdkError::Api { .. }));
1210    }
1211
1212    #[tokio::test]
1213    async fn update_list_server_error() {
1214        let server = MockServer::start().await;
1215        Mock::given(method("PATCH"))
1216            .and(path("/lists/my-list"))
1217            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1218            .mount(&server)
1219            .await;
1220        let sdk = make_sdk(format!("{}/", server.uri()));
1221        let err = sdk
1222            .kvstore
1223            .update_list("my-list", &UpdateListParams::default())
1224            .await
1225            .unwrap_err();
1226        assert!(matches!(err, SdkError::Api { .. }));
1227    }
1228
1229    #[tokio::test]
1230    async fn add_list_item_success() {
1231        let server = MockServer::start().await;
1232        Mock::given(method("POST"))
1233            .and(path("/lists/my-list/items"))
1234            .respond_with(
1235                ResponseTemplate::new(201)
1236                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1237            )
1238            .mount(&server)
1239            .await;
1240        let sdk = make_sdk(format!("{}/", server.uri()));
1241        sdk.kvstore
1242            .add_list_item(
1243                "my-list",
1244                &AddListItemParams {
1245                    item: "item1".to_string(),
1246                },
1247            )
1248            .await
1249            .unwrap();
1250    }
1251
1252    #[tokio::test]
1253    async fn add_list_item_api_error() {
1254        let server = MockServer::start().await;
1255        Mock::given(method("POST"))
1256            .and(path("/lists/my-list/items"))
1257            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
1258            .mount(&server)
1259            .await;
1260        let sdk = make_sdk(format!("{}/", server.uri()));
1261        let err = sdk
1262            .kvstore
1263            .add_list_item(
1264                "my-list",
1265                &AddListItemParams {
1266                    item: "item1".to_string(),
1267                },
1268            )
1269            .await
1270            .unwrap_err();
1271        assert!(matches!(err, SdkError::Api { .. }));
1272    }
1273
1274    #[tokio::test]
1275    async fn add_list_item_server_error() {
1276        let server = MockServer::start().await;
1277        Mock::given(method("POST"))
1278            .and(path("/lists/my-list/items"))
1279            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1280            .mount(&server)
1281            .await;
1282        let sdk = make_sdk(format!("{}/", server.uri()));
1283        let err = sdk
1284            .kvstore
1285            .add_list_item(
1286                "my-list",
1287                &AddListItemParams {
1288                    item: "item1".to_string(),
1289                },
1290            )
1291            .await
1292            .unwrap_err();
1293        assert!(matches!(err, SdkError::Api { .. }));
1294    }
1295
1296    #[tokio::test]
1297    async fn list_contains_item_success() {
1298        let server = MockServer::start().await;
1299        Mock::given(method("GET"))
1300            .and(path("/lists/my-list/contains/item1"))
1301            .respond_with(
1302                ResponseTemplate::new(200)
1303                    .set_body_json(serde_json::json!({"data": {"exists": true}})),
1304            )
1305            .mount(&server)
1306            .await;
1307        let sdk = make_sdk(format!("{}/", server.uri()));
1308        let resp = sdk
1309            .kvstore
1310            .list_contains_item("my-list", "item1")
1311            .await
1312            .unwrap();
1313        assert!(resp.exists);
1314    }
1315
1316    #[tokio::test]
1317    async fn list_contains_item_not_found() {
1318        let server = MockServer::start().await;
1319        Mock::given(method("GET"))
1320            .and(path("/lists/my-list/contains/item1"))
1321            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1322            .mount(&server)
1323            .await;
1324        let sdk = make_sdk(format!("{}/", server.uri()));
1325        let err = sdk
1326            .kvstore
1327            .list_contains_item("my-list", "item1")
1328            .await
1329            .unwrap_err();
1330        assert!(matches!(err, SdkError::Api { .. }));
1331    }
1332
1333    #[tokio::test]
1334    async fn list_contains_item_server_error() {
1335        let server = MockServer::start().await;
1336        Mock::given(method("GET"))
1337            .and(path("/lists/my-list/contains/item1"))
1338            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1339            .mount(&server)
1340            .await;
1341        let sdk = make_sdk(format!("{}/", server.uri()));
1342        let err = sdk
1343            .kvstore
1344            .list_contains_item("my-list", "item1")
1345            .await
1346            .unwrap_err();
1347        assert!(matches!(err, SdkError::Api { .. }));
1348    }
1349
1350    #[tokio::test]
1351    async fn delete_list_item_success() {
1352        let server = MockServer::start().await;
1353        Mock::given(method("DELETE"))
1354            .and(path("/lists/my-list/items/item1"))
1355            .respond_with(
1356                ResponseTemplate::new(200)
1357                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1358            )
1359            .mount(&server)
1360            .await;
1361        let sdk = make_sdk(format!("{}/", server.uri()));
1362        sdk.kvstore
1363            .delete_list_item("my-list", "item1")
1364            .await
1365            .unwrap();
1366    }
1367
1368    #[tokio::test]
1369    async fn delete_list_item_not_found() {
1370        let server = MockServer::start().await;
1371        Mock::given(method("DELETE"))
1372            .and(path("/lists/my-list/items/item1"))
1373            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1374            .mount(&server)
1375            .await;
1376        let sdk = make_sdk(format!("{}/", server.uri()));
1377        let err = sdk
1378            .kvstore
1379            .delete_list_item("my-list", "item1")
1380            .await
1381            .unwrap_err();
1382        assert!(matches!(err, SdkError::Api { .. }));
1383    }
1384
1385    #[tokio::test]
1386    async fn delete_list_item_server_error() {
1387        let server = MockServer::start().await;
1388        Mock::given(method("DELETE"))
1389            .and(path("/lists/my-list/items/item1"))
1390            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1391            .mount(&server)
1392            .await;
1393        let sdk = make_sdk(format!("{}/", server.uri()));
1394        let err = sdk
1395            .kvstore
1396            .delete_list_item("my-list", "item1")
1397            .await
1398            .unwrap_err();
1399        assert!(matches!(err, SdkError::Api { .. }));
1400    }
1401
1402    #[tokio::test]
1403    async fn delete_list_success() {
1404        let server = MockServer::start().await;
1405        Mock::given(method("DELETE"))
1406            .and(path("/lists/my-list"))
1407            .respond_with(
1408                ResponseTemplate::new(200)
1409                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1410            )
1411            .mount(&server)
1412            .await;
1413        let sdk = make_sdk(format!("{}/", server.uri()));
1414        sdk.kvstore.delete_list("my-list").await.unwrap();
1415    }
1416
1417    #[tokio::test]
1418    async fn delete_list_not_found() {
1419        let server = MockServer::start().await;
1420        Mock::given(method("DELETE"))
1421            .and(path("/lists/my-list"))
1422            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1423            .mount(&server)
1424            .await;
1425        let sdk = make_sdk(format!("{}/", server.uri()));
1426        let err = sdk.kvstore.delete_list("my-list").await.unwrap_err();
1427        assert!(matches!(err, SdkError::Api { .. }));
1428    }
1429
1430    #[tokio::test]
1431    async fn delete_list_server_error() {
1432        let server = MockServer::start().await;
1433        Mock::given(method("DELETE"))
1434            .and(path("/lists/my-list"))
1435            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1436            .mount(&server)
1437            .await;
1438        let sdk = make_sdk(format!("{}/", server.uri()));
1439        let err = sdk.kvstore.delete_list("my-list").await.unwrap_err();
1440        assert!(matches!(err, SdkError::Api { .. }));
1441    }
1442}