mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-12 06:50:31 +00:00
quality: integration tests, 466 tests total, 43% coverage (#87)
This commit is contained in:
@@ -250,4 +250,70 @@ mod tests {
|
||||
.await;
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_www_authenticate_bearer_only() {
|
||||
let params = parse_www_authenticate("Bearer ").unwrap();
|
||||
assert!(params.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_www_authenticate_missing_realm() {
|
||||
let header = r#"Bearer service="registry.docker.io""#;
|
||||
let params = parse_www_authenticate(header).unwrap();
|
||||
assert!(params.get("realm").is_none());
|
||||
assert_eq!(
|
||||
params.get("service"),
|
||||
Some(&"registry.docker.io".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_www_authenticate_missing_service() {
|
||||
let header = r#"Bearer realm="https://auth.docker.io/token""#;
|
||||
let params = parse_www_authenticate(header).unwrap();
|
||||
assert_eq!(
|
||||
params.get("realm"),
|
||||
Some(&"https://auth.docker.io/token".to_string())
|
||||
);
|
||||
assert!(params.get("service").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_www_authenticate_malformed_kv() {
|
||||
let header = r#"Bearer garbage,realm="https://auth.docker.io/token""#;
|
||||
let params = parse_www_authenticate(header).unwrap();
|
||||
assert_eq!(
|
||||
params.get("realm"),
|
||||
Some(&"https://auth.docker.io/token".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_token_invalid_url() {
|
||||
let auth = DockerAuth::new(1);
|
||||
let result = auth
|
||||
.get_token(
|
||||
"https://registry.example.com",
|
||||
"library/test",
|
||||
Some(r#"Bearer realm="http://127.0.0.1:1/token",service="test""#),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_token_missing_realm_in_header() {
|
||||
let auth = DockerAuth::default();
|
||||
let result = auth
|
||||
.get_token(
|
||||
"https://registry.example.com",
|
||||
"library/test",
|
||||
Some(r#"Bearer service="registry.docker.io""#),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,6 +232,7 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod integration_tests {
|
||||
use crate::storage::{Storage, StorageError};
|
||||
use crate::test_helpers::{
|
||||
body_bytes, create_test_context, create_test_context_with_raw_disabled, send,
|
||||
};
|
||||
@@ -312,4 +313,19 @@ mod integration_tests {
|
||||
let put = send(&ctx.app, Method::PUT, "/raw/test.txt", b"data".to_vec()).await;
|
||||
assert_eq!(put.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_upload_path_traversal_rejected() {
|
||||
let temp_dir = tempfile::TempDir::new().unwrap();
|
||||
let storage = Storage::new_local(temp_dir.path().to_str().unwrap());
|
||||
|
||||
let result = storage.put("raw/../../../etc/passwd", b"pwned").await;
|
||||
assert!(result.is_err(), "path traversal key must be rejected");
|
||||
match result {
|
||||
Err(StorageError::Validation(v)) => {
|
||||
assert_eq!(format!("{}", v), "Path traversal detected");
|
||||
}
|
||||
other => panic!("expected Validation(PathTraversal), got {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,4 +251,115 @@ mod tests {
|
||||
let storage = LocalStorage::new(temp_dir.path().to_str().unwrap());
|
||||
assert_eq!(storage.backend_name(), "local");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_concurrent_writes_same_key() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let storage = std::sync::Arc::new(LocalStorage::new(temp_dir.path().to_str().unwrap()));
|
||||
|
||||
let mut handles = Vec::new();
|
||||
for i in 0..10u8 {
|
||||
let s = storage.clone();
|
||||
handles.push(tokio::spawn(async move {
|
||||
let data = vec![i; 1024];
|
||||
s.put("shared/key", &data).await
|
||||
}));
|
||||
}
|
||||
|
||||
for h in handles {
|
||||
h.await.expect("task panicked").expect("put failed");
|
||||
}
|
||||
|
||||
let data = storage.get("shared/key").await.expect("get failed");
|
||||
assert_eq!(data.len(), 1024);
|
||||
let first = data[0];
|
||||
assert!(
|
||||
data.iter().all(|&b| b == first),
|
||||
"file is corrupted — mixed writers"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_concurrent_writes_different_keys() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let storage = std::sync::Arc::new(LocalStorage::new(temp_dir.path().to_str().unwrap()));
|
||||
|
||||
let mut handles = Vec::new();
|
||||
for i in 0..10u32 {
|
||||
let s = storage.clone();
|
||||
handles.push(tokio::spawn(async move {
|
||||
let key = format!("key/{}", i);
|
||||
s.put(&key, format!("data-{}", i).as_bytes()).await
|
||||
}));
|
||||
}
|
||||
|
||||
for h in handles {
|
||||
h.await.expect("task panicked").expect("put failed");
|
||||
}
|
||||
|
||||
for i in 0..10u32 {
|
||||
let key = format!("key/{}", i);
|
||||
let data = storage.get(&key).await.expect("get failed");
|
||||
assert_eq!(&*data, format!("data-{}", i).as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_concurrent_read_during_write() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let storage = std::sync::Arc::new(LocalStorage::new(temp_dir.path().to_str().unwrap()));
|
||||
|
||||
let old_data = vec![0u8; 4096];
|
||||
storage.put("rw/key", &old_data).await.expect("seed put");
|
||||
|
||||
let new_data = vec![1u8; 4096];
|
||||
let sw = storage.clone();
|
||||
let writer = tokio::spawn(async move {
|
||||
sw.put("rw/key", &new_data).await.expect("put failed");
|
||||
});
|
||||
|
||||
let sr = storage.clone();
|
||||
let reader = tokio::spawn(async move {
|
||||
match sr.get("rw/key").await {
|
||||
Ok(_data) => {
|
||||
// tokio::fs::write is not atomic, so partial reads
|
||||
// (mix of old and new bytes) are expected — not a bug.
|
||||
// We only verify the final state after both tasks complete.
|
||||
}
|
||||
Err(crate::storage::StorageError::NotFound) => {}
|
||||
Err(e) => panic!("unexpected error: {}", e),
|
||||
}
|
||||
});
|
||||
|
||||
writer.await.expect("writer panicked");
|
||||
reader.await.expect("reader panicked");
|
||||
|
||||
let data = storage.get("rw/key").await.expect("final get");
|
||||
assert_eq!(&*data, &vec![1u8; 4096]);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_concurrent_deletes_same_key() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let storage = std::sync::Arc::new(LocalStorage::new(temp_dir.path().to_str().unwrap()));
|
||||
|
||||
storage.put("del/key", b"ephemeral").await.expect("put");
|
||||
|
||||
let mut handles = Vec::new();
|
||||
for _ in 0..10 {
|
||||
let s = storage.clone();
|
||||
handles.push(tokio::spawn(async move {
|
||||
let _ = s.delete("del/key").await;
|
||||
}));
|
||||
}
|
||||
|
||||
for h in handles {
|
||||
h.await.expect("task panicked");
|
||||
}
|
||||
|
||||
assert!(matches!(
|
||||
storage.get("del/key").await,
|
||||
Err(crate::storage::StorageError::NotFound)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user