use micropub::draft_push::{validate_draft_id, PushResult};
use micropub::media::{find_media_references, replace_paths};
#[test]
fn test_push_result_structure() {
let result = PushResult {
url: "https://example.com/posts/draft-123".to_string(),
is_update: false,
uploads: vec![(
"photo.jpg".to_string(),
"https://example.com/media/abc.jpg".to_string(),
)],
};
assert_eq!(result.url, "https://example.com/posts/draft-123");
assert!(!result.is_update);
assert_eq!(result.uploads.len(), 1);
}
#[test]
fn test_find_media_references_in_draft_content() {
let content = r#"
# My Post
Here is an image: 
And another: 
But not this URL: 
"#;
let refs = find_media_references(content);
assert_eq!(refs.len(), 2);
assert!(refs.contains(&"~/photo.jpg".to_string()));
assert!(refs.contains(&"./images/test.png".to_string()));
assert!(!refs.contains(&"https://example.com/remote.jpg".to_string()));
}
#[test]
fn test_replace_paths_in_content() {
let content = "Image:  here";
let replacements = vec![(
"~/photo.jpg".to_string(),
"https://cdn.example.com/uploaded.jpg".to_string(),
)];
let result = replace_paths(content, &replacements);
assert!(result.contains("https://cdn.example.com/uploaded.jpg"));
assert!(!result.contains("~/photo.jpg"));
}
#[test]
fn test_micropub_request_includes_draft_status() {
use micropub::client::{MicropubAction, MicropubRequest};
use serde_json::{Map, Value};
let mut properties = Map::new();
properties.insert(
"content".to_string(),
Value::Array(vec![Value::String("Test content".to_string())]),
);
properties.insert(
"post-status".to_string(),
Value::Array(vec![Value::String("draft".to_string())]),
);
let request = MicropubRequest {
action: MicropubAction::Create,
properties,
url: None,
};
let json = request.to_json().unwrap();
assert!(json.contains("post-status"));
assert!(json.contains("\"draft\""));
}
#[test]
fn test_micropub_update_request_structure() {
use micropub::client::{MicropubAction, MicropubRequest};
use serde_json::{Map, Value};
let mut replace = Map::new();
replace.insert(
"content".to_string(),
Value::Array(vec![Value::String("Updated content".to_string())]),
);
replace.insert(
"post-status".to_string(),
Value::Array(vec![Value::String("draft".to_string())]),
);
let request = MicropubRequest {
action: MicropubAction::Update {
replace,
add: Map::new(),
delete: Vec::new(),
},
properties: Map::new(),
url: Some("https://example.com/posts/123".to_string()),
};
let json = request.to_json().unwrap();
assert!(json.contains("\"action\""));
assert!(json.contains("\"update\""));
assert!(json.contains("example.com/posts/123"));
assert!(json.contains("\"replace\""));
assert!(json.contains("Updated content"));
}
#[test]
fn test_update_request_maintains_post_status_draft() {
use micropub::client::{MicropubAction, MicropubRequest};
use serde_json::{Map, Value};
let mut properties = Map::new();
properties.insert(
"content".to_string(),
Value::Array(vec![Value::String("Updated draft content".to_string())]),
);
properties.insert(
"post-status".to_string(),
Value::Array(vec![Value::String("draft".to_string())]),
);
let mut replace = Map::new();
replace.insert(
"content".to_string(),
properties.get("content").unwrap().clone(),
);
replace.insert(
"post-status".to_string(),
properties.get("post-status").unwrap().clone(),
);
let request = MicropubRequest {
action: MicropubAction::Update {
replace,
add: Map::new(),
delete: Vec::new(),
},
properties: Map::new(),
url: Some("https://example.com/posts/draft-123".to_string()),
};
let json = request.to_json().unwrap();
assert!(
json.contains("\"replace\""),
"JSON should have replace section"
);
assert!(
json.contains("\"post-status\""),
"JSON should contain post-status field"
);
assert!(
json.contains("\"draft\""),
"JSON should contain draft value"
);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let replace_obj = parsed.get("replace").expect("Should have replace field");
let post_status = replace_obj
.get("post-status")
.expect("Replace should have post-status field");
assert_eq!(
post_status.as_array().unwrap()[0].as_str().unwrap(),
"draft",
"post-status should be 'draft'"
);
}
#[tokio::test]
async fn test_cmd_push_draft_requires_valid_draft_id() {
let result = micropub::draft_push::cmd_push_draft("nonexistent", None).await;
assert!(result.is_err());
}
#[test]
fn test_cli_draft_push_command_exists() {
use clap::Parser;
#[derive(Parser)]
#[command(name = "micropub")]
struct TestCli {
#[command(subcommand)]
command: Option<TestCommands>,
}
#[derive(clap::Subcommand)]
enum TestCommands {
#[command(subcommand)]
Draft(TestDraftCommands),
}
#[derive(clap::Subcommand)]
enum TestDraftCommands {
Push {
draft_id: String,
#[arg(long)]
backdate: Option<String>,
},
}
let result = TestCli::try_parse_from(vec!["micropub", "draft", "push", "test-draft"]);
assert!(result.is_ok());
let result = TestCli::try_parse_from(vec![
"micropub",
"draft",
"push",
"test-draft",
"--backdate",
"2024-01-15T10:00:00Z",
]);
assert!(result.is_ok());
}
#[test]
fn test_detect_server_draft_from_metadata() {
use micropub::draft::DraftMetadata;
let server_draft = DraftMetadata {
post_type: "note".to_string(),
name: None,
published: None,
category: Vec::new(),
syndicate_to: Vec::new(),
profile: None,
photo: Vec::new(),
status: Some("server-draft".to_string()),
url: Some("https://example.com/posts/draft-123".to_string()),
published_at: None,
};
let is_server_draft =
server_draft.url.is_some() && server_draft.status.as_deref() == Some("server-draft");
assert!(is_server_draft, "Should detect server draft");
let local_draft = DraftMetadata {
post_type: "note".to_string(),
name: None,
published: None,
category: Vec::new(),
syndicate_to: Vec::new(),
profile: None,
photo: Vec::new(),
status: None,
url: None,
published_at: None,
};
let is_local =
local_draft.url.is_some() && local_draft.status.as_deref() == Some("server-draft");
assert!(!is_local, "Should not detect local draft as server draft");
}
#[test]
fn test_update_request_for_publishing_server_draft() {
use micropub::client::{MicropubAction, MicropubRequest};
use serde_json::{Map, Value};
let mut replace = Map::new();
replace.insert(
"content".to_string(),
Value::Array(vec![Value::String("Final content".to_string())]),
);
replace.insert(
"name".to_string(),
Value::Array(vec![Value::String("Post Title".to_string())]),
);
replace.insert(
"post-status".to_string(),
Value::Array(vec![Value::String("published".to_string())]),
);
let request = MicropubRequest {
action: MicropubAction::Update {
replace,
add: Map::new(),
delete: Vec::new(),
},
properties: Map::new(),
url: Some("https://example.com/posts/draft-123".to_string()),
};
let json = request.to_json().unwrap();
assert!(json.contains("\"action\""));
assert!(json.contains("\"update\""));
assert!(json.contains("example.com/posts/draft-123"));
assert!(json.contains("\"replace\""));
assert!(json.contains("\"post-status\""));
assert!(json.contains("\"published\""));
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let replace_obj = parsed.get("replace").expect("Should have replace field");
let post_status = replace_obj
.get("post-status")
.expect("Replace should have post-status field");
assert_eq!(
post_status.as_array().unwrap()[0].as_str().unwrap(),
"published",
"post-status should be 'published'"
);
let content = replace_obj
.get("content")
.expect("Replace should have content field");
assert_eq!(
content.as_array().unwrap()[0].as_str().unwrap(),
"Final content"
);
let name = replace_obj
.get("name")
.expect("Replace should have name field");
assert_eq!(name.as_array().unwrap()[0].as_str().unwrap(), "Post Title");
assert!(
replace_obj.get("category").is_none(),
"Replace should NOT include category"
);
assert!(
replace_obj.get("photo").is_none(),
"Replace should NOT include photo"
);
assert!(
replace_obj.get("mp-syndicate-to").is_none(),
"Replace should NOT include mp-syndicate-to"
);
assert!(
replace_obj.get("published").is_none(),
"Replace should NOT include published date"
);
}
#[test]
fn test_publish_workflow_local_vs_server_draft() {
use micropub::client::{MicropubAction, MicropubRequest};
use micropub::draft::DraftMetadata;
use serde_json::{Map, Value};
let local_metadata = DraftMetadata {
post_type: "note".to_string(),
name: None,
published: None,
category: Vec::new(),
syndicate_to: Vec::new(),
profile: None,
photo: Vec::new(),
status: None,
url: None,
published_at: None,
};
let is_server_draft =
local_metadata.url.is_some() && local_metadata.status.as_deref() == Some("server-draft");
assert!(
!is_server_draft,
"Local draft should not be detected as server draft"
);
let mut properties = Map::new();
properties.insert(
"content".to_string(),
Value::Array(vec![Value::String("Local draft content".to_string())]),
);
let local_request = MicropubRequest {
action: MicropubAction::Create,
properties,
url: None,
};
let local_json = local_request.to_json().unwrap();
assert!(local_json.contains("\"type\""));
assert!(local_json.contains("h-entry"));
assert!(!local_json.contains("\"action\""));
let server_metadata = DraftMetadata {
post_type: "note".to_string(),
name: None,
published: None,
category: Vec::new(),
syndicate_to: Vec::new(),
profile: None,
photo: Vec::new(),
status: Some("server-draft".to_string()),
url: Some("https://example.com/posts/draft-123".to_string()),
published_at: None,
};
let is_server_draft =
server_metadata.url.is_some() && server_metadata.status.as_deref() == Some("server-draft");
assert!(is_server_draft, "Server draft should be detected");
let mut replace = Map::new();
replace.insert(
"content".to_string(),
Value::Array(vec![Value::String(
"Updated server draft content".to_string(),
)]),
);
replace.insert(
"post-status".to_string(),
Value::Array(vec![Value::String("published".to_string())]),
);
let server_request = MicropubRequest {
action: MicropubAction::Update {
replace,
add: Map::new(),
delete: Vec::new(),
},
properties: Map::new(),
url: Some("https://example.com/posts/draft-123".to_string()),
};
let server_json = server_request.to_json().unwrap();
assert!(server_json.contains("\"action\""));
assert!(server_json.contains("\"update\""));
assert!(server_json.contains("\"replace\""));
assert!(server_json.contains("\"post-status\""));
assert!(server_json.contains("\"published\""));
assert!(server_json.contains("example.com/posts/draft-123"));
}
#[test]
fn test_draft_metadata_has_required_fields() {
use micropub::draft::DraftMetadata;
let metadata = DraftMetadata {
post_type: "note".to_string(),
name: None,
published: None,
category: Vec::new(),
syndicate_to: Vec::new(),
profile: None,
photo: Vec::new(),
status: Some("server-draft".to_string()),
url: Some("https://example.com/posts/draft-123".to_string()),
published_at: None,
};
assert_eq!(metadata.status, Some("server-draft".to_string()));
assert_eq!(
metadata.url,
Some("https://example.com/posts/draft-123".to_string())
);
}
#[test]
fn test_is_update_logic() {
use micropub::draft::DraftMetadata;
let metadata_new = DraftMetadata::default();
assert!(metadata_new.url.is_none());
let is_update_new = metadata_new.url.is_some();
assert!(!is_update_new);
let metadata_existing = DraftMetadata {
url: Some("https://example.com/posts/draft-123".to_string()),
..Default::default()
};
let is_update_existing = metadata_existing.url.is_some();
assert!(is_update_existing);
}
#[test]
fn test_post_status_draft_property() {
use serde_json::{Map, Value};
let mut properties = Map::new();
properties.insert(
"post-status".to_string(),
Value::Array(vec![Value::String("draft".to_string())]),
);
let post_status = properties
.get("post-status")
.and_then(|v| v.as_array())
.and_then(|a| a.first())
.and_then(|v| v.as_str());
assert_eq!(post_status, Some("draft"));
}
#[test]
fn test_validate_draft_id_rejects_empty_string() {
let result = validate_draft_id("");
assert!(result.is_err(), "Empty string should be rejected");
let error_message = result.unwrap_err().to_string();
assert!(
error_message.contains("cannot be empty"),
"Error message should mention empty string: {}",
error_message
);
}
#[test]
fn test_validate_draft_id_rejects_invalid_characters() {
assert!(validate_draft_id("..").is_err());
assert!(validate_draft_id("../etc").is_err());
assert!(validate_draft_id("foo/bar").is_err());
assert!(validate_draft_id("foo\\bar").is_err());
assert!(validate_draft_id("foo\0bar").is_err());
assert!(validate_draft_id("foo@bar").is_err());
assert!(validate_draft_id("foo#bar").is_err());
}
#[test]
fn test_validate_draft_id_accepts_valid_ids() {
assert!(validate_draft_id("abc123").is_ok());
assert!(validate_draft_id("my-draft").is_ok());
assert!(validate_draft_id("my_draft").is_ok());
assert!(validate_draft_id("draft-123_test").is_ok());
assert!(validate_draft_id("A1B2C3").is_ok());
}