WebDAV import materialised the account root collection as a directory named after the account displayname (fixes #18)
This commit is contained in:
@@ -363,8 +363,11 @@ fn baikal_carddav_preserves_apple_item_labels() {
|
||||
let account = &b.accounts[0];
|
||||
let dav_root = b.dav_root();
|
||||
|
||||
let client =
|
||||
integration::dav_client::DavSeed::new(dav_root.clone(), &account.username, &account.password);
|
||||
let client = integration::dav_client::DavSeed::new(
|
||||
dav_root.clone(),
|
||||
&account.username,
|
||||
&account.password,
|
||||
);
|
||||
let book = format!("/addressbooks/{}/ablabels/", account.username);
|
||||
let mkbook = r#"<?xml version="1.0" encoding="utf-8"?>
|
||||
<d:mkcol xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:carddav">
|
||||
@@ -373,7 +376,9 @@ fn baikal_carddav_preserves_apple_item_labels() {
|
||||
<d:displayname>AB Labels</d:displayname>
|
||||
</d:prop></d:set>
|
||||
</d:mkcol>"#;
|
||||
client.mkcol(&book, Some(mkbook)).expect("mkcol addressbook");
|
||||
client
|
||||
.mkcol(&book, Some(mkbook))
|
||||
.expect("mkcol addressbook");
|
||||
|
||||
let uid = "apple-item-labels-1";
|
||||
let vcard = format!(
|
||||
@@ -425,7 +430,14 @@ fn baikal_carddav_preserves_apple_item_labels() {
|
||||
drop(conn);
|
||||
eprintln!("archived JSContact:\n{data}");
|
||||
|
||||
for needle in ["x-abdate", "x-ablabel", "20171111", "20111111", "Name1", "Name2"] {
|
||||
for needle in [
|
||||
"x-abdate",
|
||||
"x-ablabel",
|
||||
"20171111",
|
||||
"20111111",
|
||||
"Name1",
|
||||
"Name2",
|
||||
] {
|
||||
assert!(
|
||||
data.to_lowercase().contains(&needle.to_lowercase()),
|
||||
"Baikal CardDAV import dropped {needle:?}; stored: {data}"
|
||||
|
||||
@@ -89,8 +89,8 @@ fn webdav_starts_seeds_and_imports() {
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
let expected_dirs = account.layout.files.iter().filter(|s| s.directory).count() + 1;
|
||||
let expected_files = account.layout.files.len() - (expected_dirs - 1);
|
||||
let expected_dirs = account.layout.files.iter().filter(|s| s.directory).count();
|
||||
let expected_files = account.layout.files.len() - expected_dirs;
|
||||
|
||||
assert_eq!(
|
||||
files as usize, expected_files,
|
||||
@@ -99,16 +99,43 @@ fn webdav_starts_seeds_and_imports() {
|
||||
);
|
||||
assert_eq!(
|
||||
dirs as usize, expected_dirs,
|
||||
"{}: directory count mismatch (seeded layout dirs + account root = {expected_dirs}, imported {dirs})",
|
||||
"{}: directory count mismatch (seeded layout dirs = {expected_dirs}, imported {dirs}); the account root is a virtual mount point, not a node (issue #18)",
|
||||
seed.username
|
||||
);
|
||||
assert_eq!(
|
||||
nodes,
|
||||
account.layout.files.len() + 1,
|
||||
"{}: total file_nodes mismatch (layout + account root)",
|
||||
account.layout.files.len(),
|
||||
"{}: total file_nodes mismatch (layout only, no synthetic account-root node)",
|
||||
seed.username
|
||||
);
|
||||
|
||||
let admin_root_nodes: i64 = conn
|
||||
.query_row(
|
||||
"SELECT count(*) FROM file_nodes WHERE name = ?1",
|
||||
[&account.username],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
admin_root_nodes, 0,
|
||||
"{}: the account root must not be materialised as a directory named after the account (issue #18)",
|
||||
seed.username
|
||||
);
|
||||
for spec in account.layout.files.iter().filter(|s| s.parent.is_none()) {
|
||||
let parent_id: Option<i64> = conn
|
||||
.query_row(
|
||||
"SELECT parent_id FROM file_nodes WHERE name = ?1",
|
||||
[spec.name],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
parent_id, None,
|
||||
"{}: top-level node {} must map to the target's implicit root (NULL parent), not nest under an account-root directory (issue #18)",
|
||||
seed.username, spec.name
|
||||
);
|
||||
}
|
||||
|
||||
for spec in account.layout.files {
|
||||
let segments = layout_segments(account.layout.files, spec.key);
|
||||
let mut probes: Vec<Vec<&str>> = Vec::new();
|
||||
|
||||
@@ -984,6 +984,173 @@ fn webdav_discovery_keeps_only_self_row_as_root() {
|
||||
assert!(disc.collections[0].href.as_str().ends_with("/files/u/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn webdav_root_collection_is_not_materialised_children_map_to_target_root() {
|
||||
use rusqlite::Connection;
|
||||
use vandelay::dav::discover::DiscoveredCollection;
|
||||
use vandelay::dav::href::Href;
|
||||
use vandelay::dav::parse::ResourceProps;
|
||||
use vandelay::db;
|
||||
use vandelay::db::sources::SourceKey;
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::TypeCounts;
|
||||
use vandelay::sync::import_dav::tree::{WebDavCtx, reconcile_filenodes};
|
||||
|
||||
let mut server = mockito::Server::new();
|
||||
let url = server.url();
|
||||
|
||||
let root_body = format!(
|
||||
r#"<?xml version="1.0"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>{url}/dav/file/u/</d:href>
|
||||
<d:propstat>
|
||||
<d:prop><d:resourcetype><d:collection/></d:resourcetype><d:displayname>System administrator</d:displayname></d:prop>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
<d:response>
|
||||
<d:href>{url}/dav/file/u/sub/</d:href>
|
||||
<d:propstat>
|
||||
<d:prop><d:resourcetype><d:collection/></d:resourcetype><d:displayname>sub</d:displayname></d:prop>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
<d:response>
|
||||
<d:href>{url}/dav/file/u/top.txt</d:href>
|
||||
<d:propstat>
|
||||
<d:prop><d:resourcetype/><d:getcontenttype>text/plain</d:getcontenttype><d:getetag>"t1"</d:getetag></d:prop>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
</d:multistatus>"#
|
||||
);
|
||||
let sub_body = format!(
|
||||
r#"<?xml version="1.0"?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>{url}/dav/file/u/sub/</d:href>
|
||||
<d:propstat>
|
||||
<d:prop><d:resourcetype><d:collection/></d:resourcetype><d:displayname>sub</d:displayname></d:prop>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
<d:response>
|
||||
<d:href>{url}/dav/file/u/sub/inner.txt</d:href>
|
||||
<d:propstat>
|
||||
<d:prop><d:resourcetype/><d:getcontenttype>text/plain</d:getcontenttype><d:getetag>"i1"</d:getetag></d:prop>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
</d:propstat>
|
||||
</d:response>
|
||||
</d:multistatus>"#
|
||||
);
|
||||
let _root = multistatus_response(&mut server, "PROPFIND", "/dav/file/u/", &root_body);
|
||||
let _sub = multistatus_response(&mut server, "PROPFIND", "/dav/file/u/sub/", &sub_body);
|
||||
let _f1 = server
|
||||
.mock("GET", "/dav/file/u/top.txt")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "text/plain")
|
||||
.with_body("toplevel\n")
|
||||
.create();
|
||||
let _f2 = server
|
||||
.mock("GET", "/dav/file/u/sub/inner.txt")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "text/plain")
|
||||
.with_body("inner\n")
|
||||
.create();
|
||||
|
||||
let mut conn = Connection::open_in_memory().unwrap();
|
||||
db::init::apply_schema(&conn).unwrap();
|
||||
let source_id = db::sources::upsert_source(
|
||||
&conn,
|
||||
&SourceKey {
|
||||
kind: "webdav".to_owned(),
|
||||
session_url: url.clone(),
|
||||
account_id: format!("{url}/dav/file/u/"),
|
||||
},
|
||||
Some("u"),
|
||||
"u",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let root = DiscoveredCollection {
|
||||
url: format!("{url}/dav/file/u/"),
|
||||
href: Href::from_normalised("/dav/file/u/".to_owned()),
|
||||
props: ResourceProps {
|
||||
is_collection: true,
|
||||
displayname: Some("System administrator".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
let c = client(0);
|
||||
let ctx = WebDavCtx {
|
||||
client: &c,
|
||||
source_id,
|
||||
base_url: &url,
|
||||
dav_connections: 2,
|
||||
logger: Logger::from_flags(false, 0),
|
||||
};
|
||||
let mut counts = TypeCounts::default();
|
||||
reconcile_filenodes(&mut conn, &ctx, &root, &mut counts).expect("reconcile");
|
||||
|
||||
let total: i64 = conn
|
||||
.query_row("SELECT count(*) FROM file_nodes", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
total, 3,
|
||||
"only sub/, top.txt and inner.txt land; the root collection is a virtual mount point, not a node"
|
||||
);
|
||||
|
||||
let admin_named: i64 = conn
|
||||
.query_row(
|
||||
"SELECT count(*) FROM file_nodes WHERE name = 'System administrator'",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
admin_named, 0,
|
||||
"the account display name must not become a directory (issue #18)"
|
||||
);
|
||||
|
||||
let top_parent: Option<i64> = conn
|
||||
.query_row(
|
||||
"SELECT parent_id FROM file_nodes WHERE name = 'top.txt'",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
top_parent, None,
|
||||
"a file at the root maps to the target's implicit root (NULL parent)"
|
||||
);
|
||||
|
||||
let (sub_id, sub_parent): (i64, Option<i64>) = conn
|
||||
.query_row(
|
||||
"SELECT id, parent_id FROM file_nodes WHERE name = 'sub'",
|
||||
[],
|
||||
|r| Ok((r.get(0)?, r.get(1)?)),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
sub_parent, None,
|
||||
"a directory at the root maps to the target's implicit root (NULL parent)"
|
||||
);
|
||||
|
||||
let inner_parent: Option<i64> = conn
|
||||
.query_row(
|
||||
"SELECT parent_id FROM file_nodes WHERE name = 'inner.txt'",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
inner_parent,
|
||||
Some(sub_id),
|
||||
"a nested file keeps its real parent directory"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn streaming_propfind_strips_control_chars_before_parse() {
|
||||
let mut server = mockito::Server::new();
|
||||
|
||||
Reference in New Issue
Block a user