use crate::{
common::sanitize_url,
limits::VALIDATION_LIMITS,
traits::{HasIdPath, TimestampId, Validatable},
APP_PATH, PUBLIC_PATH,
};
use serde::{Deserialize, Serialize};
use std::{fmt, str::FromStr};
use url::Url;
const RESERVED_CONTENT_DELETED: &str = "[DELETED]";
#[cfg(target_arch = "wasm32")]
use crate::traits::Json;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg(feature = "openapi")]
use utoipa::ToSchema;
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub enum PubkyAppPostKind {
#[default]
Short,
Long,
Image,
Video,
Link,
File,
}
impl fmt::Display for PubkyAppPostKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let string_repr = serde_json::to_value(self)
.ok()
.and_then(|v| v.as_str().map(String::from))
.unwrap_or_default();
write!(f, "{}", string_repr)
}
}
impl FromStr for PubkyAppPostKind {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"short" => Ok(PubkyAppPostKind::Short),
"long" => Ok(PubkyAppPostKind::Long),
"image" => Ok(PubkyAppPostKind::Image),
"video" => Ok(PubkyAppPostKind::Video),
"link" => Ok(PubkyAppPostKind::Link),
"file" => Ok(PubkyAppPostKind::File),
_ => Err(format!("Invalid content kind: {}", s)),
}
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct PubkyAppPostEmbed {
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
pub kind: PubkyAppPostKind, #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
pub uri: String, }
#[cfg(target_arch = "wasm32")]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
impl PubkyAppPostEmbed {
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))]
pub fn new(uri: String, kind: PubkyAppPostKind) -> Self {
PubkyAppPostEmbed { uri, kind }
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))]
pub fn kind(&self) -> String {
match self.kind {
PubkyAppPostKind::Short => "Short".to_string(),
PubkyAppPostKind::Long => "Long".to_string(),
PubkyAppPostKind::Image => "Image".to_string(),
PubkyAppPostKind::Video => "Video".to_string(),
PubkyAppPostKind::Link => "Link".to_string(),
PubkyAppPostKind::File => "File".to_string(),
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))]
pub fn uri(&self) -> String {
self.uri.clone()
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[derive(Serialize, Deserialize, Default, Clone, Debug)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct PubkyAppPost {
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
pub content: String,
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
pub kind: PubkyAppPostKind,
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
pub parent: Option<String>, #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
pub embed: Option<PubkyAppPostEmbed>,
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
pub attachments: Option<Vec<String>>,
}
#[cfg(target_arch = "wasm32")]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
impl PubkyAppPost {
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))]
pub fn content(&self) -> String {
self.content.clone()
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))]
pub fn kind(&self) -> String {
match self.kind {
PubkyAppPostKind::Short => "Short".to_string(),
PubkyAppPostKind::Long => "Long".to_string(),
PubkyAppPostKind::Image => "Image".to_string(),
PubkyAppPostKind::Video => "Video".to_string(),
PubkyAppPostKind::Link => "Link".to_string(),
PubkyAppPostKind::File => "File".to_string(),
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))]
pub fn parent(&self) -> Option<String> {
self.parent.clone()
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))]
pub fn embed(&self) -> Option<PubkyAppPostEmbed> {
self.embed.clone()
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))]
pub fn attachments(&self) -> Option<Vec<String>> {
self.attachments.clone()
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = fromJson))]
pub fn from_json(js_value: &JsValue) -> Result<Self, String> {
Self::import_json(js_value)
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = toJson))]
pub fn to_json(&self) -> Result<JsValue, String> {
self.export_json()
}
}
#[cfg(target_arch = "wasm32")]
impl Json for PubkyAppPost {}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
impl PubkyAppPost {
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))]
pub fn new(
content: String,
kind: PubkyAppPostKind,
parent: Option<String>,
embed: Option<PubkyAppPostEmbed>,
attachments: Option<Vec<String>>,
) -> Self {
let post = PubkyAppPost {
content,
kind,
parent,
embed,
attachments,
};
post.sanitize()
}
}
impl TimestampId for PubkyAppPost {}
impl HasIdPath for PubkyAppPost {
const PATH_SEGMENT: &'static str = "posts/";
fn create_path(id: &str) -> String {
[PUBLIC_PATH, APP_PATH, Self::PATH_SEGMENT, id].concat()
}
}
impl Validatable for PubkyAppPost {
fn sanitize(self) -> Self {
let content = self.content.trim().to_string();
let parent = self.parent.map(|uri_str| sanitize_url(&uri_str));
let embed = self.embed.map(|e| PubkyAppPostEmbed {
kind: e.kind,
uri: sanitize_url(&e.uri),
});
let attachments = self.attachments.map(|attachments_vec| {
attachments_vec
.into_iter()
.map(|url_str| sanitize_url(&url_str))
.collect()
});
PubkyAppPost {
content,
kind: self.kind,
parent,
embed,
attachments,
}
}
fn validate(&self, id: Option<&str>) -> Result<(), String> {
if let Some(id) = id {
self.validate_id(id)?;
}
if self.content.trim().is_empty() && self.embed.is_none() && self.attachments.is_none() {
return Err(
"Validation Error: Post must have content, an embed, or attachments".into(),
);
}
if self.content == RESERVED_CONTENT_DELETED {
return Err(
"Validation Error: Content cannot be the reserved keyword '[DELETED]'".into(),
);
}
let (max_length, kind_name) = match self.kind {
PubkyAppPostKind::Short => (VALIDATION_LIMITS.post_short_content_max_length, "Short"),
PubkyAppPostKind::Long => (VALIDATION_LIMITS.post_long_content_max_length, "Long"),
PubkyAppPostKind::Image
| PubkyAppPostKind::Video
| PubkyAppPostKind::Link
| PubkyAppPostKind::File => (
VALIDATION_LIMITS.post_short_content_max_length,
"Image/Video/Link/File",
),
};
if self.content.chars().count() > max_length {
return Err(format!(
"Validation Error: Post content exceeds maximum length for {} kind (max: {} characters)",
kind_name, max_length
));
}
if let Some(ref parent_uri) = self.parent {
Url::parse(parent_uri).map_err(|_| {
format!(
"Validation Error: Invalid parent URI format: {}",
parent_uri
)
})?;
}
if let Some(ref embed) = self.embed {
Url::parse(&embed.uri).map_err(|_| {
format!("Validation Error: Invalid embed URI format: {}", embed.uri)
})?;
}
if let Some(attachments) = &self.attachments {
if attachments.len() > VALIDATION_LIMITS.post_attachments_max_count {
return Err(format!(
"Validation Error: Too many attachments (max: {})",
VALIDATION_LIMITS.post_attachments_max_count
));
}
for (index, url) in attachments.iter().enumerate() {
if url.trim().is_empty() {
return Err(format!(
"Validation Error: Attachment URL at index {} cannot be empty",
index
));
}
if url.chars().count() > VALIDATION_LIMITS.post_attachment_url_max_length {
return Err(format!(
"Validation Error: Attachment URL at index {} exceeds maximum length (max: {} characters)",
index, VALIDATION_LIMITS.post_attachment_url_max_length
));
}
let parsed_url = Url::parse(url).map_err(|_| {
format!(
"Validation Error: Invalid attachment URL format at index {}",
index
)
})?;
if !VALIDATION_LIMITS
.post_allowed_attachment_protocols
.contains(&parsed_url.scheme())
{
let allowed_protocols = VALIDATION_LIMITS
.post_allowed_attachment_protocols
.iter()
.map(|p| format!("{}://", p))
.collect::<Vec<_>>()
.join(", ");
return Err(format!(
"Validation Error: Attachment URL at index {} must use one of the allowed protocols: {}",
index, allowed_protocols
));
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{traits::Validatable, APP_PATH, PUBLIC_PATH};
#[test]
fn test_create_id() {
let post = PubkyAppPost::new(
"Hello World!".to_string(),
PubkyAppPostKind::Short,
None,
None,
None,
);
let post_id = post.create_id();
println!("Generated Post ID: {}", post_id);
assert_eq!(post_id.len(), 13);
}
#[test]
fn test_new() {
let content = "This is a test post".to_string();
let kind = PubkyAppPostKind::Short;
let post = PubkyAppPost::new(content.clone(), kind.clone(), None, None, None);
assert_eq!(post.content, content);
assert_eq!(post.kind, kind);
assert!(post.parent.is_none());
assert!(post.embed.is_none());
assert!(post.attachments.is_none());
}
#[test]
fn test_create_path() {
let post = PubkyAppPost::new(
"Test post".to_string(),
PubkyAppPostKind::Short,
None,
None,
None,
);
let post_id = post.create_id();
let path = PubkyAppPost::create_path(&post_id);
let prefix = format!("{}{}posts/", PUBLIC_PATH, APP_PATH);
assert!(path.starts_with(&prefix));
let expected_path_len = prefix.len() + post_id.len();
assert_eq!(path.len(), expected_path_len);
}
#[test]
fn test_sanitize() {
let content = " This is a test post with extra whitespace ".to_string();
let post = PubkyAppPost::new(
content.clone(),
PubkyAppPostKind::Short,
Some(" pubky://6mfxozzqmb36rc9rgy3rykoyfghfao74n8igt5tf1boehproahoy/pub/pubky.app/posts/0034A0X7NJ52G ".to_string()),
Some(PubkyAppPostEmbed {
kind: PubkyAppPostKind::Link,
uri: " pubky://6mfxozzqmb36rc9rgy3rykoyfghfao74n8igt5tf1boehproahoy/pub/pubky.app/files/0034A0X7Q3D80 ".to_string(),
}),
Some(vec![
"pubky://6mfxozzqmb36rc9rgy3rykoyfghfao74n8igt5tf1boehproahoy/pub/pubky.app/files/0034A0X7NJ52G".to_string(),
" pubky://6mfxozzqmb36rc9rgy3rykoyfghfao74n8igt5tf1boehproahoy/pub/pubky.app/files/0034A0X7Q3D80 ".to_string(), ]),
);
let sanitized_post = post.sanitize();
assert_eq!(sanitized_post.content, content.trim());
assert!(sanitized_post.parent.is_some());
let parent = sanitized_post.parent.unwrap();
assert!(!parent.starts_with(" "));
assert!(!parent.ends_with(" "));
assert!(parent.starts_with("pubky://"));
assert!(sanitized_post.embed.is_some());
let embed = sanitized_post.embed.unwrap();
assert!(!embed.uri.starts_with(" "));
assert!(!embed.uri.ends_with(" "));
assert!(embed.uri.starts_with("pubky://"));
assert!(sanitized_post.attachments.is_some());
let attachments = sanitized_post.attachments.unwrap();
assert_eq!(attachments.len(), 2);
assert!(attachments[0].starts_with("pubky://"));
assert!(attachments[1].starts_with("pubky://"));
assert!(!attachments[1].starts_with(" pubky://"));
assert!(!attachments[1].ends_with(" "));
}
#[test]
fn test_sanitize_trims_parent_and_embed() {
let valid_parent_uri = " pubky://6mfxozzqmb36rc9rgy3rykoyfghfao74n8igt5tf1boehproahoy/pub/pubky.app/posts/0034A0X7NJ52G ".to_string();
let valid_embed_uri = " pubky://6mfxozzqmb36rc9rgy3rykoyfghfao74n8igt5tf1boehproahoy/pub/pubky.app/files/0034A0X7Q3D80 ".to_string();
let post = PubkyAppPost::new(
"Test content".to_string(),
PubkyAppPostKind::Short,
Some(valid_parent_uri.clone()),
Some(PubkyAppPostEmbed {
kind: PubkyAppPostKind::Link,
uri: valid_embed_uri.clone(),
}),
None,
);
let sanitized_post = post.sanitize();
assert!(sanitized_post.parent.is_some());
let parent = sanitized_post.parent.unwrap();
assert!(!parent.starts_with(" "));
assert!(!parent.ends_with(" "));
assert!(parent.starts_with("pubky://"));
assert!(sanitized_post.embed.is_some());
let embed = sanitized_post.embed.unwrap();
assert!(!embed.uri.starts_with(" "));
assert!(!embed.uri.ends_with(" "));
assert!(embed.uri.starts_with("pubky://"));
}
#[test]
fn test_validate() {
let post = PubkyAppPost::new(
"Valid content".to_string(),
PubkyAppPostKind::Short,
None,
None,
None,
);
let id = post.create_id();
let result = post.validate(Some(&id));
assert!(result.is_ok());
}
#[test]
fn test_validate_invalid_id() {
let post = PubkyAppPost::new(
"Valid content".to_string(),
PubkyAppPostKind::Short,
None,
None,
None,
);
let invalid_id = "INVALIDID12345";
let result = post.validate(Some(invalid_id));
assert!(result.is_err());
}
#[test]
fn test_validate_invalid_parent_uri() {
let post = PubkyAppPost::new(
"Valid content".to_string(),
PubkyAppPostKind::Short,
Some("invalid uri".to_string()),
None,
None,
);
let id = post.create_id();
let sanitized = post.sanitize();
let result = sanitized.validate(Some(&id));
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid parent URI format"));
}
#[test]
fn test_validate_invalid_embed_uri() {
let post = PubkyAppPost::new(
"Valid content".to_string(),
PubkyAppPostKind::Short,
None,
Some(PubkyAppPostEmbed {
kind: PubkyAppPostKind::Link,
uri: "invalid uri".to_string(),
}),
None,
);
let id = post.create_id();
let sanitized = post.sanitize();
let result = sanitized.validate(Some(&id));
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid embed URI format"));
}
#[test]
fn test_validate_invalid_attachment_uri() {
let post = PubkyAppPost::new(
"Valid content".to_string(),
PubkyAppPostKind::Image,
None,
None,
Some(vec![
"pubky://6mfxozzqmb36rc9rgy3rykoyfghfao74n8igt5tf1boehproahoy/pub/pubky.app/files/0034A0X7NJ52G".to_string(),
"invalid uri".to_string(),
]),
);
let id = post.create_id();
let sanitized = post.sanitize();
let result = sanitized.validate(Some(&id));
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("Invalid attachment URL format"));
}
#[test]
fn test_try_from_valid() {
let post_json = r#"
{
"content": "Hello World!",
"kind": "short",
"parent": null,
"embed": null,
"attachments": null
}
"#;
let id = PubkyAppPost::new(
"Hello World!".to_string(),
PubkyAppPostKind::Short,
None,
None,
None,
)
.create_id();
let blob = post_json.as_bytes();
let post = <PubkyAppPost as Validatable>::try_from(blob, &id).unwrap();
assert_eq!(post.content, "Hello World!");
}
#[test]
fn test_validate_reserved_keyword() {
let post = PubkyAppPost::new(
"[DELETED]".to_string(),
PubkyAppPostKind::Short,
None,
None,
None,
);
let id = post.create_id();
let result = post.validate(Some(&id));
assert!(result.is_err());
assert!(result.unwrap_err().contains("reserved keyword"));
}
#[test]
fn test_try_from_invalid_content() {
let content = "[DELETED]".to_string();
let post_json = format!(
r#"{{
"content": "{}",
"kind": "short",
"parent": null,
"embed": null,
"attachments": null
}}"#,
content
);
let id = PubkyAppPost::new(content.clone(), PubkyAppPostKind::Short, None, None, None)
.create_id();
let blob = post_json.as_bytes();
let result = <PubkyAppPost as Validatable>::try_from(blob, &id);
assert!(result.is_err());
assert!(result.unwrap_err().contains("reserved keyword"));
}
#[test]
fn test_validate_attachments_valid_protocols() {
let protocols = vec![
"pubky://6mfxozzqmb36rc9rgy3rykoyfghfao74n8igt5tf1boehproahoy/pub/pubky.app/files/0034A0X7NJ52G".to_string(),
"https://example.com/file.png".to_string(),
"http://example.com/file.jpg".to_string(),
];
assert!(
protocols.len() <= VALIDATION_LIMITS.post_attachments_max_count,
"Test uses more than post_attachments_max_count"
);
let post = PubkyAppPost::new(
"Valid content".to_string(),
PubkyAppPostKind::Image,
None,
None,
Some(protocols),
);
let id = post.create_id();
let result = post.validate(Some(&id));
assert!(result.is_ok());
}
#[test]
fn test_validate_attachments_all_allowed_protocols() {
let allowed_protocols = vec![
"pubky://6mfxozzqmb36rc9rgy3rykoyfghfao74n8igt5tf1boehproahoy/pub/pubky.app/files/0034A0X7NJ52G",
"http://example.com/file.jpg",
"https://example.com/file.png",
];
for protocol_url in allowed_protocols {
let post = PubkyAppPost::new(
"Valid content".to_string(),
PubkyAppPostKind::Image,
None,
None,
Some(vec![protocol_url.to_string()]),
);
let id = post.create_id();
let result = post.validate(Some(&id));
assert!(result.is_ok(), "Should accept protocol: {}", protocol_url);
}
}
#[test]
fn test_validate_attachments_too_many() {
let mut attachments = Vec::new();
for i in 0..VALIDATION_LIMITS.post_attachments_max_count + 1 {
attachments.push(format!(
"pubky://6mfxozzqmb36rc9rgy3rykoyfghfao74n8igt5tf1boehproahoy/pub/pubky.app/files/{}",
i
));
}
let post = PubkyAppPost::new(
"Valid content".to_string(),
PubkyAppPostKind::Image,
None,
None,
Some(attachments),
);
let id = post.create_id();
let result = post.validate(Some(&id));
assert!(result.is_err());
assert!(result.unwrap_err().contains("Too many attachments"));
}
#[test]
fn test_validate_attachments_invalid_protocol() {
let invalid_protocols = vec!["ftp://example.com/file", "file:///path/to/file"];
for invalid_url in invalid_protocols {
let post = PubkyAppPost {
content: "Valid content".to_string(),
kind: PubkyAppPostKind::Image,
parent: None,
embed: None,
attachments: Some(vec![invalid_url.to_string()]),
};
let id = post.create_id();
let result = post.validate(Some(&id));
assert!(result.is_err(), "Should reject protocol: {}", invalid_url);
assert!(result.unwrap_err().contains("protocol"));
}
}
#[test]
fn test_validate_attachments_invalid_url_format() {
let post = PubkyAppPost {
content: "Valid content".to_string(),
kind: PubkyAppPostKind::Image,
parent: None,
embed: None,
attachments: Some(vec!["not a valid url".to_string()]),
};
let id = post.create_id();
let result = post.validate(Some(&id));
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("Invalid attachment URL format"));
}
#[test]
fn test_validate_attachments_url_too_long() {
let long_file_id = "a".repeat(150); let long_url = format!(
"pubky://6mfxozzqmb36rc9rgy3rykoyfghfao74n8igt5tf1boehproahoy/pub/pubky.app/files/{}",
long_file_id
);
assert!(
long_url.chars().count() > VALIDATION_LIMITS.post_attachment_url_max_length,
"URL length {} should exceed {}",
long_url.chars().count(),
VALIDATION_LIMITS.post_attachment_url_max_length
);
let post = PubkyAppPost::new(
"Valid content".to_string(),
PubkyAppPostKind::Image,
None,
None,
Some(vec![long_url]),
);
let id = post.create_id();
let result = post.validate(Some(&id));
assert!(result.is_err());
assert!(result.unwrap_err().contains("exceeds maximum length"));
}
#[test]
fn test_validate_attachments_empty_url() {
let post = PubkyAppPost {
content: "Valid content".to_string(),
kind: PubkyAppPostKind::Image,
parent: None,
embed: None,
attachments: Some(vec![" ".to_string()]), };
let id = post.create_id();
let result = post.validate(Some(&id));
assert!(result.is_err());
assert!(result.unwrap_err().contains("cannot be empty"));
}
#[test]
fn test_sanitize_attachments_preserves_all() {
let post = PubkyAppPost::new(
"Valid content".to_string(),
PubkyAppPostKind::Image,
None,
None,
Some(vec![
"pubky://6mfxozzqmb36rc9rgy3rykoyfghfao74n8igt5tf1boehproahoy/pub/pubky.app/files/0034A0X7NJ52G".to_string(),
"https://example.com/file.jpg".to_string(),
" invalid url ".to_string(), ]),
);
let id = post.create_id();
let sanitized = post.sanitize();
assert!(sanitized.attachments.is_some());
let attachments = sanitized.attachments.as_ref().unwrap();
assert_eq!(attachments.len(), 3); assert!(attachments[0].starts_with("pubky://"));
assert!(attachments[1].starts_with("https://"));
assert_eq!(attachments[2], "invalid url");
let result = sanitized.validate(Some(&id));
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("Invalid attachment URL format"));
}
#[test]
fn test_sanitize_attachments_with_all_invalid_preserved() {
let post = PubkyAppPost::new(
"Valid content".to_string(),
PubkyAppPostKind::Image,
None,
None,
Some(vec!["invalid url".to_string(), "not a url".to_string()]),
);
let id = post.create_id();
let sanitized = post.sanitize();
assert!(sanitized.attachments.is_some()); let attachments = sanitized.attachments.as_ref().unwrap();
assert_eq!(attachments.len(), 2);
let result = sanitized.validate(Some(&id));
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("Invalid attachment URL format"));
}
#[test]
fn test_validate_empty_post_rejected() {
let post = PubkyAppPost::new("".to_string(), PubkyAppPostKind::Short, None, None, None);
let id = post.create_id();
let result = post.validate(Some(&id));
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("must have content, an embed, or attachments"));
}
#[test]
fn test_validate_empty_content_with_embed_accepted() {
let post = PubkyAppPost::new(
"".to_string(),
PubkyAppPostKind::Short,
None,
Some(PubkyAppPostEmbed {
kind: PubkyAppPostKind::Short,
uri: "pubky://user123/pub/pubky.app/posts/0033SSE3B1FQ0".to_string(),
}),
None,
);
let id = post.create_id();
let result = post.validate(Some(&id));
assert!(
result.is_ok(),
"Post with embed but no content should be valid"
);
}
#[test]
fn test_validate_empty_content_with_attachments_accepted() {
let post = PubkyAppPost::new(
"".to_string(),
PubkyAppPostKind::Image,
None,
None,
Some(vec![
"pubky://user123/pub/pubky.app/files/0034A0X7NJ52G".to_string()
]),
);
let id = post.create_id();
let result = post.validate(Some(&id));
assert!(
result.is_ok(),
"Post with attachments but no content should be valid"
);
}
}