mod common;
use serde_json::json;
#[tokio::test(flavor = "multi_thread")]
async fn list_key_value_stores() {
let client = require_client!();
let page = client
.key_value_stores()
.list(Default::default())
.await
.expect("listing key-value stores should succeed");
assert!(page.total >= 0);
}
#[tokio::test(flavor = "multi_thread")]
async fn get_key_value_store() {
let client = require_client!();
let name = common::unique_name("kvs-get");
let store = client
.key_value_stores()
.get_or_create(Some(&name))
.await
.expect("create store");
let cleanup_client = client.clone();
let id = store.id.clone();
let _guard = common::Cleanup::new(move || async move {
let _ = cleanup_client.key_value_store(&id).delete().await;
});
let fetched = client
.key_value_store(&store.id)
.get()
.await
.expect("get store by id")
.expect("store should exist");
assert_eq!(fetched.id, store.id);
}
#[tokio::test(flavor = "multi_thread")]
async fn iterate_key_value_stores() {
let client = require_client!();
let name = common::unique_name("kvs-iter");
let store = client
.key_value_stores()
.get_or_create(Some(&name))
.await
.expect("create store");
let cleanup_client = client.clone();
let id = store.id.clone();
let _guard = common::Cleanup::new(move || async move {
let _ = cleanup_client.key_value_store(&id).delete().await;
});
let target = store.id.clone();
assert!(
common::iter_contains_eventually(
|| {
client
.key_value_stores()
.iterate(apify_client::StorageListOptions {
desc: Some(true),
..Default::default()
})
.with_chunk_size(5)
},
move |s| s.id == target,
)
.await,
"key-value store iteration should yield the created store"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn iterate_keys_yields_all_keys() {
let client = require_client!();
let name = common::unique_name("kvs-keys-iter");
let store = client
.key_value_stores()
.get_or_create(Some(&name))
.await
.expect("create store");
let cleanup_client = client.clone();
let id = store.id.clone();
let _guard = common::Cleanup::new(move || async move {
let _ = cleanup_client.key_value_store(&id).delete().await;
});
let store_client = client.key_value_store(&store.id);
let expected: Vec<String> = (0..5).map(|i| format!("iter-key-{i:02}")).collect();
for key in &expected {
store_client
.set_record_json(key, &json!({ "n": key }))
.await
.expect("set record");
}
let mut iter = store_client.iterate_keys(Default::default());
let mut seen = Vec::new();
while let Some(key) = iter.next().await.expect("key iteration should not error") {
seen.push(key.key);
}
for key in &expected {
assert!(
seen.contains(key),
"iterate_keys should yield every created key; missing {key}, saw {seen:?}"
);
}
let mut unique = seen.clone();
unique.sort();
unique.dedup();
assert_eq!(
unique.len(),
seen.len(),
"iterate_keys must not yield duplicate keys, saw {seen:?}"
);
store_client.delete().await.expect("delete store");
}
#[tokio::test(flavor = "multi_thread")]
async fn record_key_with_special_chars_round_trips() {
let client = require_client!();
let name = common::unique_name("kvs-key");
let store = client
.key_value_stores()
.get_or_create(Some(&name))
.await
.expect("create store");
let cleanup_client = client.clone();
let id = store.id.clone();
let _guard = common::Cleanup::new(move || async move {
let _ = cleanup_client.key_value_store(&id).delete().await;
});
let store_client = client.key_value_store(&store.id);
let key = "my!key'(v1).json";
store_client
.set_record_json(key, &json!({ "ok": true }))
.await
.expect("set record with special-char key");
assert!(
store_client.record_exists(key).await.expect("exists"),
"record with special-char key should exist (key must be percent-encoded)"
);
let record = store_client
.get_record(key)
.await
.expect("get record")
.expect("record should exist");
let value: serde_json::Value = record.json().expect("parse json");
assert_eq!(value["ok"], true);
store_client
.delete_record(key)
.await
.expect("delete record with special-char key");
assert!(!store_client.record_exists(key).await.expect("exists after"));
}
#[tokio::test(flavor = "multi_thread")]
async fn key_value_store_crud_flow() {
let client = require_client!();
let name = common::unique_name("kvs");
let store = client
.key_value_stores()
.get_or_create(Some(&name))
.await
.expect("create store");
assert_eq!(store.name.as_deref(), Some(name.as_str()));
let cleanup_client = client.clone();
let cleanup_id = store.id.clone();
let _guard = common::Cleanup::new(move || async move {
let _ = cleanup_client.key_value_store(&cleanup_id).delete().await;
});
let store_client = client.key_value_store(&store.id);
assert!(store_client.get().await.expect("get store").is_some());
store_client
.set_record_json("OUTPUT", &json!({ "hello": "world" }))
.await
.expect("set record");
assert!(store_client.record_exists("OUTPUT").await.expect("exists"));
let record = store_client
.get_record("OUTPUT")
.await
.expect("get record")
.expect("record should exist");
let value: serde_json::Value = record.json().expect("parse record json");
assert_eq!(value["hello"], "world");
let record2 = store_client
.get_record_with_options(
"OUTPUT",
apify_client::GetRecordOptions {
attachment: Some(false),
..Default::default()
},
)
.await
.expect("get record with options")
.expect("record should exist");
let value2: serde_json::Value = record2.json().expect("parse record json");
assert_eq!(value2["hello"], "world");
let keys = store_client
.list_keys(Default::default())
.await
.expect("list keys");
assert!(keys.items.iter().any(|k| k.key == "OUTPUT"));
let renamed = common::unique_name("kvs-renamed");
let updated = store_client
.update(&json!({ "name": renamed }))
.await
.expect("update store");
assert_eq!(updated.name.as_deref(), Some(renamed.as_str()));
store_client
.delete_record("OUTPUT")
.await
.expect("delete record");
assert!(!store_client
.record_exists("OUTPUT")
.await
.expect("exists after delete"));
store_client.delete().await.expect("delete store");
assert!(store_client
.get()
.await
.expect("get after delete")
.is_none());
}
#[tokio::test(flavor = "multi_thread")]
async fn record_public_url_is_fetchable() {
let client = require_client!();
let name = common::unique_name("kvs-sig");
let store = client
.key_value_stores()
.get_or_create(Some(&name))
.await
.expect("create store");
let cleanup_client = client.clone();
let cleanup_id = store.id.clone();
let _guard = common::Cleanup::new(move || async move {
let _ = cleanup_client.key_value_store(&cleanup_id).delete().await;
});
let store_client = client.key_value_store(&store.id);
store_client
.set_record_json("OUTPUT", &json!({ "signed": true }))
.await
.expect("set record");
let url = store_client
.get_record_public_url("OUTPUT")
.await
.expect("record public url");
assert!(url.contains("/key-value-stores/"));
assert!(url.contains("/records/OUTPUT"));
let resp = reqwest::Client::new()
.get(&url)
.send()
.await
.expect("fetch public url");
assert!(
resp.status().is_success(),
"public record URL should be fetchable, got {} for {url}",
resp.status()
);
store_client.delete().await.expect("delete store");
}