@ -140,3 +140,151 @@ mod ticket {
) ;
}
}
// ── store module ──────────────────────────────────────────────────────────────
/// Tests for [`crate::store`].
mod store {
use std ::path ::Path ;
use crate ::store ::{
ensure_tickets_dir , find_nbd_root_from , list_tickets , read_ticket , ticket_path ,
tickets_dir , write_ticket ,
} ;
use crate ::ticket ::{ Status , Ticket , TicketType } ;
/// Helper: create a temporary directory with `.nbd/tickets/` already set up.
async fn setup_store ( ) -> ( tempfile ::TempDir , std ::path ::PathBuf ) {
let tmp = tempfile ::tempdir ( ) . unwrap ( ) ;
let root = tmp . path ( ) . to_path_buf ( ) ;
ensure_tickets_dir ( & root ) . await . unwrap ( ) ;
( tmp , root )
}
/// Writing a ticket and reading it back produces an identical value.
#[ async_std::test ]
async fn write_and_read_roundtrip ( ) {
let ( tmp , root ) = setup_store ( ) . await ;
let ticket = Ticket {
id : "a3f9c2" . to_string ( ) ,
title : "Fix login bug" . to_string ( ) ,
body : "Users cannot log in with email addresses containing +" . to_string ( ) ,
priority : 8 ,
status : Status ::InProgress ,
dependencies : vec ! [ "b7d41e" . to_string ( ) ] ,
ticket_type : TicketType ::Bug ,
} ;
write_ticket ( & root , & ticket ) . await . unwrap ( ) ;
let restored = read_ticket ( & root , "a3f9c2" ) . await . unwrap ( ) ;
assert_eq! ( restored . id , ticket . id ) ;
assert_eq! ( restored . title , ticket . title ) ;
assert_eq! ( restored . body , ticket . body ) ;
assert_eq! ( restored . priority , ticket . priority ) ;
assert_eq! ( restored . status , ticket . status ) ;
assert_eq! ( restored . dependencies , ticket . dependencies ) ;
assert_eq! ( restored . ticket_type , ticket . ticket_type ) ;
drop ( tmp ) ;
}
/// Reading a non-existent ticket produces an error that mentions the ID.
#[ async_std::test ]
async fn read_missing_ticket_errors ( ) {
let ( tmp , root ) = setup_store ( ) . await ;
let result = read_ticket ( & root , "ffffff" ) . await ;
assert! ( result . is_err ( ) ) ;
let msg = result . unwrap_err ( ) . to_string ( ) ;
assert! (
msg . contains ( "ffffff" ) ,
"error message should mention the ticket ID, got: {msg}"
) ;
drop ( tmp ) ;
}
/// `list_tickets` returns all written tickets sorted by priority descending.
#[ async_std::test ]
async fn list_returns_all_sorted_by_priority ( ) {
let ( tmp , root ) = setup_store ( ) . await ;
let mut low = Ticket ::new ( "id0001" . to_string ( ) , "Low priority" . to_string ( ) ) ;
low . priority = 2 ;
let mut high = Ticket ::new ( "id0002" . to_string ( ) , "High priority" . to_string ( ) ) ;
high . priority = 9 ;
let mut mid = Ticket ::new ( "id0003" . to_string ( ) , "Mid priority" . to_string ( ) ) ;
mid . priority = 5 ;
write_ticket ( & root , & low ) . await . unwrap ( ) ;
write_ticket ( & root , & high ) . await . unwrap ( ) ;
write_ticket ( & root , & mid ) . await . unwrap ( ) ;
let tickets = list_tickets ( & root ) . await . unwrap ( ) ;
assert_eq! ( tickets . len ( ) , 3 ) ;
assert_eq! (
tickets [ 0 ] . priority , 9 ,
"first ticket should have highest priority"
) ;
assert_eq! ( tickets [ 1 ] . priority , 5 ) ;
assert_eq! (
tickets [ 2 ] . priority , 2 ,
"last ticket should have lowest priority"
) ;
drop ( tmp ) ;
}
/// `list_tickets` returns an empty vec when the tickets directory is absent.
#[ async_std::test ]
async fn list_empty_when_no_tickets_dir ( ) {
let tmp = tempfile ::tempdir ( ) . unwrap ( ) ;
// Create `.nbd/` but not `.nbd/tickets/`.
std ::fs ::create_dir ( tmp . path ( ) . join ( ".nbd" ) ) . unwrap ( ) ;
let root = tmp . path ( ) . to_path_buf ( ) ;
let tickets = list_tickets ( & root ) . await . unwrap ( ) ;
assert! ( tickets . is_empty ( ) ) ;
drop ( tmp ) ;
}
/// `find_nbd_root_from` finds `.nbd/` located in a grandparent directory.
#[ test ]
fn traversal_finds_nbd_in_grandparent ( ) {
let tmp = tempfile ::tempdir ( ) . unwrap ( ) ;
let root = tmp . path ( ) ;
// Place `.nbd/` at the temp root.
std ::fs ::create_dir ( root . join ( ".nbd" ) ) . unwrap ( ) ;
// Start traversal from a deeply nested subdirectory.
let grandchild = root . join ( "a" ) . join ( "b" ) ;
std ::fs ::create_dir_all ( & grandchild ) . unwrap ( ) ;
let found = find_nbd_root_from ( & grandchild ) . unwrap ( ) ;
assert_eq! ( found , root ) ;
drop ( tmp ) ;
}
/// `find_nbd_root_from` returns an error when no `.nbd/` directory exists.
#[ test ]
fn traversal_errors_when_no_nbd_dir ( ) {
let tmp = tempfile ::tempdir ( ) . unwrap ( ) ;
let result = find_nbd_root_from ( tmp . path ( ) ) ;
assert! ( result . is_err ( ) ) ;
drop ( tmp ) ;
}
/// `ticket_path` returns the expected `.nbd/tickets/{id}.json` path.
#[ test ]
fn ticket_path_is_correct ( ) {
let root = Path ::new ( "/tmp/project" ) ;
let path = ticket_path ( root , "a3f9c2" ) ;
assert_eq! ( path , Path ::new ( "/tmp/project/.nbd/tickets/a3f9c2.json" ) ) ;
}
/// `tickets_dir` returns the expected `.nbd/tickets/` path.
#[ test ]
fn tickets_dir_is_correct ( ) {
let root = Path ::new ( "/tmp/project" ) ;
let dir = tickets_dir ( root ) ;
assert_eq! ( dir , Path ::new ( "/tmp/project/.nbd/tickets" ) ) ;
}
}