mod common;
use anytype::{
prelude::*,
test_util::{TestError, TestResult, unique_suffix, with_test_context},
};
use tracing::debug;
use crate::common::{
create_object_with_retry, lookup_property_tag_with_retry, retry_definitive_rate_limit,
};
#[tokio::test]
#[test_log::test]
async fn test_types_list() -> TestResult<()> {
with_test_context(|ctx| async move {
let result = ctx.client.types(&ctx.space_id).list().await?;
let types = &result.items;
ctx.increment_calls(1);
assert!(!types.is_empty(), "expected at least 1 type");
for typ in types {
assert!(!typ.id.is_empty(), "Type ID should not be empty");
assert!(!typ.key.is_empty(), "Type key should not be empty");
}
println!("Listed {} types", types.len());
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_list_with_limit() -> TestResult<()> {
with_test_context(|ctx| async move {
let limit = 3;
let result = ctx.client.types(&ctx.space_id).limit(limit).list().await?;
let types = &result.items;
ctx.increment_calls(1);
assert!(
types.len() <= limit as usize,
"Expected at most {} types, got {}",
limit,
types.len()
);
println!("Listed {} types with limit {}", types.len(), limit);
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_list_with_offset() -> TestResult<()> {
with_test_context(|ctx| async move {
let all_types = ctx.client.types(&ctx.space_id).list().await?.items.clone();
let offset = 2;
let result = ctx
.client
.types(&ctx.space_id)
.offset(offset)
.list()
.await?;
let offset_types = &result.items;
ctx.increment_calls(2);
if all_types.len() > offset as usize {
assert!(
!offset_types.is_empty(),
"Expected types after offset {}",
offset
);
if offset_types.is_empty() {
return Err(TestError::Assertion {
message: format!("Expected types after offset {}", offset),
});
}
}
println!(
"Listed {} types with offset {} (total: {})",
offset_types.len(),
offset,
all_types.len()
);
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_list_field_presence() -> TestResult<()> {
with_test_context(|ctx| async move {
let result = ctx.client.types(&ctx.space_id).list().await?;
let types = &result.items;
ctx.increment_calls(1);
assert!(!types.is_empty(), "Expected types to be present");
for typ in types {
assert!(!typ.id.is_empty(), "Type.id is required");
assert!(!typ.key.is_empty(), "Type.key is required");
assert!(
!typ.properties.is_empty(),
"Type.properties should have at least one property"
);
}
println!("Verified field presence on {} types", types.len());
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_get_by_id() -> TestResult<()> {
with_test_context(|ctx| async move {
let types = ctx.client.types(&ctx.space_id).list().await?.items.clone();
assert!(!types.is_empty(), "Need at least one type for this test");
let first_type = &types[0];
let retrieved = ctx
.client
.get_type(&ctx.space_id, &first_type.id)
.get()
.await?;
ctx.increment_calls(2);
assert_eq!(retrieved.id, first_type.id);
assert_eq!(retrieved.key, first_type.key);
assert_eq!(retrieved.name, first_type.name);
println!(
"Successfully retrieved type: {} ({})",
retrieved.key, retrieved.id
);
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_resolve_type_by_id_bypasses_primed_cache() -> TestResult<()> {
with_test_context(|ctx| async move {
let types = ctx.client.types(&ctx.space_id).list().await?.items.clone();
let first_type = types.first().ok_or_else(|| TestError::Assertion {
message: "Need at least one type for this test".to_string(),
})?;
let resolved = ctx
.client
.resolve_type(&ctx.space_id, &first_type.id)
.await?;
ctx.increment_calls(2);
assert_eq!(resolved.id, first_type.id);
assert_eq!(resolved.key, first_type.key);
assert_eq!(resolved.name, first_type.name);
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_get_consistency() -> TestResult<()> {
with_test_context(|ctx| async move {
let types_from_list = ctx.client.types(&ctx.space_id).list().await?.items.clone();
assert!(
!types_from_list.is_empty(),
"Need types for consistency test"
);
let sample_type = &types_from_list[0];
let type_from_get = ctx
.client
.get_type(&ctx.space_id, &sample_type.id)
.get()
.await?;
ctx.increment_calls(2);
assert_eq!(type_from_get.id, sample_type.id);
assert_eq!(type_from_get.key, sample_type.key);
assert_eq!(type_from_get.name, sample_type.name);
assert_eq!(type_from_get.archived, sample_type.archived);
assert_eq!(type_from_get.properties.len(), sample_type.properties.len());
println!("Verified consistency for type: {}", sample_type.key);
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_get_nonexistent() -> TestResult<()> {
with_test_context(|ctx| async move {
let fake_id = "nonexistent-type-12345";
let result = ctx.client.get_type(&ctx.space_id, fake_id).get().await;
ctx.increment_calls(1);
assert!(result.is_err(), "Expected error for nonexistent type");
if let Err(e) = result {
match e {
AnytypeError::NotFound { obj_type, .. } if &obj_type == "Type" => {
println!("Correctly received NotFound error");
}
AnytypeError::Validation { .. } => {
println!("Correctly received Validation error for invalid type id");
}
_ => {
return Err(TestError::Assertion {
message: format!("Expected NotFound or Validation error, got: {:?}", e),
});
}
}
}
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_system_present() -> TestResult<()> {
with_test_context(|ctx| async move {
let types = ctx.client.types(&ctx.space_id).list().await?.items.clone();
ctx.increment_calls(1);
let type_keys: Vec<&str> = types.iter().map(|t| t.key.as_str()).collect();
let expected_system_types = ["page", "note", "task", "bookmark"];
for expected_key in expected_system_types {
assert!(
type_keys.contains(&expected_key),
"Expected system type '{}' to be present",
expected_key
);
}
println!(
"Verified {} system types present",
expected_system_types.len()
);
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_system_properties() -> TestResult<()> {
with_test_context(|ctx| async move {
let types = ctx.client.types(&ctx.space_id).list().await?.items.clone();
ctx.increment_calls(1);
let page_type = types
.iter()
.find(|t| t.key == "page")
.expect("Page type should exist");
assert!(
!page_type.properties.is_empty(),
"System type 'page' should have properties"
);
for prop in &page_type.properties {
assert!(!prop.id.is_empty(), "Property id should not be empty");
assert!(!prop.key.is_empty(), "Property key should not be empty");
assert!(!prop.name.is_empty(), "Property name should not be empty");
}
println!(
"System type 'page' has {} properties",
page_type.properties.len()
);
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_is_system_type() -> TestResult<()> {
with_test_context(|ctx| async move {
let types = ctx.client.types(&ctx.space_id).list().await?.items.clone();
ctx.increment_calls(1);
let mut system_count = 0;
let mut custom_count = 0;
for typ in &types {
if typ.is_system_type() {
system_count += 1;
assert!(
matches!(typ.key.as_str(), "page" | "note" | "task" | "bookmark"),
"is_system_type() returned true for non-system type: {}",
typ.key
);
} else {
custom_count += 1;
}
}
assert!(
system_count >= 4,
"Expected at least 4 system types, found {}",
system_count
);
println!(
"Found {} system types and {} custom types",
system_count, custom_count
);
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_create_simple() -> TestResult<()> {
with_test_context(|ctx| async move {
let unique_key = format!("test_type_{}", unique_suffix());
let created = ctx
.client
.new_type(&ctx.space_id, "Simple Type")
.key(&unique_key)
.create()
.await?;
ctx.register_type(&created.id);
ctx.increment_calls(1);
assert!(!created.id.is_empty(), "Created type should have an ID");
assert_eq!(created.key, unique_key);
assert_eq!(created.name.as_deref(), Some("Simple Type"));
assert!(!created.is_system_type());
debug!("Created custom type: {} ({})", created.key, created.id);
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_create_with_properties() -> TestResult<()> {
with_test_context(|ctx| async move {
let unique_key = format!("test_type_props_{}", unique_suffix());
let created = ctx
.client
.new_type(&ctx.space_id, "Product Type")
.key(&unique_key)
.property("Status", "status", PropertyFormat::Select)
.property("Priority", "priority", PropertyFormat::Number)
.create()
.await?;
ctx.increment_calls(1);
ctx.register_type(&created.id);
let custom_props: Vec<_> = created
.properties
.iter()
.filter(|p| p.key == "status" || p.key == "priority")
.collect();
assert!(
custom_props.len() >= 2,
"Expected at least 2 custom properties, found {}",
custom_props.len()
);
let status_prop = created
.properties
.iter()
.find(|p| p.key == "status")
.expect("Status property should exist");
assert_eq!(status_prop.name, "Status");
assert_eq!(status_prop.format(), PropertyFormat::Select);
println!(
"Created type with {} total properties",
created.properties.len()
);
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_create_type_and_object() -> TestResult<()> {
with_test_context(|ctx| async move {
let unique = unique_suffix();
let type_name = format!("Type {unique}");
let type_key = format!("type_{unique}");
let created = ctx
.client
.new_type(&ctx.space_id, &type_name)
.key(&type_key)
.property("Status", "status", PropertyFormat::Select)
.property("Priority", "priority", PropertyFormat::Number)
.create()
.await?;
ctx.increment_calls(1);
ctx.register_type(&created.id);
let object_name = format!("Type Object {unique}");
let object_key = type_key.clone();
let tag_done = lookup_property_tag_with_retry(ctx.as_ref(), "status", "Done").await?;
let obj = create_object_with_retry("Type Object", || async {
ctx.client
.new_object(&ctx.space_id, &object_key)
.name(&object_name)
.set_select("status", &tag_done.id)
.set_number("priority", 2)
.create()
.await
})
.await?;
ctx.increment_calls(1);
ctx.register_object(&obj.id);
assert_eq!(
obj.get_property_number("priority"),
Some(&serde_json::Number::from(2u64)),
"get priority as Number"
);
assert_eq!(
obj.get_property_u64("priority"),
Some(2u64),
"get priority as u64"
);
assert_eq!(
obj.get_property_i64("priority"),
Some(2i64),
"get priority as i64"
);
assert_eq!(
obj.get_property_f64("priority"),
Some(2.0f64),
"get priority as f64"
);
let status_value = obj
.get_property_select("status")
.expect("expected to find 'status' property");
assert_eq!(status_value.name, "Done", "Done name match");
assert_eq!(status_value.id, tag_done.id, "Done id match");
println!("Created object with custom type");
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_list_templates() -> TestResult<()> {
with_test_context(|ctx| async move {
let types = ctx.client.types(&ctx.space_id).list().await?.items.clone();
let page_type = types
.iter()
.find(|t| t.key == "page")
.expect("Page type should exist");
let result = ctx
.client
.templates(&ctx.space_id, &page_type.id)
.list()
.await?;
let templates = &result.items;
ctx.increment_calls(2);
println!("Type '{}' has {} templates", page_type.key, templates.len());
for template in templates {
assert!(!template.id.is_empty(), "Template should have an ID");
}
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_get_template() -> TestResult<()> {
with_test_context(|ctx| async move {
let types = ctx.client.types(&ctx.space_id).list().await?;
for typ in types.iter().take(10) {
let templates = ctx.client.templates(&ctx.space_id, &typ.id).list().await?;
if let Some(template) = templates.iter().next() {
let fetched = ctx
.client
.template(&ctx.space_id, &typ.id, &template.id)
.get()
.await?;
assert_eq!(
fetched.id, template.id,
"template get should match listed template id"
);
assert_eq!(fetched.space_id, ctx.space_id, "template space identity");
assert!(!fetched.archived, "selected template must not be archived");
let fetched_type = fetched.r#type.as_ref().expect("template type identity");
assert_eq!(
fetched_type.key, "template",
"template objects use the canonical generic template type"
);
assert!(!fetched_type.id.is_empty(), "template type id identity");
assert!(!fetched_type.archived, "template type must not be archived");
let resolved = ctx
.client
.resolve_template(&ctx.space_id, &typ.id, &template.id)
.await?;
assert_eq!(
resolved.id, template.id,
"direct template resolution should preserve the verified id"
);
if let Some(name) = template.name.as_deref()
&& !name.is_empty()
&& templates
.iter()
.filter(|candidate| candidate.name.as_deref() == Some(name))
.count()
== 1
{
let resolved = ctx
.client
.resolve_template(&ctx.space_id, &typ.id, name)
.await?;
assert_eq!(
resolved.id, template.id,
"unique template name should resolve to the listed template"
);
}
return Ok(());
}
}
println!("No templates found to test template get (this is OK)");
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_template_type_linkage() -> TestResult<()> {
with_test_context(|ctx| async move {
let types = ctx.client.types(&ctx.space_id).list().await?.items.clone();
ctx.increment_calls(1);
for typ in types.iter().take(3) {
let result = ctx.client.templates(&ctx.space_id, &typ.id).list().await?;
ctx.increment_calls(1);
let templates = &result.items;
if !templates.is_empty() {
println!(
"Type '{}' has {} template(s), verifying linkage",
typ.key,
templates.len()
);
let mut matched = false;
for template in templates {
let template_type = template
.r#type
.as_ref()
.expect("Template should have a type");
assert!(
!template_type.id.is_empty(),
"Template type ID should be present"
);
if template_type.id == typ.id {
matched = true;
}
}
if matched {
return Ok(());
}
println!("âš Templates returned, but none matched type '{}'", typ.key);
}
}
println!("No types with templates found in first 3 types (this is OK)");
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_update_custom() -> TestResult<()> {
with_test_context(|ctx| async move {
let unique_key = format!("test_update_{}", chrono::Utc::now().timestamp_millis());
let created = retry_definitive_rate_limit("type update setup", || async {
ctx.client
.new_type(&ctx.space_id, "Original Name")
.key(&unique_key)
.create()
.await
})
.await?;
ctx.register_type(&created.id);
let updated = ctx
.client
.update_type(&ctx.space_id, &created.id)
.name("Updated Name")
.update()
.await?;
ctx.increment_calls(2);
assert_eq!(updated.id, created.id);
assert_eq!(updated.name.as_deref(), Some("Updated Name"));
println!("Successfully updated type name");
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_replace_and_clear_recommended_properties() -> TestResult<()> {
with_test_context(|ctx| async move {
let suffix = unique_suffix();
let type_key = format!("test_replace_props_{suffix}");
let first_key = format!("first_prop_{suffix}");
let second_key = format!("second_prop_{suffix}");
let created = retry_definitive_rate_limit("type property update setup", || async {
ctx.client
.new_type(&ctx.space_id, "Property Replacement Type")
.key(&type_key)
.property("First Property", &first_key, PropertyFormat::Text)
.property("Second Property", &second_key, PropertyFormat::Number)
.create()
.await
})
.await?;
ctx.register_type(&created.id);
let cached_type_count = ctx.client.cache().num_types();
let cached_property_count = ctx.client.cache().num_properties();
let initial = ctx
.client
.get_type(&ctx.space_id, &created.id)
.classify_properties()
.await?;
assert_eq!(ctx.client.cache().num_types(), cached_type_count);
assert_eq!(ctx.client.cache().num_properties(), cached_property_count);
assert!(!initial.featured_ids.is_empty());
assert_eq!(
initial
.replaceable()
.iter()
.map(|property| property.key.as_str())
.collect::<Vec<_>>(),
vec![first_key.as_str(), second_key.as_str()]
);
let featured_ids = initial.featured_ids;
let replaced = ctx
.client
.update_type(&ctx.space_id, &created.id)
.properties([CreateTypeProperty {
name: "First Property".to_string(),
key: first_key.clone(),
format: PropertyFormat::Text,
}])
.update()
.await?;
assert!(replaced.properties.iter().any(|p| p.key == first_key));
assert!(!replaced.properties.iter().any(|p| p.key == second_key));
let replaced_classification = ctx
.client
.get_type(&ctx.space_id, &created.id)
.classify_properties()
.await?;
assert_eq!(replaced_classification.featured_ids, featured_ids);
assert_eq!(
replaced_classification
.replaceable()
.iter()
.map(|property| property.key.as_str())
.collect::<Vec<_>>(),
vec![first_key.as_str()]
);
let cleared = ctx
.client
.update_type(&ctx.space_id, &created.id)
.clear_properties()
.update()
.await?;
assert!(!cleared.properties.iter().any(|p| p.key == first_key));
assert!(!cleared.properties.iter().any(|p| p.key == second_key));
let cleared_classification = ctx
.client
.get_type(&ctx.space_id, &created.id)
.classify_properties()
.await?;
assert_eq!(cleared_classification.featured_ids, featured_ids);
assert!(cleared_classification.replaceable().is_empty());
ctx.increment_calls(9);
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_delete_custom() -> TestResult<()> {
with_test_context(|ctx| async move {
let unique_key = format!("test_delete_{}", chrono::Utc::now().timestamp_millis());
let created = retry_definitive_rate_limit("type delete setup", || async {
ctx.client
.new_type(&ctx.space_id, "To Be Deleted")
.key(&unique_key)
.create()
.await
})
.await?;
ctx.register_type(&created.id);
let deleted = ctx
.client
.get_type(&ctx.space_id, &created.id)
.delete()
.await?;
ctx.increment_calls(2);
assert_eq!(deleted.id, created.id);
if !deleted.archived {
println!("Delete returned archived=false");
}
println!("Successfully deleted (archived) type");
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_with_icon() -> TestResult<()> {
with_test_context(|ctx| async move {
let unique_key = format!("test_icon_{}", chrono::Utc::now().timestamp_millis());
let created = ctx
.client
.new_type(&ctx.space_id, "Type With Icon")
.key(&unique_key)
.icon(Icon::Emoji {
emoji: "📋".to_string(),
})
.create()
.await?;
ctx.increment_calls(1);
ctx.register_type(&created.id);
assert!(created.icon.is_some(), "Type should have an icon");
if let Some(Icon::Emoji { emoji }) = &created.icon {
assert_eq!(emoji, "📋");
} else {
return Err(TestError::Assertion {
message: "Expected Emoji icon variant".to_string(),
});
}
println!("Created type with icon: {}", created.display_name());
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_display_name() -> TestResult<()> {
with_test_context(|ctx| async move {
let unique_key = format!("test_display_{}", chrono::Utc::now().timestamp_millis());
let with_name = ctx
.client
.new_type(&ctx.space_id, "Display Name Test")
.key(&unique_key)
.create()
.await?;
ctx.increment_calls(1);
ctx.register_type(&with_name.id);
assert_eq!(with_name.display_name(), "Display Name Test");
println!("Type display_name() works correctly");
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_layouts() -> TestResult<()> {
with_test_context(|ctx| async move {
let layouts = &[
TypeLayout::Basic,
TypeLayout::Note,
TypeLayout::Action,
TypeLayout::Profile,
];
for (idx, layout) in layouts.iter().enumerate() {
let unique_key = format!(
"test_layout_{}_{}",
idx,
chrono::Utc::now().timestamp_millis()
);
let created = ctx
.client
.new_type(&ctx.space_id, format!("Layout Test {:?}", layout))
.key(&unique_key)
.layout(layout.clone())
.create()
.await?;
ctx.increment_calls(1);
ctx.register_type(&created.id);
println!("Created type with layout: {:?}", layout);
}
println!("Successfully created types with all layout variants");
Ok(())
})
.await
}
#[tokio::test]
#[test_log::test]
async fn test_types_duplicate_key() -> TestResult<()> {
with_test_context(|ctx| async move {
let unique_key = format!("test_dup_{}", chrono::Utc::now().timestamp_millis());
let first = retry_definitive_rate_limit("duplicate type setup", || async {
ctx.client
.new_type(&ctx.space_id, "First Type")
.key(&unique_key)
.create()
.await
})
.await?;
ctx.register_type(&first.id);
let result = ctx
.client
.new_type(&ctx.space_id, "Second Type")
.key(&unique_key)
.create()
.await;
ctx.increment_calls(2);
if let Ok(second) = result {
ctx.register_type(&second.id);
println!("API allows duplicate type keys (types have different IDs)");
} else {
println!("API correctly rejected duplicate type key");
}
Ok(())
})
.await
}