Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/api/types.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use clap::builder::PossibleValue;

use crate::output::Formattable;
use crate::utils::{format_date, format_datetime};
use crate::utils::datetime::{format_date, format_datetime};

// Re-export generated types as the public API for this crate.
pub use super::generated::types::{
Expand Down
129 changes: 3 additions & 126 deletions src/commands/bugs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ use crate::api::types::{
dismissal_reason_label, review_state_label, Bug, BugDismissalReason, BugId, BugReviewState,
IntroducedIn, RepoId,
};
use crate::commands::repo_helpers::resolve_repo_id;
use crate::output::{output_list, SectionRenderer};
use crate::utils::{format_datetime, page_to_offset};
use crate::utils::datetime::format_datetime;
use crate::utils::pagination::page_to_offset;
use crate::utils::repos::resolve_repo_id;

/// Return only bugs where `isSecurityVulnerability` is `true`.
fn filter_vulns_only(bugs: &[Bug]) -> Vec<Bug> {
Expand Down Expand Up @@ -414,130 +415,6 @@ pub async fn handle(command: &BugCommands, cli: &crate::Cli) -> Result<()> {
#[cfg(test)]
mod tests {
use super::*;
use crate::api::types::Repo;
use crate::commands::repo_helpers::validate_owner_repo_format;
use crate::commands::repo_helpers::{match_repo_by_name, resolve_repo_id_from_repos};

// ── validate_owner_repo_format ───────────────────────────────────

#[test]
fn valid_owner_repo() {
assert!(validate_owner_repo_format("usedetail/cli").is_ok());
}

#[test]
fn rejects_empty_owner() {
assert!(validate_owner_repo_format("/cli").is_err());
}

#[test]
fn rejects_empty_repo() {
assert!(validate_owner_repo_format("usedetail/").is_err());
}

#[test]
fn rejects_multiple_slashes() {
assert!(validate_owner_repo_format("a/b/c").is_err());
}

#[test]
fn rejects_slash_only() {
assert!(validate_owner_repo_format("/").is_err());
}

// ── match_repo_by_name ───────────────────────────────────────────

fn sample_repos() -> Vec<Repo> {
vec![
serde_json::from_value(serde_json::json!({
"id": "repo_1", "name": "cli", "ownerName": "usedetail",
"fullName": "usedetail/cli", "visibility": "public",
"primaryBranch": "main", "orgId": "org_1", "orgName": "Detail"
}))
.unwrap(),
serde_json::from_value(serde_json::json!({
"id": "repo_2", "name": "cli", "ownerName": "acme",
"fullName": "acme/cli", "visibility": "private",
"primaryBranch": "main", "orgId": "org_2", "orgName": "Acme"
}))
.unwrap(),
serde_json::from_value(serde_json::json!({
"id": "repo_3", "name": "web", "ownerName": "usedetail",
"fullName": "usedetail/web", "visibility": "public",
"primaryBranch": "main", "orgId": "org_1", "orgName": "Detail"
}))
.unwrap(),
]
}

#[test]
fn match_single_repo_by_name() {
let repos = sample_repos();
let id = match_repo_by_name("web", &repos).unwrap();
assert_eq!(id.to_string(), "repo_3");
}

#[test]
fn match_no_repo_returns_error() {
let repos = sample_repos();
let err = match_repo_by_name("nonexistent", &repos).unwrap_err();
assert!(err.to_string().contains("not found"));
}

#[test]
fn match_multiple_repos_returns_error_with_names() {
let repos = sample_repos();
let err = match_repo_by_name("cli", &repos).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Multiple repositories"));
assert!(msg.contains("usedetail/cli"));
assert!(msg.contains("acme/cli"));
}

#[test]
fn match_empty_repo_list() {
let err = match_repo_by_name("cli", &[]).unwrap_err();
assert!(err.to_string().contains("not found"));
}

// ── resolve_repo_id_from_repos ──────────────────────────────────

#[test]
fn resolve_owner_repo_exact_match() {
let repos = sample_repos();
let id = resolve_repo_id_from_repos(&repos, "usedetail/cli").unwrap();
assert_eq!(id.to_string(), "repo_1");
}

#[test]
fn resolve_owner_repo_not_found_has_access_hint() {
let repos = sample_repos();
let err = resolve_repo_id_from_repos(&repos, "usedetail/missing").unwrap_err();
assert!(err
.to_string()
.contains("Make sure you have access to this repository"));
}

#[test]
fn resolve_owner_repo_invalid_format_rejected() {
let repos = sample_repos();
let err = resolve_repo_id_from_repos(&repos, "usedetail/cli/extra").unwrap_err();
assert!(err.to_string().contains("Invalid repository format"));
}

#[test]
fn resolve_bare_repo_name_unique_match() {
let repos = sample_repos();
let id = resolve_repo_id_from_repos(&repos, "web").unwrap();
assert_eq!(id.to_string(), "repo_3");
}

#[test]
fn resolve_bare_repo_name_ambiguous_returns_error() {
let repos = sample_repos();
let err = resolve_repo_id_from_repos(&repos, "cli").unwrap_err();
assert!(err.to_string().contains("Multiple repositories"));
}

// ── validate_close_flags ─────────────────────────────────────────

Expand Down
1 change: 0 additions & 1 deletion src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
pub mod auth;
pub mod bugs;
pub mod repo_helpers;
pub mod repos;
pub mod satisfying_sort;
pub mod scans;
Expand Down
88 changes: 0 additions & 88 deletions src/commands/repo_helpers.rs

This file was deleted.

2 changes: 1 addition & 1 deletion src/commands/repos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use clap::Subcommand;
use console::{style, Term};

use crate::output::output_list;
use crate::utils::page_to_offset;
use crate::utils::pagination::page_to_offset;

#[derive(Subcommand)]
pub enum RepoCommands {
Expand Down
4 changes: 2 additions & 2 deletions src/commands/scans.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use anyhow::{Context, Result};
use clap::Subcommand;

use crate::commands::repo_helpers::resolve_repo_id;
use crate::output::output_list;
use crate::utils::page_to_offset;
use crate::utils::pagination::page_to_offset;
use crate::utils::repos::resolve_repo_id;

#[derive(Subcommand)]
pub enum ScanCommands {
Expand Down
2 changes: 1 addition & 1 deletion src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use console::{style, Term};
use serde::Serialize;
use termimad::crossterm::style::Attribute;

use crate::utils::page_to_offset;
use crate::utils::pagination::page_to_offset;

static MARKDOWN_SKIN: LazyLock<termimad::MadSkin> = LazyLock::new(|| {
let mut skin = termimad::MadSkin::default();
Expand Down
25 changes: 0 additions & 25 deletions src/utils.rs → src/utils/datetime.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
use chrono::Local;

/// Convert page number and limit to offset for pagination
pub const fn page_to_offset(page: u32, limit: u32) -> u32 {
(page - 1).saturating_mul(limit)
}

/// Conversion factor from milliseconds to seconds
const MS_TO_SECONDS: i64 = 1000;

Expand Down Expand Up @@ -38,26 +33,6 @@ mod tests {
.to_string()
}

#[test]
fn page_to_offset_first_page() {
assert_eq!(page_to_offset(1, 50), 0);
}

#[test]
fn page_to_offset_second_page() {
assert_eq!(page_to_offset(2, 50), 50);
}

#[test]
fn page_to_offset_custom_limit() {
assert_eq!(page_to_offset(3, 10), 20);
}

#[test]
fn page_to_offset_limit_one() {
assert_eq!(page_to_offset(5, 1), 4);
}

#[test]
fn format_date_unix_epoch() {
assert_eq!(format_date(0), expected_local(0, "%Y-%m-%d"));
Expand Down
3 changes: 3 additions & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod datetime;
pub mod pagination;
pub mod repos;
29 changes: 29 additions & 0 deletions src/utils/pagination.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/// Convert page number and limit to offset for pagination
pub const fn page_to_offset(page: u32, limit: u32) -> u32 {
(page - 1).saturating_mul(limit)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn page_to_offset_first_page() {
assert_eq!(page_to_offset(1, 50), 0);
}

#[test]
fn page_to_offset_second_page() {
assert_eq!(page_to_offset(2, 50), 50);
}

#[test]
fn page_to_offset_custom_limit() {
assert_eq!(page_to_offset(3, 10), 20);
}

#[test]
fn page_to_offset_limit_one() {
assert_eq!(page_to_offset(5, 1), 4);
}
}
Loading
Loading