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