use std::sync::Arc;
use axum::body::Body;
use axum::http::header::{self, HeaderMap, HeaderValue};
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use libfw_core::auth::{AuthError, PathValidator, TokenVerifier};
use libfw_core::claims::{Permission, TokenClaims};
use libfw_core::compress::{compressor, decompressor, CompressionFormat};
use libfw_core::metadata::encode_file_meta_header;
use libfw_server::{
router, FsStorage, ServerState, HEADER_COMPRESS, HEADER_FILE_META, HEADER_FINAL, HEADER_OFFSET,
HEADER_SESSION, HEADER_SESSION_STATUS,
};
use tower::ServiceExt;
#[derive(Clone)]
struct DevVerifier;
impl TokenVerifier for DevVerifier {
fn verify(&self, token: &str) -> Result<TokenClaims, AuthError> {
Ok(TokenClaims {
sub: token.to_string(),
exp: None,
permissions: vec![Permission::Read, Permission::Write],
allowed_paths: vec!["/".to_string()],
})
}
}
#[derive(Clone)]
struct RestrictedVerifier;
impl TokenVerifier for RestrictedVerifier {
fn verify(&self, token: &str) -> Result<TokenClaims, AuthError> {
Ok(TokenClaims {
sub: token.to_string(),
exp: None,
permissions: vec![Permission::Read],
allowed_paths: vec!["/public/".to_string()],
})
}
}
fn app(verifier: impl TokenVerifier) -> axum::Router {
let state = Arc::new(
ServerState::builder()
.storage(FsStorage::new(tempfile::tempdir().unwrap().path()))
.verifier(verifier)
.validator(PathValidator::new())
.build(),
);
router(state)
}
fn auth_header(token: &str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(
header::AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
);
headers
}
fn request(method: &str, uri: &str, headers: HeaderMap, body: Body) -> Request<Body> {
let mut builder = Request::builder().method(method).uri(uri);
for (name, value) in headers.iter() {
builder = builder.header(name, value);
}
builder.body(body).unwrap()
}
async fn body_string(response: axum::response::Response) -> String {
let bytes = response.into_body().collect().await.unwrap().to_bytes();
String::from_utf8(bytes.to_vec()).unwrap()
}
#[tokio::test]
async fn upload_then_download_roundtrip() {
let app = app(DevVerifier);
let data = b"hello libfw streaming transfer".repeat(100);
let meta = libfw_core::metadata::FileMeta::new("a/b.txt", data.len() as u64, 1_700_000_000);
let mut headers = auth_header("tok");
headers.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap());
let resp = app
.clone()
.oneshot(request("POST", "/file/a/b.txt", headers, Body::from(data.to_vec())))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::CREATED, "body: {}", body_string(resp).await);
let resp = app
.oneshot(request("GET", "/file/a/b.txt", auth_header("tok"), Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let got = body_string(resp).await.into_bytes();
assert_eq!(got, data);
}
#[tokio::test]
async fn session_upload_writes_chunks_out_of_order_then_commits() {
let app = app(DevVerifier);
let data = b"0123456789".to_vec();
let meta = libfw_core::metadata::FileMeta::new("sess.bin", data.len() as u64, 0);
let session = "test-sess-1";
async fn post_chunk(
app: axum::Router,
session: &str,
offset: u64,
chunk: &[u8],
final_chunk: bool,
meta: &libfw_core::metadata::FileMeta,
) -> StatusCode {
let mut headers = auth_header("tok");
headers
.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(meta)).unwrap());
headers.insert(HEADER_SESSION, HeaderValue::from_str(session).unwrap());
headers
.insert(HEADER_OFFSET, HeaderValue::from_str(&offset.to_string()).unwrap());
if final_chunk {
headers.insert(HEADER_FINAL, HeaderValue::from_static("1"));
}
let resp = app
.oneshot(request("POST", "/file/sess.bin", headers, Body::from(chunk.to_vec())))
.await
.unwrap();
resp.status()
}
assert_eq!(
post_chunk(app.clone(), session, 0, &data[0..4], false, &meta).await,
StatusCode::CREATED
);
assert_eq!(
post_chunk(app.clone(), session, 8, &data[8..10], false, &meta).await,
StatusCode::CREATED
);
assert_eq!(
post_chunk(app.clone(), session, 4, &data[4..8], false, &meta).await,
StatusCode::CREATED
);
assert_eq!(
post_chunk(app.clone(), session, data.len() as u64, &[], true, &meta).await,
StatusCode::CREATED
);
let resp = app
.oneshot(request("GET", "/file/sess.bin", auth_header("tok"), Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let got = body_string(resp).await.into_bytes();
assert_eq!(got, data);
}
#[tokio::test]
async fn session_upload_probe_reports_received_ranges_and_resumes() {
let app = app(DevVerifier);
let data = b"0123456789abcdefghij".to_vec(); let meta = libfw_core::metadata::FileMeta::new("resume.bin", data.len() as u64, 0);
let session = "resume-sess-1";
async fn post_chunk(
app: axum::Router,
session: &str,
offset: u64,
chunk: &[u8],
final_chunk: bool,
meta: &libfw_core::metadata::FileMeta,
) -> StatusCode {
let mut headers = auth_header("tok");
headers
.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(meta)).unwrap());
headers.insert(HEADER_SESSION, HeaderValue::from_str(session).unwrap());
headers
.insert(HEADER_OFFSET, HeaderValue::from_str(&offset.to_string()).unwrap());
if final_chunk {
headers.insert(HEADER_FINAL, HeaderValue::from_static("1"));
}
app.oneshot(request("POST", "/file/resume.bin", headers, Body::from(chunk.to_vec())))
.await
.unwrap()
.status()
}
async fn probe(
app: axum::Router,
session: &str,
meta: &libfw_core::metadata::FileMeta,
) -> Vec<[u64; 2]> {
let mut headers = auth_header("tok");
headers
.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(meta)).unwrap());
headers.insert(HEADER_SESSION, HeaderValue::from_str(session).unwrap());
headers.insert(HEADER_SESSION_STATUS, HeaderValue::from_static("1"));
let resp = app
.oneshot(request("POST", "/file/resume.bin", headers, Body::empty()))
.await
.unwrap();
let body = resp.into_body().collect().await.unwrap().to_bytes();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
v["ranges"]
.as_array()
.unwrap()
.iter()
.map(|r| {
let a = r.as_array().unwrap();
[a[0].as_u64().unwrap(), a[1].as_u64().unwrap()]
})
.collect()
}
assert!(probe(app.clone(), session, &meta).await.is_empty());
assert_eq!(
post_chunk(app.clone(), session, 0, &data[0..8], false, &meta).await,
StatusCode::CREATED
);
assert_eq!(
post_chunk(app.clone(), session, 16, &data[16..20], false, &meta).await,
StatusCode::CREATED
);
let ranges = probe(app.clone(), session, &meta).await;
assert_eq!(ranges, vec![[0, 8], [16, 20]]);
assert_eq!(
post_chunk(app.clone(), session, 8, &data[8..16], false, &meta).await,
StatusCode::CREATED
);
assert_eq!(
post_chunk(app.clone(), session, data.len() as u64, &[], true, &meta).await,
StatusCode::CREATED
);
let resp = app
.oneshot(request("GET", "/file/resume.bin", auth_header("tok"), Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let got = body_string(resp).await.into_bytes();
assert_eq!(got, data);
}
#[tokio::test]
async fn session_upload_rejects_commit_with_wrong_size() {
let app = app(DevVerifier);
let data = b"0123456789".to_vec();
let meta = libfw_core::metadata::FileMeta::new("sess2.bin", 10, 0);
let session = "test-sess-2";
let mut headers = auth_header("tok");
headers
.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap());
headers.insert(HEADER_SESSION, HeaderValue::from_str(session).unwrap());
headers.insert(HEADER_OFFSET, HeaderValue::from_static("0"));
let resp = app
.clone()
.oneshot(request("POST", "/file/sess2.bin", headers, Body::from(data[0..4].to_vec())))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::CREATED);
let mut headers = auth_header("tok");
headers
.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap());
headers.insert(HEADER_SESSION, HeaderValue::from_str(session).unwrap());
headers.insert(HEADER_OFFSET, HeaderValue::from_static("10"));
headers.insert(HEADER_FINAL, HeaderValue::from_static("1"));
let resp = app
.clone()
.oneshot(request("POST", "/file/sess2.bin", headers, Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp = app
.oneshot(request("GET", "/file/sess2.bin", auth_header("tok"), Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn session_probe_overwrites_existing_target() {
let app = app(DevVerifier);
let data = b"0123456789".to_vec();
let meta = libfw_core::metadata::FileMeta::new("sess3.bin", data.len() as u64, 0);
let session = "probe-overwrite-sess";
let mut headers = auth_header("tok");
headers
.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap());
headers.insert(HEADER_SESSION, HeaderValue::from_str(session).unwrap());
headers.insert(HEADER_OFFSET, HeaderValue::from_static("0"));
headers.insert(HEADER_FINAL, HeaderValue::from_static("1"));
let resp = app
.clone()
.oneshot(request("POST", "/file/sess3.bin", headers, Body::from(data.clone())))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::CREATED);
let mut headers = auth_header("tok");
headers
.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap());
headers.insert(HEADER_SESSION, HeaderValue::from_str("probe-overwrite-sess-2").unwrap());
headers.insert(HEADER_SESSION_STATUS, HeaderValue::from_static("1"));
headers.insert(HEADER_OFFSET, HeaderValue::from_static("0"));
let resp = app
.oneshot(request("POST", "/file/sess3.bin", headers, Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = resp.into_body().collect().await.unwrap().to_bytes();
assert_eq!(String::from_utf8_lossy(&body), r#"{"ranges":[]}"#);
}
#[tokio::test]
async fn range_requests_return_206() {
let app = app(DevVerifier);
let data = b"0123456789".to_vec();
let meta = libfw_core::metadata::FileMeta::new("r.bin", 10, 0);
let mut headers = auth_header("tok");
headers.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap());
app.clone()
.oneshot(request("POST", "/file/r.bin", headers, Body::from(data.clone())))
.await
.unwrap();
let mut headers = auth_header("tok");
headers.insert(header::RANGE, HeaderValue::from_static("bytes=2-5"));
let resp = app
.oneshot(request("GET", "/file/r.bin", headers, Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::PARTIAL_CONTENT);
assert_eq!(resp.headers().get(header::CONTENT_RANGE).unwrap(), "bytes 2-5/10");
assert_eq!(resp.headers().get(header::ACCEPT_RANGES).unwrap(), "bytes");
let got = body_string(resp).await.into_bytes();
assert_eq!(got, b"2345");
}
#[tokio::test]
async fn head_returns_etag_and_content_length() {
let app = app(DevVerifier);
let data = b"head metadata payload".to_vec();
let meta = libfw_core::metadata::FileMeta::new("h.bin", data.len() as u64, 0);
let mut headers = auth_header("tok");
headers
.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap());
app.clone()
.oneshot(request("POST", "/file/h.bin", headers, Body::from(data.clone())))
.await
.unwrap();
let resp = app
.clone()
.oneshot(request("HEAD", "/file/h.bin", auth_header("tok"), Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::CONTENT_LENGTH).unwrap(),
data.len().to_string().as_str()
);
assert!(resp.headers().get(header::ETAG).is_some());
assert_eq!(resp.headers().get(header::ACCEPT_RANGES).unwrap(), "bytes");
let body = resp.into_body().collect().await.unwrap().to_bytes();
assert!(body.is_empty());
let resp = app
.oneshot(request("HEAD", "/file/missing.bin", auth_header("tok"), Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn if_none_match_returns_304() {
let app = app(DevVerifier);
let meta = libfw_core::metadata::FileMeta::new("e.txt", 3, 0);
let mut headers = auth_header("tok");
headers.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap());
let upload = app
.clone()
.oneshot(request("POST", "/file/e.txt", headers, Body::from(b"abc".to_vec())))
.await
.unwrap();
assert_eq!(upload.status(), StatusCode::CREATED);
let upload_json: serde_json::Value =
serde_json::from_str(&body_string(upload).await).expect("upload response is JSON");
let etag = upload_json["file"]["etag"].as_str().expect("etag present").to_string();
let mut headers = auth_header("tok");
headers.insert(header::IF_NONE_MATCH, HeaderValue::from_str(&etag).unwrap());
let resp = app
.oneshot(request("GET", "/file/e.txt", headers, Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
}
#[tokio::test]
async fn missing_token_is_401() {
let app = app(DevVerifier);
let resp = app
.oneshot(request("GET", "/file/x.txt", HeaderMap::new(), Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn restricted_path_is_403() {
let app = app(RestrictedVerifier);
let resp = app
.clone()
.oneshot(request("GET", "/file/private/secret.txt", auth_header("tok"), Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
let resp = app
.oneshot(request("POST", "/file/public/x.txt", auth_header("tok"), Body::from(b"x".to_vec())))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn upload_then_list_dir() {
let app = app(DevVerifier);
let meta = libfw_core::metadata::FileMeta::new("sub/f.txt", 2, 0);
let mut headers = auth_header("tok");
headers.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap());
app.clone()
.oneshot(request("POST", "/file/sub/f.txt", headers, Body::from(b"hi".to_vec())))
.await
.unwrap();
let resp = app
.oneshot(request("GET", "/dir/sub", auth_header("tok"), Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = body_string(resp).await;
assert!(body.contains("f.txt"), "listing: {body}");
}
#[tokio::test]
async fn compressed_upload_and_download_roundtrip() {
let app = app(DevVerifier);
let data: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect();
let mut enc = compressor(CompressionFormat::Zrip).unwrap();
let mut payload = Vec::new();
enc.compress(&data, &mut payload).unwrap();
enc.finish(&mut payload).unwrap();
let meta = libfw_core::metadata::FileMeta::new("c.bin", data.len() as u64, 0);
let mut headers = auth_header("tok");
headers.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap());
headers.insert(HEADER_COMPRESS, HeaderValue::from_static("zrip"));
let resp = app
.clone()
.oneshot(request("POST", "/file/c.bin", headers, Body::from(payload)))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::CREATED);
let mut headers = auth_header("tok");
headers.insert(header::ACCEPT_ENCODING, HeaderValue::from_static("zrip"));
let resp = app
.oneshot(request("GET", "/file/c.bin", headers, Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.headers().get(HEADER_COMPRESS).unwrap(), "zrip");
let compressed = resp.into_body().collect().await.unwrap().to_bytes();
let mut dec = decompressor(CompressionFormat::Zrip);
let mut plain = Vec::new();
dec.decompress(&compressed, &mut plain).unwrap();
dec.finish(&mut plain).unwrap();
assert_eq!(plain, data);
}
#[tokio::test]
async fn browser_zstd_accept_encoding_does_not_trigger_zrip() {
let app = app(DevVerifier);
let data = b"plain text body".to_vec();
let meta = libfw_core::metadata::FileMeta::new("z.bin", data.len() as u64, 0);
let mut headers = auth_header("tok");
headers.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap());
app.clone()
.oneshot(request("POST", "/file/z.bin", headers, Body::from(data.clone())))
.await
.unwrap();
let mut headers = auth_header("tok");
headers.insert(
header::ACCEPT_ENCODING,
HeaderValue::from_static("gzip, deflate, br, zstd"),
);
let resp = app
.oneshot(request("GET", "/file/z.bin", headers, Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.headers().get(HEADER_COMPRESS).unwrap(), "identity");
let got = body_string(resp).await.into_bytes();
assert_eq!(got, data);
}
#[tokio::test]
async fn resume_upload_at_offset() {
let app = app(DevVerifier);
let meta = libfw_core::metadata::FileMeta::new("res.txt", 6, 0);
let mut headers = auth_header("tok");
headers.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap());
headers.insert(HEADER_OFFSET, HeaderValue::from_static("0"));
app.clone()
.oneshot(request("POST", "/file/res.txt", headers.clone(), Body::from(b"ABCD".to_vec())))
.await
.unwrap();
headers.insert(HEADER_OFFSET, HeaderValue::from_static("4"));
let resp = app
.clone()
.oneshot(request("POST", "/file/res.txt", headers, Body::from(b"EF".to_vec())))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::CREATED);
let resp = app
.oneshot(request("GET", "/file/res.txt", auth_header("tok"), Body::empty()))
.await
.unwrap();
assert_eq!(body_string(resp).await, "ABCDEF");
}
#[tokio::test]
async fn final_chunk_mismatched_size_is_rejected() {
let app = app(DevVerifier);
let meta = libfw_core::metadata::FileMeta::new("final.txt", 4, 0);
let mut headers = auth_header("tok");
headers.insert(
HEADER_FILE_META,
HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap(),
);
headers.insert(HEADER_OFFSET, HeaderValue::from_static("0"));
headers.insert(HEADER_FINAL, HeaderValue::from_static("1"));
let resp = app
.clone()
.oneshot(request(
"POST",
"/file/final.txt",
headers,
Body::from(b"abc".to_vec()),
))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp = app
.oneshot(request("GET", "/file/final.txt", auth_header("tok"), Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn final_chunk_matching_size_is_accepted() {
let app = app(DevVerifier);
let meta = libfw_core::metadata::FileMeta::new("ok.txt", 4, 0);
let mut headers = auth_header("tok");
headers.insert(
HEADER_FILE_META,
HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap(),
);
headers.insert(HEADER_OFFSET, HeaderValue::from_static("0"));
headers.insert(HEADER_FINAL, HeaderValue::from_static("1"));
let resp = app
.clone()
.oneshot(request("POST", "/file/ok.txt", headers, Body::from(b"abcd".to_vec())))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::CREATED);
let resp = app
.oneshot(request("GET", "/file/ok.txt", auth_header("tok"), Body::empty()))
.await
.unwrap();
assert_eq!(body_string(resp).await, "abcd");
}
#[tokio::test]
async fn bad_resume_offset_is_412() {
let app = app(DevVerifier);
let meta = libfw_core::metadata::FileMeta::new("bad.txt", 4, 0);
let mut headers = auth_header("tok");
headers.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap());
app.clone()
.oneshot(request("POST", "/file/bad.txt", headers.clone(), Body::from(b"ABCD".to_vec())))
.await
.unwrap();
headers.insert(HEADER_OFFSET, HeaderValue::from_static("9"));
let resp = app
.oneshot(request("POST", "/file/bad.txt", headers, Body::from(b"X".to_vec())))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED);
}
#[tokio::test]
async fn unsatisfiable_range_is_416() {
let app = app(DevVerifier);
let meta = libfw_core::metadata::FileMeta::new("s.bin", 5, 0);
let mut headers = auth_header("tok");
headers.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap());
app.clone()
.oneshot(request("POST", "/file/s.bin", headers, Body::from(b"12345".to_vec())))
.await
.unwrap();
let mut headers = auth_header("tok");
headers.insert(header::RANGE, HeaderValue::from_static("bytes=100-"));
let resp = app
.oneshot(request("GET", "/file/s.bin", headers, Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::RANGE_NOT_SATISFIABLE);
assert_eq!(resp.headers().get(header::CONTENT_RANGE).unwrap(), "bytes */5");
}
#[tokio::test]
async fn path_traversal_is_rejected() {
let app = app(DevVerifier);
let resp = app
.oneshot(request("GET", "/file/../secret", auth_header("tok"), Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn duplicate_upload_without_offset_conflicts() {
let app = app(DevVerifier);
let meta = libfw_core::metadata::FileMeta::new("dup.txt", 1, 0);
let mut headers = auth_header("tok");
headers.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap());
app.clone()
.oneshot(request("POST", "/file/dup.txt", headers.clone(), Body::from(b"a".to_vec())))
.await
.unwrap();
let resp = app
.oneshot(request("POST", "/file/dup.txt", headers, Body::from(b"b".to_vec())))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::CONFLICT);
}
#[tokio::test]
async fn upload_exceeding_declared_size_is_rejected_and_not_committed() {
let app = app(DevVerifier);
let meta = libfw_core::metadata::FileMeta::new("oversize.txt", 2, 0);
let mut headers = auth_header("tok");
headers.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap());
let resp = app
.clone()
.oneshot(request("POST", "/file/oversize.txt", headers, Body::from(b"12345".to_vec())))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp = app
.oneshot(request("GET", "/file/oversize.txt", auth_header("tok"), Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn wrong_protocol_version_is_426() {
let app = app(DevVerifier);
let mut headers = auth_header("tok");
headers.insert(
libfw_core::HEADER_PROTOCOL,
HeaderValue::from_static("libfw/999"),
);
let resp = app
.oneshot(request("GET", "/file/x.txt", headers, Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UPGRADE_REQUIRED);
}
#[tokio::test]
async fn matching_protocol_version_is_accepted() {
let app = app(DevVerifier);
let mut headers = auth_header("tok");
headers.insert(
libfw_core::HEADER_PROTOCOL,
HeaderValue::from_static("libfw/1"),
);
let resp = app
.oneshot(request("GET", "/file/missing.txt", headers, Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn absent_protocol_header_is_tolerated() {
let app = app(DevVerifier);
let resp = app
.oneshot(request("GET", "/file/missing.txt", auth_header("tok"), Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn root_listing_is_reachable_via_dir() {
let app = app(DevVerifier);
let meta = libfw_core::metadata::FileMeta::new("root.txt", 2, 0);
let mut headers = auth_header("tok");
headers.insert(HEADER_FILE_META, HeaderValue::from_str(&encode_file_meta_header(&meta)).unwrap());
app.clone()
.oneshot(request("POST", "/file/root.txt", headers, Body::from(b"hi".to_vec())))
.await
.unwrap();
let resp = app
.oneshot(request("GET", "/dir", auth_header("tok"), Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = body_string(resp).await;
assert!(body.contains("root.txt"), "root listing: {body}");
}
#[tokio::test]
async fn listing_missing_dir_is_404_not_500() {
let app = app(DevVerifier);
let resp = app
.oneshot(request("GET", "/dir/does-not-exist", auth_header("tok"), Body::empty()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}