diff --git a/Cargo.lock b/Cargo.lock index d371539..dfb1ebf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2611,6 +2611,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "quick-xml" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quinn" version = "0.11.6" @@ -3583,6 +3593,7 @@ name = "testutils" version = "0.0.0" dependencies = [ "itertools", + "quick-xml", "serde", "serde_json", "soupy", diff --git a/crates/testutils/Cargo.toml b/crates/testutils/Cargo.toml index 4843b8b..e9751c4 100644 --- a/crates/testutils/Cargo.toml +++ b/crates/testutils/Cargo.toml @@ -11,6 +11,7 @@ publish = false [dependencies] itertools = "0.14.0" +quick-xml = { version = "0.37.2", features = ["serialize"] } serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.135" soupy = { version = "0.8.3", default-features = false, features = ["html"] } diff --git a/crates/testutils/src/lib.rs b/crates/testutils/src/lib.rs index 78116a0..23a1a65 100644 --- a/crates/testutils/src/lib.rs +++ b/crates/testutils/src/lib.rs @@ -1,4 +1,6 @@ mod mockarchive; mod parsehtml; +mod parsexml; pub use crate::mockarchive::*; pub use crate::parsehtml::*; +pub use crate::parsexml::*; diff --git a/crates/testutils/src/parsexml.rs b/crates/testutils/src/parsexml.rs new file mode 100644 index 0000000..70cd686 --- /dev/null +++ b/crates/testutils/src/parsexml.rs @@ -0,0 +1,481 @@ +// Only honors tags defined in RFC 4918 +use serde::Deserialize; +use thiserror::Error; + +pub fn parse_propfind_response(xml: &str) -> Result, PropfindError> { + quick_xml::de::from_str::(xml)? + .response + .into_iter() + .map(Resource::try_from) + .collect::, _>>() + .map_err(Into::into) +} + +pub fn parse_propname_response(xml: &str) -> Result, PropfindError> { + quick_xml::de::from_str::(xml)? + .response + .into_iter() + .map(ResourceProps::try_from) + .collect::, _>>() + .map_err(Into::into) +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Resource { + pub href: String, + pub creation_date: Trinary, + pub display_name: Trinary, + pub content_length: Trinary, + pub content_type: Trinary, + pub last_modified: Trinary, + pub etag: Trinary, + pub language: Trinary, + pub is_collection: Option, +} + +impl TryFrom for Resource { + type Error = ResourceError; + + fn try_from(value: Response) -> Result { + let mut r = Resource { + href: value.href, + creation_date: Trinary::Void, + display_name: Trinary::Void, + content_length: Trinary::Void, + content_type: Trinary::Void, + last_modified: Trinary::Void, + etag: Trinary::Void, + language: Trinary::Void, + is_collection: None, + }; + let mut seen_200 = false; + let mut seen_404 = false; + for ps in value.propstat { + if ps.status == "HTTP/1.1 200 OK" { + if std::mem::replace(&mut seen_200, true) { + return Err(ResourceError::Multiple200(r.href)); + } + if let Some(s) = ps.prop.creationdate { + r.creation_date = Trinary::Set(s); + } + if let Some(s) = ps.prop.displayname { + r.display_name = Trinary::Set(s); + } + if let Some(text) = ps.prop.getcontentlength { + match text.parse::() { + Ok(i) => r.content_length = Trinary::Set(i), + Err(_) => return Err(ResourceError::BadLength { href: r.href, text }), + } + } + if let Some(s) = ps.prop.getcontenttype { + r.content_type = Trinary::Set(s); + } + if let Some(s) = ps.prop.getlastmodified { + r.last_modified = Trinary::Set(s); + } + if let Some(s) = ps.prop.getetag { + r.etag = Trinary::Set(s); + } + if let Some(s) = ps.prop.getcontentlanguage { + r.language = Trinary::Set(s); + } + if let Some(rt) = ps.prop.resourcetype { + r.is_collection = Some(rt.collection.is_some()); + } + } else if ps.status == "HTTP/1.1 404 NOT FOUND" { + if std::mem::replace(&mut seen_404, true) { + return Err(ResourceError::Multiple404(r.href)); + } + if ps.prop.creationdate.is_some() { + r.creation_date = Trinary::NotFound; + } + if ps.prop.displayname.is_some() { + r.display_name = Trinary::NotFound; + } + if ps.prop.getcontentlength.is_some() { + r.content_length = Trinary::NotFound; + } + if ps.prop.getcontenttype.is_some() { + r.content_type = Trinary::NotFound; + } + if ps.prop.getlastmodified.is_some() { + r.last_modified = Trinary::NotFound; + } + if ps.prop.getetag.is_some() { + r.etag = Trinary::NotFound; + } + if ps.prop.getcontentlanguage.is_some() { + r.language = Trinary::NotFound; + } + if ps.prop.resourcetype.is_some() { + return Err(ResourceError::ResourceType404(r.href)); + } + } else { + return Err(ResourceError::BadStatus { + href: r.href, + status: ps.status, + }); + } + } + Ok(r) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ResourceProps { + pub href: String, + pub creation_date: bool, + pub display_name: bool, + pub content_length: bool, + pub content_type: bool, + pub last_modified: bool, + pub etag: bool, + pub language: bool, + pub resource_type: bool, +} + +impl TryFrom for ResourceProps { + type Error = ResourceError; + + fn try_from(value: Response) -> Result { + let mut r = ResourceProps { + href: value.href, + creation_date: false, + display_name: false, + content_length: false, + content_type: false, + last_modified: false, + etag: false, + language: false, + resource_type: false, + }; + let mut seen_200 = false; + for ps in value.propstat { + if ps.status == "HTTP/1.1 200 OK" { + if std::mem::replace(&mut seen_200, true) { + return Err(ResourceError::Multiple200(r.href)); + } + if ps.prop.creationdate.is_some() { + r.creation_date = true; + } + if ps.prop.displayname.is_some() { + r.display_name = true; + } + if ps.prop.getcontentlength.is_some() { + r.content_length = true; + } + if ps.prop.getcontenttype.is_some() { + r.content_type = true; + } + if ps.prop.getlastmodified.is_some() { + r.last_modified = true; + } + if ps.prop.getetag.is_some() { + r.etag = true; + } + if ps.prop.getcontentlanguage.is_some() { + r.language = true; + } + if ps.prop.resourcetype.is_some() { + r.resource_type = true; + } + } else { + return Err(ResourceError::BadStatus { + href: r.href, + status: ps.status, + }); + } + } + Ok(r) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Trinary { + Set(T), + NotFound, + Void, +} + +#[derive(Clone, Debug, Error)] +pub enum PropfindError { + #[error("failed to deserialize XML")] + Deserialize(#[from] quick_xml::errors::serialize::DeError), + #[error(transparent)] + Resource(#[from] ResourceError), +} + +#[derive(Clone, Debug, Eq, Error, PartialEq)] +pub enum ResourceError { + #[error("response for {0:?} contains multiple propstats with status 200")] + Multiple200(String), + #[error("response for {0:?} contains multiple propstats with status 404")] + Multiple404(String), + #[error("response for {href:?} contains propstat with unrecognized status {status:?}")] + BadStatus { href: String, status: String }, + #[error("response for {0:?} lists resourcetype as undefined property")] + ResourceType404(String), + #[error("response for {href:?} contains unparseable getcontentlength: {text:?}")] + BadLength { href: String, text: String }, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +struct Multistatus { + response: Vec, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +struct Response { + href: String, + propstat: Vec, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +struct Propstat { + prop: Prop, + status: String, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +struct Prop { + creationdate: Option, + displayname: Option, + getcontentlanguage: Option, + // We can't use Option here, as that won't work with empty + // tags + getcontentlength: Option, + getcontenttype: Option, + getlastmodified: Option, + getetag: Option, + resourcetype: Option, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +struct ResourceType { + collection: Option<()>, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_propfind_response() { + let xml = include_str!("testdata/propfind.xml"); + let resources = parse_propfind_response(xml).unwrap(); + assert_eq!( + resources, + vec![ + Resource { + href: "/dandisets/000108/draft/".into(), + creation_date: Trinary::Set("2021-06-01T14:35:34.214567Z".into()), + display_name: Trinary::Set("draft".into()), + content_length: Trinary::Set(374730983049354), + last_modified: Trinary::Set("Fri, 03 Nov 2023 11:01:20 GMT".into()), + is_collection: Some(true), + content_type: Trinary::NotFound, + etag: Trinary::NotFound, + language: Trinary::Void, + }, + Resource { + href: "/dandisets/000108/draft/dataset_description.json".into(), + creation_date: Trinary::Set("2021-07-03T04:23:52.38146Z".into()), + display_name: Trinary::Set("dataset_description.json".into()), + content_length: Trinary::Set(71), + content_type: Trinary::Set("application/json".into()), + etag: Trinary::Set("f4a034fbf965f76828fa027c29860bc0-1".into()), + last_modified: Trinary::Set("Wed, 13 Jul 2022 21:40:28 GMT".into()), + is_collection: Some(false), + language: Trinary::Void, + }, + Resource { + href: "/dandisets/000108/draft/samples.tsv".into(), + creation_date: Trinary::Set("2021-07-21T23:39:29.733695Z".into()), + display_name: Trinary::Set("samples.tsv".into()), + content_length: Trinary::Set(572), + content_type: Trinary::Set("text/tab-separated-values".into()), + etag: Trinary::Set("a6ac1fb127e17b2e3360c64154f69a57-1".into()), + last_modified: Trinary::Set("Wed, 13 Jul 2022 21:41:07 GMT".into()), + is_collection: Some(false), + language: Trinary::Void, + }, + Resource { + href: "/dandisets/000108/draft/sub-mEhm/".into(), + display_name: Trinary::Set("sub-mEhm".into()), + is_collection: Some(true), + creation_date: Trinary::NotFound, + content_length: Trinary::NotFound, + content_type: Trinary::NotFound, + etag: Trinary::NotFound, + last_modified: Trinary::NotFound, + language: Trinary::Void, + }, + Resource { + href: "/dandisets/000108/draft/sub-MITU01/".into(), + display_name: Trinary::Set("sub-MITU01".into()), + is_collection: Some(true), + creation_date: Trinary::NotFound, + content_length: Trinary::NotFound, + content_type: Trinary::NotFound, + etag: Trinary::NotFound, + last_modified: Trinary::NotFound, + language: Trinary::Void, + }, + Resource { + href: "/dandisets/000108/draft/sub-MITU01h3/".into(), + display_name: Trinary::Set("sub-MITU01h3".into()), + is_collection: Some(true), + creation_date: Trinary::NotFound, + content_length: Trinary::NotFound, + content_type: Trinary::NotFound, + etag: Trinary::NotFound, + last_modified: Trinary::NotFound, + language: Trinary::Void, + }, + Resource { + href: "/dandisets/000108/draft/sub-SChmi53/".into(), + display_name: Trinary::Set("sub-SChmi53".into()), + is_collection: Some(true), + creation_date: Trinary::NotFound, + content_length: Trinary::NotFound, + content_type: Trinary::NotFound, + etag: Trinary::NotFound, + last_modified: Trinary::NotFound, + language: Trinary::Void, + }, + Resource { + href: "/dandisets/000108/draft/sub-U01hm15x/".into(), + display_name: Trinary::Set("sub-U01hm15x".into()), + is_collection: Some(true), + creation_date: Trinary::NotFound, + content_length: Trinary::NotFound, + content_type: Trinary::NotFound, + etag: Trinary::NotFound, + last_modified: Trinary::NotFound, + language: Trinary::Void, + }, + Resource { + href: "/dandisets/000108/draft/dandiset.yaml".into(), + creation_date: Trinary::NotFound, + display_name: Trinary::Set("dandiset.yaml".into()), + content_length: Trinary::Set(4543), + content_type: Trinary::Set("text/yaml; charset=utf-8".into()), + etag: Trinary::NotFound, + last_modified: Trinary::NotFound, + language: Trinary::Void, + is_collection: Some(false), + }, + ] + ); + } + + #[test] + fn test_parse_propname_response() { + let xml = include_str!("testdata/propname.xml"); + let resources = parse_propname_response(xml).unwrap(); + assert_eq!( + resources, + vec![ + ResourceProps { + href: "/dandisets/000108/draft/".into(), + creation_date: true, + display_name: true, + content_length: true, + last_modified: true, + resource_type: true, + content_type: false, + etag: false, + language: false, + }, + ResourceProps { + href: "/dandisets/000108/draft/dataset_description.json".into(), + creation_date: true, + display_name: true, + content_length: true, + content_type: true, + etag: true, + last_modified: true, + resource_type: true, + language: false, + }, + ResourceProps { + href: "/dandisets/000108/draft/samples.tsv".into(), + creation_date: true, + display_name: true, + content_length: true, + content_type: true, + etag: true, + last_modified: true, + resource_type: true, + language: false, + }, + ResourceProps { + href: "/dandisets/000108/draft/sub-mEhm/".into(), + display_name: true, + resource_type: true, + creation_date: false, + content_length: false, + content_type: false, + etag: false, + last_modified: false, + language: false, + }, + ResourceProps { + href: "/dandisets/000108/draft/sub-MITU01/".into(), + display_name: true, + resource_type: true, + creation_date: false, + content_length: false, + content_type: false, + etag: false, + last_modified: false, + language: false, + }, + ResourceProps { + href: "/dandisets/000108/draft/sub-MITU01h3/".into(), + display_name: true, + resource_type: true, + creation_date: false, + content_length: false, + content_type: false, + etag: false, + last_modified: false, + language: false, + }, + ResourceProps { + href: "/dandisets/000108/draft/sub-SChmi53/".into(), + display_name: true, + resource_type: true, + creation_date: false, + content_length: false, + content_type: false, + etag: false, + last_modified: false, + language: false, + }, + ResourceProps { + href: "/dandisets/000108/draft/sub-U01hm15x/".into(), + display_name: true, + resource_type: true, + creation_date: false, + content_length: false, + content_type: false, + etag: false, + last_modified: false, + language: false, + }, + ResourceProps { + href: "/dandisets/000108/draft/dandiset.yaml".into(), + creation_date: false, + display_name: true, + content_length: true, + content_type: true, + etag: false, + last_modified: false, + language: false, + resource_type: true, + }, + ] + ); + } +} diff --git a/crates/testutils/src/testdata/propfind.xml b/crates/testutils/src/testdata/propfind.xml new file mode 100644 index 0000000..0a511fc --- /dev/null +++ b/crates/testutils/src/testdata/propfind.xml @@ -0,0 +1,185 @@ + + + + /dandisets/000108/draft/ + + + 2021-06-01T14:35:34.214567Z + draft + 374730983049354 + Fri, 03 Nov 2023 11:01:20 GMT + + + + + HTTP/1.1 200 OK + + + + + + + HTTP/1.1 404 NOT FOUND + + + + /dandisets/000108/draft/dataset_description.json + + + 2021-07-03T04:23:52.38146Z + dataset_description.json + 71 + application/json + f4a034fbf965f76828fa027c29860bc0-1 + Wed, 13 Jul 2022 21:40:28 GMT + + + HTTP/1.1 200 OK + + + + /dandisets/000108/draft/samples.tsv + + + 2021-07-21T23:39:29.733695Z + samples.tsv + 572 + text/tab-separated-values + a6ac1fb127e17b2e3360c64154f69a57-1 + Wed, 13 Jul 2022 21:41:07 GMT + + + HTTP/1.1 200 OK + + + + /dandisets/000108/draft/sub-mEhm/ + + + sub-mEhm + + + + + HTTP/1.1 200 OK + + + + + + + + + + HTTP/1.1 404 NOT FOUND + + + + /dandisets/000108/draft/sub-MITU01/ + + + sub-MITU01 + + + + + HTTP/1.1 200 OK + + + + + + + + + + HTTP/1.1 404 NOT FOUND + + + + /dandisets/000108/draft/sub-MITU01h3/ + + + sub-MITU01h3 + + + + + HTTP/1.1 200 OK + + + + + + + + + + HTTP/1.1 404 NOT FOUND + + + + /dandisets/000108/draft/sub-SChmi53/ + + + sub-SChmi53 + + + + + HTTP/1.1 200 OK + + + + + + + + + + HTTP/1.1 404 NOT FOUND + + + + /dandisets/000108/draft/sub-U01hm15x/ + + + sub-U01hm15x + + + + + HTTP/1.1 200 OK + + + + + + + + + + HTTP/1.1 404 NOT FOUND + + + + /dandisets/000108/draft/dandiset.yaml + + + dandiset.yaml + 4543 + text/yaml; charset=utf-8 + + + HTTP/1.1 200 OK + + + + + + + + HTTP/1.1 404 NOT FOUND + + + diff --git a/crates/testutils/src/testdata/propname.xml b/crates/testutils/src/testdata/propname.xml new file mode 100644 index 0000000..51a283b --- /dev/null +++ b/crates/testutils/src/testdata/propname.xml @@ -0,0 +1,108 @@ + + + + /dandisets/000108/draft/ + + + + + + + + + HTTP/1.1 200 OK + + + + /dandisets/000108/draft/dataset_description.json + + + + + + + + + + + HTTP/1.1 200 OK + + + + /dandisets/000108/draft/samples.tsv + + + + + + + + + + + HTTP/1.1 200 OK + + + + /dandisets/000108/draft/sub-mEhm/ + + + + + + HTTP/1.1 200 OK + + + + /dandisets/000108/draft/sub-MITU01/ + + + + + + HTTP/1.1 200 OK + + + + /dandisets/000108/draft/sub-MITU01h3/ + + + + + + HTTP/1.1 200 OK + + + + /dandisets/000108/draft/sub-SChmi53/ + + + + + + HTTP/1.1 200 OK + + + + /dandisets/000108/draft/sub-U01hm15x/ + + + + + + HTTP/1.1 200 OK + + + + /dandisets/000108/draft/dandiset.yaml + + + + + + + + HTTP/1.1 200 OK + + + diff --git a/mockspecs/assets/000001/0.210512.1623.yaml b/mockspecs/assets/000001/0.210512.1623.yaml index 014e549..c6a8076 100644 --- a/mockspecs/assets/000001/0.210512.1623.yaml +++ b/mockspecs/assets/000001/0.210512.1623.yaml @@ -1,82 +1,57 @@ -- asset_id: "07fe0648-3590-4478-b516-a9cb72ba6a36" - blob: "1e931716-9d7e-4c86-9d22-4f9f3a18cb82" - zarr: null - path: "fT.json" - size: 10882 - created: "2022-08-31T17:56:31.788054+00:00" - modified: "2024-12-25T06:56:57.603784+00:00" - metadata: - "@context": "https://raw.githubusercontent.com/dandi/schema/master/releases/0.6.3/context.json" - schemaVersion: "0.6.3" - schemaKey: "Asset" - id: "dandiasset:07fe0648-3590-4478-b516-a9cb72ba6a36" - identifier: "07fe0648-3590-4478-b516-a9cb72ba6a36" - path: "fT.json" - encodingFormat: "application/json" - dateModified: "2024-05-13T01:35:33.232988+00:00" - blobDateModified: "2024-12-21T23:07:07.844796+00:00" - contentUrl: - - "https://api.dandiarchive.org/api/assets/07fe0648-3590-4478-b516-a9cb72ba6a36/download/" - - "https://dandiarchive.s3.amazonaws.com/blobs/1e9/317/1e931716-9d7e-4c86-9d22-4f9f3a18cb82" - contentSize: 10882 - digest: - dandi:dandi-etag: "9b4e051977ac679b72719c8dc025df4d-1" - dandi:sha2-256: "f0751a6c730203df5bd1f0e12df64ba7b0c1455287c897060aede19447235df4" - -- asset_id: "3da246c1-e51c-453c-b8a6-0407ea556642" - blob: "f9b19e1a-80e9-4563-84ee-1369532f6c48" +- asset_id: "d0bc22db-65af-4dfe-90b8-2840b96f74ae" + blob: "9db44c9d-b8be-404e-ab3b-413fd98fd797" zarr: null - path: "FCLX3M8q5.json" - size: 5844 - created: "2020-04-03T19:33:34.841107+00:00" - modified: "2024-05-15T03:29:42.794075+00:00" + path: "participants.tsv" + size: 5968 + created: "2022-08-26T03:21:32.305654+00:00" + modified: "2024-10-04T05:53:14.697984+00:00" metadata: "@context": "https://raw.githubusercontent.com/dandi/schema/master/releases/0.6.3/context.json" schemaVersion: "0.6.3" schemaKey: "Asset" - id: "dandiasset:3da246c1-e51c-453c-b8a6-0407ea556642" - identifier: "3da246c1-e51c-453c-b8a6-0407ea556642" - path: "FCLX3M8q5.json" - encodingFormat: "application/json" - dateModified: "2023-01-10T05:51:45.737965+00:00" - blobDateModified: "2023-12-21T15:32:46.792136+00:00" + id: "dandiasset:d0bc22db-65af-4dfe-90b8-2840b96f74ae" + identifier: "d0bc22db-65af-4dfe-90b8-2840b96f74ae" + path: "participants.tsv" + encodingFormat: "text/tab-separated-values" + dateModified: "2024-03-10T13:46:11.461772+00:00" + blobDateModified: "2024-07-18T16:12:55.367373+00:00" contentUrl: - - "https://api.dandiarchive.org/api/assets/3da246c1-e51c-453c-b8a6-0407ea556642/download/" - - "https://dandiarchive.s3.amazonaws.com/blobs/f9b/19e/f9b19e1a-80e9-4563-84ee-1369532f6c48" - contentSize: 5844 + - "https://api.dandiarchive.org/api/assets/d0bc22db-65af-4dfe-90b8-2840b96f74ae/download/" + - "https://dandiarchive.s3.amazonaws.com/blobs/9db/44c/9db44c9d-b8be-404e-ab3b-413fd98fd797" + contentSize: 5968 digest: - dandi:dandi-etag: "5b690f98abbdb5c9155648a58bce8e6c-1" - dandi:sha2-256: "a919fd57174c20c75439c68ef57a755fb4797550488407d3e10a3bd0a345d6b2" + dandi:dandi-etag: "d80b74152eed942fca5845273a4f1256-1" + dandi:sha2-256: "3153f9edfa04e600424480060f6e8f04b9b098050c27573bbe28a3ebea45cf8d" -- asset_id: "ed09859a-b85b-4d9c-92be-bee049fd49f8" - blob: "e9016ac3-a210-4646-b1d1-ceb5906b8eeb" +- asset_id: "838bab7b-9ab4-4d66-97b3-898a367c9c7e" + blob: "2dbaf0fd-5003-4a0a-b4c0-bc8cdbdb3826" zarr: null - path: "LBCcTPeA.nwb" - size: 2086 - created: "2021-02-03T21:54:27.951779+00:00" - modified: "2024-11-12T05:15:52.110876+00:00" + path: "sub-RAT123/sub-RAT123.nwb" + size: 18792 + created: "2023-03-02T22:10:45.985334Z" + modified: "2023-03-02T22:10:46.064360Z" metadata: "@context": "https://raw.githubusercontent.com/dandi/schema/master/releases/0.6.3/context.json" schemaVersion: "0.6.3" schemaKey: "Asset" - id: "dandiasset:ed09859a-b85b-4d9c-92be-bee049fd49f8" - identifier: "ed09859a-b85b-4d9c-92be-bee049fd49f8" - path: "LBCcTPeA.nwb" + id: "dandiasset:838bab7b-9ab4-4d66-97b3-898a367c9c7e" + identifier: "838bab7b-9ab4-4d66-97b3-898a367c9c7e" + path: "sub-RAT123/sub-RAT123.nwb" encodingFormat: "application/x-nwb" - dateModified: "2022-09-10T19:16:27.694085+00:00" - blobDateModified: "2023-12-22T17:16:28.262283+00:00" + dateModified: "2023-03-02T17:10:45.742644-05:00" + blobDateModified: "2020-10-21T10:10:35.457789-04:00" contentUrl: - - "https://api.dandiarchive.org/api/assets/ed09859a-b85b-4d9c-92be-bee049fd49f8/download/" - - "https://dandiarchive.s3.amazonaws.com/blobs/e90/16a/e9016ac3-a210-4646-b1d1-ceb5906b8eeb" - contentSize: 2086 + - "https://api.dandiarchive.org/api/assets/838bab7b-9ab4-4d66-97b3-898a367c9c7e/download/" + - "https://dandiarchive.s3.amazonaws.com/blobs/2db/af0/2dbaf0fd-5003-4a0a-b4c0-bc8cdbdb3826" + contentSize: 18792 digest: - dandi:dandi-etag: "8d7d9a4b3b114cddb2a0c1af1714ae97-1" - dandi:sha2-256: "b4df6782549260d168e4f13b773dfcbfdb91c91ac78e788e885e2d2b6dc1f630" + dandi:dandi-etag: "6ec084ca9d3be17ec194a8f700d65344-1" + dandi:sha2-256: "1a765509384ea96b7b12136353d9c5b94f23d764ad0431e049197f7875eb352c" - asset_id: "3262d292-cf9d-4aa0-bdce-e3cfc9420768" blob: null zarr: "9eea4d89-c304-4a94-9117-66334b704cbd" - path: "19xqu.zarr" + path: "sub-RAT123/sub-RAT456.zarr" size: 42464419 created: "2022-12-03T20:19:13.983328+00:00" modified: "2024-12-03T10:09:28.139614+00:00" @@ -86,7 +61,7 @@ schemaKey: "Asset" id: "dandiasset:3262d292-cf9d-4aa0-bdce-e3cfc9420768" identifier: "3262d292-cf9d-4aa0-bdce-e3cfc9420768" - path: "19xqu.zarr" + path: "sub-RAT123/sub-RAT456.zarr" encodingFormat: "application/x-zarr" dateModified: "2024-08-28T14:12:31.000174+00:00" blobDateModified: "2024-11-28T16:44:05.012634+00:00" @@ -96,53 +71,3 @@ contentSize: 42464419 digest: dandi:dandi-zarr-checksum: "312272472968a8a5aa0423daeb63fa9e-2587--42464419" - -- asset_id: "d0bc22db-65af-4dfe-90b8-2840b96f74ae" - blob: "9db44c9d-b8be-404e-ab3b-413fd98fd797" - zarr: null - path: "Df.json" - size: 5968 - created: "2022-08-26T03:21:32.305654+00:00" - modified: "2024-10-04T05:53:14.697984+00:00" - metadata: - "@context": "https://raw.githubusercontent.com/dandi/schema/master/releases/0.6.3/context.json" - schemaVersion: "0.6.3" - schemaKey: "Asset" - id: "dandiasset:d0bc22db-65af-4dfe-90b8-2840b96f74ae" - identifier: "d0bc22db-65af-4dfe-90b8-2840b96f74ae" - path: "Df.json" - encodingFormat: "application/json" - dateModified: "2024-03-10T13:46:11.461772+00:00" - blobDateModified: "2024-07-18T16:12:55.367373+00:00" - contentUrl: - - "https://api.dandiarchive.org/api/assets/d0bc22db-65af-4dfe-90b8-2840b96f74ae/download/" - - "https://dandiarchive.s3.amazonaws.com/blobs/9db/44c/9db44c9d-b8be-404e-ab3b-413fd98fd797" - contentSize: 5968 - digest: - dandi:dandi-etag: "d80b74152eed942fca5845273a4f1256-1" - dandi:sha2-256: "3153f9edfa04e600424480060f6e8f04b9b098050c27573bbe28a3ebea45cf8d" - -- asset_id: "49a2be41-dfc2-4518-bc99-d679f25f6f2e" - blob: "b1af222a-14a6-4d2a-8779-d6b36dd808cc" - zarr: null - path: "dPvw8J.nwb" - size: 19254 - created: "2024-06-23T22:57:49.843737+00:00" - modified: "2024-11-25T17:37:47.766855+00:00" - metadata: - "@context": "https://raw.githubusercontent.com/dandi/schema/master/releases/0.6.3/context.json" - schemaVersion: "0.6.3" - schemaKey: "Asset" - id: "dandiasset:49a2be41-dfc2-4518-bc99-d679f25f6f2e" - identifier: "49a2be41-dfc2-4518-bc99-d679f25f6f2e" - path: "dPvw8J.nwb" - encodingFormat: "application/x-nwb" - dateModified: "2024-06-24T04:28:21.774416+00:00" - blobDateModified: "2024-08-11T13:51:44.649217+00:00" - contentUrl: - - "https://api.dandiarchive.org/api/assets/49a2be41-dfc2-4518-bc99-d679f25f6f2e/download/" - - "https://dandiarchive.s3.amazonaws.com/blobs/b1a/f22/b1af222a-14a6-4d2a-8779-d6b36dd808cc" - contentSize: 19254 - digest: - dandi:dandi-etag: "32ed7fe55660ddd4cd561b2b90b12157-1" - dandi:sha2-256: "07d3ba6768bf8a54967047873ecaded1c80592d5a04f47066dd5373d140fb20a" diff --git a/mockspecs/dandisets.yaml b/mockspecs/dandisets.yaml index ff3ca99..dbdde3f 100644 --- a/mockspecs/dandisets.yaml +++ b/mockspecs/dandisets.yaml @@ -39,6 +39,9 @@ version: "0.210512.1623" dateCreated: "2021-05-12T16:23:14.388489Z" description: "Researcher is seeking funding for surgery to fix goring injuries." + asset_dirs: + - null + - sub-RAT123 - version: "0.230629.1955" name: "Brainscan of a Unicorn" diff --git a/src/testdata/stubs/api/dandisets/000001/versions.json b/src/testdata/stubs/api/dandisets/000001/versions.json index 74e7c0d..c72cc54 100644 --- a/src/testdata/stubs/api/dandisets/000001/versions.json +++ b/src/testdata/stubs/api/dandisets/000001/versions.json @@ -17,8 +17,8 @@ { "version": "0.210512.1623", "name": "Brainscan of a Unicorn", - "asset_count": 6, - "size": 42508453, + "asset_count": 3, + "size": 42489179, "status": "Valid", "created": "2021-05-12T16:23:14.388489Z", "modified": "2021-05-12T16:23:19.080882Z" diff --git a/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/assets.json b/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/assets.json new file mode 100644 index 0000000..56bde4c --- /dev/null +++ b/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/assets.json @@ -0,0 +1,72 @@ +[ + { + "params": { + "metadata": "1", + "order": "path", + "path": "sub-RAT123" + }, + "response": { + "count": 2, + "next": null, + "results": [ + { + "asset_id": "838bab7b-9ab4-4d66-97b3-898a367c9c7e", + "blob": "2dbaf0fd-5003-4a0a-b4c0-bc8cdbdb3826", + "zarr": null, + "path": "sub-RAT123/sub-RAT123.nwb", + "size": 18792, + "created": "2023-03-02T22:10:45.985334Z", + "modified": "2023-03-02T22:10:46.064360Z", + "metadata": { + "@context": "https://raw.githubusercontent.com/dandi/schema/master/releases/0.6.3/context.json", + "blobDateModified": "2020-10-21T10:10:35.457789-04:00", + "contentSize": 18792, + "contentUrl": [ + "https://api.dandiarchive.org/api/assets/838bab7b-9ab4-4d66-97b3-898a367c9c7e/download/", + "https://dandiarchive.s3.amazonaws.com/blobs/2db/af0/2dbaf0fd-5003-4a0a-b4c0-bc8cdbdb3826" + ], + "dateModified": "2023-03-02T17:10:45.742644-05:00", + "digest": { + "dandi:dandi-etag": "6ec084ca9d3be17ec194a8f700d65344-1", + "dandi:sha2-256": "1a765509384ea96b7b12136353d9c5b94f23d764ad0431e049197f7875eb352c" + }, + "encodingFormat": "application/x-nwb", + "id": "dandiasset:838bab7b-9ab4-4d66-97b3-898a367c9c7e", + "identifier": "838bab7b-9ab4-4d66-97b3-898a367c9c7e", + "path": "sub-RAT123/sub-RAT123.nwb", + "schemaKey": "Asset", + "schemaVersion": "0.6.3" + } + }, + { + "asset_id": "3262d292-cf9d-4aa0-bdce-e3cfc9420768", + "blob": null, + "zarr": "9eea4d89-c304-4a94-9117-66334b704cbd", + "path": "sub-RAT123/sub-RAT456.zarr", + "size": 42464419, + "created": "2022-12-03T20:19:13.983328+00:00", + "modified": "2024-12-03T10:09:28.139614+00:00", + "metadata": { + "@context": "https://raw.githubusercontent.com/dandi/schema/master/releases/0.6.3/context.json", + "blobDateModified": "2024-11-28T16:44:05.012634+00:00", + "contentSize": 42464419, + "contentUrl": [ + "https://api.dandiarchive.org/api/assets/3262d292-cf9d-4aa0-bdce-e3cfc9420768/download/", + "https://dandiarchive.s3.amazonaws.com/zarr/9eea4d89-c304-4a94-9117-66334b704cbd/" + ], + "dateModified": "2024-08-28T14:12:31.000174+00:00", + "digest": { + "dandi:dandi-zarr-checksum": "312272472968a8a5aa0423daeb63fa9e-2587--42464419" + }, + "encodingFormat": "application/x-zarr", + "id": "dandiasset:3262d292-cf9d-4aa0-bdce-e3cfc9420768", + "identifier": "3262d292-cf9d-4aa0-bdce-e3cfc9420768", + "path": "sub-RAT123/sub-RAT456.zarr", + "schemaKey": "Asset", + "schemaVersion": "0.6.3" + } + } + ] + } + } +] diff --git a/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/assets/3262d292-cf9d-4aa0-bdce-e3cfc9420768/info.json b/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/assets/3262d292-cf9d-4aa0-bdce-e3cfc9420768/info.json new file mode 100644 index 0000000..4c66a5d --- /dev/null +++ b/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/assets/3262d292-cf9d-4aa0-bdce-e3cfc9420768/info.json @@ -0,0 +1,33 @@ +[ + { + "params": {}, + "response": { + "asset_id": "3262d292-cf9d-4aa0-bdce-e3cfc9420768", + "blob": null, + "zarr": "9eea4d89-c304-4a94-9117-66334b704cbd", + "path": "sub-RAT123/sub-RAT456.zarr", + "size": 42464419, + "created": "2022-12-03T20:19:13.983328+00:00", + "modified": "2024-12-03T10:09:28.139614+00:00", + "metadata": { + "@context": "https://raw.githubusercontent.com/dandi/schema/master/releases/0.6.3/context.json", + "blobDateModified": "2024-11-28T16:44:05.012634+00:00", + "contentSize": 42464419, + "contentUrl": [ + "https://api.dandiarchive.org/api/assets/3262d292-cf9d-4aa0-bdce-e3cfc9420768/download/", + "https://dandiarchive.s3.amazonaws.com/zarr/9eea4d89-c304-4a94-9117-66334b704cbd/" + ], + "dateModified": "2024-08-28T14:12:31.000174+00:00", + "digest": { + "dandi:dandi-zarr-checksum": "312272472968a8a5aa0423daeb63fa9e-2587--42464419" + }, + "encodingFormat": "application/x-zarr", + "id": "dandiasset:3262d292-cf9d-4aa0-bdce-e3cfc9420768", + "identifier": "3262d292-cf9d-4aa0-bdce-e3cfc9420768", + "path": "sub-RAT123/sub-RAT456.zarr", + "schemaKey": "Asset", + "schemaVersion": "0.6.3" + } + } + } +] diff --git a/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/assets/838bab7b-9ab4-4d66-97b3-898a367c9c7e/info.json b/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/assets/838bab7b-9ab4-4d66-97b3-898a367c9c7e/info.json new file mode 100644 index 0000000..cef0b81 --- /dev/null +++ b/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/assets/838bab7b-9ab4-4d66-97b3-898a367c9c7e/info.json @@ -0,0 +1,34 @@ +[ + { + "params": {}, + "response": { + "asset_id": "838bab7b-9ab4-4d66-97b3-898a367c9c7e", + "blob": "2dbaf0fd-5003-4a0a-b4c0-bc8cdbdb3826", + "zarr": null, + "path": "sub-RAT123/sub-RAT123.nwb", + "size": 18792, + "created": "2023-03-02T22:10:45.985334Z", + "modified": "2023-03-02T22:10:46.064360Z", + "metadata": { + "@context": "https://raw.githubusercontent.com/dandi/schema/master/releases/0.6.3/context.json", + "blobDateModified": "2020-10-21T10:10:35.457789-04:00", + "contentSize": 18792, + "contentUrl": [ + "https://api.dandiarchive.org/api/assets/838bab7b-9ab4-4d66-97b3-898a367c9c7e/download/", + "https://dandiarchive.s3.amazonaws.com/blobs/2db/af0/2dbaf0fd-5003-4a0a-b4c0-bc8cdbdb3826" + ], + "dateModified": "2023-03-02T17:10:45.742644-05:00", + "digest": { + "dandi:dandi-etag": "6ec084ca9d3be17ec194a8f700d65344-1", + "dandi:sha2-256": "1a765509384ea96b7b12136353d9c5b94f23d764ad0431e049197f7875eb352c" + }, + "encodingFormat": "application/x-nwb", + "id": "dandiasset:838bab7b-9ab4-4d66-97b3-898a367c9c7e", + "identifier": "838bab7b-9ab4-4d66-97b3-898a367c9c7e", + "path": "sub-RAT123/sub-RAT123.nwb", + "schemaKey": "Asset", + "schemaVersion": "0.6.3" + } + } + } +] diff --git a/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/assets/d0bc22db-65af-4dfe-90b8-2840b96f74ae/info.json b/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/assets/d0bc22db-65af-4dfe-90b8-2840b96f74ae/info.json new file mode 100644 index 0000000..075ff1d --- /dev/null +++ b/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/assets/d0bc22db-65af-4dfe-90b8-2840b96f74ae/info.json @@ -0,0 +1,34 @@ +[ + { + "params": {}, + "response": { + "asset_id": "d0bc22db-65af-4dfe-90b8-2840b96f74ae", + "blob": "9db44c9d-b8be-404e-ab3b-413fd98fd797", + "zarr": null, + "path": "participants.tsv", + "size": 5968, + "created": "2022-08-26T03:21:32.305654+00:00", + "modified": "2024-10-04T05:53:14.697984+00:00", + "metadata": { + "@context": "https://raw.githubusercontent.com/dandi/schema/master/releases/0.6.3/context.json", + "blobDateModified": "2024-07-18T16:12:55.367373+00:00", + "contentSize": 5968, + "contentUrl": [ + "https://api.dandiarchive.org/api/assets/d0bc22db-65af-4dfe-90b8-2840b96f74ae/download/", + "https://dandiarchive.s3.amazonaws.com/blobs/9db/44c/9db44c9d-b8be-404e-ab3b-413fd98fd797" + ], + "dateModified": "2024-03-10T13:46:11.461772+00:00", + "digest": { + "dandi:dandi-etag": "d80b74152eed942fca5845273a4f1256-1", + "dandi:sha2-256": "3153f9edfa04e600424480060f6e8f04b9b098050c27573bbe28a3ebea45cf8d" + }, + "encodingFormat": "text/tab-separated-values", + "id": "dandiasset:d0bc22db-65af-4dfe-90b8-2840b96f74ae", + "identifier": "d0bc22db-65af-4dfe-90b8-2840b96f74ae", + "path": "participants.tsv", + "schemaKey": "Asset", + "schemaVersion": "0.6.3" + } + } + } +] diff --git a/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/assets/paths.json b/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/assets/paths.json new file mode 100644 index 0000000..8ed448c --- /dev/null +++ b/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/assets/paths.json @@ -0,0 +1,52 @@ +[ + { + "params": {}, + "response": { + "count": 2, + "next": null, + "results": [ + { + "path": "participants.tsv", + "aggregate_files": 1, + "aggregate_size": 5968, + "asset": { + "asset_id": "d0bc22db-65af-4dfe-90b8-2840b96f74ae" + } + }, + { + "path": "sub-RAT123", + "aggregate_files": 2, + "aggregate_size": 42483211, + "asset": null + } + ] + } + }, + { + "params": { + "path_prefix": "sub-RAT123/" + }, + "response": { + "count": 2, + "next": null, + "results": [ + { + "path": "sub-RAT123/sub-RAT123.nwb", + "aggregate_files": 1, + "aggregate_size": 18792, + "asset": { + "asset_id": "838bab7b-9ab4-4d66-97b3-898a367c9c7e" + } + }, + { + "path": "sub-RAT123/sub-RAT456.zarr", + "aggregate_files": 1, + "aggregate_size": 42464419, + "asset": { + "asset_id": "3262d292-cf9d-4aa0-bdce-e3cfc9420768" + } + } + ] + } + } +] diff --git a/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/info.json b/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/info.json index 732253f..7cd8716 100644 --- a/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/info.json +++ b/src/testdata/stubs/api/dandisets/000001/versions/0.210512.1623/info.json @@ -4,8 +4,8 @@ "response": { "version": "0.210512.1623", "name": "Brainscan of a Unicorn", - "asset_count": 6, - "size": 42508453, + "asset_count": 3, + "size": 42489179, "status": "Valid", "created": "2021-05-12T16:23:14.388489Z", "modified": "2021-05-12T16:23:19.080882Z", diff --git a/src/tests.rs b/src/tests.rs index 3bf7d6d..265f690 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,10 +1,10 @@ #![cfg(test)] use super::*; -use crate::consts::YAML_CONTENT_TYPE; +use crate::consts::{DAV_XML_CONTENT_TYPE, YAML_CONTENT_TYPE}; use axum::body::Bytes; use http_body_util::BodyExt; // for `collect` use indoc::indoc; -use testutils::{CollectionEntry, CollectionPage, Link}; +use testutils::{CollectionEntry, CollectionPage, Link, Resource, ResourceProps, Trinary}; use tower::{Service, ServiceExt}; // for `ready` fn fill_html_footer(html: &str) -> String { @@ -50,17 +50,11 @@ impl MockApp { } } - async fn get(&mut self, path: &str) -> Response { + async fn request(&mut self, req: Request) -> Response { let response = >>::ready(&mut self.app) .await .unwrap() - .call( - Request::builder() - .uri(path) - .header("X-Forwarded-For", "127.0.0.1") - .body(Body::empty()) - .unwrap(), - ) + .call(req) .await .unwrap(); let (parts, body) = response.into_parts(); @@ -68,23 +62,27 @@ impl MockApp { Response::from_parts(parts, body) } + async fn get(&mut self, path: &str) -> Response { + self.request( + Request::builder() + .uri(path) + .header("X-Forwarded-For", "127.0.0.1") + .body(Body::empty()) + .unwrap(), + ) + .await + } + async fn head(&mut self, path: &str) -> Response { - let response = >>::ready(&mut self.app) - .await - .unwrap() - .call( - Request::builder() - .method(Method::HEAD) - .uri(path) - .header("X-Forwarded-For", "127.0.0.1") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - let (parts, body) = response.into_parts(); - let body = body.collect().await.unwrap().to_bytes(); - Response::from_parts(parts, body) + self.request( + Request::builder() + .method(Method::HEAD) + .uri(path) + .header("X-Forwarded-For", "127.0.0.1") + .body(Body::empty()) + .unwrap(), + ) + .await } async fn get_collection_html(&mut self, path: &str) -> CollectionPage { @@ -100,10 +98,102 @@ impl MockApp { let body = std::str::from_utf8(response.body()).unwrap(); testutils::parse_collection_page(body).unwrap() } + + fn propfind(&mut self, path: &'static str) -> Propfinder<'_> { + Propfinder::new(self, path) + } +} + +#[derive(Debug)] +struct Propfinder<'a> { + app: &'a mut MockApp, + path: &'static str, + body: Option<&'static str>, + depth: Option<&'static str>, +} + +impl<'a> Propfinder<'a> { + fn new(app: &'a mut MockApp, path: &'static str) -> Self { + Propfinder { + app, + path, + body: None, + depth: Some("1"), + } + } + + fn body(mut self, body: &'static str) -> Self { + self.body = Some(body); + self + } + + fn depth(mut self, depth: &'static str) -> Self { + self.depth = Some(depth); + self + } + + fn no_depth(mut self) -> Self { + self.depth = None; + self + } + + async fn send(self) -> PropfindResponse { + let mut req = Request::builder() + .method("PROPFIND") + .uri(self.path) + .header("X-Forwarded-For", "127.0.0.1"); + if let Some(depth) = self.depth { + req = req.header("Depth", depth); + } + let req = req + .body(self.body.map_or_else(Body::empty, Body::from)) + .unwrap(); + let resp = self.app.request(req).await; + PropfindResponse(resp) + } +} + +#[derive(Clone, Debug)] +struct PropfindResponse(Response); + +impl PropfindResponse { + fn assert_status(self, status: StatusCode) -> Self { + assert_eq!(self.0.status(), status); + self + } + + fn assert_header(self, header: axum::http::header::HeaderName, value: &str) -> Self { + assert_eq!( + self.0.headers().get(header).and_then(|v| v.to_str().ok()), + Some(value) + ); + self + } + + fn success(self) -> Self { + self.assert_status(StatusCode::MULTI_STATUS) + .assert_header(CONTENT_TYPE, DAV_XML_CONTENT_TYPE) + } + + fn into_resources(self) -> Vec { + let body = std::str::from_utf8(self.0.body()).unwrap(); + testutils::parse_propfind_response(body).unwrap() + } + + fn into_propnames(self) -> Vec { + let body = std::str::from_utf8(self.0.body()).unwrap(); + testutils::parse_propname_response(body).unwrap() + } + + fn assert_body(self, expected: &str) -> Self { + let body = std::str::from_utf8(self.0.body()).unwrap(); + pretty_assertions::assert_eq!(body, expected); + self + } } #[tokio::test] -async fn test_get_styles() { +async fn get_styles() { let mut app = MockApp::new().await; let response = app.get("/.static/styles.css").await; assert_eq!(response.status(), StatusCode::OK); @@ -120,7 +210,7 @@ async fn test_get_styles() { } #[tokio::test] -async fn test_get_root() { +async fn get_root() { let mut app = MockApp::new().await; let response = app.get("/").await; assert_eq!(response.status(), StatusCode::OK); @@ -138,7 +228,7 @@ async fn test_get_root() { } #[tokio::test] -async fn test_head_styles() { +async fn head_styles() { let mut app = MockApp::new().await; let response = app.head("/.static/styles.css").await; assert_eq!(response.status(), StatusCode::OK); @@ -162,7 +252,7 @@ async fn test_head_styles() { } #[tokio::test] -async fn test_head_root() { +async fn head_root() { let mut app = MockApp::new().await; let response = app.head("/").await; assert_eq!(response.status(), StatusCode::OK); @@ -187,7 +277,83 @@ async fn test_head_root() { } #[tokio::test] -async fn test_get_dandisets_index() { +async fn propfind_root_depth_0() { + let mut app = MockApp::new().await; + let resources = app + .propfind("/") + .depth("0") + .send() + .await + .success() + .into_resources(); + pretty_assertions::assert_eq!( + resources, + vec![Resource { + href: "/".into(), + creation_date: Trinary::Void, + display_name: Trinary::Void, + content_length: Trinary::Void, + content_type: Trinary::Void, + last_modified: Trinary::Void, + etag: Trinary::Void, + language: Trinary::Void, + is_collection: Some(true), + }], + ); +} + +#[tokio::test] +async fn propfind_root_depth_1() { + let mut app = MockApp::new().await; + let resources = app + .propfind("/") + .depth("1") + .send() + .await + .success() + .into_resources(); + pretty_assertions::assert_eq!( + resources, + vec![ + Resource { + href: "/".into(), + creation_date: Trinary::Void, + display_name: Trinary::Void, + content_length: Trinary::Void, + content_type: Trinary::Void, + last_modified: Trinary::Void, + etag: Trinary::Void, + language: Trinary::Void, + is_collection: Some(true), + }, + Resource { + href: "/dandisets/".into(), + creation_date: Trinary::Void, + display_name: Trinary::Set("dandisets".into()), + content_length: Trinary::Void, + content_type: Trinary::Void, + last_modified: Trinary::Void, + etag: Trinary::Void, + language: Trinary::Void, + is_collection: Some(true), + }, + Resource { + href: "/zarrs/".into(), + creation_date: Trinary::Void, + display_name: Trinary::Set("zarrs".into()), + content_length: Trinary::Void, + content_type: Trinary::Void, + last_modified: Trinary::Void, + etag: Trinary::Void, + language: Trinary::Void, + is_collection: Some(true), + }, + ], + ); +} + +#[tokio::test] +async fn get_dandisets_index() { let mut app = MockApp::new().await; let page = app.get_collection_html("/dandisets/").await; pretty_assertions::assert_eq!( @@ -254,7 +420,7 @@ async fn test_get_dandisets_index() { } #[tokio::test] -async fn test_get_dandiset_with_published() { +async fn get_dandiset_with_published() { let mut app = MockApp::new().await; let page = app.get_collection_html("/dandisets/000001/").await; pretty_assertions::assert_eq!( @@ -331,7 +497,7 @@ async fn test_get_dandiset_with_published() { } #[tokio::test] -async fn test_get_dandiset_without_published() { +async fn get_dandiset_without_published() { let mut app = MockApp::new().await; let page = app.get_collection_html("/dandisets/000003/").await; pretty_assertions::assert_eq!( @@ -383,7 +549,7 @@ async fn test_get_dandiset_without_published() { } #[tokio::test] -async fn test_get_dandiset_releases() { +async fn get_dandiset_releases() { let mut app = MockApp::new().await; let page = app.get_collection_html("/dandisets/000001/releases/").await; pretty_assertions::assert_eq!( @@ -429,7 +595,7 @@ async fn test_get_dandiset_releases() { app.archive_url )), typekind: "Dandiset version".into(), - size: "40.54 MiB".into(), + size: "40.52 MiB".into(), created: "2021-05-12 16:23:14Z".into(), modified: "2021-05-12 16:23:19Z".into(), }, @@ -453,7 +619,7 @@ async fn test_get_dandiset_releases() { } #[tokio::test] -async fn test_get_version_toplevel() { +async fn get_version_toplevel() { let mut app = MockApp::new().await; let page = app.get_collection_html("/dandisets/000002/draft/").await; pretty_assertions::assert_eq!( @@ -640,7 +806,7 @@ async fn test_get_version_toplevel() { } #[tokio::test] -async fn test_get_asset_folder() { +async fn get_asset_folder() { let mut app = MockApp::new().await; let page = app .get_collection_html("/dandisets/000002/draft/fRLy/") @@ -850,6 +1016,34 @@ async fn get_dandiset_yaml() { ); } +#[tokio::test] +async fn propfind_dandiset_yaml() { + let mut app = MockApp::new().await; + for depth in ["0", "1"] { + let resources = app + .propfind("/dandisets/000001/draft/dandiset.yaml") + .depth(depth) + .send() + .await + .success() + .into_resources(); + pretty_assertions::assert_eq!( + resources, + vec![Resource { + href: "/dandisets/000001/draft/dandiset.yaml".into(), + creation_date: Trinary::Void, + display_name: Trinary::Set("dandiset.yaml".into()), + content_length: Trinary::Set(410), + content_type: Trinary::Set(YAML_CONTENT_TYPE.into()), + last_modified: Trinary::Void, + etag: Trinary::Void, + language: Trinary::Void, + is_collection: Some(false), + }], + ); + } +} + #[tokio::test] async fn get_paginated_assets() { let mut app = MockApp::new().await; @@ -1027,12 +1221,40 @@ async fn get_blob_asset_prefer_s3_redirects() { .headers() .get(axum::http::header::LOCATION) .and_then(|v| v.to_str().ok()), - Some("https://dandiarchive.s3.amazonaws.com/blobs/2db/af0/2dbaf0fd-5003-4a0a-b4c0-bc8cdbdb3826"), + Some("https://dandiarchive.s3.amazonaws.com/blobs/2db/af0/2dbaf0fd-5003-4a0a-b4c0-bc8cdbdb3826"), ); assert!(response.headers().contains_key("DAV")); assert!(response.body().is_empty()); } +#[tokio::test] +async fn propfind_blob_asset() { + let mut app = MockApp::new().await; + for depth in ["0", "1"] { + let resources = app + .propfind("/dandisets/000001/draft/sub-RAT123/sub-RAT123.nwb") + .depth(depth) + .send() + .await + .success() + .into_resources(); + pretty_assertions::assert_eq!( + resources, + vec![Resource { + href: "/dandisets/000001/draft/sub-RAT123/sub-RAT123.nwb".into(), + creation_date: Trinary::Set("2023-03-02T22:10:45.985334Z".into()), + display_name: Trinary::Set("sub-RAT123.nwb".into()), + content_length: Trinary::Set(18792), + content_type: Trinary::Set("application/x-nwb".into()), + last_modified: Trinary::Set("Thu, 02 Mar 2023 22:10:46 GMT".into()), + etag: Trinary::Set("6ec084ca9d3be17ec194a8f700d65344-1".into()), + language: Trinary::Void, + is_collection: Some(false), + }], + ); + } +} + #[tokio::test] async fn get_latest_version() { let mut app = MockApp::new().await; @@ -1169,3 +1391,423 @@ async fn get_latest_version() { } ); } + +#[tokio::test] +async fn get_404() { + let mut app = MockApp::new().await; + let response = app.get("/dandisets/999999/").await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn propfind_404() { + let mut app = MockApp::new().await; + for depth in ["0", "1"] { + app.propfind("/dandisets/999999/") + .depth(depth) + .send() + .await + .assert_status(StatusCode::NOT_FOUND); + } +} + +#[tokio::test] +async fn propfind_infinite_depth() { + let mut app = MockApp::new().await; + app.propfind("/") + .depth("infinity") + .send() + .await + .assert_status(StatusCode::FORBIDDEN) + .assert_header(CONTENT_TYPE, DAV_XML_CONTENT_TYPE) + .assert_body(indoc! {r#" + + + + + "#}); +} + +#[tokio::test] +async fn propfind_no_depth() { + let mut app = MockApp::new().await; + app.propfind("/") + .no_depth() + .send() + .await + .assert_status(StatusCode::FORBIDDEN) + .assert_header(CONTENT_TYPE, DAV_XML_CONTENT_TYPE) + .assert_body(indoc! {r#" + + + + + "#}); +} + +#[tokio::test] +async fn propfind_invalid_depth() { + let mut app = MockApp::new().await; + app.propfind("/") + .depth("2") + .send() + .await + .assert_status(StatusCode::BAD_REQUEST); +} + +#[tokio::test] +async fn propfind_version_toplevel() { + let mut app = MockApp::new().await; + let resources = app + .propfind("/dandisets/000001/releases/0.210512.1623/") + .send() + .await + .success() + .into_resources(); + pretty_assertions::assert_eq!( + resources, + vec![ + Resource { + href: "/dandisets/000001/releases/0.210512.1623/".into(), + creation_date: Trinary::Set("2021-05-12T16:23:14.388489Z".into()), + display_name: Trinary::Set("0.210512.1623".into()), + content_length: Trinary::Set(42489179), + content_type: Trinary::Void, + last_modified: Trinary::Set("Wed, 12 May 2021 16:23:19 GMT".into()), + etag: Trinary::Void, + language: Trinary::Void, + is_collection: Some(true), + }, + Resource { + href: "/dandisets/000001/releases/0.210512.1623/participants.tsv".into(), + creation_date: Trinary::Set("2022-08-26T03:21:32.305654Z".into()), + display_name: Trinary::Set("participants.tsv".into()), + content_length: Trinary::Set(5968), + content_type: Trinary::Set("text/tab-separated-values".into()), + last_modified: Trinary::Set("Fri, 04 Oct 2024 05:53:14 GMT".into()), + etag: Trinary::Set("d80b74152eed942fca5845273a4f1256-1".into()), + language: Trinary::Void, + is_collection: Some(false), + }, + Resource { + href: "/dandisets/000001/releases/0.210512.1623/sub-RAT123/".into(), + creation_date: Trinary::Void, + display_name: Trinary::Set("sub-RAT123".into()), + content_length: Trinary::Void, + content_type: Trinary::Void, + last_modified: Trinary::Void, + etag: Trinary::Void, + language: Trinary::Void, + is_collection: Some(true), + }, + Resource { + href: "/dandisets/000001/releases/0.210512.1623/dandiset.yaml".into(), + creation_date: Trinary::Void, + display_name: Trinary::Set("dandiset.yaml".into()), + content_length: Trinary::Set(429), + content_type: Trinary::Set(YAML_CONTENT_TYPE.into()), + last_modified: Trinary::Void, + etag: Trinary::Void, + language: Trinary::Void, + is_collection: Some(false), + }, + ] + ); +} + +#[tokio::test] +async fn propfind_asset_folder() { + let mut app = MockApp::new().await; + let resources = app + .propfind("/dandisets/000001/releases/0.210512.1623/sub-RAT123/") + .send() + .await + .success() + .into_resources(); + pretty_assertions::assert_eq!( + resources, + vec![ + Resource { + href: "/dandisets/000001/releases/0.210512.1623/sub-RAT123/".into(), + creation_date: Trinary::Void, + display_name: Trinary::Set("sub-RAT123".into()), + content_length: Trinary::Void, + content_type: Trinary::Void, + last_modified: Trinary::Void, + etag: Trinary::Void, + language: Trinary::Void, + is_collection: Some(true), + }, + Resource { + href: "/dandisets/000001/releases/0.210512.1623/sub-RAT123/sub-RAT123.nwb".into(), + creation_date: Trinary::Set("2023-03-02T22:10:45.985334Z".into()), + display_name: Trinary::Set("sub-RAT123.nwb".into()), + content_length: Trinary::Set(18792), + content_type: Trinary::Set("application/x-nwb".into()), + last_modified: Trinary::Set("Thu, 02 Mar 2023 22:10:46 GMT".into()), + etag: Trinary::Set("6ec084ca9d3be17ec194a8f700d65344-1".into()), + language: Trinary::Void, + is_collection: Some(false), + }, + Resource { + href: "/dandisets/000001/releases/0.210512.1623/sub-RAT123/sub-RAT456.zarr/".into(), + creation_date: Trinary::Set("2022-12-03T20:19:13.983328Z".into()), + display_name: Trinary::Set("sub-RAT456.zarr".into()), + content_length: Trinary::Set(42464419), + content_type: Trinary::Void, + last_modified: Trinary::Set("Tue, 03 Dec 2024 10:09:28 GMT".into()), + etag: Trinary::Void, + language: Trinary::Void, + is_collection: Some(true), + }, + ] + ); +} + +#[tokio::test] +async fn propfind_propname() { + let mut app = MockApp::new().await; + let props = app + .propfind("/dandisets/000001/releases/0.210512.1623/") + .body(r#""#) + .send() + .await + .success() + .into_propnames(); + pretty_assertions::assert_eq!( + props, + vec![ + ResourceProps { + href: "/dandisets/000001/releases/0.210512.1623/".into(), + creation_date: true, + display_name: true, + content_length: true, + content_type: false, + last_modified: true, + etag: false, + language: false, + resource_type: true, + }, + ResourceProps { + href: "/dandisets/000001/releases/0.210512.1623/participants.tsv".into(), + creation_date: true, + display_name: true, + content_length: true, + content_type: true, + last_modified: true, + etag: true, + language: false, + resource_type: true, + }, + ResourceProps { + href: "/dandisets/000001/releases/0.210512.1623/sub-RAT123/".into(), + creation_date: false, + display_name: true, + content_length: false, + content_type: false, + last_modified: false, + etag: false, + language: false, + resource_type: true, + }, + ResourceProps { + href: "/dandisets/000001/releases/0.210512.1623/dandiset.yaml".into(), + creation_date: false, + display_name: true, + content_length: true, + content_type: true, + last_modified: false, + etag: false, + language: false, + resource_type: true, + }, + ] + ); +} + +#[tokio::test] +async fn propfind_prop() { + let mut app = MockApp::new().await; + let resources = app + .propfind("/dandisets/000001/releases/0.210512.1623/") + .body(indoc! {r#" + + + + + + + + + + "#}) + .send() + .await + .success() + .into_resources(); + pretty_assertions::assert_eq!( + resources, + vec![ + Resource { + href: "/dandisets/000001/releases/0.210512.1623/".into(), + creation_date: Trinary::Set("2021-05-12T16:23:14.388489Z".into()), + display_name: Trinary::Set("0.210512.1623".into()), + content_length: Trinary::Set(42489179), + content_type: Trinary::Void, + last_modified: Trinary::Void, + etag: Trinary::Void, + language: Trinary::Void, + is_collection: Some(true), + }, + Resource { + href: "/dandisets/000001/releases/0.210512.1623/participants.tsv".into(), + creation_date: Trinary::Set("2022-08-26T03:21:32.305654Z".into()), + display_name: Trinary::Set("participants.tsv".into()), + content_length: Trinary::Set(5968), + content_type: Trinary::Void, + last_modified: Trinary::Void, + etag: Trinary::Void, + language: Trinary::Void, + is_collection: Some(false), + }, + Resource { + href: "/dandisets/000001/releases/0.210512.1623/sub-RAT123/".into(), + creation_date: Trinary::NotFound, + display_name: Trinary::Set("sub-RAT123".into()), + content_length: Trinary::NotFound, + content_type: Trinary::Void, + last_modified: Trinary::Void, + etag: Trinary::Void, + language: Trinary::Void, + is_collection: Some(true), + }, + Resource { + href: "/dandisets/000001/releases/0.210512.1623/dandiset.yaml".into(), + creation_date: Trinary::NotFound, + display_name: Trinary::Set("dandiset.yaml".into()), + content_length: Trinary::Set(429), + content_type: Trinary::Void, + last_modified: Trinary::Void, + etag: Trinary::Void, + language: Trinary::Void, + is_collection: Some(false), + }, + ] + ); +} + +#[tokio::test] +async fn propfind_include_unknown_properties() { + let mut app = MockApp::new().await; + app.propfind("/dandisets/000001/draft/dandiset.yaml") + .body(indoc! {r#" + + + + + + + + + "#}) + .send() + .await + .success() + .assert_body(indoc! {r#" + + + + /dandisets/000001/draft/dandiset.yaml + + + dandiset.yaml + 410 + text/yaml; charset=utf-8 + + + HTTP/1.1 200 OK + + + + + + + HTTP/1.1 404 NOT FOUND + + + + "#}); +} + +#[tokio::test] +async fn propfind_prop_unknown_properties() { + let mut app = MockApp::new().await; + app.propfind("/dandisets/000001/draft/dandiset.yaml") + .body(indoc! {r#" + + + + + + + + "#}) + .send() + .await + .success() + .assert_body(indoc! {r#" + + + + /dandisets/000001/draft/dandiset.yaml + + + + + + HTTP/1.1 404 NOT FOUND + + + + "#}); +} + +#[tokio::test] +async fn propfind_prop_with_unknown_properties() { + let mut app = MockApp::new().await; + app.propfind("/dandisets/000001/draft/dandiset.yaml") + .body(indoc! {r#" + + + + + + + + + "#}) + .send() + .await + .success() + .assert_body(indoc! {r#" + + + + /dandisets/000001/draft/dandiset.yaml + + + + + HTTP/1.1 200 OK + + + + + + + HTTP/1.1 404 NOT FOUND + + + + "#}); +} diff --git a/tools/test-propfind.sh b/tools/test-propfind.sh index cb3902a..598222e 100755 --- a/tools/test-propfind.sh +++ b/tools/test-propfind.sh @@ -36,6 +36,23 @@ curl -fsSL \ -d '' \ "$endpoint/$collection" > "$outdir"/resourcetype.xml +curl -fsSL \ + -X PROPFIND \ + -H "Depth: 1" \ + -d ' + + + + + + + + + + + ' \ + "$endpoint/$collection" > "$outdir"/everyprop.xml + curl -fsSL \ -X PROPFIND \ -H "Depth: 0" \