use anyhow::Result;
use serde_json;
use crate::db::Database;
use crate::utils::{format_issue_id, truncate};
pub fn run_json(db: &Database, query: &str) -> Result<()> {
let results = db.search_issues(query)?;
println!("{}", serde_json::to_string_pretty(&results)?);
Ok(())
}
pub fn run(db: &Database, query: &str) -> Result<()> {
let results = db.search_issues(query)?;
if results.is_empty() {
println!("No issues found matching '{query}'");
return Ok(());
}
println!("Found {} issue(s) matching '{}':\n", results.len(), query);
for issue in results {
let status_marker = if issue.status == crate::models::IssueStatus::Closed {
"✓"
} else {
" "
};
let parent_str = issue
.parent_id
.map(|p| format!(" (sub of {})", format_issue_id(p)))
.unwrap_or_default();
println!(
"{:<5} [{}] {:8} {}{} {}",
format_issue_id(issue.id),
status_marker,
issue.priority,
issue.title,
parent_str,
if issue.status == crate::models::IssueStatus::Closed {
"(closed)"
} else {
""
}
);
if let Some(ref desc) = issue.description {
if desc.to_lowercase().contains(&query.to_lowercase()) {
let flat = desc.replace('\n', " ");
println!(" └─ {}", truncate(&flat, 60));
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
fn setup_test_db() -> (Database, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("test.db");
let db = Database::open(&db_path).unwrap();
(db, dir)
}
#[test]
fn test_search_finds_by_title() {
let (db, _dir) = setup_test_db();
let id = db
.create_issue("Fix authentication bug", None, "high")
.unwrap();
db.create_issue("Add dark mode", None, "medium").unwrap();
run(&db, "authentication").unwrap();
let results = db.search_issues("authentication").unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, id);
}
#[test]
fn test_search_finds_by_description() {
let (db, _dir) = setup_test_db();
let id = db
.create_issue("Feature A", Some("This relates to user login"), "medium")
.unwrap();
run(&db, "login").unwrap();
let results = db.search_issues("login").unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, id);
}
#[test]
fn test_search_case_insensitive() {
let (db, _dir) = setup_test_db();
let id = db
.create_issue("Fix AUTHENTICATION Bug", None, "high")
.unwrap();
run(&db, "authentication").unwrap();
let results = db.search_issues("authentication").unwrap();
assert_eq!(
results.len(),
1,
"Case-insensitive search should find the issue"
);
assert_eq!(results[0].id, id);
}
#[test]
fn test_search_no_results() {
let (db, _dir) = setup_test_db();
db.create_issue("Some issue", None, "medium").unwrap();
run(&db, "nonexistent").unwrap();
let results = db.search_issues("nonexistent").unwrap();
assert!(
results.is_empty(),
"Search for nonexistent term should return empty"
);
}
#[test]
fn test_search_empty_database() {
let (db, _dir) = setup_test_db();
run(&db, "anything").unwrap();
let results = db.search_issues("anything").unwrap();
assert!(results.is_empty());
}
#[test]
fn test_search_empty_query() {
let (db, _dir) = setup_test_db();
db.create_issue("Test issue", None, "medium").unwrap();
run(&db, "").unwrap();
let _results = db.search_issues("").unwrap();
}
#[test]
fn test_search_special_characters() {
let (db, _dir) = setup_test_db();
let id = db
.create_issue("Fix bug with @mentions", None, "medium")
.unwrap();
run(&db, "@mentions").unwrap();
let results = db.search_issues("@mentions").unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, id);
}
#[test]
fn test_search_unicode() {
let (db, _dir) = setup_test_db();
let id = db
.create_issue("Fix 日本語 support", None, "medium")
.unwrap();
run(&db, "日本語").unwrap();
let results = db.search_issues("日本語").unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, id);
}
#[test]
fn test_search_sql_injection() {
let (db, _dir) = setup_test_db();
db.create_issue("Normal issue", None, "medium").unwrap();
run(&db, "'; DROP TABLE issues; --").unwrap();
let issues = db.list_issues(None, None, None).unwrap();
assert_eq!(
issues.len(),
1,
"Database should be intact after SQL injection attempt"
);
}
#[test]
fn test_search_with_wildcards() {
let (db, _dir) = setup_test_db();
db.create_issue("Test issue with pattern", None, "medium")
.unwrap();
run(&db, "%pattern%").unwrap();
let results = db.search_issues("%pattern%").unwrap();
assert!(
results.is_empty(),
"Literal SQL wildcards should be escaped and not match"
);
}
#[test]
fn test_search_finds_in_comments() {
let (db, _dir) = setup_test_db();
let id = db.create_issue("Generic issue", None, "medium").unwrap();
db.add_comment(id, "Found the root cause in authentication module", "note")
.unwrap();
run(&db, "authentication").unwrap();
let results = db.search_issues("authentication").unwrap();
assert_eq!(
results.len(),
1,
"Search should find issues via comment content"
);
assert_eq!(results[0].id, id);
}
#[test]
fn test_search_subissue_shows_parent() {
let (db, _dir) = setup_test_db();
let parent_id = db.create_issue("Parent feature", None, "high").unwrap();
let sub_id = db
.create_subissue(parent_id, "Sub task authentication", None, "medium")
.unwrap();
run(&db, "authentication").unwrap();
let results = db.search_issues("authentication").unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, sub_id);
assert_eq!(results[0].parent_id, Some(parent_id));
}
#[test]
fn test_search_closed_issue() {
let (db, _dir) = setup_test_db();
let id = db
.create_issue("Fix authentication bug", None, "high")
.unwrap();
db.close_issue(id).unwrap();
run(&db, "authentication").unwrap();
let results = db.search_issues("authentication").unwrap();
assert_eq!(results.len(), 1, "Search should find closed issues too");
assert_eq!(results[0].status, "closed");
}
proptest! {
#[test]
fn prop_search_never_panics(query in ".*") {
let (db, _dir) = setup_test_db();
db.create_issue("Test issue", None, "medium").unwrap();
let _ = run(&db, &query);
}
#[test]
fn prop_search_with_issues_never_panics(
title in "[a-zA-Z0-9 ]{1,50}",
query in "[a-zA-Z0-9]{1,20}"
) {
let (db, _dir) = setup_test_db();
db.create_issue(&title, None, "medium").unwrap();
let result = run(&db, &query);
prop_assert!(result.is_ok());
}
#[test]
fn prop_search_unicode_never_panics(
title in "[\\p{L}\\p{N} ]{1,30}",
query in "[\\p{L}\\p{N}]{1,10}"
) {
let (db, _dir) = setup_test_db();
db.create_issue(&title, None, "medium").unwrap();
let result = run(&db, &query);
prop_assert!(result.is_ok());
}
}
}