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