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        })
700        .unwrap()
701    }
702
703    // ── Sets ─────────────────────────────────────────────────────────────────
704
705    #[tokio::test]
706    async fn create_set_success() {
707        let server = MockServer::start().await;
708        Mock::given(method("POST"))
709            .and(path("/sets"))
710            .respond_with(
711                ResponseTemplate::new(201)
712                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
713            )
714            .mount(&server)
715            .await;
716        let sdk = make_sdk(format!("{}/", server.uri()));
717        sdk.kvstore
718            .create_set(&CreateSetParams {
719                key: "k".to_string(),
720                value: "v".to_string(),
721            })
722            .await
723            .unwrap();
724    }
725
726    #[tokio::test]
727    async fn create_set_api_error() {
728        let server = MockServer::start().await;
729        Mock::given(method("POST"))
730            .and(path("/sets"))
731            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
732            .mount(&server)
733            .await;
734        let sdk = make_sdk(format!("{}/", server.uri()));
735        let err = sdk
736            .kvstore
737            .create_set(&CreateSetParams {
738                key: "k".to_string(),
739                value: "v".to_string(),
740            })
741            .await
742            .unwrap_err();
743        assert!(matches!(err, SdkError::Api { .. }));
744    }
745
746    #[tokio::test]
747    async fn create_set_server_error() {
748        let server = MockServer::start().await;
749        Mock::given(method("POST"))
750            .and(path("/sets"))
751            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
752            .mount(&server)
753            .await;
754        let sdk = make_sdk(format!("{}/", server.uri()));
755        let err = sdk
756            .kvstore
757            .create_set(&CreateSetParams {
758                key: "k".to_string(),
759                value: "v".to_string(),
760            })
761            .await
762            .unwrap_err();
763        assert!(matches!(err, SdkError::Api { .. }));
764    }
765
766    #[tokio::test]
767    async fn get_sets_success() {
768        let server = MockServer::start().await;
769        Mock::given(method("GET"))
770            .and(path("/sets"))
771            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"data": [{"key": "k1", "value": "v1"}, {"key": "k2", "value": "v2"}], "cursor": ""})))
772            .mount(&server)
773            .await;
774        let sdk = make_sdk(format!("{}/", server.uri()));
775        let resp = sdk
776            .kvstore
777            .get_sets(&GetSetsParams::default())
778            .await
779            .unwrap();
780        assert_eq!(resp.data.len(), 2);
781        assert_eq!(resp.data[0].key, "k1");
782    }
783
784    #[tokio::test]
785    async fn get_sets_null_data_empty_store() {
786        let server = MockServer::start().await;
787        Mock::given(method("GET"))
788            .and(path("/sets"))
789            .respond_with(ResponseTemplate::new(200).set_body_json(
790                serde_json::json!({"code": 200, "msg": "", "data": null, "cursor": ""}),
791            ))
792            .mount(&server)
793            .await;
794        let sdk = make_sdk(format!("{}/", server.uri()));
795        let resp = sdk
796            .kvstore
797            .get_sets(&GetSetsParams::default())
798            .await
799            .unwrap();
800        assert!(resp.data.is_empty());
801        assert_eq!(resp.cursor, "");
802    }
803
804    #[tokio::test]
805    async fn get_sets_api_error() {
806        let server = MockServer::start().await;
807        Mock::given(method("GET"))
808            .and(path("/sets"))
809            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
810            .mount(&server)
811            .await;
812        let sdk = make_sdk(format!("{}/", server.uri()));
813        let err = sdk
814            .kvstore
815            .get_sets(&GetSetsParams::default())
816            .await
817            .unwrap_err();
818        assert!(matches!(err, SdkError::Api { .. }));
819    }
820
821    #[tokio::test]
822    async fn get_sets_server_error() {
823        let server = MockServer::start().await;
824        Mock::given(method("GET"))
825            .and(path("/sets"))
826            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
827            .mount(&server)
828            .await;
829        let sdk = make_sdk(format!("{}/", server.uri()));
830        let err = sdk
831            .kvstore
832            .get_sets(&GetSetsParams::default())
833            .await
834            .unwrap_err();
835        assert!(matches!(err, SdkError::Api { .. }));
836    }
837
838    #[tokio::test]
839    async fn get_set_success() {
840        let server = MockServer::start().await;
841        Mock::given(method("GET"))
842            .and(path("/sets/my-key"))
843            .respond_with(
844                ResponseTemplate::new(200)
845                    .set_body_json(serde_json::json!({"data": {"value": "my-value"}})),
846            )
847            .mount(&server)
848            .await;
849        let sdk = make_sdk(format!("{}/", server.uri()));
850        let resp = sdk.kvstore.get_set("my-key").await.unwrap();
851        assert_eq!(resp.value, "my-value");
852    }
853
854    #[tokio::test]
855    async fn get_set_not_found() {
856        let server = MockServer::start().await;
857        Mock::given(method("GET"))
858            .and(path("/sets/my-key"))
859            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
860            .mount(&server)
861            .await;
862        let sdk = make_sdk(format!("{}/", server.uri()));
863        let err = sdk.kvstore.get_set("my-key").await.unwrap_err();
864        assert!(matches!(err, SdkError::Api { .. }));
865    }
866
867    #[tokio::test]
868    async fn get_set_server_error() {
869        let server = MockServer::start().await;
870        Mock::given(method("GET"))
871            .and(path("/sets/my-key"))
872            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
873            .mount(&server)
874            .await;
875        let sdk = make_sdk(format!("{}/", server.uri()));
876        let err = sdk.kvstore.get_set("my-key").await.unwrap_err();
877        assert!(matches!(err, SdkError::Api { .. }));
878    }
879
880    #[tokio::test]
881    async fn bulk_sets_success() {
882        let server = MockServer::start().await;
883        Mock::given(method("POST"))
884            .and(path("/sets/bulk"))
885            .respond_with(
886                ResponseTemplate::new(201)
887                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
888            )
889            .mount(&server)
890            .await;
891        let sdk = make_sdk(format!("{}/", server.uri()));
892        let mut add = HashMap::new();
893        add.insert("k1".to_string(), "v1".to_string());
894        sdk.kvstore
895            .bulk_sets(&BulkSetsParams {
896                add_sets: Some(add),
897                delete_sets: None,
898            })
899            .await
900            .unwrap();
901    }
902
903    #[tokio::test]
904    async fn bulk_sets_api_error() {
905        let server = MockServer::start().await;
906        Mock::given(method("POST"))
907            .and(path("/sets/bulk"))
908            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
909            .mount(&server)
910            .await;
911        let sdk = make_sdk(format!("{}/", server.uri()));
912        let err = sdk
913            .kvstore
914            .bulk_sets(&BulkSetsParams {
915                add_sets: None,
916                delete_sets: None,
917            })
918            .await
919            .unwrap_err();
920        assert!(matches!(err, SdkError::Api { .. }));
921    }
922
923    #[tokio::test]
924    async fn bulk_sets_server_error() {
925        let server = MockServer::start().await;
926        Mock::given(method("POST"))
927            .and(path("/sets/bulk"))
928            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
929            .mount(&server)
930            .await;
931        let sdk = make_sdk(format!("{}/", server.uri()));
932        let err = sdk
933            .kvstore
934            .bulk_sets(&BulkSetsParams {
935                add_sets: None,
936                delete_sets: None,
937            })
938            .await
939            .unwrap_err();
940        assert!(matches!(err, SdkError::Api { .. }));
941    }
942
943    // Wire-inspection regressions: confirm that addSets and deleteSets reach
944    // the wire under the names the API expects, so any future serde rename of
945    // these fields fails loudly.
946    #[tokio::test]
947    async fn bulk_sets_wire_body_add_sets() {
948        let server = MockServer::start().await;
949        Mock::given(method("POST"))
950            .and(path("/sets/bulk"))
951            .and(body_json(serde_json::json!({
952                "addSets": {"k1": "v1"}
953            })))
954            .respond_with(
955                ResponseTemplate::new(201)
956                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
957            )
958            .mount(&server)
959            .await;
960        let sdk = make_sdk(format!("{}/", server.uri()));
961        let mut add = HashMap::new();
962        add.insert("k1".to_string(), "v1".to_string());
963        sdk.kvstore
964            .bulk_sets(&BulkSetsParams {
965                add_sets: Some(add),
966                delete_sets: None,
967            })
968            .await
969            .unwrap();
970    }
971
972    #[tokio::test]
973    async fn bulk_sets_wire_body_delete_sets() {
974        let server = MockServer::start().await;
975        Mock::given(method("POST"))
976            .and(path("/sets/bulk"))
977            .and(body_json(serde_json::json!({
978                "deleteSets": ["k1", "k2"]
979            })))
980            .respond_with(
981                ResponseTemplate::new(201)
982                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
983            )
984            .mount(&server)
985            .await;
986        let sdk = make_sdk(format!("{}/", server.uri()));
987        sdk.kvstore
988            .bulk_sets(&BulkSetsParams {
989                add_sets: None,
990                delete_sets: Some(vec!["k1".to_string(), "k2".to_string()]),
991            })
992            .await
993            .unwrap();
994    }
995
996    #[tokio::test]
997    async fn delete_set_success() {
998        let server = MockServer::start().await;
999        Mock::given(method("DELETE"))
1000            .and(path("/sets/my-key"))
1001            .respond_with(
1002                ResponseTemplate::new(200)
1003                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1004            )
1005            .mount(&server)
1006            .await;
1007        let sdk = make_sdk(format!("{}/", server.uri()));
1008        sdk.kvstore.delete_set("my-key").await.unwrap();
1009    }
1010
1011    #[tokio::test]
1012    async fn delete_set_not_found() {
1013        let server = MockServer::start().await;
1014        Mock::given(method("DELETE"))
1015            .and(path("/sets/my-key"))
1016            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1017            .mount(&server)
1018            .await;
1019        let sdk = make_sdk(format!("{}/", server.uri()));
1020        let err = sdk.kvstore.delete_set("my-key").await.unwrap_err();
1021        assert!(matches!(err, SdkError::Api { .. }));
1022    }
1023
1024    #[tokio::test]
1025    async fn delete_set_server_error() {
1026        let server = MockServer::start().await;
1027        Mock::given(method("DELETE"))
1028            .and(path("/sets/my-key"))
1029            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1030            .mount(&server)
1031            .await;
1032        let sdk = make_sdk(format!("{}/", server.uri()));
1033        let err = sdk.kvstore.delete_set("my-key").await.unwrap_err();
1034        assert!(matches!(err, SdkError::Api { .. }));
1035    }
1036
1037    // ── Lists ─────────────────────────────────────────────────────────────────
1038
1039    #[tokio::test]
1040    async fn create_list_success() {
1041        let server = MockServer::start().await;
1042        Mock::given(method("POST"))
1043            .and(path("/lists"))
1044            .respond_with(
1045                ResponseTemplate::new(201)
1046                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1047            )
1048            .mount(&server)
1049            .await;
1050        let sdk = make_sdk(format!("{}/", server.uri()));
1051        sdk.kvstore
1052            .create_list(&CreateListParams {
1053                key: "my-list".to_string(),
1054                items: vec!["item1".to_string()],
1055            })
1056            .await
1057            .unwrap();
1058    }
1059
1060    #[tokio::test]
1061    async fn create_list_api_error() {
1062        let server = MockServer::start().await;
1063        Mock::given(method("POST"))
1064            .and(path("/lists"))
1065            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
1066            .mount(&server)
1067            .await;
1068        let sdk = make_sdk(format!("{}/", server.uri()));
1069        let err = sdk
1070            .kvstore
1071            .create_list(&CreateListParams {
1072                key: "my-list".to_string(),
1073                items: vec![],
1074            })
1075            .await
1076            .unwrap_err();
1077        assert!(matches!(err, SdkError::Api { .. }));
1078    }
1079
1080    #[tokio::test]
1081    async fn create_list_server_error() {
1082        let server = MockServer::start().await;
1083        Mock::given(method("POST"))
1084            .and(path("/lists"))
1085            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1086            .mount(&server)
1087            .await;
1088        let sdk = make_sdk(format!("{}/", server.uri()));
1089        let err = sdk
1090            .kvstore
1091            .create_list(&CreateListParams {
1092                key: "my-list".to_string(),
1093                items: vec![],
1094            })
1095            .await
1096            .unwrap_err();
1097        assert!(matches!(err, SdkError::Api { .. }));
1098    }
1099
1100    #[tokio::test]
1101    async fn get_lists_success() {
1102        let server = MockServer::start().await;
1103        Mock::given(method("GET"))
1104            .and(path("/lists"))
1105            .respond_with(ResponseTemplate::new(200).set_body_json(
1106                serde_json::json!({"data": {"keys": ["list1", "list2"]}, "cursor": ""}),
1107            ))
1108            .mount(&server)
1109            .await;
1110        let sdk = make_sdk(format!("{}/", server.uri()));
1111        let resp = sdk
1112            .kvstore
1113            .get_lists(&GetListsParams::default())
1114            .await
1115            .unwrap();
1116        assert_eq!(resp.data.keys, vec!["list1", "list2"]);
1117    }
1118
1119    #[tokio::test]
1120    async fn get_lists_null_data_empty_store() {
1121        let server = MockServer::start().await;
1122        Mock::given(method("GET"))
1123            .and(path("/lists"))
1124            .respond_with(ResponseTemplate::new(200).set_body_json(
1125                serde_json::json!({"code": 200, "msg": "", "data": null, "cursor": ""}),
1126            ))
1127            .mount(&server)
1128            .await;
1129        let sdk = make_sdk(format!("{}/", server.uri()));
1130        let resp = sdk
1131            .kvstore
1132            .get_lists(&GetListsParams::default())
1133            .await
1134            .unwrap();
1135        assert!(resp.data.keys.is_empty());
1136        assert_eq!(resp.cursor, "");
1137    }
1138
1139    #[tokio::test]
1140    async fn get_lists_api_error() {
1141        let server = MockServer::start().await;
1142        Mock::given(method("GET"))
1143            .and(path("/lists"))
1144            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
1145            .mount(&server)
1146            .await;
1147        let sdk = make_sdk(format!("{}/", server.uri()));
1148        let err = sdk
1149            .kvstore
1150            .get_lists(&GetListsParams::default())
1151            .await
1152            .unwrap_err();
1153        assert!(matches!(err, SdkError::Api { .. }));
1154    }
1155
1156    #[tokio::test]
1157    async fn get_lists_server_error() {
1158        let server = MockServer::start().await;
1159        Mock::given(method("GET"))
1160            .and(path("/lists"))
1161            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1162            .mount(&server)
1163            .await;
1164        let sdk = make_sdk(format!("{}/", server.uri()));
1165        let err = sdk
1166            .kvstore
1167            .get_lists(&GetListsParams::default())
1168            .await
1169            .unwrap_err();
1170        assert!(matches!(err, SdkError::Api { .. }));
1171    }
1172
1173    #[tokio::test]
1174    async fn get_list_success() {
1175        let server = MockServer::start().await;
1176        Mock::given(method("GET"))
1177            .and(path("/lists/my-list"))
1178            .respond_with(ResponseTemplate::new(200).set_body_json(
1179                serde_json::json!({"data": {"items": ["item1", "item2"]}, "cursor": ""}),
1180            ))
1181            .mount(&server)
1182            .await;
1183        let sdk = make_sdk(format!("{}/", server.uri()));
1184        let resp = sdk
1185            .kvstore
1186            .get_list("my-list", &GetListParams::default())
1187            .await
1188            .unwrap();
1189        assert_eq!(resp.data.items, vec!["item1", "item2"]);
1190    }
1191
1192    #[tokio::test]
1193    async fn get_list_not_found() {
1194        let server = MockServer::start().await;
1195        Mock::given(method("GET"))
1196            .and(path("/lists/my-list"))
1197            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1198            .mount(&server)
1199            .await;
1200        let sdk = make_sdk(format!("{}/", server.uri()));
1201        let err = sdk
1202            .kvstore
1203            .get_list("my-list", &GetListParams::default())
1204            .await
1205            .unwrap_err();
1206        assert!(matches!(err, SdkError::Api { .. }));
1207    }
1208
1209    #[tokio::test]
1210    async fn get_list_server_error() {
1211        let server = MockServer::start().await;
1212        Mock::given(method("GET"))
1213            .and(path("/lists/my-list"))
1214            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1215            .mount(&server)
1216            .await;
1217        let sdk = make_sdk(format!("{}/", server.uri()));
1218        let err = sdk
1219            .kvstore
1220            .get_list("my-list", &GetListParams::default())
1221            .await
1222            .unwrap_err();
1223        assert!(matches!(err, SdkError::Api { .. }));
1224    }
1225
1226    #[tokio::test]
1227    async fn update_list_success() {
1228        let server = MockServer::start().await;
1229        Mock::given(method("PATCH"))
1230            .and(path("/lists/my-list"))
1231            .respond_with(
1232                ResponseTemplate::new(200)
1233                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1234            )
1235            .mount(&server)
1236            .await;
1237        let sdk = make_sdk(format!("{}/", server.uri()));
1238        sdk.kvstore
1239            .update_list(
1240                "my-list",
1241                &UpdateListParams {
1242                    add_items: Some(vec!["item3".to_string()]),
1243                    remove_items: None,
1244                },
1245            )
1246            .await
1247            .unwrap();
1248    }
1249
1250    // Wire-inspection regression: confirm addItems/removeItems reach the wire
1251    // under the names the API expects, so any future serde rename of these
1252    // fields fails loudly.
1253    #[tokio::test]
1254    async fn update_list_wire_body() {
1255        let server = MockServer::start().await;
1256        Mock::given(method("PATCH"))
1257            .and(path("/lists/my-list"))
1258            .and(body_json(serde_json::json!({
1259                "addItems": ["c"],
1260                "removeItems": ["a"]
1261            })))
1262            .respond_with(
1263                ResponseTemplate::new(200)
1264                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1265            )
1266            .mount(&server)
1267            .await;
1268        let sdk = make_sdk(format!("{}/", server.uri()));
1269        sdk.kvstore
1270            .update_list(
1271                "my-list",
1272                &UpdateListParams {
1273                    add_items: Some(vec!["c".to_string()]),
1274                    remove_items: Some(vec!["a".to_string()]),
1275                },
1276            )
1277            .await
1278            .unwrap();
1279    }
1280
1281    #[tokio::test]
1282    async fn update_list_api_error() {
1283        let server = MockServer::start().await;
1284        Mock::given(method("PATCH"))
1285            .and(path("/lists/my-list"))
1286            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
1287            .mount(&server)
1288            .await;
1289        let sdk = make_sdk(format!("{}/", server.uri()));
1290        let err = sdk
1291            .kvstore
1292            .update_list("my-list", &UpdateListParams::default())
1293            .await
1294            .unwrap_err();
1295        assert!(matches!(err, SdkError::Api { .. }));
1296    }
1297
1298    #[tokio::test]
1299    async fn update_list_server_error() {
1300        let server = MockServer::start().await;
1301        Mock::given(method("PATCH"))
1302            .and(path("/lists/my-list"))
1303            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1304            .mount(&server)
1305            .await;
1306        let sdk = make_sdk(format!("{}/", server.uri()));
1307        let err = sdk
1308            .kvstore
1309            .update_list("my-list", &UpdateListParams::default())
1310            .await
1311            .unwrap_err();
1312        assert!(matches!(err, SdkError::Api { .. }));
1313    }
1314
1315    #[tokio::test]
1316    async fn add_list_item_success() {
1317        let server = MockServer::start().await;
1318        Mock::given(method("POST"))
1319            .and(path("/lists/my-list/items"))
1320            .respond_with(
1321                ResponseTemplate::new(201)
1322                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1323            )
1324            .mount(&server)
1325            .await;
1326        let sdk = make_sdk(format!("{}/", server.uri()));
1327        sdk.kvstore
1328            .add_list_item(
1329                "my-list",
1330                &AddListItemParams {
1331                    item: "item1".to_string(),
1332                },
1333            )
1334            .await
1335            .unwrap();
1336    }
1337
1338    #[tokio::test]
1339    async fn add_list_item_api_error() {
1340        let server = MockServer::start().await;
1341        Mock::given(method("POST"))
1342            .and(path("/lists/my-list/items"))
1343            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
1344            .mount(&server)
1345            .await;
1346        let sdk = make_sdk(format!("{}/", server.uri()));
1347        let err = sdk
1348            .kvstore
1349            .add_list_item(
1350                "my-list",
1351                &AddListItemParams {
1352                    item: "item1".to_string(),
1353                },
1354            )
1355            .await
1356            .unwrap_err();
1357        assert!(matches!(err, SdkError::Api { .. }));
1358    }
1359
1360    #[tokio::test]
1361    async fn add_list_item_server_error() {
1362        let server = MockServer::start().await;
1363        Mock::given(method("POST"))
1364            .and(path("/lists/my-list/items"))
1365            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1366            .mount(&server)
1367            .await;
1368        let sdk = make_sdk(format!("{}/", server.uri()));
1369        let err = sdk
1370            .kvstore
1371            .add_list_item(
1372                "my-list",
1373                &AddListItemParams {
1374                    item: "item1".to_string(),
1375                },
1376            )
1377            .await
1378            .unwrap_err();
1379        assert!(matches!(err, SdkError::Api { .. }));
1380    }
1381
1382    #[tokio::test]
1383    async fn list_contains_item_success() {
1384        let server = MockServer::start().await;
1385        Mock::given(method("GET"))
1386            .and(path("/lists/my-list/contains/item1"))
1387            .respond_with(
1388                ResponseTemplate::new(200)
1389                    .set_body_json(serde_json::json!({"data": {"exists": true}})),
1390            )
1391            .mount(&server)
1392            .await;
1393        let sdk = make_sdk(format!("{}/", server.uri()));
1394        let resp = sdk
1395            .kvstore
1396            .list_contains_item("my-list", "item1")
1397            .await
1398            .unwrap();
1399        assert!(resp.exists);
1400    }
1401
1402    #[tokio::test]
1403    async fn list_contains_item_not_found() {
1404        let server = MockServer::start().await;
1405        Mock::given(method("GET"))
1406            .and(path("/lists/my-list/contains/item1"))
1407            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1408            .mount(&server)
1409            .await;
1410        let sdk = make_sdk(format!("{}/", server.uri()));
1411        let err = sdk
1412            .kvstore
1413            .list_contains_item("my-list", "item1")
1414            .await
1415            .unwrap_err();
1416        assert!(matches!(err, SdkError::Api { .. }));
1417    }
1418
1419    #[tokio::test]
1420    async fn list_contains_item_server_error() {
1421        let server = MockServer::start().await;
1422        Mock::given(method("GET"))
1423            .and(path("/lists/my-list/contains/item1"))
1424            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1425            .mount(&server)
1426            .await;
1427        let sdk = make_sdk(format!("{}/", server.uri()));
1428        let err = sdk
1429            .kvstore
1430            .list_contains_item("my-list", "item1")
1431            .await
1432            .unwrap_err();
1433        assert!(matches!(err, SdkError::Api { .. }));
1434    }
1435
1436    #[tokio::test]
1437    async fn delete_list_item_success() {
1438        let server = MockServer::start().await;
1439        Mock::given(method("DELETE"))
1440            .and(path("/lists/my-list/items/item1"))
1441            .respond_with(
1442                ResponseTemplate::new(200)
1443                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1444            )
1445            .mount(&server)
1446            .await;
1447        let sdk = make_sdk(format!("{}/", server.uri()));
1448        sdk.kvstore
1449            .delete_list_item("my-list", "item1")
1450            .await
1451            .unwrap();
1452    }
1453
1454    #[tokio::test]
1455    async fn delete_list_item_not_found() {
1456        let server = MockServer::start().await;
1457        Mock::given(method("DELETE"))
1458            .and(path("/lists/my-list/items/item1"))
1459            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1460            .mount(&server)
1461            .await;
1462        let sdk = make_sdk(format!("{}/", server.uri()));
1463        let err = sdk
1464            .kvstore
1465            .delete_list_item("my-list", "item1")
1466            .await
1467            .unwrap_err();
1468        assert!(matches!(err, SdkError::Api { .. }));
1469    }
1470
1471    #[tokio::test]
1472    async fn delete_list_item_server_error() {
1473        let server = MockServer::start().await;
1474        Mock::given(method("DELETE"))
1475            .and(path("/lists/my-list/items/item1"))
1476            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1477            .mount(&server)
1478            .await;
1479        let sdk = make_sdk(format!("{}/", server.uri()));
1480        let err = sdk
1481            .kvstore
1482            .delete_list_item("my-list", "item1")
1483            .await
1484            .unwrap_err();
1485        assert!(matches!(err, SdkError::Api { .. }));
1486    }
1487
1488    #[tokio::test]
1489    async fn delete_list_success() {
1490        let server = MockServer::start().await;
1491        Mock::given(method("DELETE"))
1492            .and(path("/lists/my-list"))
1493            .respond_with(
1494                ResponseTemplate::new(200)
1495                    .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})),
1496            )
1497            .mount(&server)
1498            .await;
1499        let sdk = make_sdk(format!("{}/", server.uri()));
1500        sdk.kvstore.delete_list("my-list").await.unwrap();
1501    }
1502
1503    #[tokio::test]
1504    async fn delete_list_not_found() {
1505        let server = MockServer::start().await;
1506        Mock::given(method("DELETE"))
1507            .and(path("/lists/my-list"))
1508            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
1509            .mount(&server)
1510            .await;
1511        let sdk = make_sdk(format!("{}/", server.uri()));
1512        let err = sdk.kvstore.delete_list("my-list").await.unwrap_err();
1513        assert!(matches!(err, SdkError::Api { .. }));
1514    }
1515
1516    #[tokio::test]
1517    async fn delete_list_server_error() {
1518        let server = MockServer::start().await;
1519        Mock::given(method("DELETE"))
1520            .and(path("/lists/my-list"))
1521            .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
1522            .mount(&server)
1523            .await;
1524        let sdk = make_sdk(format!("{}/", server.uri()));
1525        let err = sdk.kvstore.delete_list("my-list").await.unwrap_err();
1526        assert!(matches!(err, SdkError::Api { .. }));
1527    }
1528}