use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use directories::ProjectDirs;
use reqwest::Client;
use serde_json::Value;
use tempfile::TempDir;
use tokio::runtime::Runtime;
use url::Url;
use walkdir::WalkDir;
#[derive(Clone, Debug)]
struct E2eConfig {
bin: PathBuf,
profile: Option<String>,
envs: Vec<(String, String)>,
space: String,
}
impl E2eConfig {
fn command(&self) -> Command {
let mut command = Command::new(&self.bin);
if let Some(profile) = &self.profile {
command.arg("--profile").arg(profile);
}
for (key, value) in &self.envs {
command.env(key, value);
}
command
}
fn run(&self, args: &[&str]) -> String {
let output = self.command().args(args).output().expect("command to run");
if !output.status.success() {
panic!(
"command failed: {:?}\nstatus: {}\nstdout:\n{}\nstderr:\n{}",
args,
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
String::from_utf8(output.stdout).expect("utf8 stdout")
}
fn run_json(&self, args: &[&str]) -> Value {
let mut all_args = Vec::with_capacity(args.len() + 1);
all_args.push("--json");
all_args.extend_from_slice(args);
let stdout = self.run(&all_args);
serde_json::from_str(&stdout).unwrap_or_else(|err| {
panic!(
"failed to parse JSON output for {:?}: {err}\n{stdout}",
args
)
})
}
fn best_effort(&self, args: &[&str]) {
let _ = self.command().args(args).output();
}
}
#[derive(Debug)]
struct Cleanup {
cfg: E2eConfig,
page_id: Option<String>,
extra_page_ids: Vec<String>,
blog_id: Option<String>,
comment_id: Option<String>,
attachment_id: Option<String>,
}
impl Cleanup {
fn new(cfg: E2eConfig) -> Self {
Self {
cfg,
page_id: None,
extra_page_ids: Vec::new(),
blog_id: None,
comment_id: None,
attachment_id: None,
}
}
}
impl Drop for Cleanup {
fn drop(&mut self) {
if let (Some(page_id), Some(attachment_id)) =
(self.page_id.as_deref(), self.attachment_id.as_deref())
{
self.cfg
.best_effort(&["attachment", "delete", page_id, attachment_id]);
}
if let Some(comment_id) = self.comment_id.as_deref() {
self.cfg.best_effort(&["comment", "delete", comment_id]);
}
if let Some(blog_id) = self.blog_id.as_deref() {
self.cfg.best_effort(&["blog", "delete", blog_id]);
}
if let Some(page_id) = self.page_id.as_deref() {
self.cfg.best_effort(&["page", "delete", page_id]);
}
for page_id in self.extra_page_ids.iter().rev() {
self.cfg.best_effort(&["page", "delete", page_id]);
}
}
}
fn e2e_config() -> Option<E2eConfig> {
let bin = find_binary_path();
let space = env::var("CONFLUENCE_E2E_SPACE").unwrap_or_else(|_| "TEST".to_string());
if let Some(profile) = env::var("CONFLUENCE_E2E_PROFILE")
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
{
return Some(E2eConfig {
bin,
profile: Some(profile),
envs: Vec::new(),
space,
});
}
let base_url = env::var("CONFLUENCE_E2E_BASE_URL").ok();
let token = env::var("CONFLUENCE_E2E_TOKEN")
.ok()
.or_else(|| env::var("CONFLUENCE_E2E_BEARER_TOKEN").ok());
if base_url.is_none() || token.is_none() {
return None;
}
let auth_type = env::var("CONFLUENCE_E2E_AUTH_TYPE").unwrap_or_else(|_| "bearer".to_string());
let provider =
env::var("CONFLUENCE_E2E_PROVIDER").unwrap_or_else(|_| "data-center".to_string());
let api_path = env::var("CONFLUENCE_E2E_API_PATH").unwrap_or_else(|_| "/rest/api".to_string());
let mut envs = vec![
("CONFLUENCE_PROFILE".to_string(), "__e2e_env__".to_string()),
(
"CONFLUENCE_DOMAIN".to_string(),
base_url.expect("base url present"),
),
("CONFLUENCE_PROVIDER".to_string(), provider),
("CONFLUENCE_AUTH_TYPE".to_string(), auth_type.clone()),
("CONFLUENCE_API_PATH".to_string(), api_path),
];
match auth_type.as_str() {
"basic" => {
let username = env::var("CONFLUENCE_E2E_USERNAME")
.or_else(|_| env::var("CONFLUENCE_E2E_EMAIL"))
.expect(
"basic auth e2e mode requires CONFLUENCE_E2E_USERNAME or CONFLUENCE_E2E_EMAIL",
);
envs.push(("CONFLUENCE_USERNAME".to_string(), username));
envs.push((
"CONFLUENCE_API_TOKEN".to_string(),
token.expect("token present"),
));
}
_ => {
envs.push((
"CONFLUENCE_BEARER_TOKEN".to_string(),
token.expect("token present"),
));
}
}
Some(E2eConfig {
bin,
profile: None,
envs,
space,
})
}
#[derive(Clone, Debug)]
enum TestAuth {
Basic { username: String, token: String },
Bearer { token: String },
}
#[derive(Clone, Debug)]
struct TestHttpConfig {
base_url: String,
api_path: String,
auth: TestAuth,
}
fn http_config(cfg: &E2eConfig) -> TestHttpConfig {
if let Some(profile_name) = &cfg.profile {
let dirs = ProjectDirs::from("dev", "ruben", "confluence-cli")
.expect("config directory available");
let config_path = dirs.config_dir().join("config.json");
let config_raw = fs::read_to_string(&config_path)
.unwrap_or_else(|err| panic!("failed to read {}: {err}", config_path.display()));
let config: Value = serde_json::from_str(&config_raw)
.unwrap_or_else(|err| panic!("failed to parse {}: {err}", config_path.display()));
let profile = config
.get("profiles")
.and_then(|profiles| profiles.get(profile_name))
.unwrap_or_else(|| {
panic!(
"profile `{profile_name}` not found in {}",
config_path.display()
)
});
let auth = match profile
.get("auth")
.and_then(|auth| auth.get("type"))
.and_then(Value::as_str)
.expect("auth type")
{
"basic" => TestAuth::Basic {
username: string_field(profile.get("auth").expect("auth"), "username").to_string(),
token: string_field(profile.get("auth").expect("auth"), "token").to_string(),
},
"bearer" => TestAuth::Bearer {
token: string_field(profile.get("auth").expect("auth"), "token").to_string(),
},
other => panic!("unsupported auth type in profile: {other}"),
};
return TestHttpConfig {
base_url: string_field(profile, "base_url").to_string(),
api_path: string_field(profile, "api_path").to_string(),
auth,
};
}
let env_value = |key: &str| {
cfg.envs
.iter()
.find(|(name, _)| name == key)
.map(|(_, value)| value.clone())
};
let auth = if let Some(token) = env_value("CONFLUENCE_BEARER_TOKEN") {
TestAuth::Bearer { token }
} else {
TestAuth::Basic {
username: env_value("CONFLUENCE_USERNAME").expect("env username"),
token: env_value("CONFLUENCE_API_TOKEN").expect("env token"),
}
};
TestHttpConfig {
base_url: env_value("CONFLUENCE_DOMAIN").expect("env base url"),
api_path: env_value("CONFLUENCE_API_PATH").expect("env api path"),
auth,
}
}
fn current_user_placeholder(cfg: &E2eConfig) -> String {
let http = http_config(cfg);
let url = format!(
"{}{}{}",
http.base_url.trim_end_matches('/'),
http.api_path.trim_end_matches('/'),
"/user/current"
);
let runtime = Runtime::new().expect("tokio runtime");
let current_user = runtime.block_on(async move {
let client = Client::new();
let request = match http.auth {
TestAuth::Basic { username, token } => {
client.get(&url).basic_auth(username, Some(token))
}
TestAuth::Bearer { token } => client.get(&url).bearer_auth(token),
};
let response = request.send().await.expect("current user request");
let response = response
.error_for_status()
.unwrap_or_else(|err| panic!("current user request failed for {url}: {err}"));
response
.json::<Value>()
.await
.expect("current user json response")
});
let mut placeholder = Url::parse("confluence-user://user").expect("valid user placeholder");
{
let mut pairs = placeholder.query_pairs_mut();
if let Some(account_id) = current_user.get("accountId").and_then(Value::as_str) {
pairs.append_pair("account-id", account_id);
}
if let Some(user_key) = current_user.get("userKey").and_then(Value::as_str) {
pairs.append_pair("userkey", user_key);
}
if let Some(username) = current_user.get("username").and_then(Value::as_str) {
pairs.append_pair("username", username);
}
}
placeholder.to_string()
}
fn expected_user_resource_fragment(user_placeholder: &str) -> String {
let placeholder = Url::parse(user_placeholder).expect("valid user placeholder url");
for (key, value) in placeholder.query_pairs() {
match key.as_ref() {
"account-id" => {
return format!(r#"ri:account-id="{}""#, value);
}
"userkey" => {
return format!(r#"ri:userkey="{}""#, value);
}
"username" => {
return format!(r#"ri:username="{}""#, value);
}
_ => {}
}
}
panic!("expected at least one user identifier in {user_placeholder}");
}
fn expected_user_placeholder_fragment(user_placeholder: &str) -> String {
let placeholder = Url::parse(user_placeholder).expect("valid user placeholder url");
for pair in placeholder.query().unwrap_or_default().split('&') {
if pair.starts_with("account-id=")
|| pair.starts_with("userkey=")
|| pair.starts_with("username=")
{
return pair.to_string();
}
}
panic!("expected at least one user identifier in {user_placeholder}");
}
fn find_binary_path() -> PathBuf {
if let Some(path) = env::var_os("CONFLUENCE_CLI_BIN") {
return PathBuf::from(path);
}
if let Some(path) = env::var_os("CARGO_BIN_EXE_confluence-cli") {
return PathBuf::from(path);
}
let current_exe = env::current_exe().expect("current exe path");
for ancestor in current_exe.ancestors() {
let candidate = ancestor.join("confluence-cli");
if candidate.is_file() {
return candidate;
}
let exe_candidate = ancestor.join("confluence-cli.exe");
if exe_candidate.is_file() {
return exe_candidate;
}
}
panic!(
"failed to locate confluence-cli binary from {}",
current_exe.display()
);
}
fn first_item<'a>(value: &'a Value, context: &str) -> &'a Value {
value
.as_array()
.and_then(|items| items.first())
.unwrap_or_else(|| panic!("expected non-empty array for {context}: {value}"))
}
fn string_field<'a>(value: &'a Value, key: &str) -> &'a str {
value
.get(key)
.and_then(Value::as_str)
.unwrap_or_else(|| panic!("expected string field `{key}` in {value}"))
}
fn u64_field(value: &Value, key: &str) -> u64 {
value
.get(key)
.and_then(Value::as_u64)
.unwrap_or_else(|| panic!("expected u64 field `{key}` in {value}"))
}
fn unique_name(prefix: &str) -> String {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock after epoch")
.as_millis();
format!("{prefix} {millis}")
}
fn find_index_md(root: &PathBuf) -> PathBuf {
WalkDir::new(root)
.into_iter()
.filter_map(Result::ok)
.find(|entry| entry.file_type().is_file() && entry.file_name() == "index.md")
.map(|entry| entry.into_path())
.unwrap_or_else(|| panic!("failed to find index.md under {}", root.display()))
}
fn find_index_md_by_title(root: &PathBuf, title: &str) -> PathBuf {
WalkDir::new(root)
.into_iter()
.filter_map(Result::ok)
.find_map(|entry| {
if !entry.file_type().is_file() || entry.file_name() != "index.md" {
return None;
}
let contents = fs::read_to_string(entry.path()).ok()?;
contents
.contains(&format!("title: {title}\n"))
.then(|| entry.into_path())
})
.unwrap_or_else(|| {
panic!(
"failed to find index.md for title `{title}` under {}",
root.display()
)
})
}
fn wait_until<F>(timeout: Duration, interval: Duration, mut check: F) -> bool
where
F: FnMut() -> bool,
{
let start = std::time::Instant::now();
loop {
if check() {
return true;
}
if start.elapsed() >= timeout {
return false;
}
thread::sleep(interval);
}
}
#[test]
#[ignore = "requires a real Confluence instance"]
fn e2e_cli_lifecycle() {
let Some(cfg) = e2e_config() else {
eprintln!(
"Skipping e2e_cli_lifecycle: set CONFLUENCE_E2E_PROFILE or CONFLUENCE_E2E_BASE_URL / CONFLUENCE_E2E_TOKEN"
);
return;
};
let temp = TempDir::new().expect("tempdir");
let page_body_1 = temp.path().join("page-body-1.md");
let page_body_2 = temp.path().join("page-body-2.md");
let blog_body_1 = temp.path().join("blog-body-1.md");
let blog_body_2 = temp.path().join("blog-body-2.md");
let comment_body = temp.path().join("comment.md");
let attachment_file = temp.path().join("attachment.txt");
let download_path = temp.path().join("downloaded.txt");
let pull_dir = temp.path().join("pull");
let untouched_pull_dir = temp.path().join("untouched-pull");
let fresh_links_dir = temp.path().join("fresh-links");
let macro_tree_dir = temp.path().join("macro-tree");
let macro_pull_dir = temp.path().join("macro-pull");
fs::write(
&page_body_1,
"# E2E Scratch\n\nInitial page body from the e2e lifecycle.\n",
)
.expect("write page body 1");
fs::write(
&page_body_2,
"# E2E Scratch\n\nUpdated page body from the e2e lifecycle.\n",
)
.expect("write page body 2");
fs::write(
&blog_body_1,
"# E2E Blog\n\nInitial blog body from the e2e lifecycle.\n",
)
.expect("write blog body 1");
fs::write(
&blog_body_2,
"# E2E Blog\n\nUpdated blog body from the e2e lifecycle.\n",
)
.expect("write blog body 2");
fs::write(&comment_body, "CLI verification comment\n").expect("write comment body");
fs::write(&attachment_file, "attachment payload from rust e2e\n")
.expect("write attachment file");
let page_title = unique_name("E2E Page");
let updated_page_title = format!("{page_title} Updated");
let blog_title = unique_name("E2E Blog");
let updated_blog_title = format!("{blog_title} Updated");
let mut cleanup = Cleanup::new(cfg.clone());
let space_list = cfg.run_json(&["space", "list"]);
assert!(
space_list.as_array().is_some_and(|items| items
.iter()
.any(|item| string_field(item, "key") == cfg.space)),
"expected space list to include {}: {space_list}",
cfg.space
);
let page_body_1_arg = page_body_1.to_string_lossy().into_owned();
let page_create = cfg.run_json(&[
"page",
"create",
&page_title,
cfg.space.as_str(),
"--body-file",
&page_body_1_arg,
]);
let page_id = string_field(first_item(&page_create, "page create"), "id").to_string();
cleanup.page_id = Some(page_id.clone());
let page_get = cfg.run_json(&["page", "get", &page_id, "--show-body"]);
assert_eq!(
string_field(first_item(&page_get, "page get"), "id"),
page_id
);
let found_search_hit = wait_until(Duration::from_secs(30), Duration::from_secs(2), || {
let search = cfg.run_json(&["search", &page_title, "--limit", "10"]);
search.as_array().is_some_and(|items| {
items
.iter()
.any(|item| item.get("id").and_then(Value::as_str) == Some(page_id.as_str()))
})
});
assert!(found_search_hit, "expected search to find page {page_id}");
let page_body_2_arg = page_body_2.to_string_lossy().into_owned();
let page_update = cfg.run_json(&[
"page",
"update",
&page_id,
"--title",
&updated_page_title,
"--body-file",
&page_body_2_arg,
]);
assert_eq!(
string_field(first_item(&page_update, "page update"), "title"),
updated_page_title
);
cfg.run(&["label", "add", &page_id, "e2e-auto"]);
let labels = cfg.run_json(&["label", "list", &page_id]);
assert!(
labels
.as_array()
.is_some_and(|items| items.iter().any(|item| item.as_str() == Some("e2e-auto"))),
"expected label list to contain e2e-auto: {labels}"
);
cfg.run(&["label", "remove", &page_id, "e2e-auto"]);
let property_set = cfg.run_json(&[
"property",
"set",
&page_id,
"e2e_verify",
r#"{"ok":true,"n":1}"#,
]);
assert_eq!(
string_field(first_item(&property_set, "property set"), "key"),
"e2e_verify"
);
let property_get = cfg.run_json(&["property", "get", &page_id, "e2e_verify"]);
assert_eq!(
first_item(&property_get, "property get")
.get("value")
.and_then(|value| value.get("ok"))
.and_then(Value::as_bool),
Some(true)
);
cfg.run(&["property", "delete", &page_id, "e2e_verify"]);
let comment_body_arg = comment_body.to_string_lossy().into_owned();
let comment_add = cfg.run_json(&["comment", "add", &page_id, "--body-file", &comment_body_arg]);
let comment_id = string_field(first_item(&comment_add, "comment add"), "id").to_string();
cleanup.comment_id = Some(comment_id.clone());
let comments = cfg.run_json(&["comment", "list", &page_id]);
assert!(
comments.as_array().is_some_and(|items| items
.iter()
.any(|item| item.get("id").and_then(Value::as_str) == Some(comment_id.as_str()))),
"expected comment list to contain {comment_id}: {comments}"
);
cfg.run(&["comment", "delete", &comment_id]);
cleanup.comment_id = None;
let attachment_arg = attachment_file.to_string_lossy().into_owned();
let attachment_upload = cfg.run_json(&[
"attachment",
"upload",
&page_id,
"--file",
&attachment_arg,
"--comment",
"e2e upload",
]);
let attachment_id =
string_field(first_item(&attachment_upload, "attachment upload"), "id").to_string();
cleanup.attachment_id = Some(attachment_id.clone());
let attachments = cfg.run_json(&["attachment", "list", &page_id]);
assert!(
attachments.as_array().is_some_and(|items| {
items
.iter()
.any(|item| item.get("id").and_then(Value::as_str) == Some(attachment_id.as_str()))
}),
"expected attachment list to contain {attachment_id}: {attachments}"
);
let download_arg = download_path.to_string_lossy().into_owned();
cfg.run(&[
"attachment",
"download",
&page_id,
&attachment_id,
&download_arg,
]);
assert_eq!(
fs::read_to_string(&attachment_file).expect("read uploaded attachment"),
fs::read_to_string(&download_path).expect("read downloaded attachment")
);
cfg.run(&["attachment", "delete", &page_id, &attachment_id]);
cleanup.attachment_id = None;
let page_before_untouched_pull = cfg.run_json(&["page", "get", &page_id, "--show-body"]);
let untouched_version = u64_field(
first_item(&page_before_untouched_pull, "page get"),
"version",
);
let untouched_pull_arg = untouched_pull_dir.to_string_lossy().into_owned();
cfg.run(&["pull", "page", &page_id, &untouched_pull_arg]);
let untouched_plan_before = cfg.run_json(&["plan", &untouched_pull_arg]);
assert!(
untouched_plan_before
.get("items")
.and_then(Value::as_array)
.is_some_and(|items| {
!items.is_empty()
&& items
.iter()
.all(|item| item.get("action").and_then(Value::as_str) == Some("noop"))
}),
"expected untouched pull plan to be noop: {untouched_plan_before}"
);
let untouched_apply = cfg.run_json(&["apply", &untouched_pull_arg]);
assert!(
untouched_apply
.get("items")
.and_then(Value::as_array)
.is_some_and(|items| {
!items.is_empty()
&& items
.iter()
.all(|item| item.get("action").and_then(Value::as_str) == Some("noop"))
}),
"expected untouched apply to be noop: {untouched_apply}"
);
let page_after_untouched_apply = cfg.run_json(&["page", "get", &page_id, "--show-body"]);
let untouched_version_after = u64_field(
first_item(&page_after_untouched_apply, "page get"),
"version",
);
assert_eq!(
untouched_version_after, untouched_version,
"expected untouched apply to preserve remote version"
);
let pull_arg = pull_dir.to_string_lossy().into_owned();
cfg.run(&["pull", "page", &page_id, &pull_arg]);
let index_md = find_index_md(&pull_dir);
let original_markdown = fs::read_to_string(&index_md).expect("read pulled markdown");
let updated_markdown = format!("{original_markdown}\nApplied from Rust e2e.\n");
fs::write(&index_md, updated_markdown).expect("update pulled markdown");
let plan_before = cfg.run_json(&["plan", &pull_arg]);
assert!(
plan_before
.get("items")
.and_then(Value::as_array)
.is_some_and(|items| {
items.iter().any(|item| {
item.get("action").and_then(Value::as_str) == Some("update_content")
})
}),
"expected plan to include update_content: {plan_before}"
);
let _apply = cfg.run_json(&["apply", &pull_arg]);
let plan_after = cfg.run_json(&["plan", &pull_arg]);
assert!(
plan_after
.get("items")
.and_then(Value::as_array)
.is_some_and(|items| {
!items.is_empty()
&& items
.iter()
.all(|item| item.get("action").and_then(Value::as_str) == Some("noop"))
}),
"expected post-apply plan to be noop: {plan_after}"
);
let fresh_source_title = unique_name("E2E Fresh Source");
let fresh_target_title = unique_name("E2E Fresh Target");
let macro_root_title = unique_name("E2E Macro Root");
let macro_source_title = unique_name("E2E Macro Source");
let macro_target_title = unique_name("E2E Macro Target");
let macro_child_title = unique_name("E2E Macro Child");
let fresh_source_dir = fresh_links_dir.join("source");
let fresh_target_dir = fresh_links_dir.join("target");
fs::create_dir_all(&fresh_source_dir).expect("create fresh source dir");
fs::create_dir_all(&fresh_target_dir).expect("create fresh target dir");
fs::write(
fresh_source_dir.join("index.md"),
format!(
"---\ntitle: {fresh_source_title}\ntype: page\nlabels: []\nstatus: current\nparent: null\nproperties: {{}}\n---\n\n[Go to target](../target/index.md#intro)\n"
),
)
.expect("write fresh source markdown");
fs::write(
fresh_target_dir.join("index.md"),
format!(
"---\ntitle: {fresh_target_title}\ntype: page\nlabels: []\nstatus: current\nparent: null\nproperties: {{}}\n---\n\n# Intro\n\nFresh target page.\n"
),
)
.expect("write fresh target markdown");
fs::write(
fresh_source_dir.join(".confluence.json"),
format!("{{\n \"space_key\": \"{}\"\n}}\n", cfg.space),
)
.expect("write fresh source sidecar");
fs::write(
fresh_target_dir.join(".confluence.json"),
format!("{{\n \"space_key\": \"{}\"\n}}\n", cfg.space),
)
.expect("write fresh target sidecar");
let fresh_links_arg = fresh_links_dir.to_string_lossy().into_owned();
let fresh_apply = cfg.run_json(&["apply", &fresh_links_arg]);
let fresh_items = fresh_apply
.get("items")
.and_then(Value::as_array)
.expect("fresh apply items");
let fresh_source_id = fresh_items
.iter()
.find(|item| {
item.get("action").and_then(Value::as_str) == Some("create_content")
&& item.get("title").and_then(Value::as_str) == Some(fresh_source_title.as_str())
})
.and_then(|item| item.get("content_id"))
.and_then(Value::as_str)
.expect("fresh source content id")
.to_string();
let fresh_target_id = fresh_items
.iter()
.find(|item| {
item.get("action").and_then(Value::as_str) == Some("create_content")
&& item.get("title").and_then(Value::as_str) == Some(fresh_target_title.as_str())
})
.and_then(|item| item.get("content_id"))
.and_then(Value::as_str)
.expect("fresh target content id")
.to_string();
cleanup.extra_page_ids.push(fresh_source_id.clone());
cleanup.extra_page_ids.push(fresh_target_id.clone());
let fresh_source_get = cfg.run_json(&["page", "get", &fresh_source_id, "--show-body"]);
let fresh_source_body = string_field(
first_item(&fresh_source_get, "fresh source get"),
"body_storage",
);
assert!(
fresh_source_body.contains(&format!("pageId={fresh_target_id}#intro")),
"expected fresh source body to link to target id {fresh_target_id}: {fresh_source_body}"
);
let fresh_plan = cfg.run_json(&["plan", &fresh_links_arg]);
assert!(
fresh_plan
.get("items")
.and_then(Value::as_array)
.is_some_and(|items| {
!items.is_empty()
&& items
.iter()
.all(|item| item.get("action").and_then(Value::as_str) == Some("noop"))
}),
"expected fresh-links plan to be noop: {fresh_plan}"
);
let macro_root_dir = macro_tree_dir.join("macro-root");
let macro_source_dir = macro_root_dir.join("source");
let macro_target_dir = macro_root_dir.join("target");
let macro_child_dir = macro_source_dir.join("child");
let macro_source_attachments_dir = macro_source_dir.join("attachments");
let macro_target_attachments_dir = macro_target_dir.join("attachments");
let macro_user = current_user_placeholder(&cfg);
let macro_user_fragment = expected_user_resource_fragment(¯o_user);
let macro_user_placeholder_fragment = expected_user_placeholder_fragment(¯o_user);
fs::create_dir_all(¯o_root_dir).expect("create macro root dir");
fs::create_dir_all(¯o_source_dir).expect("create macro source dir");
fs::create_dir_all(¯o_target_dir).expect("create macro target dir");
fs::create_dir_all(¯o_child_dir).expect("create macro child dir");
fs::create_dir_all(¯o_source_attachments_dir).expect("create macro source attachments dir");
fs::create_dir_all(¯o_target_attachments_dir).expect("create macro target attachments dir");
fs::write(
macro_source_attachments_dir.join("preview.pdf"),
"preview pdf payload\n",
)
.expect("write macro source preview attachment");
fs::write(
macro_source_attachments_dir.join("sheet.xlsx"),
"spreadsheet payload\n",
)
.expect("write macro source xls attachment");
fs::write(
macro_source_attachments_dir.join("slides.pptx"),
"slides payload\n",
)
.expect("write macro source ppt attachment");
fs::write(
macro_target_attachments_dir.join("manual.docx"),
"manual payload\n",
)
.expect("write macro target doc attachment");
fs::write(
macro_root_dir.join("index.md"),
format!(
"---\ntitle: {macro_root_title}\ntype: page\nlabels: []\nstatus: current\nparent: null\nproperties: {{}}\n---\n\n# Macro Root\n"
),
)
.expect("write macro root markdown");
fs::write(
macro_source_dir.join("index.md"),
format!(
"---\ntitle: {macro_source_title}\ntype: page\nlabels: []\nstatus: current\nparent: null\nproperties: {{}}\n---\n\n# Macro Source\n\n:::confluence-anchor\nname: macro-source-anchor\n:::\n\n:::confluence-navmap\n$default: Macro Root,Macro Source,Shared Excerpt\ntitle: Macro navigation\nwrapAfter: 4\n:::\n\n:::confluence-excerpt-include\nnopanel: true\npage: ../target/index.md\n:::\n\n:::confluence-include-page\npage: ../target/index.md\n:::\n\n:::confluence-page-tree\nroot: index.md\nsearchBox: true\n:::\n\n:::confluence-page-tree-search\nroot: ../target/index.md\nspaceKey: {space}\n:::\n\n:::confluence-content-by-label\ncql: label = \"e2e-macro-target\"\nmaxResults: 5\n:::\n\n:::confluence-content-by-user\nuser: {macro_user}\n:::\n\n:::confluence-content-report-table\nlabels: e2e-macro-target\nspaces: {space}\nmaxResults: 5\n:::\n\n:::confluence-search\nspacekey: !space {space}\ncontributor: !user {macro_user}\nquery: Macro Source\n:::\n\n:::confluence-task-report\nspaceAndPage: {space}\nlabels: e2e-macro-target\nstatus: incomplete\npageSize: 20\ncolumns: description,assignee,location\nsortBy: page title\nreverseSort: false\n:::\n\n:::confluence-macro userlister\ncontributor: !user {macro_user}\nrelated: !page-link ../target/index.md\n:::\n\n:::confluence-content-properties-report\nlabel: e2e-content-properties\nid: decision\n:::\n\n:::confluence-attachments\npatterns: *.pdf\nsortBy: name\n:::\n\n:::confluence-view-file\nattachment: preview.pdf\n:::\n\n:::confluence-view-doc\npage: ../target/index.md\nattachment: manual.docx\n:::\n\n:::confluence-view-xls\nattachment: sheet.xlsx\n:::\n\n:::confluence-view-ppt\nattachment: slides.pptx\n:::\n\n:::confluence-blog-posts\nauthor: {macro_user}\nspaces: {space}\nmax: 5\ntime: 7\n:::\n\n:::confluence-contributors\nspaces: {space},@personal\nlabels: e2e-macro-target\nmode: list\n:::\n\n:::confluence-contributors-summary\nspaces: {space}\ncolumns: edits,comments,labels\nlimit: 10\n:::\n\n:::confluence-recently-updated\nauthor: {macro_user}\nspaces: {space}\nmax: 10\n:::\n\n:::confluence-recently-updated-dashboard\nspaces: {space}\nlimit: 10\ntheme: concise\n:::\n\n:::confluence-livesearch\nspaceKey: {space}\nlabels: e2e-macro-target\nsize: large\n:::\n\n:::confluence-page-index\n:::\n\n:::confluence-toc-zone\nlocation: top\nmaxLevel: 3\n---\n## Zoned Heading\n\nOnly this section counts.\n:::\n\n:::confluence-labels-list\nspaceKey: {space}\nexcludedLabels: drafts,test\n:::\n\n:::confluence-popular-labels\nspaceKey: {space}\ncount: 25\nstyle: heatmap\n:::\n\n:::confluence-related-labels\nlabels: e2e-macro-target\n:::\n\n:::confluence-recently-used-labels\nscope: space\nstyle: cloud\n:::\n\n:::confluence-gallery\nsortBy: name\ncolumns: 2\n:::\n\n:::confluence-favorite-pages\n:::\n\n:::confluence-change-history\n:::\n\n:::confluence-spaces-list\nscope: all\nwidth: 80%\n:::\n\n:::confluence-space-details\nwidth: 50%\n:::\n\n:::confluence-space-attachments\nspace: {space}\nshowFilter: false\n:::\n\n~~~confluence-noformat\nnopanel: true\n---\n<xml>literal</xml>\nline 2\n~~~\n\n:::confluence-profile\nuser: {macro_user}\n:::\n\n:::confluence-status-list\nusername: {macro_user}\n:::\n\n:::confluence-network\nmode: followers\nusername: {macro_user}\nmax: 10\ntheme: full\n:::\n\n:::confluence-children\npage: ../index.md\nall: true\nsort: creation\n:::\n",
space = cfg.space,
macro_user = macro_user
),
)
.expect("write macro source markdown");
fs::write(
macro_target_dir.join("index.md"),
format!(
"---\ntitle: {macro_target_title}\ntype: page\nlabels:\n - e2e-macro-target\n - e2e-content-properties\nstatus: current\nparent: null\nproperties: {{}}\n---\n\n# Shared Excerpt\n\nPulled and reapplied through the e2e macro test.\n\n:::confluence-content-properties\nid: decision\n---\n| Field | Value |\n| --- | --- |\n| Owner | Ada |\n| Status | Approved |\n:::\n"
),
)
.expect("write macro target markdown");
fs::write(
macro_child_dir.join("index.md"),
format!(
"---\ntitle: {macro_child_title}\ntype: page\nlabels: []\nstatus: current\nparent: null\nproperties: {{}}\n---\n\n# Nested Child\n\nUsed to back the children macro.\n"
),
)
.expect("write macro child markdown");
fs::write(
macro_root_dir.join(".confluence.json"),
format!("{{\n \"space_key\": \"{}\"\n}}\n", cfg.space),
)
.expect("write macro root sidecar");
fs::write(
macro_source_dir.join(".confluence.json"),
format!("{{\n \"space_key\": \"{}\"\n}}\n", cfg.space),
)
.expect("write macro source sidecar");
fs::write(
macro_target_dir.join(".confluence.json"),
format!("{{\n \"space_key\": \"{}\"\n}}\n", cfg.space),
)
.expect("write macro target sidecar");
fs::write(
macro_child_dir.join(".confluence.json"),
format!("{{\n \"space_key\": \"{}\"\n}}\n", cfg.space),
)
.expect("write macro child sidecar");
let macro_tree_arg = macro_tree_dir.to_string_lossy().into_owned();
let macro_apply = cfg.run_json(&["apply", ¯o_tree_arg]);
let macro_items = macro_apply
.get("items")
.and_then(Value::as_array)
.expect("macro apply items");
let macro_root_id = macro_items
.iter()
.find(|item| {
item.get("action").and_then(Value::as_str) == Some("create_content")
&& item.get("title").and_then(Value::as_str) == Some(macro_root_title.as_str())
})
.and_then(|item| item.get("content_id"))
.and_then(Value::as_str)
.expect("macro root content id")
.to_string();
let macro_source_id = macro_items
.iter()
.find(|item| {
item.get("action").and_then(Value::as_str) == Some("create_content")
&& item.get("title").and_then(Value::as_str) == Some(macro_source_title.as_str())
})
.and_then(|item| item.get("content_id"))
.and_then(Value::as_str)
.expect("macro source content id")
.to_string();
let macro_target_id = macro_items
.iter()
.find(|item| {
item.get("action").and_then(Value::as_str) == Some("create_content")
&& item.get("title").and_then(Value::as_str) == Some(macro_target_title.as_str())
})
.and_then(|item| item.get("content_id"))
.and_then(Value::as_str)
.expect("macro target content id")
.to_string();
let macro_child_id = macro_items
.iter()
.find(|item| {
item.get("action").and_then(Value::as_str) == Some("create_content")
&& item.get("title").and_then(Value::as_str) == Some(macro_child_title.as_str())
})
.and_then(|item| item.get("content_id"))
.and_then(Value::as_str)
.expect("macro child content id")
.to_string();
cleanup.extra_page_ids.push(macro_root_id.clone());
cleanup.extra_page_ids.push(macro_target_id.clone());
cleanup.extra_page_ids.push(macro_source_id.clone());
cleanup.extra_page_ids.push(macro_child_id.clone());
let macro_source_get = cfg.run_json(&["page", "get", ¯o_source_id, "--show-body"]);
let macro_source_body = string_field(
first_item(¯o_source_get, "macro source get"),
"body_storage",
);
assert!(
macro_source_body.contains(r#"ac:name="anchor""#),
"expected anchor macro in source body: {macro_source_body}"
);
assert!(
macro_source_body
.contains(r#"<ac:parameter ac:name="">macro-source-anchor</ac:parameter>"#),
"expected anchor name parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="navmap""#),
"expected navmap macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(
r#"<ac:parameter ac:name="">Macro Root,Macro Source,Shared Excerpt</ac:parameter>"#
),
"expected navmap default parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body
.contains(r#"<ac:parameter ac:name="title">Macro navigation</ac:parameter>"#)
&& macro_source_body.contains(r#"<ac:parameter ac:name="wrapAfter">4</ac:parameter>"#),
"expected navmap parameters to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="excerpt-include""#),
"expected excerpt-include macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(&format!(
r#"<ac:parameter ac:name="default-parameter">{}:{}</ac:parameter>"#,
cfg.space, macro_target_title
)),
"expected excerpt-include page reference to target title {macro_target_title}: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="children""#),
"expected children macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"<ac:parameter ac:name="page"><ac:link><ri:page "#)
&& macro_source_body.contains(&format!(r#"ri:content-title="{}""#, macro_root_title))
&& macro_source_body.contains(&format!(r#"ri:space-key="{}""#, cfg.space)),
"expected children page parameter to reference root title {macro_root_title}: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="include""#),
"expected include-page macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(&format!(r#"ri:content-title="{macro_target_title}""#)),
"expected include-page macro to reference target title {macro_target_title}: {macro_source_body}"
);
assert!(
macro_source_body.contains(&format!(r#"ri:space-key="{}""#, cfg.space)),
"expected include-page macro to reference space {}: {macro_source_body}",
cfg.space
);
assert!(
macro_source_body.contains(r#"ac:name="pagetree""#),
"expected page-tree macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"<ac:parameter ac:name="root"><ac:link><ri:page "#)
&& macro_source_body.contains(&format!(r#"ri:content-title="{}""#, macro_source_title))
&& macro_source_body.contains(&format!(r#"ri:space-key="{}""#, cfg.space)),
"expected page-tree root to reference source title {macro_source_title}: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="pagetreesearch""#),
"expected page-tree-search macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(&format!(
r#"<ac:parameter ac:name="root">{}:{}</ac:parameter>"#,
cfg.space, macro_target_title
)),
"expected page-tree-search root to reference target title {macro_target_title}: {macro_source_body}"
);
assert!(
macro_source_body.contains(&format!(
r#"<ac:parameter ac:name="spaceKey">{}</ac:parameter>"#,
cfg.space
)) || macro_source_body.contains(&format!(
r#"<ac:parameter ac:name="spaceKey"><ri:space ri:space-key="{}" /></ac:parameter>"#,
cfg.space
)),
"expected page-tree-search spaceKey parameter to survive storage rendering as either a plain string or a space resource: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="contentbylabel""#),
"expected content-by-label macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(
r#"<ac:parameter ac:name="cql">label = "e2e-macro-target"</ac:parameter>"#
),
"expected content-by-label cql to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="content-by-user""#),
"expected content-by-user macro in source body: {macro_source_body}"
);
assert!(
(macro_source_body.contains(r#"<ac:parameter ac:name=""><ri:user "#)
&& macro_source_body.contains(¯o_user_fragment))
|| (macro_source_body.contains(r#"<ac:parameter ac:name="">"#)
&& macro_source_body.contains("UserResourceIdentifier@")),
"expected content-by-user user parameter to survive storage rendering as either a user resource or a provider-specific user identifier string: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="content-report-table""#),
"expected content-report-table macro in source body: {macro_source_body}"
);
assert!(
macro_source_body
.contains(r#"<ac:parameter ac:name="labels">e2e-macro-target</ac:parameter>"#)
&& macro_source_body.contains(&format!(
r#"<ac:parameter ac:name="spaces"><ri:space ri:space-key="{}" /></ac:parameter>"#,
cfg.space
))
&& macro_source_body.contains(r#"<ac:parameter ac:name="maxResults">5</ac:parameter>"#),
"expected content-report-table parameters to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="search""#),
"expected search macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(&format!(
r#"<ac:parameter ac:name="spacekey"><ri:space ri:space-key="{}" /></ac:parameter>"#,
cfg.space
)),
"expected search spacekey resource to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"<ac:parameter ac:name="contributor"><ri:user "#)
&& macro_source_body.contains(¯o_user_fragment),
"expected search contributor resource to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"<ac:parameter ac:name="query">Macro Source</ac:parameter>"#),
"expected search query parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="tasks-report-macro""#),
"expected task-report macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(&format!(
r#"<ac:parameter ac:name="spaceAndPage">{}</ac:parameter>"#,
cfg.space
)) && macro_source_body
.contains(r#"<ac:parameter ac:name="labels">e2e-macro-target</ac:parameter>"#)
&& macro_source_body
.contains(r#"<ac:parameter ac:name="status">incomplete</ac:parameter>"#)
&& macro_source_body.contains(r#"<ac:parameter ac:name="pageSize">20</ac:parameter>"#)
&& macro_source_body.contains(
r#"<ac:parameter ac:name="columns">description,assignee,location</ac:parameter>"#
)
&& macro_source_body
.contains(r#"<ac:parameter ac:name="sortBy">page title</ac:parameter>"#)
&& macro_source_body
.contains(r#"<ac:parameter ac:name="reverseSort">false</ac:parameter>"#),
"expected task-report parameters to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="userlister""#),
"expected generic userlister macro in source body: {macro_source_body}"
);
assert!(
((macro_source_body.contains(r#"<ac:parameter ac:name="related"><ac:link><ri:page "#)
&& macro_source_body.contains(&format!(r#"ri:space-key="{}""#, cfg.space))
&& macro_source_body.contains(&format!(r#"ri:content-title="{macro_target_title}""#)))
|| (macro_source_body.contains(r#"<ac:parameter ac:name="related">DefaultLink["#)
&& macro_source_body.contains(&format!("spaceKey={}", cfg.space))
&& macro_source_body.contains(&format!("title={macro_target_title}")))),
"expected generic userlister page-link parameter to survive storage rendering: {macro_source_body}"
);
assert!(
((macro_source_body.contains(r#"<ac:parameter ac:name="contributor"><ri:user "#)
&& macro_source_body.contains(¯o_user_fragment))
|| (macro_source_body.contains(r#"<ac:parameter ac:name="contributor">"#)
&& macro_source_body.contains("UserResourceIdentifier@"))),
"expected generic userlister contributor parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="detailssummary""#),
"expected content-properties-report macro in source body: {macro_source_body}"
);
assert!(
macro_source_body
.contains(r#"<ac:parameter ac:name="label">e2e-content-properties</ac:parameter>"#)
&& macro_source_body.contains(r#"<ac:parameter ac:name="id">decision</ac:parameter>"#),
"expected content-properties-report parameters to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="attachments""#),
"expected attachments macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"<ac:parameter ac:name="patterns">*.pdf</ac:parameter>"#)
&& macro_source_body.contains(r#"<ac:parameter ac:name="sortBy">name</ac:parameter>"#),
"expected attachments parameters to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="view-file""#),
"expected view-file macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(
r#"<ac:parameter ac:name="name"><ri:attachment ri:filename="preview.pdf" /></ac:parameter>"#
),
"expected view-file attachment parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="viewdoc""#),
"expected view-doc macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"<ac:parameter ac:name="page"><ac:link><ri:page "#)
&& macro_source_body.contains(&format!(r#"ri:content-title="{}""#, macro_target_title))
&& macro_source_body.contains(&format!(r#"ri:space-key="{}""#, cfg.space))
&& macro_source_body.contains(
r#"<ac:parameter ac:name="name"><ri:attachment ri:filename="manual.docx" /></ac:parameter>"#
),
"expected view-doc parameters to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="viewxls""#),
"expected view-xls macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(
r#"<ac:parameter ac:name="name"><ri:attachment ri:filename="sheet.xlsx" /></ac:parameter>"#
),
"expected view-xls attachment parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="viewppt""#),
"expected view-ppt macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(
r#"<ac:parameter ac:name="name"><ri:attachment ri:filename="slides.pptx" /></ac:parameter>"#
),
"expected view-ppt attachment parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="blog-posts""#),
"expected blog-posts macro in source body: {macro_source_body}"
);
assert!(
((macro_source_body.contains(r#"<ac:parameter ac:name="author"><ri:user "#)
&& macro_source_body.contains(¯o_user_fragment))
|| (macro_source_body.contains(r#"<ac:parameter ac:name="author">"#)
&& macro_source_body.contains("UserResourceIdentifier@")))
&& macro_source_body.contains(&format!(
r#"<ac:parameter ac:name="spaces"><ri:space ri:space-key="{}" /></ac:parameter>"#,
cfg.space
))
&& macro_source_body.contains(r#"<ac:parameter ac:name="max">5</ac:parameter>"#)
&& macro_source_body.contains(r#"<ac:parameter ac:name="time">7</ac:parameter>"#),
"expected blog-posts parameters to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="contributors""#),
"expected contributors macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(&format!(
r#"<ac:parameter ac:name="spaces"><ri:space ri:space-key="{}" /><ri:space ri:space-key="@personal" /></ac:parameter>"#,
cfg.space
)),
"expected contributors spaces parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body
.contains(r#"<ac:parameter ac:name="labels">e2e-macro-target</ac:parameter>"#)
&& macro_source_body.contains(r#"<ac:parameter ac:name="mode">list</ac:parameter>"#),
"expected contributors parameters to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="contributors-summary""#),
"expected contributors-summary macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(&format!(
r#"<ac:parameter ac:name="spaces"><ri:space ri:space-key="{}" /></ac:parameter>"#,
cfg.space
)),
"expected contributors-summary spaces parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body
.contains(r#"<ac:parameter ac:name="columns">edits,comments,labels</ac:parameter>"#)
&& macro_source_body.contains(r#"<ac:parameter ac:name="limit">10</ac:parameter>"#),
"expected contributors-summary parameters to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="recently-updated""#),
"expected recently-updated macro in source body: {macro_source_body}"
);
assert!(
((macro_source_body.contains(r#"<ac:parameter ac:name="author"><ri:user "#)
&& macro_source_body.contains(¯o_user_fragment))
|| (macro_source_body.contains(r#"<ac:parameter ac:name="author">"#)
&& macro_source_body.contains("UserResourceIdentifier@")))
&& macro_source_body.contains(&format!(
r#"<ac:parameter ac:name="spaces"><ri:space ri:space-key="{}" /></ac:parameter>"#,
cfg.space
)),
"expected recently-updated author and spaces parameters to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="recently-updated-dashboard""#),
"expected recently-updated-dashboard macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(&format!(
r#"<ac:parameter ac:name="spaces"><ri:space ri:space-key="{}" /></ac:parameter>"#,
cfg.space
)),
"expected recently-updated-dashboard spaces parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"<ac:parameter ac:name="limit">10</ac:parameter>"#)
&& macro_source_body
.contains(r#"<ac:parameter ac:name="theme">concise</ac:parameter>"#),
"expected recently-updated-dashboard parameters to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="livesearch""#),
"expected livesearch macro in source body: {macro_source_body}"
);
assert!(
macro_source_body
.contains(r#"<ac:parameter ac:name="labels">e2e-macro-target</ac:parameter>"#),
"expected livesearch labels parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"<ac:parameter ac:name="size">large</ac:parameter>"#),
"expected livesearch size parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(&format!(
r#"<ac:parameter ac:name="spaceKey">{}</ac:parameter>"#,
cfg.space
)) || macro_source_body.contains(&format!(
r#"<ac:parameter ac:name="spaceKey"><ri:space ri:space-key="{}" /></ac:parameter>"#,
cfg.space
)),
"expected livesearch spaceKey parameter to survive storage rendering as either a plain string or a space resource: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="page-index""#)
|| macro_source_body.contains(r#"ac:name="pageindex""#),
"expected page-index macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="toc-zone""#),
"expected toc-zone macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"<ac:parameter ac:name="location">top</ac:parameter>"#)
&& macro_source_body.contains(r#"<ac:parameter ac:name="maxLevel">3</ac:parameter>"#)
&& macro_source_body.contains("<ac:rich-text-body>")
&& macro_source_body.contains("<h2>Zoned Heading</h2>"),
"expected toc-zone parameters and body to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="listlabels""#),
"expected labels-list macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(&format!(
r#"<ac:parameter ac:name="spaceKey"><ri:space ri:space-key="{}" /></ac:parameter>"#,
cfg.space
)),
"expected labels-list spaceKey parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body
.contains(r#"<ac:parameter ac:name="excludedLabels">drafts,test</ac:parameter>"#),
"expected labels-list excludedLabels parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="popular-labels""#),
"expected popular-labels macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(&format!(
r#"<ac:parameter ac:name="spaceKey"><ri:space ri:space-key="{}" /></ac:parameter>"#,
cfg.space
)),
"expected popular-labels spaceKey parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"<ac:parameter ac:name="count">25</ac:parameter>"#),
"expected popular-labels count parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"<ac:parameter ac:name="style">heatmap</ac:parameter>"#),
"expected popular-labels style parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="related-labels""#),
"expected related-labels macro in source body: {macro_source_body}"
);
assert!(
macro_source_body
.contains(r#"<ac:parameter ac:name="labels">e2e-macro-target</ac:parameter>"#),
"expected related-labels labels parameter to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="recently-used-labels""#),
"expected recently-used-labels macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"<ac:parameter ac:name="scope">space</ac:parameter>"#)
&& macro_source_body.contains(r#"<ac:parameter ac:name="style">cloud</ac:parameter>"#),
"expected recently-used-labels parameters to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="gallery""#),
"expected gallery macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"<ac:parameter ac:name="sortBy">name</ac:parameter>"#)
&& macro_source_body.contains(r#"<ac:parameter ac:name="columns">2</ac:parameter>"#),
"expected gallery parameters to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="favpages""#),
"expected favorite-pages macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="change-history""#),
"expected change-history macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="spaces""#),
"expected spaces-list macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"<ac:parameter ac:name="">all</ac:parameter>"#)
&& macro_source_body.contains(r#"<ac:parameter ac:name="width">80%</ac:parameter>"#),
"expected spaces-list parameters to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="space-details""#),
"expected space-details macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"<ac:parameter ac:name="width">50%</ac:parameter>"#),
"expected space-details parameters to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="space-attachments""#),
"expected space-attachments macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(&format!(
r#"<ac:parameter ac:name="space"><ri:space ri:space-key="{}" /></ac:parameter>"#,
cfg.space
)) && macro_source_body
.contains(r#"<ac:parameter ac:name="showFilter">false</ac:parameter>"#),
"expected space-attachments parameters to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="noformat""#),
"expected noformat macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"<ac:parameter ac:name="nopanel">true</ac:parameter>"#)
&& macro_source_body.contains("<ac:plain-text-body><![CDATA[<xml>literal</xml>")
&& macro_source_body.contains("line 2"),
"expected noformat parameters and plain-text body to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="profile""#),
"expected profile macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(¯o_user_fragment),
"expected profile user resource to survive storage rendering: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="status-list""#),
"expected status-list macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(¯o_user_fragment)
|| macro_source_body.contains(r#"<ac:parameter ac:name="username">"#),
"expected status-list user parameter to survive storage rendering as either a user resource or a plain username: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"ac:name="network""#),
"expected network macro in source body: {macro_source_body}"
);
assert!(
macro_source_body.contains(r#"<ac:parameter ac:name="">followers</ac:parameter>"#)
&& macro_source_body.contains(¯o_user_fragment)
&& macro_source_body.contains(r#"<ac:parameter ac:name="max">10</ac:parameter>"#)
&& macro_source_body.contains(r#"<ac:parameter ac:name="theme">full</ac:parameter>"#),
"expected network parameters to survive storage rendering: {macro_source_body}"
);
let macro_target_get = cfg.run_json(&["page", "get", ¯o_target_id, "--show-body"]);
let macro_target_body = string_field(
first_item(¯o_target_get, "macro target get"),
"body_storage",
);
assert!(
macro_target_body.contains(r#"ac:name="details""#),
"expected content-properties macro in target body: {macro_target_body}"
);
assert!(
macro_target_body.contains(r#"<ac:parameter ac:name="id">decision</ac:parameter>"#)
&& macro_target_body.contains("<ac:rich-text-body>")
&& macro_target_body.contains("<table>")
&& macro_target_body.contains("<th>Field</th>")
&& macro_target_body.contains("<td>Ada</td>"),
"expected content-properties parameters and table body to survive storage rendering: {macro_target_body}"
);
let macro_pull_arg = macro_pull_dir.to_string_lossy().into_owned();
cfg.run(&["pull", "tree", ¯o_root_id, ¯o_pull_arg]);
let pulled_macro_source = find_index_md_by_title(¯o_pull_dir, ¯o_source_title);
let pulled_macro_source_markdown =
fs::read_to_string(&pulled_macro_source).expect("read pulled macro source markdown");
assert!(
pulled_macro_source_markdown.contains(":::confluence-excerpt-include"),
"expected pulled macro source to preserve excerpt-include block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("page: ../")
&& pulled_macro_source_markdown.contains("/index.md")
&& !pulled_macro_source_markdown.contains("confluence-page://page?"),
"expected pulled macro source to rewrite excerpt target to a local path: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-children"),
"expected pulled macro source to preserve children block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("page: ../index.md")
&& !pulled_macro_source_markdown.contains("page: confluence-page://page?"),
"expected pulled children page parameter to rewrite to a local path: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-include-page"),
"expected pulled macro source to preserve include-page block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("page: ../")
&& pulled_macro_source_markdown.contains("/index.md")
&& !pulled_macro_source_markdown.contains("confluence-page://page?"),
"expected pulled include-page target to rewrite to a local path: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-page-tree"),
"expected pulled macro source to preserve page-tree block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("root: index.md")
&& !pulled_macro_source_markdown.contains("root: confluence-page://page?"),
"expected pulled page-tree root to rewrite to a local path: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-page-tree-search"),
"expected pulled macro source to preserve page-tree-search block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("root: ../")
&& pulled_macro_source_markdown.contains("/index.md")
&& !pulled_macro_source_markdown.contains("root: confluence-page://page?"),
"expected pulled page-tree-search root to rewrite to a local path: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(&format!("spaceKey: {}", cfg.space)),
"expected pulled page-tree-search spaceKey to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-anchor"),
"expected pulled macro source to preserve anchor block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("name: macro-source-anchor"),
"expected pulled anchor name to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-navmap"),
"expected pulled macro source to preserve navmap block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("$default: Macro Root,Macro Source,Shared Excerpt")
&& pulled_macro_source_markdown.contains("title: Macro navigation")
&& pulled_macro_source_markdown.contains("wrapAfter: 4"),
"expected pulled navmap parameters to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-content-by-label"),
"expected pulled macro source to preserve content-by-label block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(r#"cql: label = "e2e-macro-target""#),
"expected pulled content-by-label cql to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-content-by-user"),
"expected pulled macro source to preserve content-by-user block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("user: confluence-user://user?")
&& pulled_macro_source_markdown.contains(¯o_user_placeholder_fragment),
"expected pulled content-by-user user parameter to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-content-report-table"),
"expected pulled macro source to preserve content-report-table block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("labels: e2e-macro-target")
&& pulled_macro_source_markdown.contains(&format!("spaces: {}", cfg.space))
&& pulled_macro_source_markdown.contains("maxResults: 5"),
"expected pulled content-report-table parameters to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-task-report"),
"expected pulled macro source to preserve task-report block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(&format!("spaceAndPage: {}", cfg.space))
&& pulled_macro_source_markdown.contains("labels: e2e-macro-target")
&& pulled_macro_source_markdown.contains("status: incomplete")
&& pulled_macro_source_markdown.contains("pageSize: 20")
&& pulled_macro_source_markdown.contains("columns: description,assignee,location")
&& pulled_macro_source_markdown.contains("sortBy: page title")
&& pulled_macro_source_markdown.contains("reverseSort: false"),
"expected pulled task-report parameters to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-macro userlister"),
"expected pulled macro source to preserve generic userlister block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("contributor: !user confluence-user://user?")
&& pulled_macro_source_markdown.contains(¯o_user_placeholder_fragment),
"expected pulled generic userlister contributor parameter to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("related: !page-link ../")
&& pulled_macro_source_markdown.contains("/index.md")
&& !pulled_macro_source_markdown.contains("!page-link confluence-page://page?"),
"expected pulled generic userlister page-link parameter to rewrite to a local path: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-search"),
"expected pulled macro source to preserve search block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(&format!("spacekey: !space {}", cfg.space)),
"expected pulled search spacekey resource to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("contributor: !user confluence-user://user?")
&& pulled_macro_source_markdown.contains(¯o_user_placeholder_fragment),
"expected pulled search contributor resource to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("query: Macro Source"),
"expected pulled search query parameter to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-content-properties-report"),
"expected pulled macro source to preserve content-properties-report block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("label: e2e-content-properties")
&& pulled_macro_source_markdown.contains("id: decision"),
"expected pulled content-properties-report parameters to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-attachments"),
"expected pulled macro source to preserve attachments block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("patterns: *.pdf")
&& pulled_macro_source_markdown.contains("sortBy: name"),
"expected pulled attachments parameters to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-view-file"),
"expected pulled macro source to preserve view-file block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("attachment: preview.pdf"),
"expected pulled view-file attachment parameter to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-view-doc"),
"expected pulled macro source to preserve view-doc block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("page: ../")
&& pulled_macro_source_markdown.contains("/index.md")
&& pulled_macro_source_markdown.contains("attachment: manual.docx")
&& !pulled_macro_source_markdown.contains("page: confluence-page://page?"),
"expected pulled view-doc parameters to survive export and rewrite the page reference locally: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-view-xls"),
"expected pulled macro source to preserve view-xls block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("attachment: sheet.xlsx"),
"expected pulled view-xls attachment parameter to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-view-ppt"),
"expected pulled macro source to preserve view-ppt block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("attachment: slides.pptx"),
"expected pulled view-ppt attachment parameter to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-blog-posts"),
"expected pulled macro source to preserve blog-posts block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("author: confluence-user://user?")
&& pulled_macro_source_markdown.contains(¯o_user_placeholder_fragment)
&& pulled_macro_source_markdown.contains(&format!("spaces: {}", cfg.space))
&& pulled_macro_source_markdown.contains("max: 5")
&& pulled_macro_source_markdown.contains("time: 7"),
"expected pulled blog-posts parameters to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-contributors"),
"expected pulled macro source to preserve contributors block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(&format!("spaces: {},@personal", cfg.space))
&& pulled_macro_source_markdown.contains("labels: e2e-macro-target")
&& pulled_macro_source_markdown.contains("mode: list"),
"expected pulled contributors parameters to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-contributors-summary"),
"expected pulled macro source to preserve contributors-summary block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(&format!("spaces: {}", cfg.space))
&& pulled_macro_source_markdown.contains("columns: edits,comments,labels")
&& pulled_macro_source_markdown.contains("limit: 10"),
"expected pulled contributors-summary parameters to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-recently-updated"),
"expected pulled macro source to preserve recently-updated block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("author: confluence-user://user?")
&& pulled_macro_source_markdown.contains(¯o_user_placeholder_fragment)
&& pulled_macro_source_markdown.contains(&format!("spaces: {}", cfg.space)),
"expected pulled recently-updated author and spaces to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-recently-updated-dashboard"),
"expected pulled macro source to preserve recently-updated-dashboard block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("limit: 10")
&& pulled_macro_source_markdown.contains("theme: concise"),
"expected pulled recently-updated-dashboard parameters to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(&format!("spaces: {}", cfg.space)),
"expected pulled recently-updated-dashboard spaces to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-livesearch"),
"expected pulled macro source to preserve livesearch block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("labels: e2e-macro-target")
&& pulled_macro_source_markdown.contains("size: large"),
"expected pulled livesearch parameters to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-page-index"),
"expected pulled macro source to preserve page-index block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-toc-zone"),
"expected pulled macro source to preserve toc-zone block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("location: top")
&& pulled_macro_source_markdown.contains("maxLevel: 3")
&& pulled_macro_source_markdown.contains("## Zoned Heading"),
"expected pulled toc-zone parameters and body to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-labels-list"),
"expected pulled macro source to preserve labels-list block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(&format!("spaceKey: {}", cfg.space)),
"expected pulled labels-list spaceKey to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("excludedLabels: drafts,test"),
"expected pulled labels-list excludedLabels to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-popular-labels"),
"expected pulled macro source to preserve popular-labels block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("count: 25")
&& pulled_macro_source_markdown.contains("style: heatmap"),
"expected pulled popular-labels parameters to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-related-labels"),
"expected pulled macro source to preserve related-labels block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("labels: e2e-macro-target"),
"expected pulled related-labels labels parameter to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-recently-used-labels"),
"expected pulled macro source to preserve recently-used-labels block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("scope: space")
&& pulled_macro_source_markdown.contains("style: cloud"),
"expected pulled recently-used-labels parameters to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-gallery"),
"expected pulled macro source to preserve gallery block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("sortBy: name")
&& pulled_macro_source_markdown.contains("columns: 2"),
"expected pulled gallery parameters to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-favorite-pages"),
"expected pulled macro source to preserve favorite-pages block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-change-history"),
"expected pulled macro source to preserve change-history block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-spaces-list"),
"expected pulled macro source to preserve spaces-list block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("scope: all")
&& pulled_macro_source_markdown.contains("width: 80%"),
"expected pulled spaces-list parameters to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-space-details"),
"expected pulled macro source to preserve space-details block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("width: 50%"),
"expected pulled space-details parameters to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-space-attachments"),
"expected pulled macro source to preserve space-attachments block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(&format!("space: {}", cfg.space))
&& pulled_macro_source_markdown.contains("showFilter: false"),
"expected pulled space-attachments parameters to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("~~~confluence-noformat"),
"expected pulled macro source to preserve noformat block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("nopanel: true")
&& pulled_macro_source_markdown.contains("<xml>literal</xml>")
&& pulled_macro_source_markdown.contains("line 2"),
"expected pulled noformat parameters and body to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-profile"),
"expected pulled macro source to preserve profile block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("user: confluence-user://user?"),
"expected pulled profile user parameter to survive export as a user placeholder: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-status-list"),
"expected pulled macro source to preserve status-list block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("username: "),
"expected pulled status-list user parameter to survive export: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains(":::confluence-network"),
"expected pulled macro source to preserve network block: {pulled_macro_source_markdown}"
);
assert!(
pulled_macro_source_markdown.contains("mode: followers")
&& pulled_macro_source_markdown.contains("username: confluence-user://user?")
&& pulled_macro_source_markdown.contains("max: 10")
&& pulled_macro_source_markdown.contains("theme: full"),
"expected pulled network parameters to survive export: {pulled_macro_source_markdown}"
);
let pulled_macro_target = find_index_md_by_title(¯o_pull_dir, ¯o_target_title);
let pulled_macro_target_markdown =
fs::read_to_string(&pulled_macro_target).expect("read pulled macro target markdown");
assert!(
pulled_macro_target_markdown.contains(":::confluence-content-properties"),
"expected pulled macro target to preserve content-properties block: {pulled_macro_target_markdown}"
);
assert!(
pulled_macro_target_markdown.contains("id: decision")
&& pulled_macro_target_markdown.contains("| Field | Value |")
&& pulled_macro_target_markdown.contains("| Owner | Ada |")
&& pulled_macro_target_markdown.contains("| Status | Approved |"),
"expected pulled content-properties parameters and table to survive export: {pulled_macro_target_markdown}"
);
let macro_plan = cfg.run_json(&["plan", ¯o_pull_arg]);
assert!(
macro_plan
.get("items")
.and_then(Value::as_array)
.is_some_and(|items| {
!items.is_empty()
&& items
.iter()
.all(|item| item.get("action").and_then(Value::as_str) == Some("noop"))
}),
"expected macro pull plan to be noop: {macro_plan}"
);
let blog_body_1_arg = blog_body_1.to_string_lossy().into_owned();
let blog_create = cfg.run_json(&[
"blog",
"create",
&blog_title,
cfg.space.as_str(),
"--body-file",
&blog_body_1_arg,
]);
let blog_id = string_field(first_item(&blog_create, "blog create"), "id").to_string();
cleanup.blog_id = Some(blog_id.clone());
let blog_get = cfg.run_json(&["blog", "get", &blog_id, "--show-body"]);
assert_eq!(
string_field(first_item(&blog_get, "blog get"), "id"),
blog_id
);
let blog_body_2_arg = blog_body_2.to_string_lossy().into_owned();
let blog_update = cfg.run_json(&[
"blog",
"update",
&blog_id,
"--title",
&updated_blog_title,
"--body-file",
&blog_body_2_arg,
]);
assert_eq!(
string_field(first_item(&blog_update, "blog update"), "title"),
updated_blog_title
);
cfg.run(&["blog", "delete", &blog_id]);
cleanup.blog_id = None;
cfg.run(&["page", "delete", &page_id]);
cleanup.page_id = None;
}