mirror of
https://github.com/marcoallegretti/karapace.git
synced 2026-03-27 14:03:09 +00:00
295 lines
9.5 KiB
Rust
295 lines
9.5 KiB
Rust
|
|
//! IG-M3: HTTP client ↔ server E2E integration tests.
|
||
|
|
//!
|
||
|
|
//! These tests start a real `karapace-server` in-process on a random port
|
||
|
|
//! and exercise the real `HttpBackend` client against it. No mocks.
|
||
|
|
|
||
|
|
use karapace_remote::http::HttpBackend;
|
||
|
|
use karapace_remote::{BlobKind, RemoteBackend, RemoteConfig};
|
||
|
|
use karapace_server::TestServer;
|
||
|
|
use karapace_store::{
|
||
|
|
EnvMetadata, EnvState, LayerKind, LayerManifest, LayerStore, MetadataStore, ObjectStore,
|
||
|
|
StoreLayout,
|
||
|
|
};
|
||
|
|
fn start_server() -> (TestServer, tempfile::TempDir) {
|
||
|
|
let dir = tempfile::tempdir().unwrap();
|
||
|
|
let server = TestServer::start(dir.path().to_path_buf());
|
||
|
|
(server, dir)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn make_client(url: &str) -> HttpBackend {
|
||
|
|
HttpBackend::new(RemoteConfig {
|
||
|
|
url: url.to_owned(),
|
||
|
|
auth_token: None,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Create a local store with a mock-built environment for push/pull testing.
|
||
|
|
fn setup_local_env(dir: &std::path::Path) -> (StoreLayout, String) {
|
||
|
|
let layout = StoreLayout::new(dir);
|
||
|
|
layout.initialize().unwrap();
|
||
|
|
|
||
|
|
let obj_store = ObjectStore::new(layout.clone());
|
||
|
|
let layer_store = LayerStore::new(layout.clone());
|
||
|
|
let meta_store = MetadataStore::new(layout.clone());
|
||
|
|
|
||
|
|
let obj_hash = obj_store.put(b"test data content").unwrap();
|
||
|
|
let manifest_hash = obj_store.put(b"{\"manifest\": \"test\"}").unwrap();
|
||
|
|
|
||
|
|
let layer = LayerManifest {
|
||
|
|
hash: "layer_hash_001".to_owned(),
|
||
|
|
kind: LayerKind::Base,
|
||
|
|
parent: None,
|
||
|
|
object_refs: vec![obj_hash],
|
||
|
|
read_only: true,
|
||
|
|
tar_hash: String::new(),
|
||
|
|
};
|
||
|
|
let layer_content_hash = layer_store.put(&layer).unwrap();
|
||
|
|
|
||
|
|
let meta = EnvMetadata {
|
||
|
|
env_id: "env_abc123".into(),
|
||
|
|
short_id: "env_abc123".into(),
|
||
|
|
name: Some("test-env".to_owned()),
|
||
|
|
state: EnvState::Built,
|
||
|
|
base_layer: layer_content_hash.into(),
|
||
|
|
dependency_layers: vec![],
|
||
|
|
policy_layer: None,
|
||
|
|
manifest_hash: manifest_hash.into(),
|
||
|
|
ref_count: 1,
|
||
|
|
created_at: "2025-01-01T00:00:00Z".to_owned(),
|
||
|
|
updated_at: "2025-01-01T00:00:00Z".to_owned(),
|
||
|
|
checksum: None,
|
||
|
|
};
|
||
|
|
meta_store.put(&meta).unwrap();
|
||
|
|
|
||
|
|
(layout, "env_abc123".to_owned())
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Tests ---
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn http_e2e_blob_roundtrip() {
|
||
|
|
let (server, _dir) = start_server();
|
||
|
|
let client = make_client(&server.url);
|
||
|
|
|
||
|
|
// PUT
|
||
|
|
client
|
||
|
|
.put_blob(BlobKind::Object, "hash1", b"hello world")
|
||
|
|
.unwrap();
|
||
|
|
|
||
|
|
// GET
|
||
|
|
let data = client.get_blob(BlobKind::Object, "hash1").unwrap();
|
||
|
|
assert_eq!(data, b"hello world");
|
||
|
|
|
||
|
|
// HEAD — exists
|
||
|
|
assert!(client.has_blob(BlobKind::Object, "hash1").unwrap());
|
||
|
|
|
||
|
|
// HEAD — missing
|
||
|
|
assert!(!client.has_blob(BlobKind::Object, "missing").unwrap());
|
||
|
|
|
||
|
|
// Multiple kinds
|
||
|
|
client
|
||
|
|
.put_blob(BlobKind::Layer, "l1", b"layer-data")
|
||
|
|
.unwrap();
|
||
|
|
client
|
||
|
|
.put_blob(BlobKind::Metadata, "m1", b"meta-data")
|
||
|
|
.unwrap();
|
||
|
|
assert_eq!(
|
||
|
|
client.get_blob(BlobKind::Layer, "l1").unwrap(),
|
||
|
|
b"layer-data"
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
client.get_blob(BlobKind::Metadata, "m1").unwrap(),
|
||
|
|
b"meta-data"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn http_e2e_push_pull_full_env() {
|
||
|
|
let (server, _srv_dir) = start_server();
|
||
|
|
let client = make_client(&server.url);
|
||
|
|
|
||
|
|
// Set up source store with a mock environment
|
||
|
|
let src_dir = tempfile::tempdir().unwrap();
|
||
|
|
let (src_layout, env_id) = setup_local_env(src_dir.path());
|
||
|
|
|
||
|
|
// Push to real server
|
||
|
|
let push_result =
|
||
|
|
karapace_remote::push_env(&src_layout, &env_id, &client, Some("test@latest")).unwrap();
|
||
|
|
assert_eq!(push_result.objects_pushed, 2);
|
||
|
|
assert_eq!(push_result.layers_pushed, 1);
|
||
|
|
|
||
|
|
// Pull into a fresh store
|
||
|
|
let dst_dir = tempfile::tempdir().unwrap();
|
||
|
|
let dst_layout = StoreLayout::new(dst_dir.path());
|
||
|
|
dst_layout.initialize().unwrap();
|
||
|
|
|
||
|
|
let pull_result = karapace_remote::pull_env(&dst_layout, &env_id, &client).unwrap();
|
||
|
|
assert_eq!(pull_result.objects_pulled, 2);
|
||
|
|
assert_eq!(pull_result.layers_pulled, 1);
|
||
|
|
|
||
|
|
// Verify metadata identical
|
||
|
|
let src_meta = MetadataStore::new(src_layout).get(&env_id).unwrap();
|
||
|
|
let dst_meta = MetadataStore::new(dst_layout.clone()).get(&env_id).unwrap();
|
||
|
|
assert_eq!(src_meta.env_id, dst_meta.env_id);
|
||
|
|
assert_eq!(src_meta.name, dst_meta.name);
|
||
|
|
assert_eq!(src_meta.base_layer, dst_meta.base_layer);
|
||
|
|
assert_eq!(src_meta.manifest_hash, dst_meta.manifest_hash);
|
||
|
|
|
||
|
|
// Verify objects byte-for-byte identical
|
||
|
|
let src_obj = ObjectStore::new(StoreLayout::new(src_dir.path()));
|
||
|
|
let dst_obj = ObjectStore::new(dst_layout.clone());
|
||
|
|
let src_data = src_obj.get(&src_meta.manifest_hash).unwrap();
|
||
|
|
let dst_data = dst_obj.get(&dst_meta.manifest_hash).unwrap();
|
||
|
|
assert_eq!(src_data, dst_data);
|
||
|
|
|
||
|
|
// Verify layers identical
|
||
|
|
let src_layer = LayerStore::new(StoreLayout::new(src_dir.path()))
|
||
|
|
.get(&src_meta.base_layer)
|
||
|
|
.unwrap();
|
||
|
|
let dst_layer = LayerStore::new(dst_layout)
|
||
|
|
.get(&dst_meta.base_layer)
|
||
|
|
.unwrap();
|
||
|
|
assert_eq!(src_layer.object_refs, dst_layer.object_refs);
|
||
|
|
assert_eq!(src_layer.kind, dst_layer.kind);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn http_e2e_registry_roundtrip() {
|
||
|
|
let (server, _dir) = start_server();
|
||
|
|
let client = make_client(&server.url);
|
||
|
|
|
||
|
|
// Set up and push an environment with a tag
|
||
|
|
let src_dir = tempfile::tempdir().unwrap();
|
||
|
|
let (src_layout, env_id) = setup_local_env(src_dir.path());
|
||
|
|
karapace_remote::push_env(&src_layout, &env_id, &client, Some("myapp@latest")).unwrap();
|
||
|
|
|
||
|
|
// Resolve the reference
|
||
|
|
let resolved = karapace_remote::resolve_ref(&client, "myapp@latest").unwrap();
|
||
|
|
assert_eq!(resolved, env_id);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn http_e2e_concurrent_4_clients() {
|
||
|
|
let (server, _dir) = start_server();
|
||
|
|
let url = server.url.clone();
|
||
|
|
|
||
|
|
let handles: Vec<_> = (0..4)
|
||
|
|
.map(|thread_idx| {
|
||
|
|
let u = url.clone();
|
||
|
|
std::thread::spawn(move || {
|
||
|
|
let client = make_client(&u);
|
||
|
|
for i in 0..10 {
|
||
|
|
let key = format!("t{thread_idx}_blob_{i}");
|
||
|
|
let data = format!("data-{thread_idx}-{i}");
|
||
|
|
client
|
||
|
|
.put_blob(BlobKind::Object, &key, data.as_bytes())
|
||
|
|
.unwrap();
|
||
|
|
}
|
||
|
|
})
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
for h in handles {
|
||
|
|
h.join().unwrap();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verify all 40 blobs exist
|
||
|
|
let client = make_client(&server.url);
|
||
|
|
for thread_idx in 0..4 {
|
||
|
|
for i in 0..10 {
|
||
|
|
let key = format!("t{thread_idx}_blob_{i}");
|
||
|
|
let expected = format!("data-{thread_idx}-{i}");
|
||
|
|
let data = client.get_blob(BlobKind::Object, &key).unwrap();
|
||
|
|
assert_eq!(data, expected.as_bytes(), "blob {key} data mismatch");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn http_e2e_server_restart_persistence() {
|
||
|
|
let data_dir = tempfile::tempdir().unwrap();
|
||
|
|
|
||
|
|
// Start server, push data
|
||
|
|
{
|
||
|
|
let server = TestServer::start(data_dir.path().to_path_buf());
|
||
|
|
let client = make_client(&server.url);
|
||
|
|
|
||
|
|
client
|
||
|
|
.put_blob(BlobKind::Object, "persist1", b"data1")
|
||
|
|
.unwrap();
|
||
|
|
client
|
||
|
|
.put_blob(BlobKind::Layer, "persist2", b"data2")
|
||
|
|
.unwrap();
|
||
|
|
client.put_registry(b"{\"entries\":{}}").unwrap();
|
||
|
|
// server drops here — stops listening
|
||
|
|
}
|
||
|
|
|
||
|
|
// Start new server on same data_dir
|
||
|
|
{
|
||
|
|
let server2 = TestServer::start(data_dir.path().to_path_buf());
|
||
|
|
let client2 = make_client(&server2.url);
|
||
|
|
|
||
|
|
// All data must survive
|
||
|
|
assert_eq!(
|
||
|
|
client2.get_blob(BlobKind::Object, "persist1").unwrap(),
|
||
|
|
b"data1"
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
client2.get_blob(BlobKind::Layer, "persist2").unwrap(),
|
||
|
|
b"data2"
|
||
|
|
);
|
||
|
|
assert_eq!(client2.get_registry().unwrap(), b"{\"entries\":{}}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn http_e2e_integrity_on_pull() {
|
||
|
|
let (server, server_data) = start_server();
|
||
|
|
let client = make_client(&server.url);
|
||
|
|
|
||
|
|
// Push a real environment
|
||
|
|
let src_dir = tempfile::tempdir().unwrap();
|
||
|
|
let (src_layout, env_id) = setup_local_env(src_dir.path());
|
||
|
|
karapace_remote::push_env(&src_layout, &env_id, &client, None).unwrap();
|
||
|
|
|
||
|
|
// Tamper with an object on the server's filesystem directly
|
||
|
|
let src_meta = MetadataStore::new(src_layout).get(&env_id).unwrap();
|
||
|
|
let manifest_hash = src_meta.manifest_hash.to_string();
|
||
|
|
let tampered_path = server_data
|
||
|
|
.path()
|
||
|
|
.join("blobs")
|
||
|
|
.join("Object")
|
||
|
|
.join(&manifest_hash);
|
||
|
|
std::fs::write(&tampered_path, b"CORRUPTED DATA").unwrap();
|
||
|
|
|
||
|
|
// Pull into a fresh store — must detect integrity failure
|
||
|
|
let dst_dir = tempfile::tempdir().unwrap();
|
||
|
|
let dst_layout = StoreLayout::new(dst_dir.path());
|
||
|
|
dst_layout.initialize().unwrap();
|
||
|
|
|
||
|
|
let result = karapace_remote::pull_env(&dst_layout, &env_id, &client);
|
||
|
|
assert!(
|
||
|
|
result.is_err(),
|
||
|
|
"pull must fail when a blob has been tampered with"
|
||
|
|
);
|
||
|
|
let err_msg = format!("{}", result.unwrap_err());
|
||
|
|
assert!(
|
||
|
|
err_msg.contains("integrity") || err_msg.contains("Integrity"),
|
||
|
|
"error must mention integrity failure, got: {err_msg}"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn http_e2e_404_on_missing() {
|
||
|
|
let (server, _dir) = start_server();
|
||
|
|
let client = make_client(&server.url);
|
||
|
|
|
||
|
|
let result = client.get_blob(BlobKind::Object, "nonexistent");
|
||
|
|
assert!(result.is_err(), "GET missing blob must return error");
|
||
|
|
let err_msg = format!("{}", result.unwrap_err());
|
||
|
|
assert!(
|
||
|
|
err_msg.contains("404") || err_msg.contains("not found") || err_msg.contains("Not Found"),
|
||
|
|
"error must indicate 404, got: {err_msg}"
|
||
|
|
);
|
||
|
|
}
|