Files
gitlore/src/cli/commands/list/list_tests.rs

1305 lines
40 KiB
Rust

use super::*;
use crate::cli::render;
use crate::core::time::now_ms;
use crate::test_support::{
insert_project as insert_test_project, setup_test_db as setup_note_test_db, test_config,
};
#[test]
fn truncate_leaves_short_strings_alone() {
assert_eq!(render::truncate("short", 10), "short");
}
#[test]
fn truncate_adds_ellipsis_to_long_strings() {
assert_eq!(
render::truncate("this is a very long title", 15),
"this is a ve..."
);
}
#[test]
fn truncate_handles_exact_length() {
assert_eq!(render::truncate("exactly10!", 10), "exactly10!");
}
#[test]
fn relative_time_formats_correctly() {
let now = now_ms();
assert_eq!(render::format_relative_time(now - 30_000), "just now");
assert_eq!(render::format_relative_time(now - 120_000), "2 min ago");
assert_eq!(render::format_relative_time(now - 7_200_000), "2 hours ago");
assert_eq!(
render::format_relative_time(now - 172_800_000),
"2 days ago"
);
}
#[test]
fn format_labels_empty() {
assert_eq!(render::format_labels(&[], 2), "");
}
#[test]
fn format_labels_single() {
assert_eq!(render::format_labels(&["bug".to_string()], 2), "[bug]");
}
#[test]
fn format_labels_multiple() {
let labels = vec!["bug".to_string(), "urgent".to_string()];
assert_eq!(render::format_labels(&labels, 2), "[bug, urgent]");
}
#[test]
fn format_labels_overflow() {
let labels = vec![
"bug".to_string(),
"urgent".to_string(),
"wip".to_string(),
"blocked".to_string(),
];
assert_eq!(render::format_labels(&labels, 2), "[bug, urgent +2]");
}
#[test]
fn format_discussions_empty() {
assert_eq!(format_discussions(0, 0).text, "");
}
#[test]
fn format_discussions_no_unresolved() {
assert_eq!(format_discussions(5, 0).text, "5");
}
#[test]
fn format_discussions_with_unresolved() {
let cell = format_discussions(5, 2);
// Text contains styled ANSI for warning-colored unresolved count
assert!(cell.text.starts_with("5/"), "got: {}", cell.text);
assert!(cell.text.contains("2!"), "got: {}", cell.text);
}
// -----------------------------------------------------------------------
// Note query layer tests
// -----------------------------------------------------------------------
fn default_note_filters() -> NoteListFilters {
NoteListFilters {
limit: 50,
project: None,
author: None,
note_type: None,
include_system: false,
for_issue_iid: None,
for_mr_iid: None,
note_id: None,
gitlab_note_id: None,
discussion_id: None,
since: None,
until: None,
path: None,
contains: None,
resolution: None,
sort: "created".to_string(),
order: "desc".to_string(),
}
}
fn insert_test_issue(conn: &Connection, id: i64, project_id: i64, iid: i64, title: &str) {
conn.execute(
"INSERT INTO issues (id, gitlab_id, project_id, iid, title, state, author_username,
created_at, updated_at, last_seen_at)
VALUES (?1, ?2, ?3, ?4, ?5, 'opened', 'author', 1000, 2000, 3000)",
rusqlite::params![id, id * 10, project_id, iid, title],
)
.unwrap();
}
fn insert_test_mr(conn: &Connection, id: i64, project_id: i64, iid: i64, title: &str) {
conn.execute(
"INSERT INTO merge_requests (id, gitlab_id, project_id, iid, title, state,
author_username, source_branch, target_branch,
created_at, updated_at, last_seen_at)
VALUES (?1, ?2, ?3, ?4, ?5, 'opened', 'author', 'feat', 'main',
1000, 2000, 3000)",
rusqlite::params![id, id * 10, project_id, iid, title],
)
.unwrap();
}
#[allow(clippy::too_many_arguments)]
fn insert_test_discussion(
conn: &Connection,
id: i64,
gitlab_disc_id: &str,
project_id: i64,
issue_id: Option<i64>,
mr_id: Option<i64>,
noteable_type: &str,
) {
conn.execute(
"INSERT INTO discussions (id, gitlab_discussion_id, project_id, issue_id,
merge_request_id, noteable_type, last_seen_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1000)",
rusqlite::params![
id,
gitlab_disc_id,
project_id,
issue_id,
mr_id,
noteable_type
],
)
.unwrap();
}
#[allow(clippy::too_many_arguments)]
fn insert_test_note(
conn: &Connection,
id: i64,
gitlab_id: i64,
discussion_id: i64,
project_id: i64,
author: &str,
body: &str,
is_system: bool,
created_at: i64,
updated_at: i64,
) {
conn.execute(
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, author_username,
body, is_system, created_at, updated_at, last_seen_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
rusqlite::params![
id,
gitlab_id,
discussion_id,
project_id,
author,
body,
is_system as i64,
created_at,
updated_at,
updated_at,
],
)
.unwrap();
}
#[allow(clippy::too_many_arguments)]
fn insert_note_with_position(
conn: &Connection,
id: i64,
gitlab_id: i64,
discussion_id: i64,
project_id: i64,
author: &str,
body: &str,
created_at: i64,
note_type: Option<&str>,
new_path: Option<&str>,
new_line: Option<i64>,
) {
conn.execute(
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, author_username,
body, is_system, created_at, updated_at, last_seen_at,
note_type, position_new_path, position_new_line)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0, ?7, ?7, ?7, ?8, ?9, ?10)",
rusqlite::params![
id,
gitlab_id,
discussion_id,
project_id,
author,
body,
created_at,
note_type,
new_path,
new_line,
],
)
.unwrap();
}
#[allow(clippy::too_many_arguments)]
fn insert_resolvable_note(
conn: &Connection,
id: i64,
gitlab_id: i64,
discussion_id: i64,
project_id: i64,
author: &str,
body: &str,
created_at: i64,
resolvable: bool,
resolved: bool,
resolved_by: Option<&str>,
) {
conn.execute(
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, author_username,
body, is_system, created_at, updated_at, last_seen_at,
resolvable, resolved, resolved_by)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0, ?7, ?7, ?7, ?8, ?9, ?10)",
rusqlite::params![
id,
gitlab_id,
discussion_id,
project_id,
author,
body,
created_at,
resolvable as i64,
resolved as i64,
resolved_by,
],
)
.unwrap();
}
#[test]
fn test_query_notes_empty_db() {
let conn = setup_note_test_db();
let config = test_config(None);
let filters = default_note_filters();
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 0);
assert!(result.notes.is_empty());
}
#[test]
fn test_query_notes_basic() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 42, "Test Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_note(
&conn,
1,
100,
1,
1,
"alice",
"Hello world",
false,
1000,
2000,
);
let filters = default_note_filters();
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes.len(), 1);
assert_eq!(result.notes[0].author_username, "alice");
assert_eq!(result.notes[0].body.as_deref(), Some("Hello world"));
assert_eq!(result.notes[0].parent_iid, Some(42));
assert_eq!(result.notes[0].parent_title.as_deref(), Some("Test Issue"));
assert_eq!(result.notes[0].project_path, "group/project");
}
#[test]
fn test_query_notes_excludes_system_by_default() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_note(&conn, 1, 100, 1, 1, "alice", "User note", false, 1000, 1000);
insert_test_note(&conn, 2, 101, 1, 1, "bot", "System note", true, 2000, 2000);
let filters = default_note_filters();
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].author_username, "alice");
}
#[test]
fn test_query_notes_include_system() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_note(&conn, 1, 100, 1, 1, "alice", "User note", false, 1000, 1000);
insert_test_note(&conn, 2, 101, 1, 1, "bot", "System note", true, 2000, 2000);
let mut filters = default_note_filters();
filters.include_system = true;
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 2);
}
#[test]
fn test_query_notes_filter_author() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_note(&conn, 1, 100, 1, 1, "alice", "Note 1", false, 1000, 1000);
insert_test_note(&conn, 2, 101, 1, 1, "bob", "Note 2", false, 2000, 2000);
let mut filters = default_note_filters();
filters.author = Some("alice".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].author_username, "alice");
}
#[test]
fn test_query_notes_filter_author_case_insensitive() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_note(&conn, 1, 100, 1, 1, "Alice", "Note", false, 1000, 1000);
let mut filters = default_note_filters();
filters.author = Some("ALICE".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
}
#[test]
fn test_query_notes_filter_author_strips_at() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_note(&conn, 1, 100, 1, 1, "alice", "Note", false, 1000, 1000);
let mut filters = default_note_filters();
filters.author = Some("@alice".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
}
#[test]
fn test_query_notes_filter_since() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
let ts = crate::core::time::iso_to_ms("2024-01-15T00:00:00Z").unwrap();
insert_test_note(&conn, 1, 100, 1, 1, "alice", "Old note", false, ts, ts);
let ts2 = crate::core::time::iso_to_ms("2024-06-15T00:00:00Z").unwrap();
insert_test_note(&conn, 2, 101, 1, 1, "bob", "New note", false, ts2, ts2);
let mut filters = default_note_filters();
filters.since = Some("2024-03-01".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].author_username, "bob");
}
#[test]
fn test_query_notes_filter_until() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
let ts = crate::core::time::iso_to_ms("2024-01-15T00:00:00Z").unwrap();
insert_test_note(&conn, 1, 100, 1, 1, "alice", "Old note", false, ts, ts);
let ts2 = crate::core::time::iso_to_ms("2024-06-15T00:00:00Z").unwrap();
insert_test_note(&conn, 2, 101, 1, 1, "bob", "New note", false, ts2, ts2);
let mut filters = default_note_filters();
filters.until = Some("2024-03-01".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].author_username, "alice");
}
#[test]
fn test_query_notes_filter_since_until_combined() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
let ts1 = crate::core::time::iso_to_ms("2024-01-15T00:00:00Z").unwrap();
insert_test_note(&conn, 1, 100, 1, 1, "alice", "Old", false, ts1, ts1);
let ts2 = crate::core::time::iso_to_ms("2024-03-15T00:00:00Z").unwrap();
insert_test_note(&conn, 2, 101, 1, 1, "bob", "Middle", false, ts2, ts2);
let ts3 = crate::core::time::iso_to_ms("2024-06-15T00:00:00Z").unwrap();
insert_test_note(&conn, 3, 102, 1, 1, "carol", "New", false, ts3, ts3);
let mut filters = default_note_filters();
filters.since = Some("2024-02-01".to_string());
filters.until = Some("2024-04-01".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].author_username, "bob");
}
#[test]
fn test_query_notes_invalid_time_window_rejected() {
let conn = setup_note_test_db();
let config = test_config(None);
let mut filters = default_note_filters();
filters.since = Some("2024-06-01".to_string());
filters.until = Some("2024-01-01".to_string());
let result = query_notes(&conn, &filters, &config);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("--since is after --until"),
"Expected time window error, got: {msg}"
);
}
#[test]
fn test_query_notes_until_date_uses_end_of_day() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
let ts = crate::core::time::iso_to_ms("2024-03-01T23:30:00Z").unwrap();
insert_test_note(&conn, 1, 100, 1, 1, "alice", "Late night", false, ts, ts);
let mut filters = default_note_filters();
filters.until = Some("2024-03-01".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(
result.total_count, 1,
"Note at 23:30 should be included when --until is the same date"
);
}
#[test]
fn test_query_notes_filter_contains() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_note(
&conn,
1,
100,
1,
1,
"alice",
"This has a BUG in it",
false,
1000,
1000,
);
insert_test_note(
&conn,
2,
101,
1,
1,
"bob",
"Everything is fine",
false,
2000,
2000,
);
let mut filters = default_note_filters();
filters.contains = Some("bug".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].author_username, "alice");
}
#[test]
fn test_query_notes_filter_contains_escapes_like_wildcards() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_note(&conn, 1, 100, 1, 1, "alice", "100% done", false, 1000, 1000);
insert_test_note(&conn, 2, 101, 1, 1, "bob", "100 things", false, 2000, 2000);
let mut filters = default_note_filters();
filters.contains = Some("100%".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].author_username, "alice");
}
#[test]
fn test_query_notes_filter_path_exact() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_note_with_position(
&conn,
1,
100,
1,
1,
"alice",
"Change here",
1000,
Some("DiffNote"),
Some("src/main.rs"),
Some(42),
);
insert_note_with_position(
&conn,
2,
101,
1,
1,
"bob",
"And here",
2000,
Some("DiffNote"),
Some("src/lib.rs"),
Some(10),
);
let mut filters = default_note_filters();
filters.path = Some("src/main.rs".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].author_username, "alice");
}
#[test]
fn test_query_notes_filter_path_prefix() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_note_with_position(
&conn,
1,
100,
1,
1,
"alice",
"In src",
1000,
Some("DiffNote"),
Some("src/main.rs"),
Some(42),
);
insert_note_with_position(
&conn,
2,
101,
1,
1,
"bob",
"Also in src",
2000,
Some("DiffNote"),
Some("src/lib.rs"),
Some(10),
);
insert_note_with_position(
&conn,
3,
102,
1,
1,
"carol",
"In tests",
3000,
Some("DiffNote"),
Some("tests/test.rs"),
Some(1),
);
let mut filters = default_note_filters();
filters.path = Some("src/".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 2);
}
#[test]
fn test_query_notes_filter_for_issue_requires_project() {
let conn = setup_note_test_db();
let config = test_config(None);
let mut filters = default_note_filters();
filters.for_issue_iid = Some(42);
let result = query_notes(&conn, &filters, &config);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("project context"),
"Expected project context error, got: {msg}"
);
}
#[test]
fn test_query_notes_filter_for_mr_requires_project() {
let conn = setup_note_test_db();
let config = test_config(None);
let mut filters = default_note_filters();
filters.for_mr_iid = Some(10);
let result = query_notes(&conn, &filters, &config);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("project context"),
"Expected project context error, got: {msg}"
);
}
#[test]
fn test_query_notes_filter_for_issue_with_project() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 42, "Target Issue");
insert_test_issue(&conn, 2, 1, 43, "Other Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_discussion(&conn, 2, "disc-2", 1, Some(2), None, "Issue");
insert_test_note(
&conn,
1,
100,
1,
1,
"alice",
"On issue 42",
false,
1000,
1000,
);
insert_test_note(&conn, 2, 101, 2, 1, "bob", "On issue 43", false, 2000, 2000);
let mut filters = default_note_filters();
filters.for_issue_iid = Some(42);
filters.project = Some("group/project".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].author_username, "alice");
}
#[test]
fn test_query_notes_filter_for_issue_uses_default_project() {
let conn = setup_note_test_db();
let config = test_config(Some("group/project"));
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 42, "Target Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_note(
&conn,
1,
100,
1,
1,
"alice",
"On issue 42",
false,
1000,
1000,
);
let mut filters = default_note_filters();
filters.for_issue_iid = Some(42);
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
}
#[test]
fn test_query_notes_filter_for_mr_with_project() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_mr(&conn, 1, 1, 10, "Target MR");
insert_test_discussion(&conn, 1, "disc-1", 1, None, Some(1), "MergeRequest");
insert_test_note(&conn, 1, 100, 1, 1, "alice", "On MR 10", false, 1000, 1000);
let mut filters = default_note_filters();
filters.for_mr_iid = Some(10);
filters.project = Some("group/project".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].author_username, "alice");
}
#[test]
fn test_query_notes_filter_resolution_unresolved() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_resolvable_note(
&conn,
1,
100,
1,
1,
"alice",
"Unresolved",
1000,
true,
false,
None,
);
insert_resolvable_note(
&conn,
2,
101,
1,
1,
"bob",
"Resolved",
2000,
true,
true,
Some("carol"),
);
insert_test_note(
&conn,
3,
102,
1,
1,
"dave",
"Not resolvable",
false,
3000,
3000,
);
let mut filters = default_note_filters();
filters.resolution = Some("unresolved".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].author_username, "alice");
}
#[test]
fn test_query_notes_filter_resolution_resolved() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_resolvable_note(
&conn,
1,
100,
1,
1,
"alice",
"Unresolved",
1000,
true,
false,
None,
);
insert_resolvable_note(
&conn,
2,
101,
1,
1,
"bob",
"Resolved",
2000,
true,
true,
Some("carol"),
);
let mut filters = default_note_filters();
filters.resolution = Some("resolved".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].author_username, "bob");
assert!(result.notes[0].resolved);
assert_eq!(result.notes[0].resolved_by.as_deref(), Some("carol"));
}
#[test]
fn test_query_notes_sort_created_desc() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_note(&conn, 1, 100, 1, 1, "alice", "First", false, 1000, 1000);
insert_test_note(&conn, 2, 101, 1, 1, "bob", "Second", false, 2000, 2000);
insert_test_note(&conn, 3, 102, 1, 1, "carol", "Third", false, 3000, 3000);
let filters = default_note_filters();
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.notes[0].author_username, "carol");
assert_eq!(result.notes[1].author_username, "bob");
assert_eq!(result.notes[2].author_username, "alice");
}
#[test]
fn test_query_notes_sort_created_asc() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_note(&conn, 1, 100, 1, 1, "alice", "First", false, 1000, 1000);
insert_test_note(&conn, 2, 101, 1, 1, "bob", "Second", false, 2000, 2000);
insert_test_note(&conn, 3, 102, 1, 1, "carol", "Third", false, 3000, 3000);
let mut filters = default_note_filters();
filters.order = "asc".to_string();
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.notes[0].author_username, "alice");
assert_eq!(result.notes[1].author_username, "bob");
assert_eq!(result.notes[2].author_username, "carol");
}
#[test]
fn test_query_notes_deterministic_tiebreak() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_note(&conn, 1, 100, 1, 1, "alice", "A", false, 1000, 1000);
insert_test_note(&conn, 2, 101, 1, 1, "bob", "B", false, 1000, 1000);
insert_test_note(&conn, 3, 102, 1, 1, "carol", "C", false, 1000, 1000);
let filters = default_note_filters();
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.notes[0].id, 3);
assert_eq!(result.notes[1].id, 2);
assert_eq!(result.notes[2].id, 1);
let mut filters = default_note_filters();
filters.order = "asc".to_string();
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.notes[0].id, 1);
assert_eq!(result.notes[1].id, 2);
assert_eq!(result.notes[2].id, 3);
}
#[test]
fn test_query_notes_limit() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
for i in 1..=10 {
insert_test_note(
&conn,
i,
100 + i,
1,
1,
"alice",
&format!("Note {i}"),
false,
i * 1000,
i * 1000,
);
}
let mut filters = default_note_filters();
filters.limit = 3;
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 10);
assert_eq!(result.notes.len(), 3);
}
#[test]
fn test_query_notes_combined_filters() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_note(
&conn,
1,
100,
1,
1,
"alice",
"Found a bug here",
false,
1000,
1000,
);
insert_test_note(
&conn,
2,
101,
1,
1,
"alice",
"Looks good",
false,
2000,
2000,
);
insert_test_note(
&conn,
3,
102,
1,
1,
"bob",
"Another bug fix",
false,
3000,
3000,
);
let mut filters = default_note_filters();
filters.author = Some("alice".to_string());
filters.contains = Some("bug".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].id, 1);
}
#[test]
fn test_query_notes_filter_note_type() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_note_with_position(
&conn,
1,
100,
1,
1,
"alice",
"Diff comment",
1000,
Some("DiffNote"),
Some("src/main.rs"),
Some(10),
);
insert_test_note(
&conn,
2,
101,
1,
1,
"bob",
"Discussion note",
false,
2000,
2000,
);
let mut filters = default_note_filters();
filters.note_type = Some("DiffNote".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].author_username, "alice");
}
#[test]
fn test_query_notes_filter_discussion_id() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-aaa", 1, Some(1), None, "Issue");
insert_test_discussion(&conn, 2, "disc-bbb", 1, Some(1), None, "Issue");
insert_test_note(&conn, 1, 100, 1, 1, "alice", "In disc A", false, 1000, 1000);
insert_test_note(&conn, 2, 101, 2, 1, "bob", "In disc B", false, 2000, 2000);
let mut filters = default_note_filters();
filters.discussion_id = Some("disc-aaa".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].author_username, "alice");
}
#[test]
fn test_query_notes_filter_note_id() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_note(&conn, 1, 100, 1, 1, "alice", "First", false, 1000, 1000);
insert_test_note(&conn, 2, 101, 1, 1, "bob", "Second", false, 2000, 2000);
let mut filters = default_note_filters();
filters.note_id = Some(2);
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].author_username, "bob");
}
#[test]
fn test_query_notes_filter_gitlab_note_id() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_note(&conn, 1, 100, 1, 1, "alice", "First", false, 1000, 1000);
insert_test_note(&conn, 2, 200, 1, 1, "bob", "Second", false, 2000, 2000);
let mut filters = default_note_filters();
filters.gitlab_note_id = Some(200);
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].author_username, "bob");
}
#[test]
fn test_query_notes_filter_project() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project-a");
insert_test_project(&conn, 2, "group/project-b");
insert_test_issue(&conn, 1, 1, 1, "Issue A");
insert_test_issue(&conn, 2, 2, 1, "Issue B");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_discussion(&conn, 2, "disc-2", 2, Some(2), None, "Issue");
insert_test_note(&conn, 1, 100, 1, 1, "alice", "In A", false, 1000, 1000);
insert_test_note(&conn, 2, 101, 2, 2, "bob", "In B", false, 2000, 2000);
let mut filters = default_note_filters();
filters.project = Some("group/project-a".to_string());
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(result.notes[0].author_username, "alice");
}
#[test]
fn test_query_notes_mr_parent() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_mr(&conn, 1, 1, 99, "My MR");
insert_test_discussion(&conn, 1, "disc-1", 1, None, Some(1), "MergeRequest");
insert_test_note(
&conn,
1,
100,
1,
1,
"alice",
"MR comment",
false,
1000,
1000,
);
let filters = default_note_filters();
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.total_count, 1);
assert_eq!(
result.notes[0].noteable_type.as_deref(),
Some("MergeRequest")
);
assert_eq!(result.notes[0].parent_iid, Some(99));
assert_eq!(result.notes[0].parent_title.as_deref(), Some("My MR"));
}
#[test]
fn test_note_list_row_json_conversion() {
let row = NoteListRow {
id: 1,
gitlab_id: 100,
author_username: "alice".to_string(),
body: Some("Test body".to_string()),
note_type: Some("DiffNote".to_string()),
is_system: false,
created_at: 1_705_315_800_000,
updated_at: 1_705_315_800_000,
position_new_path: Some("src/main.rs".to_string()),
position_new_line: Some(42),
position_old_path: None,
position_old_line: None,
resolvable: true,
resolved: false,
resolved_by: None,
noteable_type: Some("Issue".to_string()),
parent_iid: Some(5),
parent_title: Some("Test Issue".to_string()),
project_path: "group/project".to_string(),
};
let json_row = NoteListRowJson::from(&row);
assert_eq!(json_row.id, 1);
assert_eq!(json_row.gitlab_id, 100);
assert_eq!(json_row.author_username, "alice");
assert!(json_row.created_at_iso.contains("2024-01-15"));
assert!(json_row.updated_at_iso.contains("2024-01-15"));
assert_eq!(json_row.position_new_path.as_deref(), Some("src/main.rs"));
assert_eq!(json_row.position_new_line, Some(42));
assert!(!json_row.is_system);
assert!(json_row.resolvable);
assert!(!json_row.resolved);
}
#[test]
fn test_note_list_result_json_conversion() {
let result = NoteListResult {
notes: vec![NoteListRow {
id: 1,
gitlab_id: 100,
author_username: "alice".to_string(),
body: Some("Test".to_string()),
note_type: None,
is_system: false,
created_at: 1000,
updated_at: 2000,
position_new_path: None,
position_new_line: None,
position_old_path: None,
position_old_line: None,
resolvable: false,
resolved: false,
resolved_by: None,
noteable_type: Some("Issue".to_string()),
parent_iid: Some(1),
parent_title: Some("Issue".to_string()),
project_path: "group/project".to_string(),
}],
total_count: 5,
};
let json_result = NoteListResultJson::from(&result);
assert_eq!(json_result.total_count, 5);
assert_eq!(json_result.showing, 1);
assert_eq!(json_result.notes.len(), 1);
}
#[test]
fn test_query_notes_sort_updated() {
let conn = setup_note_test_db();
let config = test_config(None);
insert_test_project(&conn, 1, "group/project");
insert_test_issue(&conn, 1, 1, 1, "Issue");
insert_test_discussion(&conn, 1, "disc-1", 1, Some(1), None, "Issue");
insert_test_note(&conn, 1, 100, 1, 1, "alice", "A", false, 1000, 3000);
insert_test_note(&conn, 2, 101, 1, 1, "bob", "B", false, 2000, 1000);
insert_test_note(&conn, 3, 102, 1, 1, "carol", "C", false, 3000, 2000);
let mut filters = default_note_filters();
filters.sort = "updated".to_string();
filters.order = "desc".to_string();
let result = query_notes(&conn, &filters, &config).unwrap();
assert_eq!(result.notes[0].author_username, "alice");
assert_eq!(result.notes[1].author_username, "carol");
assert_eq!(result.notes[2].author_username, "bob");
}
#[test]
fn test_note_escape_like() {
assert_eq!(note_escape_like("normal/path"), "normal/path");
assert_eq!(note_escape_like("has_underscore"), "has\\_underscore");
assert_eq!(note_escape_like("has%percent"), "has\\%percent");
assert_eq!(note_escape_like("has\\backslash"), "has\\\\backslash");
}
// -----------------------------------------------------------------------
// Note output formatting tests
// -----------------------------------------------------------------------
#[test]
fn test_truncate_note_body() {
let short = "short body";
assert_eq!(truncate_body(short, 60), "short body");
let long: String = "a".repeat(200);
let result = truncate_body(&long, 60);
assert_eq!(result.chars().count(), 63); // 60 chars + "..."
assert!(result.ends_with("..."));
}
#[test]
fn test_jsonl_output_one_per_line() {
let result = NoteListResult {
notes: vec![
NoteListRow {
id: 1,
gitlab_id: 100,
author_username: "alice".to_string(),
body: Some("First note".to_string()),
note_type: None,
is_system: false,
created_at: 1_000_000,
updated_at: 2_000_000,
position_new_path: None,
position_new_line: None,
position_old_path: None,
position_old_line: None,
resolvable: false,
resolved: false,
resolved_by: None,
noteable_type: None,
parent_iid: None,
parent_title: None,
project_path: "group/project".to_string(),
},
NoteListRow {
id: 2,
gitlab_id: 101,
author_username: "bob".to_string(),
body: Some("Second note".to_string()),
note_type: Some("DiffNote".to_string()),
is_system: false,
created_at: 3_000_000,
updated_at: 4_000_000,
position_new_path: None,
position_new_line: None,
position_old_path: None,
position_old_line: None,
resolvable: false,
resolved: false,
resolved_by: None,
noteable_type: None,
parent_iid: None,
parent_title: None,
project_path: "group/project".to_string(),
},
],
total_count: 2,
};
// Each note should produce valid JSON when serialized individually
for note in &result.notes {
let json_row = NoteListRowJson::from(note);
let json_str = serde_json::to_string(&json_row).unwrap();
// Verify it parses back as valid JSON
let _: serde_json::Value = serde_json::from_str(&json_str).unwrap();
// Verify no embedded newlines in the JSON line
assert!(!json_str.contains('\n'));
}
}
#[test]
fn test_format_note_parent_variants() {
assert_eq!(format_note_parent(Some("Issue"), Some(42)), "Issue #42");
assert_eq!(format_note_parent(Some("MergeRequest"), Some(99)), "MR !99");
assert_eq!(format_note_parent(None, None), "-");
assert_eq!(format_note_parent(Some("Issue"), None), "-");
}
#[test]
fn test_format_note_type_variants() {
assert_eq!(format_note_type(Some("DiffNote")), "Diff");
assert_eq!(format_note_type(Some("DiscussionNote")), "Disc");
assert_eq!(format_note_type(None), "-");
assert_eq!(format_note_type(Some("Other")), "-");
}