use std::path::PathBuf;
mod blog_post;
mod draft_builder;
use blog_post::BlogPost;
use draft_builder::DraftBuilder;
use thiserror::Error;
use url::Url;
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum DraftError {
#[error("Future capacity is too large")]
FutureCapacityTooLarge,
#[error("path not found: `{0}`")]
PathNotFound(String),
#[error("file extension invalid (must be `{1}`): {0}")]
FileExtensionInvalid(String, String),
#[error("blog post list is empty")]
BlogPostListEmpty,
#[error("blog post list is empty after qualifications have been applied")]
QualifiedBlogPostListEmpty,
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("URL parse error: {0}")]
UrlParse(#[from] url::ParseError),
#[error("TOML datetime parse error: {0}")]
TomlDatetimeParse(#[from] toml::value::DatetimeParseError),
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct Draft {
blog_posts: Vec<BlogPost>,
bsky_store: PathBuf,
referrer_store: PathBuf,
base_url: Url,
root: PathBuf,
}
impl Draft {
pub fn builder(base_url: Url, root: Option<&PathBuf>) -> DraftBuilder {
DraftBuilder::new(base_url, root)
}
pub fn write_referrers(
&mut self,
referrer_store: Option<PathBuf>,
) -> Result<&mut Self, DraftError> {
let referrer_store = if let Some(p) = referrer_store.as_deref() {
p
} else {
self.referrer_store.as_ref()
};
let referrer_store = self.root.join(referrer_store);
if !referrer_store.exists() {
std::fs::create_dir_all(&referrer_store)?;
}
for blog_post in &mut self.blog_posts {
match blog_post.write_referrer_file_to(&referrer_store, &self.base_url, &self.root) {
Ok(_) => continue,
Err(e) => {
log::warn!(
"Blog post: `{}` skipped because of error `{e}`",
blog_post.title()
);
continue;
}
}
}
Ok(self)
}
pub async fn write_bluesky_posts(
&mut self,
bluesky_post_store: Option<PathBuf>,
) -> Result<(), DraftError> {
let bluesky_post_store = if let Some(p) = bluesky_post_store.as_deref() {
p
} else {
self.bsky_store.as_ref()
};
let bluesky_post_store = self.root.join(bluesky_post_store);
if !bluesky_post_store.exists() {
std::fs::create_dir_all(&bluesky_post_store)?;
}
for blog_post in self.blog_posts.iter_mut() {
match blog_post.write_bluesky_record_to(&bluesky_post_store).await {
Ok(_) => continue,
Err(e) => {
log::warn!(
"Blog post: `{}` skipped because of error `{e}`",
blog_post.title()
);
continue;
}
}
}
Ok(())
}
}
pub(crate) fn today() -> toml::value::Datetime {
use chrono::{Datelike, Utc};
let now = Utc::now();
let date = toml::value::Date {
year: now.year() as u16,
month: now.month() as u8,
day: now.day() as u8,
};
toml::value::Datetime {
date: Some(date),
time: None,
offset: None,
}
}
#[cfg(test)]
mod tests {
use std::{fs, fs::File, io::Write, path::Path, str::FromStr};
use log::LevelFilter;
use tempfile::TempDir;
use super::{blog_post::front_matter::FrontMatter, *};
fn get_test_logger(level: LevelFilter) {
let mut builder = env_logger::Builder::new();
builder.filter(None, level);
builder.format_timestamp_secs().format_module_path(false);
let _ = builder.try_init();
}
fn setup_test_environment() -> (TempDir, Url) {
get_test_logger(LevelFilter::Debug);
let temp_dir = tempfile::tempdir().unwrap();
log::debug!("Created temp directory: {temp_dir:?}");
let base_url = Url::from_str("https://www.example.com/").unwrap();
(temp_dir, base_url)
}
fn create_frontmatter_blog_post(dir: &Path, name: &str, front_matter: &FrontMatter) {
log::debug!(
"path: `{}`, name: `{name}`, frontmatter: {front_matter:?}",
dir.display()
);
let blog_store = dir.join("content").join("blog");
if !blog_store.exists() {
log::debug!("creating blog store: `{}`", blog_store.display());
std::fs::create_dir_all(&blog_store).unwrap();
}
let blog_name = blog_store.join(name);
let mut fd = File::create(blog_name).unwrap();
let buffer = format!("+++\n{}+++\n", toml::to_string(front_matter).unwrap());
fd.write_all(buffer.as_bytes()).unwrap();
}
fn create_free_form_blog_post(dir: &Path, name: &str, fm_text: &str) {
log::debug!(
"path: `{}`, name: `{name}`, frontmatter: {fm_text:?}",
dir.display()
);
let blog_store = dir.join("content").join("blog");
if !blog_store.exists() {
log::debug!("creating blog store: `{}`", blog_store.display());
std::fs::create_dir_all(&blog_store).unwrap();
}
let blog_name = blog_store.join(name);
let mut fd = File::create(blog_name).unwrap();
let buffer = format!("+++\n{fm_text}+++\n");
fd.write_all(buffer.as_bytes()).unwrap();
}
#[test]
fn test_builder_with_valid_url_and_no_root() {
let base_url = Url::parse("https://example.com").unwrap();
let builder = Draft::builder(base_url.clone(), None);
assert_eq!(builder.base_url(), &base_url);
assert_eq!(builder.root(), &PathBuf::new().join("."));
}
#[test]
fn test_builder_with_valid_url_and_root_path() {
let base_url = Url::parse("https://example.com").unwrap();
let root_path = PathBuf::from("/home/user/blog");
let builder = Draft::builder(base_url.clone(), Some(&root_path));
assert_eq!(builder.base_url(), &base_url);
assert_eq!(builder.root(), &root_path);
}
#[test]
fn test_builder_with_http_url() {
let base_url = Url::parse("http://localhost:3000").unwrap();
let builder = Draft::builder(base_url.clone(), None);
assert_eq!(builder.base_url(), &base_url);
assert_eq!(builder.base_url().scheme(), "http");
}
#[test]
fn test_builder_with_https_url() {
let base_url = Url::parse("https://myblog.com").unwrap();
let builder = Draft::builder(base_url.clone(), None);
assert_eq!(builder.base_url(), &base_url);
assert_eq!(builder.base_url().scheme(), "https");
}
#[test]
fn test_builder_with_url_containing_path() {
let base_url = Url::parse("https://example.com/blog/").unwrap();
let builder = Draft::builder(base_url.clone(), None);
assert_eq!(builder.base_url(), &base_url);
assert_eq!(builder.base_url().path(), "/blog/");
}
#[test]
fn test_builder_with_url_containing_port() {
let base_url = Url::parse("https://example.com:8080").unwrap();
let builder = Draft::builder(base_url.clone(), None);
assert_eq!(builder.base_url(), &base_url);
assert_eq!(builder.base_url().port(), Some(8080));
}
#[test]
fn test_builder_with_relative_root_path() {
let base_url = Url::parse("https://example.com").unwrap();
let root_path = PathBuf::from("./content");
let builder = Draft::builder(base_url.clone(), Some(&root_path));
assert_eq!(builder.base_url(), &base_url);
assert_eq!(builder.root(), &root_path);
}
#[test]
fn test_builder_with_absolute_root_path() {
let base_url = Url::parse("https://example.com").unwrap();
let root_path = PathBuf::from("/var/www/html");
let builder = Draft::builder(base_url.clone(), Some(&root_path));
assert_eq!(builder.base_url(), &base_url);
assert_eq!(builder.root(), &root_path);
}
#[test]
fn test_builder_with_windows_style_path() {
let base_url = Url::parse("https://example.com").unwrap();
let root_path = PathBuf::from(r"C:\Users\user\blog");
let builder = Draft::builder(base_url.clone(), Some(&root_path));
assert_eq!(builder.base_url(), &base_url);
assert_eq!(builder.root(), &root_path);
}
#[test]
fn test_builder_creates_new_instance_each_time() {
let base_url = Url::parse("https://example.com").unwrap();
let root_path = PathBuf::from("/path/to/blog");
let builder1 = Draft::builder(base_url.clone(), Some(&root_path));
let builder2 = Draft::builder(base_url.clone(), Some(&root_path));
assert_eq!(builder1.base_url(), builder2.base_url());
assert_eq!(builder1.root(), builder2.root());
}
#[test]
fn test_builder_url_ownership() {
let base_url = Url::parse("https://example.com").unwrap();
let original_url = base_url.clone();
let builder = Draft::builder(base_url, None);
assert_eq!(builder.base_url(), &original_url);
assert_eq!(original_url.as_str(), "https://example.com/");
}
#[test]
fn test_builder_path_ownership() {
let root_path = PathBuf::from("/home/user/blog");
let original_path = root_path.clone();
let base_url = Url::parse("https://example.com").unwrap();
let builder = Draft::builder(base_url, Some(&root_path));
assert_eq!(builder.root(), &original_path);
assert_eq!(original_path.to_str().unwrap(), "/home/user/blog");
}
#[test]
fn test_builder_with_complex_url() {
let base_url =
Url::parse("https://user:pass@example.com:8080/blog/?param=value#fragment").unwrap();
let root_path = PathBuf::from("/complex/path/with spaces/and-dashes");
let builder = Draft::builder(base_url.clone(), Some(&root_path));
assert_eq!(builder.base_url(), &base_url);
assert_eq!(builder.root(), &root_path);
assert_eq!(builder.base_url().username(), "user");
assert_eq!(builder.base_url().password(), Some("pass"));
assert_eq!(builder.base_url().host_str(), Some("example.com"));
assert_eq!(builder.base_url().port(), Some(8080));
assert_eq!(builder.base_url().path(), "/blog/");
assert_eq!(builder.base_url().query(), Some("param=value"));
assert_eq!(builder.base_url().fragment(), Some("fragment"));
}
#[test]
fn test_builder_functional_style() {
let create_builder = |url_str: &str, root: Option<&str>| -> DraftBuilder {
let base_url = Url::parse(url_str).unwrap();
let root_path = root.map(PathBuf::from);
Draft::builder(base_url, root_path.as_ref())
};
let builder = create_builder("https://example.com", Some("/path/to/blog"));
assert_eq!(builder.base_url().as_str(), "https://example.com/");
assert_eq!(builder.root().to_str().unwrap(), "/path/to/blog");
}
#[test]
fn test_builder_with_empty_path() {
let base_url = Url::parse("https://example.com").unwrap();
let empty_path = PathBuf::new();
let builder = Draft::builder(base_url.clone(), Some(&empty_path));
assert_eq!(builder.base_url(), &base_url);
assert_eq!(builder.root(), &empty_path);
assert!(builder.root().as_os_str().is_empty());
}
#[tokio::test]
async fn test_write_referrers_creates_directory_if_not_exists() {
let (temp_dir, base_url) = setup_test_environment();
let post_one = FrontMatter::new("Test Post One will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_1.md", &post_one);
let mut draft = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await
.unwrap();
let result = draft.write_referrers(None);
assert!(result.is_ok());
let expected_path = temp_dir.path().join("static/s");
assert!(expected_path.exists());
assert!(expected_path.is_dir());
}
#[tokio::test]
async fn test_write_referrers_uses_existing_directory() {
let (temp_dir, base_url) = setup_test_environment();
let post_one = FrontMatter::new("Test Post One will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_1.md", &post_one);
let mut draft = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await
.unwrap();
let referrer_path = temp_dir.path().join("static/s");
fs::create_dir_all(&referrer_path).unwrap();
let result = draft.write_referrers(None);
assert!(result.is_ok());
assert!(referrer_path.exists());
}
#[tokio::test]
async fn test_write_referrers_with_custom_path() {
let (temp_dir, base_url) = setup_test_environment();
let post_one = FrontMatter::new("Test Post One will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_1.md", &post_one);
let mut draft = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await
.unwrap();
let custom_path = PathBuf::from("custom/referrers");
let result = draft.write_referrers(Some(custom_path.clone()));
assert!(result.is_ok());
let expected_path = temp_dir.path().join(&custom_path);
assert!(expected_path.exists());
assert!(expected_path.is_dir());
}
#[tokio::test]
async fn test_write_referrers_processes_all_blog_posts() {
let (temp_dir, base_url) = setup_test_environment();
let post_one = FrontMatter::new("Test Post One will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_1.md", &post_one);
let post_two = FrontMatter::new("Test Post Two will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_2.md", &post_two);
let post_three = FrontMatter::new("Test Post Three will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_3.md", &post_three);
let mut draft = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await
.unwrap();
let result = draft.write_referrers(None);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_write_referrers_continues_on_individual_errors() {
let (temp_dir, base_url) = setup_test_environment();
let post_one = FrontMatter::new("Test Post One will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_1.md", &post_one);
let post_two = "Title: Test Post Two will Fail";
create_free_form_blog_post(temp_dir.path(), "post_2.md", post_two);
let post_three = FrontMatter::new("Test Post Three will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_3.md", &post_three);
let mut draft = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await
.unwrap();
let result = draft.write_referrers(None);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_write_referrers_returns_self_reference() {
let (temp_dir, base_url) = setup_test_environment();
let post_one = FrontMatter::new("Test Post One will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_1.md", &post_one);
let mut draft = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await
.unwrap();
let result = draft.write_referrers(None);
assert!(result.is_ok());
let returned_draft = result.unwrap();
assert_eq!(returned_draft.blog_posts.len(), 1);
assert_eq!(returned_draft.base_url.as_str(), "https://www.example.com/");
}
#[tokio::test]
async fn test_write_referrers_handles_empty_blog_posts() {
let (temp_dir, base_url) = setup_test_environment();
let result = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await;
assert!(result.is_err());
let expected_path = temp_dir.path().join("static/s");
assert!(!expected_path.exists());
}
#[tokio::test]
async fn test_write_referrers_directory_creation_failure() {
let (temp_dir, base_url) = setup_test_environment();
let post_one = FrontMatter::new("Test Post One will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_1.md", &post_one);
let post_two = "Title: Test Post Two will Fail";
create_free_form_blog_post(temp_dir.path(), "post_2.md", post_two);
let post_three = FrontMatter::new("Test Post Three will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_3.md", &post_three);
let mut draft = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await
.unwrap();
let conflicting_path = temp_dir.path().join("static");
fs::write(&conflicting_path, "file content").unwrap();
let result = draft.write_referrers(None);
assert!(result.is_err());
match result {
Err(DraftError::Io(_)) => {} _ => panic!("Expected IO error"),
}
}
#[tokio::test]
async fn test_write_referrers_with_nested_custom_path() {
let (temp_dir, base_url) = setup_test_environment();
let post_one = FrontMatter::new("Test Post One will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_1.md", &post_one);
let mut draft = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await
.unwrap();
let nested_path = PathBuf::from("deeply/nested/custom/path");
let result = draft.write_referrers(Some(nested_path.clone()));
assert!(result.is_ok());
let expected_path = temp_dir.path().join(&nested_path);
assert!(expected_path.exists());
assert!(expected_path.is_dir());
}
#[tokio::test]
async fn test_write_referrers_absolute_vs_relative_path_handling() {
let (temp_dir, base_url) = setup_test_environment();
let post_one = FrontMatter::new("Test Post One will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_1.md", &post_one);
let mut draft = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await
.unwrap();
let result1 = draft.write_referrers(None);
assert!(result1.is_ok());
let relative_path = PathBuf::from("relative/path");
let result2 = draft.write_referrers(Some(relative_path.clone()));
assert!(result2.is_ok());
let expected_relative = temp_dir.path().join(&relative_path);
assert!(expected_relative.exists());
}
#[tokio::test]
async fn test_write_referrers_method_chaining() {
let (temp_dir, base_url) = setup_test_environment();
let post_one = FrontMatter::new("Test Post One will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_1.md", &post_one);
let mut draft = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await
.unwrap();
let result = draft
.write_referrers(None)
.and_then(|d| d.write_referrers(Some(PathBuf::from("another/path"))));
assert!(result.is_ok());
assert!(temp_dir.path().join("static/s").exists());
assert!(temp_dir.path().join("another/path").exists());
}
#[tokio::test]
async fn test_write_bluesky_posts_with_default_store() {
get_test_logger(LevelFilter::Debug);
let random_name = crate::util::test_utils::random_name();
log::debug!("Random name for test directory: `{random_name}`");
let temp_dir = PathBuf::new().join(random_name);
fs::create_dir(&temp_dir).unwrap();
log::trace!("Created temp directory: {temp_dir:?}");
let base_url = Url::from_str("https://www.example.com/").unwrap();
let first_post = FrontMatter::new("First Post", "Description of first post");
let second_post = FrontMatter::new("Second Post", "Description of second post");
create_frontmatter_blog_post(temp_dir.as_path(), "first-post.md", &first_post);
create_frontmatter_blog_post(temp_dir.as_path(), "second-post.md", &second_post);
for entry in temp_dir
.as_path()
.join(crate::util::default_blog_dir())
.read_dir()
.expect("read_dir call failed")
.flatten()
{
log::debug!("Entry found: `{}`", entry.file_name().to_string_lossy());
}
let mut draft = Draft::builder(base_url, Some(&temp_dir))
.build()
.await
.unwrap();
let result = draft.write_bluesky_posts(None).await;
assert!(result.is_ok());
let expected_path = temp_dir.join("bluesky");
assert!(expected_path.exists());
assert!(expected_path.is_dir());
for post in &draft.blog_posts {
log::debug!("Checking if written post file: {post:#?}");
assert_eq!(post.bluesky_count(), 1);
}
fs::remove_dir_all(temp_dir).unwrap();
}
#[tokio::test]
async fn test_write_bluesky_posts_with_custom_store() {
get_test_logger(LevelFilter::Debug);
let temp_dir = tempfile::tempdir().unwrap();
log::debug!("Created temp directory: {temp_dir:?}");
let base_url = Url::from_str("https://www.example.com/").unwrap();
let first_post = FrontMatter::new("Test Post", "Description of test post");
create_frontmatter_blog_post(temp_dir.path(), "test-post.md", &first_post);
let mut draft = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await
.unwrap();
let custom_store = PathBuf::from("custom/bluesky/path");
let result = draft.write_bluesky_posts(Some(custom_store.clone())).await;
assert!(result.is_ok());
let expected_path = temp_dir.path().join(custom_store);
assert!(expected_path.exists());
assert!(expected_path.is_dir());
assert_eq!(draft.blog_posts[0].bluesky_count(), 1);
}
#[tokio::test]
async fn test_write_bluesky_posts_creates_nested_directories() {
let (temp_dir, base_url) = setup_test_environment();
let first_post = FrontMatter::new("Test Post", "Description of test post");
create_frontmatter_blog_post(temp_dir.path(), "test-post.md", &first_post);
let mut draft = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await
.unwrap();
let nested_store = PathBuf::from("deeply/nested/bluesky/directory");
let result = draft.write_bluesky_posts(Some(nested_store.clone())).await;
assert!(result.is_ok());
let expected_path = temp_dir.path().join(nested_store);
assert!(expected_path.exists());
assert!(expected_path.is_dir());
}
#[tokio::test]
async fn test_write_bluesky_posts_continues_on_individual_failures() {
let (temp_dir, base_url) = setup_test_environment();
let post_one = FrontMatter::new("Test Post One will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_1.md", &post_one);
let post_two = "Title: Test Post Two will Fail";
create_free_form_blog_post(temp_dir.path(), "post_2.md", post_two);
let post_three = FrontMatter::new("Test Post Three will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_3.md", &post_three);
let mut draft = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await
.unwrap();
let result = draft.write_bluesky_posts(None).await;
assert!(result.is_ok());
for post in &draft.blog_posts {
assert_eq!(post.bluesky_count(), 1);
}
assert_eq!(2, draft.blog_posts.len());
let expected_path = temp_dir.path().join("bluesky");
assert!(expected_path.exists());
}
#[tokio::test]
async fn test_write_bluesky_posts_empty_blog_posts() {
let (temp_dir, base_url) = setup_test_environment();
let result = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
DraftError::BlogPostListEmpty.to_string()
);
let expected_path = temp_dir.path().join("bluesky");
assert!(!expected_path.exists());
}
#[tokio::test]
async fn test_write_bluesky_posts_existing_directory() {
let (temp_dir, base_url) = setup_test_environment();
let post_one = FrontMatter::new("Test Post One will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_1.md", &post_one);
let mut draft = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await
.unwrap();
let store_path = temp_dir.path().join("bluesky");
std::fs::create_dir_all(&store_path).unwrap();
let result = draft.write_bluesky_posts(None).await;
assert!(result.is_ok());
assert!(store_path.exists());
assert_eq!(draft.blog_posts[0].bluesky_count(), 1);
}
#[tokio::test]
async fn test_write_bluesky_posts_path_resolution() {
let (temp_dir, base_url) = setup_test_environment();
let post_one = FrontMatter::new("Test Post One will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_1.md", &post_one);
let mut draft = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await
.unwrap();
let result = draft.write_bluesky_posts(None).await;
assert!(result.is_ok());
let default_path = temp_dir.path().join(crate::util::default_bluesky_dir());
assert!(default_path.exists());
let override_path = PathBuf::from("override/bsky");
let result = draft.write_bluesky_posts(Some(override_path.clone())).await;
assert!(result.is_ok());
let override_full_path = temp_dir.path().join(override_path);
assert!(override_full_path.exists());
}
#[tokio::test]
async fn test_write_bluesky_posts_multiple_calls() {
let (temp_dir, base_url) = setup_test_environment();
let post_one = FrontMatter::new("Test Post One will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_1.md", &post_one);
let post_two = FrontMatter::new("Test Post two will Pass", "This post will pass");
create_frontmatter_blog_post(temp_dir.path(), "post_2.md", &post_two);
let mut draft = Draft::builder(base_url, Some(&temp_dir.path().to_path_buf()))
.build()
.await
.unwrap();
let result1 = draft.write_bluesky_posts(None).await;
assert!(result1.is_ok());
for post in &draft.blog_posts {
assert_eq!(post.bluesky_count(), 1);
}
let result2 = draft.write_bluesky_posts(None).await;
assert!(result2.is_ok());
for post in &draft.blog_posts {
assert_eq!(
post.bluesky_count(),
1,
"idempotent: count must not change on repeat"
);
}
}
}