KeiSeiKit-1.0/_primitives/_rust/kei-store/tests/s3_smoke.rs
Parfii-bot e5cd0d6790 feat(v0.21): kei-store real S3 backend behind opt-in 's3' feature flag
Promotes S3 from MVP stub to functional via aws-sdk-s3. Default builds
unchanged (zero new deps). Feature flag ensures users who don't need
S3 don't pay the ~5MB binary / C-toolchain cost.

Cargo.toml: new [features] s3 = [...] gating 4 optional deps:
  aws-sdk-s3 = 1.130.0
  aws-config = 1.8.16 (with behavior-version-latest)
  tokio = 1.52.1 (current-thread runtime, no multi-threaded bloat)
  bytes = 1 (S3 body passthrough)

s3_cloud/ module (4 files, Constructor Pattern):
  mod.rs (190 LOC) — S3CloudStore + MemoryStore trait impl
  client.rs (81 LOC) — aws-config builder, KEI_STORE_S3_ENDPOINT
    override for R2 / Wasabi / MinIO / any S3-compat
  keys.rs (60 LOC) — path-traversal guard + DJB2 hash helper
  tests.rs (63 LOC) — builder + prefix + key-guard unit tests

Factory routing (factory.rs):
  with 's3' feature + bucket URL → S3CloudStore (real network)
  without 's3' feature → S3Store stub (existing MVP, preserved)

Security posture:
  - Branch-prefix isolation rejects  traversal at keys.rs layer
  - aws-config default credential chain (env → ~/.aws → IMDS);
    no bespoke credential handling
  - rustls, not OpenSSL (matches existing crate tree)

Tests: 22 existing + 11 new (4 keys + 3 client + 5 mod + 5 smoke)
  cargo test -p kei-store (default features): 9 passed
  cargo test -p kei-store --features s3: 22 + 9 + 5 = 36 passed
  cargo clippy -p kei-store --features s3: clean

Real stdout verified for all verify criteria. No fabrication.

MANIFEST.toml [primitive.kei-store] deps updated to reflect feature
opt-in model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:59:11 +08:00

81 lines
2.7 KiB
Rust

//! Smoke tests for the S3 cloud backend (behind `s3` feature).
//!
//! These tests never hit real AWS. They verify:
//! - the `S3CloudStore` builder accepts a mock endpoint without panic
//! - the library re-exports `s3_cloud` when the feature is enabled
//! - path-safety guards reject traversal attempts
//!
//! Run with: `cargo test -p kei-store --features s3 --test s3_smoke`.
//! Without the feature, this file compiles to an empty crate — harmless.
#![cfg(feature = "s3")]
use kei_store::config::S3Cfg;
use kei_store::s3_cloud::S3CloudStore;
use kei_store::MemoryStore;
fn mock_cfg(endpoint: &str) -> S3Cfg {
S3Cfg {
endpoint: Some(endpoint.to_string()),
bucket: Some("test-bucket".to_string()),
region: Some("us-east-1".to_string()),
access_key_env: None,
secret_key_env: None,
cache_path: None,
}
}
#[test]
fn builder_accepts_mock_endpoint() {
let store = S3CloudStore::new(mock_cfg("http://127.0.0.1:9999"))
.expect("builder must not require network");
assert_eq!(store.backend_name(), "s3-cloud");
}
#[test]
fn builder_rejects_missing_bucket() {
let cfg = S3Cfg {
endpoint: Some("http://127.0.0.1:9999".to_string()),
region: Some("us-east-1".to_string()),
..Default::default()
};
let err = S3CloudStore::new(cfg)
.err()
.expect("missing bucket should error");
assert!(format!("{err:#}").contains("bucket"));
}
#[test]
fn branch_switches_prefix() {
let store = S3CloudStore::new(mock_cfg("http://127.0.0.1:9999")).unwrap();
store.branch("agent/foo").unwrap();
// No network IO — just verify branch() does not error and backend_name
// stays stable.
assert_eq!(store.backend_name(), "s3-cloud");
}
#[test]
fn write_fails_gracefully_on_unreachable_endpoint() {
// Point at a closed port — real put_object must error, NOT panic.
let store = S3CloudStore::new(mock_cfg("http://127.0.0.1:9")).unwrap();
let err = store.write("traces/x.jsonl", b"hello").unwrap_err();
let msg = format!("{err:#}");
// We only assert that an error propagates — the exact wording depends
// on the aws-smithy layer.
assert!(!msg.is_empty());
}
#[test]
fn endpoint_env_var_is_honoured() {
std::env::set_var("KEI_STORE_S3_ENDPOINT", "http://127.0.0.1:9999");
// cfg endpoint differs — env should win. Builder still succeeds.
let cfg = S3Cfg {
endpoint: Some("http://unused:1".to_string()),
bucket: Some("b".to_string()),
region: Some("us-east-1".to_string()),
..Default::default()
};
let s = S3CloudStore::new(cfg);
std::env::remove_var("KEI_STORE_S3_ENDPOINT");
assert!(s.is_ok());
}