use chrono::{TimeZone, Utc};
use proptest::prelude::*;
use tracing::info;
use beads_rust::model::{Issue, IssueType, Priority, Status};
use beads_rust::validation::{IssueValidator, LabelValidator};
fn init_test_logging() {
let _ = tracing_subscriber::fmt()
.with_env_filter("info")
.with_test_writer()
.try_init();
}
fn make_valid_issue(title: &str) -> Issue {
let now = Utc::now();
Issue {
id: "bd-test123".to_string(),
content_hash: None,
title: title.to_string(),
description: None,
design: None,
acceptance_criteria: None,
notes: None,
status: Status::Open,
priority: Priority::MEDIUM,
issue_type: IssueType::Task,
assignee: None,
owner: None,
estimated_minutes: None,
created_at: now,
created_by: None,
updated_at: now,
closed_at: None,
close_reason: None,
closed_by_session: None,
due_at: None,
defer_until: None,
external_ref: None,
source_system: None,
source_repo: None,
deleted_at: None,
deleted_by: None,
delete_reason: None,
original_type: None,
compaction_level: None,
compacted_at: None,
compacted_at_commit: None,
original_size: None,
sender: None,
ephemeral: false,
pinned: false,
is_template: false,
labels: vec![],
dependencies: vec![],
comments: vec![],
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 100,
..Default::default()
})]
#[test]
fn valid_issue_passes(title in "[a-zA-Z0-9 ]{1,100}") {
init_test_logging();
info!("proptest_valid_issue: title_len={len}", len = title.len());
prop_assume!(!title.trim().is_empty());
let issue = make_valid_issue(&title);
let result = IssueValidator::validate(&issue);
prop_assert!(
result.is_ok(),
"Valid issue should pass validation: {result:?}"
);
}
#[test]
fn invalid_priority_fails(priority in 5i32..100i32) {
init_test_logging();
info!("proptest_invalid_priority: priority={priority}");
let mut issue = make_valid_issue("Test Issue");
issue.priority = Priority(priority);
let result = IssueValidator::validate(&issue);
prop_assert!(result.is_err(), "Priority {priority} should fail validation");
let errors = result.unwrap_err();
prop_assert!(
errors.iter().any(|e| e.field == "priority"),
"Should have priority error"
);
}
#[test]
fn negative_priority_fails(priority in -100i32..-1i32) {
init_test_logging();
info!("proptest_negative_priority: priority={priority}");
let mut issue = make_valid_issue("Test Issue");
issue.priority = Priority(priority);
let result = IssueValidator::validate(&issue);
prop_assert!(result.is_err(), "Priority {priority} should fail validation");
}
#[test]
fn valid_priority_passes(priority in 0i32..=4i32) {
init_test_logging();
info!("proptest_valid_priority: priority={priority}");
let mut issue = make_valid_issue("Test Issue");
issue.priority = Priority(priority);
let result = IssueValidator::validate(&issue);
prop_assert!(result.is_ok(), "Priority {priority} should pass validation");
}
#[test]
fn empty_title_fails(whitespace in "\\s{0,10}") {
init_test_logging();
info!(
"proptest_empty_title: whitespace_len={len}",
len = whitespace.len()
);
let mut issue = make_valid_issue("Valid");
issue.title = whitespace;
let result = IssueValidator::validate(&issue);
prop_assert!(result.is_err(), "Empty/whitespace title should fail");
let errors = result.unwrap_err();
prop_assert!(
errors.iter().any(|e| e.field == "title"),
"Should have title error"
);
}
#[test]
fn long_title_fails(len in 501usize..600usize) {
init_test_logging();
info!("proptest_long_title: len={len}");
let mut issue = make_valid_issue("Valid");
issue.title = "x".repeat(len);
let result = IssueValidator::validate(&issue);
prop_assert!(result.is_err(), "Title with {len} chars should fail");
let errors = result.unwrap_err();
prop_assert!(
errors.iter().any(|e| e.field == "title"),
"Should have title error"
);
}
#[test]
fn title_at_limit_passes(len in 1usize..=500usize) {
init_test_logging();
info!("proptest_title_limit: len={len}");
let mut issue = make_valid_issue("Valid");
issue.title = "x".repeat(len);
let result = IssueValidator::validate(&issue);
prop_assert!(result.is_ok(), "Title with {len} chars should pass");
}
#[test]
fn large_description_fails(extra_bytes in 1usize..1000usize) {
init_test_logging();
let len = 102_400 + extra_bytes;
info!("proptest_large_desc: len={len}");
let mut issue = make_valid_issue("Test Issue");
issue.description = Some("x".repeat(len));
let result = IssueValidator::validate(&issue);
prop_assert!(result.is_err(), "Description with {len} bytes should fail");
let errors = result.unwrap_err();
prop_assert!(
errors.iter().any(|e| e.field == "description"),
"Should have description error"
);
}
#[test]
fn updated_before_created_fails(days_before in 1u32..100u32) {
init_test_logging();
info!("proptest_timestamp_order: days_before={days_before}");
let mut issue = make_valid_issue("Test Issue");
issue.created_at = Utc.with_ymd_and_hms(2026, 6, 15, 12, 0, 0).unwrap();
issue.updated_at = issue.created_at - chrono::Duration::days(i64::from(days_before));
let result = IssueValidator::validate(&issue);
prop_assert!(result.is_err(), "updated_at before created_at should fail");
let errors = result.unwrap_err();
prop_assert!(
errors.iter().any(|e| e.field == "updated_at"),
"Should have updated_at error"
);
}
#[test]
fn valid_label_passes(label in "[a-zA-Z0-9_:-]{1,50}") {
init_test_logging();
info!("proptest_valid_label: label={label}");
let result = LabelValidator::validate(&label);
prop_assert!(result.is_ok(), "Label '{label}' should pass validation");
}
#[test]
fn label_with_space_fails(
prefix in "[a-z]{1,10}",
suffix in "[a-z]{1,10}",
) {
init_test_logging();
let label = format!("{prefix} {suffix}");
info!("proptest_label_space: label={label}");
let result = LabelValidator::validate(&label);
prop_assert!(result.is_err(), "Label with space should fail: '{label}'");
}
#[test]
fn empty_label_fails(_dummy in 0..1u8) {
init_test_logging();
let result = LabelValidator::validate("");
prop_assert!(result.is_err(), "Empty label should fail");
}
#[test]
fn long_label_fails(len in 51usize..100usize) {
init_test_logging();
let label = "x".repeat(len);
info!("proptest_long_label: len={len}");
let result = LabelValidator::validate(&label);
prop_assert!(result.is_err(), "Label with {len} chars should fail");
}
#[test]
fn external_ref_whitespace_fails(
prefix in "[a-z]{1,10}",
suffix in "[a-z]{1,10}",
) {
init_test_logging();
let external_ref = format!("{prefix} {suffix}");
info!("proptest_external_ref: external_ref={external_ref}");
let mut issue = make_valid_issue("Test Issue");
issue.external_ref = Some(external_ref);
let result = IssueValidator::validate(&issue);
prop_assert!(result.is_err(), "External ref with whitespace should fail");
let errors = result.unwrap_err();
prop_assert!(
errors.iter().any(|e| e.field == "external_ref"),
"Should have external_ref error"
);
}
#[test]
fn valid_external_ref_passes(external_ref in "[a-zA-Z0-9_/-]{1,50}") {
init_test_logging();
info!("proptest_valid_external_ref: external_ref={external_ref}");
let mut issue = make_valid_issue("Test Issue");
issue.external_ref = Some(external_ref.clone());
let result = IssueValidator::validate(&issue);
prop_assert!(result.is_ok(), "Valid external ref should pass: '{external_ref}'");
}
}
#[test]
fn all_standard_statuses_valid() {
init_test_logging();
info!("proptest_statuses: testing all standard statuses");
let statuses = [
Status::Open,
Status::InProgress,
Status::Blocked,
Status::Deferred,
Status::Closed,
Status::Tombstone,
Status::Pinned,
];
for status in statuses {
let mut issue = make_valid_issue("Test Issue");
issue.status = status.clone();
if status == Status::Closed {
issue.closed_at = Some(issue.updated_at);
}
let result = IssueValidator::validate(&issue);
assert!(result.is_ok(), "Status {status:?} should be valid");
}
info!("proptest_statuses: PASS - all standard statuses valid");
}
#[test]
fn all_standard_types_valid() {
init_test_logging();
info!("proptest_types: testing all standard issue types");
let types = [
IssueType::Task,
IssueType::Bug,
IssueType::Feature,
IssueType::Epic,
IssueType::Chore,
IssueType::Docs,
IssueType::Question,
];
for issue_type in types {
let mut issue = make_valid_issue("Test Issue");
issue.issue_type = issue_type.clone();
let result = IssueValidator::validate(&issue);
assert!(result.is_ok(), "IssueType {issue_type:?} should be valid");
}
info!("proptest_types: PASS - all standard types valid");
}