Add integration tests
This commit is contained in:
parent
d7c9192ebb
commit
68ae92e4bf
6 changed files with 392 additions and 83 deletions
254
tests/integration.rs
Normal file
254
tests/integration.rs
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
use reqwest::StatusCode;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
/// Start the mkv server in-process on a random port with its own DB.
|
||||
/// Returns the base URL (e.g. "http://localhost:12345").
|
||||
async fn start_server() -> String {
|
||||
let id = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let db_path = format!("/tmp/mkv-test/index-{id}.db");
|
||||
|
||||
// Clean up any previous test database
|
||||
let _ = std::fs::remove_file(&db_path);
|
||||
let _ = std::fs::remove_file(format!("{db_path}-wal"));
|
||||
let _ = std::fs::remove_file(format!("{db_path}-shm"));
|
||||
|
||||
let mut config =
|
||||
mkv::config::Config::load(Path::new("tests/test_config.toml")).expect("load test config");
|
||||
config.database.path = db_path;
|
||||
|
||||
// Bind to port 0 to get a random available port
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
|
||||
let app = mkv::build_app(config).await;
|
||||
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
// Give the server a moment to start
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
|
||||
format!("http://127.0.0.1:{port}")
|
||||
}
|
||||
|
||||
fn client() -> reqwest::Client {
|
||||
reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_put_and_head() {
|
||||
let base = start_server().await;
|
||||
let c = client();
|
||||
|
||||
// PUT a key
|
||||
let resp = c
|
||||
.put(format!("{base}/hello"))
|
||||
.body("world")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::CREATED);
|
||||
|
||||
// HEAD should return 200 with content-length
|
||||
let resp = c.head(format!("{base}/hello")).send().await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.headers()
|
||||
.get("content-length")
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
"5"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_put_and_get_redirect() {
|
||||
let base = start_server().await;
|
||||
let c = client();
|
||||
|
||||
// PUT
|
||||
let resp = c
|
||||
.put(format!("{base}/redirect-test"))
|
||||
.body("some data")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::CREATED);
|
||||
|
||||
// GET should return 302 with Location header pointing to a volume
|
||||
let resp = c.get(format!("{base}/redirect-test")).send().await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
|
||||
let location = resp.headers().get("location").unwrap().to_str().unwrap();
|
||||
assert!(
|
||||
location.starts_with("http://localhost:310"),
|
||||
"location should point to a volume, got: {location}"
|
||||
);
|
||||
|
||||
// Follow the redirect manually and verify the blob content
|
||||
let blob_resp = reqwest::get(location).await.unwrap();
|
||||
assert_eq!(blob_resp.status(), StatusCode::OK);
|
||||
assert_eq!(blob_resp.text().await.unwrap(), "some data");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_nonexistent_returns_404() {
|
||||
let base = start_server().await;
|
||||
let c = client();
|
||||
|
||||
let resp = c.get(format!("{base}/does-not-exist")).send().await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_put_get_delete_get() {
|
||||
let base = start_server().await;
|
||||
let c = client();
|
||||
|
||||
// PUT
|
||||
let resp = c
|
||||
.put(format!("{base}/delete-me"))
|
||||
.body("temporary")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::CREATED);
|
||||
|
||||
// GET → 302
|
||||
let resp = c.get(format!("{base}/delete-me")).send().await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
|
||||
// DELETE → 204
|
||||
let resp = c.delete(format!("{base}/delete-me")).send().await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
|
||||
|
||||
// GET after delete → 404
|
||||
let resp = c.get(format!("{base}/delete-me")).send().await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_nonexistent_returns_404() {
|
||||
let base = start_server().await;
|
||||
let c = client();
|
||||
|
||||
let resp = c
|
||||
.delete(format!("{base}/never-existed"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_keys() {
|
||||
let base = start_server().await;
|
||||
let c = client();
|
||||
|
||||
// PUT a few keys with a common prefix
|
||||
for name in ["docs/a", "docs/b", "docs/c", "other/x"] {
|
||||
c.put(format!("{base}/{name}"))
|
||||
.body("data")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// List all
|
||||
let resp = c.get(format!("{base}/")).send().await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = resp.text().await.unwrap();
|
||||
assert!(body.contains("docs/a"));
|
||||
assert!(body.contains("other/x"));
|
||||
|
||||
// List with prefix filter
|
||||
let resp = c
|
||||
.get(format!("{base}/?prefix=docs/"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = resp.text().await.unwrap();
|
||||
let lines: Vec<&str> = body.lines().collect();
|
||||
assert_eq!(lines.len(), 3);
|
||||
assert!(!body.contains("other/x"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_put_overwrite() {
|
||||
let base = start_server().await;
|
||||
let c = client();
|
||||
|
||||
// PUT v1
|
||||
c.put(format!("{base}/overwrite"))
|
||||
.body("version1")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// PUT v2 (same key)
|
||||
let resp = c
|
||||
.put(format!("{base}/overwrite"))
|
||||
.body("version2")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::CREATED);
|
||||
|
||||
// HEAD should reflect new size
|
||||
let resp = c.head(format!("{base}/overwrite")).send().await.unwrap();
|
||||
assert_eq!(
|
||||
resp.headers()
|
||||
.get("content-length")
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
"8"
|
||||
);
|
||||
|
||||
// Follow redirect, verify content is v2
|
||||
let resp = c.get(format!("{base}/overwrite")).send().await.unwrap();
|
||||
let location = resp.headers().get("location").unwrap().to_str().unwrap();
|
||||
let body = reqwest::get(location).await.unwrap().text().await.unwrap();
|
||||
assert_eq!(body, "version2");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_replication_writes_to_multiple_volumes() {
|
||||
let base = start_server().await;
|
||||
let c = client();
|
||||
|
||||
// PUT a key (replication_factor=2 in test config)
|
||||
c.put(format!("{base}/replicated"))
|
||||
.body("replica-data")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// HEAD to confirm it exists
|
||||
let resp = c
|
||||
.head(format!("{base}/replicated"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
// GET and verify the blob is accessible
|
||||
let resp = c
|
||||
.get(format!("{base}/replicated"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
let location = resp.headers().get("location").unwrap().to_str().unwrap();
|
||||
let body = reqwest::get(location).await.unwrap().text().await.unwrap();
|
||||
assert_eq!(body, "replica-data");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue