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::{body_json, 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    // Wire-inspection regressions: confirm that addSets and deleteSets reach
942    // the wire under the names the API expects, so any future serde rename of
943    // these fields fails loudly.
944    #[tokio::test]
945    async fn bulk_sets_wire_body_add_sets() {
946        let server = MockServer::start().await;
947        Mock::given(method("POST"))
948            .and(path("/sets/bulk"))
949            .and(body_json(serde_json::json!({
950                "add_sets": {"k1": "v1"}
951            })))
952            .respond_with(
953                ResponseTemplate::new(201)
954                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
955            )
956            .mount(&server)
957            .await;
958        let sdk = make_sdk(format!("{}/", server.uri()));
959        let mut add = HashMap::new();
960        add.insert("k1".to_string(), "v1".to_string());
961        sdk.kvstore
962            .bulk_sets(&BulkSetsParams {
963                add_sets: Some(add),
964                delete_sets: None,
965            })
966            .await
967            .unwrap();
968    }
969
970    #[tokio::test]
971    async fn bulk_sets_wire_body_delete_sets() {
972        let server = MockServer::start().await;
973        Mock::given(method("POST"))
974            .and(path("/sets/bulk"))
975            .and(body_json(serde_json::json!({
976                "delete_sets": ["k1", "k2"]
977            })))
978            .respond_with(
979                ResponseTemplate::new(201)
980                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
981            )
982            .mount(&server)
983            .await;
984        let sdk = make_sdk(format!("{}/", server.uri()));
985        sdk.kvstore
986            .bulk_sets(&BulkSetsParams {
987                add_sets: None,
988                delete_sets: Some(vec!["k1".to_string(), "k2".to_string()]),
989            })
990            .await
991            .unwrap();
992    }
993
994    #[tokio::test]
995    async fn delete_set_success() {
996        let server = MockServer::start().await;
997        Mock::given(method("DELETE"))
998            .and(path("/sets/my-key"))
999            .respond_with(
1000                ResponseTemplate::new(200)
1001                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1002            )
1003            .mount(&server)
1004            .await;
1005        let sdk = make_sdk(format!("{}/", server.uri()));
1006        sdk.kvstore.delete_set("my-key").await.unwrap();
1007    }
1008
1009    #[tokio::test]
1010    async fn delete_set_not_found() {
1011        let server = MockServer::start().await;
1012        Mock::given(method("DELETE"))
1013            .and(path("/sets/my-key"))
1014            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1015            .mount(&server)
1016            .await;
1017        let sdk = make_sdk(format!("{}/", server.uri()));
1018        let err = sdk.kvstore.delete_set("my-key").await.unwrap_err();
1019        assert!(matches!(err, SdkError::Api { .. }));
1020    }
1021
1022    #[tokio::test]
1023    async fn delete_set_server_error() {
1024        let server = MockServer::start().await;
1025        Mock::given(method("DELETE"))
1026            .and(path("/sets/my-key"))
1027            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1028            .mount(&server)
1029            .await;
1030        let sdk = make_sdk(format!("{}/", server.uri()));
1031        let err = sdk.kvstore.delete_set("my-key").await.unwrap_err();
1032        assert!(matches!(err, SdkError::Api { .. }));
1033    }
1034
1035    // ── Lists ─────────────────────────────────────────────────────────────────
1036
1037    #[tokio::test]
1038    async fn create_list_success() {
1039        let server = MockServer::start().await;
1040        Mock::given(method("POST"))
1041            .and(path("/lists"))
1042            .respond_with(
1043                ResponseTemplate::new(201)
1044                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1045            )
1046            .mount(&server)
1047            .await;
1048        let sdk = make_sdk(format!("{}/", server.uri()));
1049        sdk.kvstore
1050            .create_list(&CreateListParams {
1051                key: "my-list".to_string(),
1052                items: vec!["item1".to_string()],
1053            })
1054            .await
1055            .unwrap();
1056    }
1057
1058    #[tokio::test]
1059    async fn create_list_api_error() {
1060        let server = MockServer::start().await;
1061        Mock::given(method("POST"))
1062            .and(path("/lists"))
1063            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
1064            .mount(&server)
1065            .await;
1066        let sdk = make_sdk(format!("{}/", server.uri()));
1067        let err = sdk
1068            .kvstore
1069            .create_list(&CreateListParams {
1070                key: "my-list".to_string(),
1071                items: vec![],
1072            })
1073            .await
1074            .unwrap_err();
1075        assert!(matches!(err, SdkError::Api { .. }));
1076    }
1077
1078    #[tokio::test]
1079    async fn create_list_server_error() {
1080        let server = MockServer::start().await;
1081        Mock::given(method("POST"))
1082            .and(path("/lists"))
1083            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1084            .mount(&server)
1085            .await;
1086        let sdk = make_sdk(format!("{}/", server.uri()));
1087        let err = sdk
1088            .kvstore
1089            .create_list(&CreateListParams {
1090                key: "my-list".to_string(),
1091                items: vec![],
1092            })
1093            .await
1094            .unwrap_err();
1095        assert!(matches!(err, SdkError::Api { .. }));
1096    }
1097
1098    #[tokio::test]
1099    async fn get_lists_success() {
1100        let server = MockServer::start().await;
1101        Mock::given(method("GET"))
1102            .and(path("/lists"))
1103            .respond_with(ResponseTemplate::new(200).set_body_json(
1104                serde_json::json!({"data": {"keys": ["list1", "list2"]}, "cursor": ""}),
1105            ))
1106            .mount(&server)
1107            .await;
1108        let sdk = make_sdk(format!("{}/", server.uri()));
1109        let resp = sdk
1110            .kvstore
1111            .get_lists(&GetListsParams::default())
1112            .await
1113            .unwrap();
1114        assert_eq!(resp.data.keys, vec!["list1", "list2"]);
1115    }
1116
1117    #[tokio::test]
1118    async fn get_lists_null_data_empty_store() {
1119        let server = MockServer::start().await;
1120        Mock::given(method("GET"))
1121            .and(path("/lists"))
1122            .respond_with(ResponseTemplate::new(200).set_body_json(
1123                serde_json::json!({"code": 200, "msg": "", "data": null, "cursor": ""}),
1124            ))
1125            .mount(&server)
1126            .await;
1127        let sdk = make_sdk(format!("{}/", server.uri()));
1128        let resp = sdk
1129            .kvstore
1130            .get_lists(&GetListsParams::default())
1131            .await
1132            .unwrap();
1133        assert!(resp.data.keys.is_empty());
1134        assert_eq!(resp.cursor, "");
1135    }
1136
1137    #[tokio::test]
1138    async fn get_lists_api_error() {
1139        let server = MockServer::start().await;
1140        Mock::given(method("GET"))
1141            .and(path("/lists"))
1142            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
1143            .mount(&server)
1144            .await;
1145        let sdk = make_sdk(format!("{}/", server.uri()));
1146        let err = sdk
1147            .kvstore
1148            .get_lists(&GetListsParams::default())
1149            .await
1150            .unwrap_err();
1151        assert!(matches!(err, SdkError::Api { .. }));
1152    }
1153
1154    #[tokio::test]
1155    async fn get_lists_server_error() {
1156        let server = MockServer::start().await;
1157        Mock::given(method("GET"))
1158            .and(path("/lists"))
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_lists(&GetListsParams::default())
1166            .await
1167            .unwrap_err();
1168        assert!(matches!(err, SdkError::Api { .. }));
1169    }
1170
1171    #[tokio::test]
1172    async fn get_list_success() {
1173        let server = MockServer::start().await;
1174        Mock::given(method("GET"))
1175            .and(path("/lists/my-list"))
1176            .respond_with(ResponseTemplate::new(200).set_body_json(
1177                serde_json::json!({"data": {"items": ["item1", "item2"]}, "cursor": ""}),
1178            ))
1179            .mount(&server)
1180            .await;
1181        let sdk = make_sdk(format!("{}/", server.uri()));
1182        let resp = sdk
1183            .kvstore
1184            .get_list("my-list", &GetListParams::default())
1185            .await
1186            .unwrap();
1187        assert_eq!(resp.data.items, vec!["item1", "item2"]);
1188    }
1189
1190    #[tokio::test]
1191    async fn get_list_not_found() {
1192        let server = MockServer::start().await;
1193        Mock::given(method("GET"))
1194            .and(path("/lists/my-list"))
1195            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1196            .mount(&server)
1197            .await;
1198        let sdk = make_sdk(format!("{}/", server.uri()));
1199        let err = sdk
1200            .kvstore
1201            .get_list("my-list", &GetListParams::default())
1202            .await
1203            .unwrap_err();
1204        assert!(matches!(err, SdkError::Api { .. }));
1205    }
1206
1207    #[tokio::test]
1208    async fn get_list_server_error() {
1209        let server = MockServer::start().await;
1210        Mock::given(method("GET"))
1211            .and(path("/lists/my-list"))
1212            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1213            .mount(&server)
1214            .await;
1215        let sdk = make_sdk(format!("{}/", server.uri()));
1216        let err = sdk
1217            .kvstore
1218            .get_list("my-list", &GetListParams::default())
1219            .await
1220            .unwrap_err();
1221        assert!(matches!(err, SdkError::Api { .. }));
1222    }
1223
1224    #[tokio::test]
1225    async fn update_list_success() {
1226        let server = MockServer::start().await;
1227        Mock::given(method("PATCH"))
1228            .and(path("/lists/my-list"))
1229            .respond_with(
1230                ResponseTemplate::new(200)
1231                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1232            )
1233            .mount(&server)
1234            .await;
1235        let sdk = make_sdk(format!("{}/", server.uri()));
1236        sdk.kvstore
1237            .update_list(
1238                "my-list",
1239                &UpdateListParams {
1240                    add_items: Some(vec!["item3".to_string()]),
1241                    remove_items: None,
1242                },
1243            )
1244            .await
1245            .unwrap();
1246    }
1247
1248    // Wire-inspection regression: confirm addItems/removeItems reach the wire
1249    // under the names the API expects, so any future serde rename of these
1250    // fields fails loudly.
1251    #[tokio::test]
1252    async fn update_list_wire_body() {
1253        let server = MockServer::start().await;
1254        Mock::given(method("PATCH"))
1255            .and(path("/lists/my-list"))
1256            .and(body_json(serde_json::json!({
1257                "add_items": ["c"],
1258                "remove_items": ["a"]
1259            })))
1260            .respond_with(
1261                ResponseTemplate::new(200)
1262                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1263            )
1264            .mount(&server)
1265            .await;
1266        let sdk = make_sdk(format!("{}/", server.uri()));
1267        sdk.kvstore
1268            .update_list(
1269                "my-list",
1270                &UpdateListParams {
1271                    add_items: Some(vec!["c".to_string()]),
1272                    remove_items: Some(vec!["a".to_string()]),
1273                },
1274            )
1275            .await
1276            .unwrap();
1277    }
1278
1279    #[tokio::test]
1280    async fn update_list_api_error() {
1281        let server = MockServer::start().await;
1282        Mock::given(method("PATCH"))
1283            .and(path("/lists/my-list"))
1284            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
1285            .mount(&server)
1286            .await;
1287        let sdk = make_sdk(format!("{}/", server.uri()));
1288        let err = sdk
1289            .kvstore
1290            .update_list("my-list", &UpdateListParams::default())
1291            .await
1292            .unwrap_err();
1293        assert!(matches!(err, SdkError::Api { .. }));
1294    }
1295
1296    #[tokio::test]
1297    async fn update_list_server_error() {
1298        let server = MockServer::start().await;
1299        Mock::given(method("PATCH"))
1300            .and(path("/lists/my-list"))
1301            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1302            .mount(&server)
1303            .await;
1304        let sdk = make_sdk(format!("{}/", server.uri()));
1305        let err = sdk
1306            .kvstore
1307            .update_list("my-list", &UpdateListParams::default())
1308            .await
1309            .unwrap_err();
1310        assert!(matches!(err, SdkError::Api { .. }));
1311    }
1312
1313    #[tokio::test]
1314    async fn add_list_item_success() {
1315        let server = MockServer::start().await;
1316        Mock::given(method("POST"))
1317            .and(path("/lists/my-list/items"))
1318            .respond_with(
1319                ResponseTemplate::new(201)
1320                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1321            )
1322            .mount(&server)
1323            .await;
1324        let sdk = make_sdk(format!("{}/", server.uri()));
1325        sdk.kvstore
1326            .add_list_item(
1327                "my-list",
1328                &AddListItemParams {
1329                    item: "item1".to_string(),
1330                },
1331            )
1332            .await
1333            .unwrap();
1334    }
1335
1336    #[tokio::test]
1337    async fn add_list_item_api_error() {
1338        let server = MockServer::start().await;
1339        Mock::given(method("POST"))
1340            .and(path("/lists/my-list/items"))
1341            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
1342            .mount(&server)
1343            .await;
1344        let sdk = make_sdk(format!("{}/", server.uri()));
1345        let err = sdk
1346            .kvstore
1347            .add_list_item(
1348                "my-list",
1349                &AddListItemParams {
1350                    item: "item1".to_string(),
1351                },
1352            )
1353            .await
1354            .unwrap_err();
1355        assert!(matches!(err, SdkError::Api { .. }));
1356    }
1357
1358    #[tokio::test]
1359    async fn add_list_item_server_error() {
1360        let server = MockServer::start().await;
1361        Mock::given(method("POST"))
1362            .and(path("/lists/my-list/items"))
1363            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1364            .mount(&server)
1365            .await;
1366        let sdk = make_sdk(format!("{}/", server.uri()));
1367        let err = sdk
1368            .kvstore
1369            .add_list_item(
1370                "my-list",
1371                &AddListItemParams {
1372                    item: "item1".to_string(),
1373                },
1374            )
1375            .await
1376            .unwrap_err();
1377        assert!(matches!(err, SdkError::Api { .. }));
1378    }
1379
1380    #[tokio::test]
1381    async fn list_contains_item_success() {
1382        let server = MockServer::start().await;
1383        Mock::given(method("GET"))
1384            .and(path("/lists/my-list/contains/item1"))
1385            .respond_with(
1386                ResponseTemplate::new(200)
1387                    .set_body_json(serde_json::json!({"data": {"exists": true}})),
1388            )
1389            .mount(&server)
1390            .await;
1391        let sdk = make_sdk(format!("{}/", server.uri()));
1392        let resp = sdk
1393            .kvstore
1394            .list_contains_item("my-list", "item1")
1395            .await
1396            .unwrap();
1397        assert!(resp.exists);
1398    }
1399
1400    #[tokio::test]
1401    async fn list_contains_item_not_found() {
1402        let server = MockServer::start().await;
1403        Mock::given(method("GET"))
1404            .and(path("/lists/my-list/contains/item1"))
1405            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1406            .mount(&server)
1407            .await;
1408        let sdk = make_sdk(format!("{}/", server.uri()));
1409        let err = sdk
1410            .kvstore
1411            .list_contains_item("my-list", "item1")
1412            .await
1413            .unwrap_err();
1414        assert!(matches!(err, SdkError::Api { .. }));
1415    }
1416
1417    #[tokio::test]
1418    async fn list_contains_item_server_error() {
1419        let server = MockServer::start().await;
1420        Mock::given(method("GET"))
1421            .and(path("/lists/my-list/contains/item1"))
1422            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1423            .mount(&server)
1424            .await;
1425        let sdk = make_sdk(format!("{}/", server.uri()));
1426        let err = sdk
1427            .kvstore
1428            .list_contains_item("my-list", "item1")
1429            .await
1430            .unwrap_err();
1431        assert!(matches!(err, SdkError::Api { .. }));
1432    }
1433
1434    #[tokio::test]
1435    async fn delete_list_item_success() {
1436        let server = MockServer::start().await;
1437        Mock::given(method("DELETE"))
1438            .and(path("/lists/my-list/items/item1"))
1439            .respond_with(
1440                ResponseTemplate::new(200)
1441                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1442            )
1443            .mount(&server)
1444            .await;
1445        let sdk = make_sdk(format!("{}/", server.uri()));
1446        sdk.kvstore
1447            .delete_list_item("my-list", "item1")
1448            .await
1449            .unwrap();
1450    }
1451
1452    #[tokio::test]
1453    async fn delete_list_item_not_found() {
1454        let server = MockServer::start().await;
1455        Mock::given(method("DELETE"))
1456            .and(path("/lists/my-list/items/item1"))
1457            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1458            .mount(&server)
1459            .await;
1460        let sdk = make_sdk(format!("{}/", server.uri()));
1461        let err = sdk
1462            .kvstore
1463            .delete_list_item("my-list", "item1")
1464            .await
1465            .unwrap_err();
1466        assert!(matches!(err, SdkError::Api { .. }));
1467    }
1468
1469    #[tokio::test]
1470    async fn delete_list_item_server_error() {
1471        let server = MockServer::start().await;
1472        Mock::given(method("DELETE"))
1473            .and(path("/lists/my-list/items/item1"))
1474            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1475            .mount(&server)
1476            .await;
1477        let sdk = make_sdk(format!("{}/", server.uri()));
1478        let err = sdk
1479            .kvstore
1480            .delete_list_item("my-list", "item1")
1481            .await
1482            .unwrap_err();
1483        assert!(matches!(err, SdkError::Api { .. }));
1484    }
1485
1486    #[tokio::test]
1487    async fn delete_list_success() {
1488        let server = MockServer::start().await;
1489        Mock::given(method("DELETE"))
1490            .and(path("/lists/my-list"))
1491            .respond_with(
1492                ResponseTemplate::new(200)
1493                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1494            )
1495            .mount(&server)
1496            .await;
1497        let sdk = make_sdk(format!("{}/", server.uri()));
1498        sdk.kvstore.delete_list("my-list").await.unwrap();
1499    }
1500
1501    #[tokio::test]
1502    async fn delete_list_not_found() {
1503        let server = MockServer::start().await;
1504        Mock::given(method("DELETE"))
1505            .and(path("/lists/my-list"))
1506            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1507            .mount(&server)
1508            .await;
1509        let sdk = make_sdk(format!("{}/", server.uri()));
1510        let err = sdk.kvstore.delete_list("my-list").await.unwrap_err();
1511        assert!(matches!(err, SdkError::Api { .. }));
1512    }
1513
1514    #[tokio::test]
1515    async fn delete_list_server_error() {
1516        let server = MockServer::start().await;
1517        Mock::given(method("DELETE"))
1518            .and(path("/lists/my-list"))
1519            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1520            .mount(&server)
1521            .await;
1522        let sdk = make_sdk(format!("{}/", server.uri()));
1523        let err = sdk.kvstore.delete_list("my-list").await.unwrap_err();
1524        assert!(matches!(err, SdkError::Api { .. }));
1525    }
1526}