//! WebDAV 백엔드 통합 테스트 — 자체 완결형(in-process) 최소 WebDAV 서버 대상. //! PROPFIND(Depth 1) + Range GET을 검증한다 (브라우즈 + 원격 재생 경로). use archive_vfs::webdav::{WebDavConfig, WebDavFs}; use archive_vfs::VfsProvider; use std::convert::Infallible; use std::net::SocketAddr; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpListener; /// 아주 작은 WebDAV 서버: 고정 디렉터리 하나(/dav)에 파일 2개. /// PROPFIND → multistatus XML, GET → Range 지원. async fn run_server() -> SocketAddr { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); tokio::spawn(async move { loop { let (mut sock, _) = listener.accept().await.unwrap(); tokio::spawn(async move { let _ = handle(&mut sock).await; }); } }); addr } const FILE_A: &[u8] = b"0123456789ABCDEF"; // 16바이트 const FILE_B: &[u8] = b"hello webdav world"; // 18바이트 async fn handle(sock: &mut tokio::net::TcpStream) -> Result<(), Infallible> { let mut buf = vec![0u8; 8192]; let n = sock.read(&mut buf).await.unwrap_or(0); if n == 0 { return Ok(()); } let req = String::from_utf8_lossy(&buf[..n]); let method = req.split_whitespace().next().unwrap_or(""); let path = req.split_whitespace().nth(1).unwrap_or("/"); let response: Vec = match method { "PROPFIND" => { let xml = format!( r#" /dav/ Wed, 01 Jan 2025 00:00:00 GMT HTTP/1.1 200 OK /dav/a.jpg {} Wed, 01 Jan 2025 00:00:00 GMT HTTP/1.1 200 OK /dav/b.mp4 {} Wed, 01 Jan 2025 00:00:00 GMT HTTP/1.1 200 OK "#, FILE_A.len(), FILE_B.len() ); http_response("207 Multi-Status", "application/xml", xml.as_bytes(), None) } "GET" => { let body = if path.contains("a.jpg") { FILE_A } else { FILE_B }; // Range 헤더 파싱 if let Some(range_line) = req.lines().find(|l| l.to_ascii_lowercase().starts_with("range:")) { let spec = range_line.split("bytes=").nth(1).unwrap_or("").trim(); let (s, e) = spec.split_once('-').unwrap_or(("0", "")); let start: usize = s.trim().parse().unwrap_or(0); let end: usize = e.trim().parse().unwrap_or(body.len() - 1); let slice = &body[start..=end.min(body.len() - 1)]; let cr = format!("bytes {}-{}/{}", start, end.min(body.len() - 1), body.len()); http_response("206 Partial Content", "application/octet-stream", slice, Some(&cr)) } else { http_response("200 OK", "application/octet-stream", body, None) } } _ => http_response("405 Method Not Allowed", "text/plain", b"", None), }; let _ = sock.write_all(&response).await; let _ = sock.flush().await; Ok(()) } fn http_response(status: &str, ctype: &str, body: &[u8], content_range: Option<&str>) -> Vec { let mut head = format!( "HTTP/1.1 {status}\r\nContent-Type: {ctype}\r\nContent-Length: {}\r\nAccept-Ranges: bytes\r\n", body.len() ); if let Some(cr) = content_range { head.push_str(&format!("Content-Range: {cr}\r\n")); } head.push_str("Connection: close\r\n\r\n"); let mut out = head.into_bytes(); out.extend_from_slice(body); out } #[tokio::test] async fn webdav_list_and_range_read() { let addr = run_server().await; let fs = WebDavFs::connect(&WebDavConfig { url: format!("http://{addr}/dav"), username: "u".into(), password: "p".into(), base_path: String::new(), }) .await .unwrap(); // list_dir → 파일 2개 (디렉터리 자신 제외) let entries = fs.list_dir("").await.unwrap(); let files: Vec<_> = entries.iter().filter(|e| !e.is_dir).collect(); assert_eq!(files.len(), 2, "파일 2개여야 함: {entries:?}"); let a = files.iter().find(|e| e.name == "a.jpg").expect("a.jpg 없음"); assert_eq!(a.size, FILE_A.len() as u64); // 전체 읽기 let mut r = fs.open_range("a.jpg", None).await.unwrap(); let mut buf = Vec::new(); r.read_to_end(&mut buf).await.unwrap(); assert_eq!(buf, FILE_A); // Range 읽기 (재생 시킹 시나리오) let mut r = fs.open_range("a.jpg", Some(4..10)).await.unwrap(); let mut buf = Vec::new(); r.read_to_end(&mut buf).await.unwrap(); assert_eq!(buf, &FILE_A[4..10]); }