diff --git a/src/cli/commands/show.rs b/src/cli/commands/show.rs index b9370de..4f520ad 100644 --- a/src/cli/commands/show.rs +++ b/src/cli/commands/show.rs @@ -160,6 +160,7 @@ pub fn run_show_issue( }) } +#[derive(Debug)] struct IssueRow { id: i64, iid: i64, @@ -1218,6 +1219,162 @@ mod tests { .unwrap(); } + fn seed_second_project(conn: &Connection) { + conn.execute( + "INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url, created_at, updated_at) + VALUES (2, 101, 'other/repo', 'https://gitlab.example.com/other', 1000, 2000)", + [], + ) + .unwrap(); + } + + fn seed_discussion_with_notes(conn: &Connection, issue_id: i64, project_id: i64, user_notes: usize, system_notes: usize) { + let disc_id: i64 = conn + .query_row("SELECT COALESCE(MAX(id), 0) + 1 FROM discussions", [], |r| r.get(0)) + .unwrap(); + conn.execute( + "INSERT INTO discussions (id, gitlab_discussion_id, project_id, issue_id, noteable_type, first_note_at, last_note_at, last_seen_at) + VALUES (?1, ?2, ?3, ?4, 'Issue', 1000, 2000, 2000)", + rusqlite::params![disc_id, format!("disc-{}", disc_id), project_id, issue_id], + ) + .unwrap(); + for i in 0..user_notes { + conn.execute( + "INSERT INTO notes (gitlab_id, discussion_id, project_id, author_username, body, created_at, updated_at, last_seen_at, is_system, position) + VALUES (?1, ?2, ?3, 'user1', 'comment', 1000, 2000, 2000, 0, ?4)", + rusqlite::params![1000 + disc_id * 100 + i as i64, disc_id, project_id, i as i64], + ) + .unwrap(); + } + for i in 0..system_notes { + conn.execute( + "INSERT INTO notes (gitlab_id, discussion_id, project_id, author_username, body, created_at, updated_at, last_seen_at, is_system, position) + VALUES (?1, ?2, ?3, 'system', 'status changed', 1000, 2000, 2000, 1, ?4)", + rusqlite::params![2000 + disc_id * 100 + i as i64, disc_id, project_id, (user_notes + i) as i64], + ) + .unwrap(); + } + } + + // --- find_issue tests --- + + #[test] + fn test_find_issue_basic() { + let conn = setup_test_db(); + seed_issue(&conn); + let row = find_issue(&conn, 10, None).unwrap(); + assert_eq!(row.iid, 10); + assert_eq!(row.title, "Test issue"); + assert_eq!(row.state, "opened"); + assert_eq!(row.author_username, "author"); + assert_eq!(row.project_path, "group/repo"); + } + + #[test] + fn test_find_issue_with_project_filter() { + let conn = setup_test_db(); + seed_issue(&conn); + let row = find_issue(&conn, 10, Some("group/repo")).unwrap(); + assert_eq!(row.iid, 10); + assert_eq!(row.project_path, "group/repo"); + } + + #[test] + fn test_find_issue_not_found() { + let conn = setup_test_db(); + seed_issue(&conn); + let err = find_issue(&conn, 999, None).unwrap_err(); + assert!(matches!(err, LoreError::NotFound(_))); + } + + #[test] + fn test_find_issue_wrong_project_filter() { + let conn = setup_test_db(); + seed_issue(&conn); + seed_second_project(&conn); + // Issue 10 only exists in project 1, not project 2 + let err = find_issue(&conn, 10, Some("other/repo")).unwrap_err(); + assert!(matches!(err, LoreError::NotFound(_))); + } + + #[test] + fn test_find_issue_ambiguous_without_project() { + let conn = setup_test_db(); + seed_issue(&conn); // issue iid=10 in project 1 + seed_second_project(&conn); + conn.execute( + "INSERT INTO issues (id, gitlab_id, iid, project_id, title, state, author_username, + created_at, updated_at, last_seen_at) + VALUES (2, 201, 10, 2, 'Same iid different project', 'opened', 'author', 1000, 2000, 2000)", + [], + ) + .unwrap(); + let err = find_issue(&conn, 10, None).unwrap_err(); + assert!(matches!(err, LoreError::Ambiguous(_))); + } + + #[test] + fn test_find_issue_ambiguous_resolved_with_project() { + let conn = setup_test_db(); + seed_issue(&conn); + seed_second_project(&conn); + conn.execute( + "INSERT INTO issues (id, gitlab_id, iid, project_id, title, state, author_username, + created_at, updated_at, last_seen_at) + VALUES (2, 201, 10, 2, 'Same iid different project', 'opened', 'author', 1000, 2000, 2000)", + [], + ) + .unwrap(); + let row = find_issue(&conn, 10, Some("other/repo")).unwrap(); + assert_eq!(row.title, "Same iid different project"); + } + + #[test] + fn test_find_issue_user_notes_count_zero() { + let conn = setup_test_db(); + seed_issue(&conn); + let row = find_issue(&conn, 10, None).unwrap(); + assert_eq!(row.user_notes_count, 0); + } + + #[test] + fn test_find_issue_user_notes_count_excludes_system() { + let conn = setup_test_db(); + seed_issue(&conn); + // 2 user notes + 3 system notes = should count only 2 + seed_discussion_with_notes(&conn, 1, 1, 2, 3); + let row = find_issue(&conn, 10, None).unwrap(); + assert_eq!(row.user_notes_count, 2); + } + + #[test] + fn test_find_issue_user_notes_count_across_discussions() { + let conn = setup_test_db(); + seed_issue(&conn); + seed_discussion_with_notes(&conn, 1, 1, 3, 0); // 3 user notes + seed_discussion_with_notes(&conn, 1, 1, 1, 2); // 1 user note + 2 system + let row = find_issue(&conn, 10, None).unwrap(); + assert_eq!(row.user_notes_count, 4); + } + + #[test] + fn test_find_issue_notes_count_ignores_other_issues() { + let conn = setup_test_db(); + seed_issue(&conn); + // Add a second issue + conn.execute( + "INSERT INTO issues (id, gitlab_id, iid, project_id, title, state, author_username, + created_at, updated_at, last_seen_at) + VALUES (2, 201, 20, 1, 'Other issue', 'opened', 'author', 1000, 2000, 2000)", + [], + ) + .unwrap(); + // Notes on issue 2, not issue 1 + seed_discussion_with_notes(&conn, 2, 1, 5, 0); + let row = find_issue(&conn, 10, None).unwrap(); + assert_eq!(row.user_notes_count, 0); // Issue 10 has no notes + } + #[test] fn test_ansi256_from_rgb() { assert_eq!(ansi256_from_rgb(0, 0, 0), 16);