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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ list of commands as built.
| API Domain | Status | Pup Commands | Notes |
|------------|--------|--------------|-------|
| Incidents | ✅ | `incidents list`, `incidents get`, `incidents attachments`, `incidents settings`, `incidents handles`, `incidents postmortem-templates` | Incident management with settings, handles, and postmortem templates |
| On-Call | ✅ | `on-call teams` (CRUD, memberships with roles), `on-call pages` (list, get, create) | Team management and on-call page access |
| On-Call | ✅ | `on-call teams` (CRUD, memberships with roles), `on-call pages` (list, get, create) | Team management and newest-first on-call page access |
| Case Management | ✅ | `cases` (create, search, assign, archive, projects, jira, servicenow, move) | Complete case management with Jira/ServiceNow linking |
| Error Tracking | ✅ | `error-tracking issues search`, `error-tracking issues get` | Error issue search and details |
| Service Catalog | ✅ | `service-catalog list`, `service-catalog get` | Service registry management |
Expand Down
4 changes: 2 additions & 2 deletions docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pup <domain> <subgroup> <action> [options] # Nested commands
| downtime | list, get, cancel | src/commands/downtime.rs | ✅ |
| tags | list, get, add, update, delete | src/commands/tags.rs | ✅ |
| events | post, list, search, get | src/commands/events.rs | ✅ |
| on-call | teams (CRUD, memberships), pages (list, get, create) | src/commands/on_call.rs | ✅ |
| on-call | teams (CRUD, memberships), pages (newest-first list, get, create) | src/commands/on_call.rs | ✅ |
| audit-logs | list, search | src/commands/audit_logs.rs | ✅ |
| api-keys | list, get, create, delete | src/commands/api_keys.rs | ✅ |
| app-keys | list, get, create, update, delete | src/commands/app_keys.rs | ✅ |
Expand Down Expand Up @@ -188,7 +188,7 @@ pup infrastructure hosts list

### Operations & Incident Response
- **incidents** - Incident management (list, get, attachments, settings, handles, postmortem-templates)
- **on-call** - Team management (create, update, delete teams; manage memberships with roles) and pages (list, get, create)
- **on-call** - Team management (create, update, delete teams; manage memberships with roles) and pages (newest-first list, get, create)
- **cases** - Case management (create, search, assign, archive, unarchive, update, projects, jira, servicenow, move)
- **hamr** - High Availability Multi-Region connections
- **fleet** - Fleet Automation (agents, deployments, schedules, tracers, clusters, instrumented-pods)
Expand Down
31 changes: 28 additions & 3 deletions src/commands/on_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,14 +480,16 @@ pub async fn pages_list(
team: Option<&str>,
responder: Option<&str>,
page_size: u32,
sort: &str,
) -> Result<()> {
if !(1..=1000).contains(&page_size) {
anyhow::bail!("invalid page_size: {page_size}. Expected a value from 1 to 1000");
}
validate_pages_sort(sort)?;

let page_size = page_size.to_string();
let team_filter = team.map(|t| format!("team:{t}"));
let mut query = vec![("page[size]", page_size.as_str())];
let mut query = vec![("page[size]", page_size.as_str()), ("sort", sort)];
if let Some(filter) = team_filter.as_deref() {
query.push(("filter", filter));
}
Expand All @@ -503,6 +505,15 @@ pub async fn pages_list(
formatter::output(cfg, &resp)
}

fn validate_pages_sort(sort: &str) -> Result<()> {
match sort {
"created_at" | "-created_at" => Ok(()),
other => {
anyhow::bail!("invalid --sort value: {other:?}\nExpected: created_at, -created_at")
}
}
}

fn filter_pages_by_responder(resp: &mut serde_json::Value, responder: &str) {
if let Some(pages) = resp
.get_mut("data")
Expand Down Expand Up @@ -952,14 +963,15 @@ mod tests {
.mock("GET", "/api/unstable/on-call/pages")
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("page[size]".into(), "42".into()),
mockito::Matcher::UrlEncoded("sort".into(), "-created_at".into()),
mockito::Matcher::UrlEncoded("filter".into(), "team:core-platform".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"data": []}"#)
.create_async()
.await;
let result = super::pages_list(&cfg, Some("core-platform"), None, 42).await;
let result = super::pages_list(&cfg, Some("core-platform"), None, 42, "-created_at").await;
assert!(result.is_ok(), "pages_list failed: {:?}", result.err());
mock.assert_async().await;
cleanup_env();
Expand All @@ -969,7 +981,7 @@ mod tests {
async fn test_on_call_pages_list_rejects_invalid_page_size() {
let _lock = lock_env().await;
let cfg = test_config("http://unused.local");
let result = super::pages_list(&cfg, None, None, 0).await;
let result = super::pages_list(&cfg, None, None, 0, "-created_at").await;
assert!(result.is_err());
assert!(result
.unwrap_err()
Expand All @@ -978,6 +990,19 @@ mod tests {
cleanup_env();
}

#[tokio::test]
async fn test_on_call_pages_list_rejects_invalid_sort() {
let _lock = lock_env().await;
let cfg = test_config("http://unused.local");
let result = super::pages_list(&cfg, None, None, 100, "started_at").await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("invalid --sort value"));
cleanup_env();
}

#[test]
fn test_page_has_responder_matches_and_rejects() {
let page = serde_json::json!({
Expand Down
10 changes: 10 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6812,6 +6812,14 @@ enum OnCallPagesActions {
team: Option<String>,
#[arg(long, help = "Filter by responder user id (client-side)")]
responder: Option<String>,
#[arg(
long,
allow_hyphen_values = true,
value_parser = ["created_at", "-created_at"],
default_value = "-created_at",
help = "Sort field (created_at or -created_at; defaults to newest first)"
)]
sort: String,
#[arg(
long,
default_value_t = 1000,
Expand Down Expand Up @@ -14512,13 +14520,15 @@ async fn main_inner() -> anyhow::Result<()> {
OnCallPagesActions::List {
team,
responder,
sort,
page_size,
} => {
commands::on_call::pages_list(
&cfg,
team.as_deref(),
responder.as_deref(),
page_size,
&sort,
)
.await?;
}
Expand Down
15 changes: 15 additions & 0 deletions src/test_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ fn test_read_only_guard_on_call_pages_list() {
"core-platform",
"--responder",
"user-1",
"--sort",
"-created_at",
])
.unwrap();
let leaf = crate::get_leaf_subcommand_name(&matches).unwrap();
Expand All @@ -119,6 +121,19 @@ fn test_on_call_pages_list_rejects_invalid_page_size() {
assert!(result.is_err());
}

#[test]
fn test_on_call_pages_list_rejects_invalid_sort() {
let result = crate::Cli::command().try_get_matches_from([
"pup",
"on-call",
"pages",
"list",
"--sort",
"started_at",
]);
assert!(result.is_err());
}

#[test]
fn test_read_only_guard_nested_write() {
let matches = crate::Cli::command()
Expand Down