From 7946fbadff7f5a66af487320bdbfed62a04e2c99 Mon Sep 17 00:00:00 2001 From: Ben Blier Date: Wed, 8 Apr 2026 10:59:22 -0400 Subject: [PATCH 1/2] geolocation: standardize CLI flag naming and validators --- CHANGELOG.md | 1 + e2e/geoprobe_test.go | 6 +- .../cli/src/geolocation/probe/add_parent.rs | 49 ++++++++--- .../cli/src/geolocation/probe/create.rs | 8 +- .../cli/src/geolocation/probe/delete.rs | 48 ++++++++-- .../cli/src/geolocation/probe/get.rs | 26 ++++-- .../src/geolocation/probe/remove_parent.rs | 49 ++++++++--- .../cli/src/geolocation/probe/update.rs | 59 ++++++++++--- .../cli/src/geolocation/user/add_target.rs | 87 ++++++++++++++----- .../cli/src/geolocation/user/delete.rs | 48 ++++++++-- smartcontract/cli/src/geolocation/user/get.rs | 22 +++-- .../cli/src/geolocation/user/remove_target.rs | 66 +++++++++++--- .../geolocation/user/update_payment_status.rs | 47 ++++++++-- 13 files changed, 410 insertions(+), 106 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe698c1cac..0e62416a1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ All notable changes to this project will be documented in this file. - Add `OutboundIcmp` target type (`= 2`) to the geolocation onchain program, enabling ICMP-based probing as an alternative to TWAMP for outbound geolocation targets - Allow pending users with subs to be deleted - Geolocation + - Standardize CLI flag naming: probe mutation commands use `--probe` (was `--code`) accepting pubkey or code; rename `--signing-keypair` → `--signing-pubkey` and `--target-pk` → `--target-signing-pubkey`; add `--json-compact` to `get` commands - geoprobe-target can now store LocationOffset messages in ClickHouse - Add ICMP pinger to geoprobe-agent for measuring outbound ICMP targets with interleaved batch send/receive, integrated into the existing measurement cycle alongside TWAMP - Remove `--additional-parent`, `--additional-targets`, `--additional-icmp-targets`, and `--allowed-pubkeys` CLI flags from geoprobe-agent; all configuration now comes from onchain state via parent and target discovery diff --git a/e2e/geoprobe_test.go b/e2e/geoprobe_test.go index 63a5073d2c..ed505ece2a 100644 --- a/e2e/geoprobe_test.go +++ b/e2e/geoprobe_test.go @@ -317,7 +317,7 @@ func createGeoprobeOnchain(t *testing.T, dn *devnet.Devnet, code, exchangePK, pu "--exchange", exchangePK, "--public-ip", publicIP, "--port", "8923", - "--signing-keypair", signingKeypair, + "--signing-pubkey", signingKeypair, }) require.NoError(t, err, "probe create failed: %s", string(output)) @@ -358,7 +358,7 @@ func addGeoprobeParent(t *testing.T, dn *devnet.Devnet, code, devicePK string) { t.Helper() output, err := dn.Manager.Exec(t.Context(), []string{ "doublezero-geolocation", "probe", "add-parent", - "--code", code, + "--probe", code, "--device", devicePK, }) require.NoError(t, err, "probe add-parent failed: %s", string(output)) @@ -912,7 +912,7 @@ func addGeolocationInboundTarget(t *testing.T, dn *devnet.Devnet, userCode, targ "doublezero-geolocation", "user", "add-target", "--user", userCode, "--type", "inbound", - "--target-pk", targetPK, + "--target-signing-pubkey", targetPK, "--probe", probeCode, }) require.NoError(t, err, "user add-target inbound failed: %s", string(output)) diff --git a/smartcontract/cli/src/geolocation/probe/add_parent.rs b/smartcontract/cli/src/geolocation/probe/add_parent.rs index 640bffa734..4020062de9 100644 --- a/smartcontract/cli/src/geolocation/probe/add_parent.rs +++ b/smartcontract/cli/src/geolocation/probe/add_parent.rs @@ -1,16 +1,15 @@ -use crate::{ - geoclicommand::GeoCliCommand, - validators::{validate_code, validate_pubkey_or_code}, -}; +use crate::{geoclicommand::GeoCliCommand, validators::validate_pubkey_or_code}; use clap::Args; -use doublezero_sdk::geolocation::geo_probe::add_parent_device::AddParentDeviceCommand; +use doublezero_sdk::geolocation::geo_probe::{ + add_parent_device::AddParentDeviceCommand, get::GetGeoProbeCommand, +}; use std::io::Write; #[derive(Args, Debug)] pub struct AddParentGeoProbeCliCommand { - /// Probe code - #[arg(long, value_name = "PROBE_CODE", value_parser = validate_code)] - pub code: String, + /// Probe pubkey or code + #[arg(long, value_parser = validate_pubkey_or_code)] + pub probe: String, /// Device pubkey or code to add as parent #[arg(long, value_name = "PARENT_DEVICE", value_parser = validate_pubkey_or_code)] pub device: String, @@ -18,11 +17,16 @@ pub struct AddParentGeoProbeCliCommand { impl AddParentGeoProbeCliCommand { pub fn execute(self, client: &C, out: &mut W) -> eyre::Result<()> { + let (_, resolved_probe) = client.get_geo_probe(GetGeoProbeCommand { + pubkey_or_code: self.probe, + })?; + let code = resolved_probe.code; + let device_pk = client.resolve_device_pk(self.device)?; let serviceability_globalstate_pk = client.get_serviceability_globalstate_pk(); let sig = client.add_parent_device(AddParentDeviceCommand { - code: self.code, + code, device_pk, serviceability_globalstate_pk, })?; @@ -37,8 +41,10 @@ impl AddParentGeoProbeCliCommand { mod tests { use super::*; use crate::geoclicommand::MockGeoCliCommand; + use doublezero_geolocation::state::{accounttype::AccountType, geo_probe::GeoProbe}; use mockall::predicate; use solana_sdk::{pubkey::Pubkey, signature::Signature}; + use std::net::Ipv4Addr; #[test] fn test_cli_geo_probe_add_parent() { @@ -53,6 +59,29 @@ mod tests { 100, 221, 20, 137, 4, 5, ]); + client + .expect_get_geo_probe() + .with(predicate::eq(GetGeoProbeCommand { + pubkey_or_code: "ams-probe-01".to_string(), + })) + .returning(move |_| { + Ok(( + Pubkey::new_unique(), + GeoProbe { + account_type: AccountType::GeoProbe, + owner: Pubkey::new_unique(), + exchange_pk: Pubkey::new_unique(), + public_ip: Ipv4Addr::new(10, 0, 0, 1), + location_offset_port: 8923, + code: "ams-probe-01".to_string(), + parent_devices: vec![], + metrics_publisher_pk: Pubkey::new_unique(), + reference_count: 0, + target_update_count: 0, + }, + )) + }); + client .expect_resolve_device_pk() .with(predicate::eq(device_pk.to_string())) @@ -73,7 +102,7 @@ mod tests { let mut output = Vec::new(); let res = AddParentGeoProbeCliCommand { - code: "ams-probe-01".to_string(), + probe: "ams-probe-01".to_string(), device: device_pk.to_string(), } .execute(&client, &mut output); diff --git a/smartcontract/cli/src/geolocation/probe/create.rs b/smartcontract/cli/src/geolocation/probe/create.rs index af31745b13..ec88a4a952 100644 --- a/smartcontract/cli/src/geolocation/probe/create.rs +++ b/smartcontract/cli/src/geolocation/probe/create.rs @@ -21,15 +21,15 @@ pub struct CreateGeoProbeCliCommand { /// UDP listen port for location offsets #[arg(long, default_value_t = 8923)] pub port: u16, - /// Signing keypair public key + /// Signing public key #[arg(long, value_parser = validate_pubkey)] - pub signing_keypair: String, + pub signing_pubkey: String, } impl CreateGeoProbeCliCommand { pub fn execute(self, client: &C, out: &mut W) -> eyre::Result<()> { let exchange_pk = client.resolve_exchange_pk(self.exchange)?; - let metrics_publisher_pk: Pubkey = self.signing_keypair.parse().expect("validated by clap"); + let metrics_publisher_pk: Pubkey = self.signing_pubkey.parse().expect("validated by clap"); let serviceability_globalstate_pk = client.get_serviceability_globalstate_pk(); @@ -97,7 +97,7 @@ mod tests { exchange: exchange_pk.to_string(), public_ip: Ipv4Addr::new(10, 0, 0, 1), port: 8923, - signing_keypair: metrics_pk.to_string(), + signing_pubkey: metrics_pk.to_string(), } .execute(&client, &mut output); assert!(res.is_ok()); diff --git a/smartcontract/cli/src/geolocation/probe/delete.rs b/smartcontract/cli/src/geolocation/probe/delete.rs index dedb3003a3..0c55f82e81 100644 --- a/smartcontract/cli/src/geolocation/probe/delete.rs +++ b/smartcontract/cli/src/geolocation/probe/delete.rs @@ -1,13 +1,15 @@ -use crate::{geoclicommand::GeoCliCommand, validators::validate_code}; +use crate::{geoclicommand::GeoCliCommand, validators::validate_pubkey_or_code}; use clap::Args; -use doublezero_sdk::geolocation::geo_probe::delete::DeleteGeoProbeCommand; +use doublezero_sdk::geolocation::geo_probe::{ + delete::DeleteGeoProbeCommand, get::GetGeoProbeCommand, +}; use std::io::Write; #[derive(Args, Debug)] pub struct DeleteGeoProbeCliCommand { - /// Probe code to delete - #[arg(long, value_name = "PROBE_CODE", value_parser = validate_code)] - pub code: String, + /// Probe pubkey or code to delete + #[arg(long, value_parser = validate_pubkey_or_code)] + pub probe: String, /// Skip confirmation prompt #[arg(long, default_value_t = false)] pub yes: bool, @@ -15,8 +17,13 @@ pub struct DeleteGeoProbeCliCommand { impl DeleteGeoProbeCliCommand { pub fn execute(self, client: &C, out: &mut W) -> eyre::Result<()> { + let (_, resolved_probe) = client.get_geo_probe(GetGeoProbeCommand { + pubkey_or_code: self.probe, + })?; + let code = resolved_probe.code; + if !self.yes { - eprint!("Delete probe '{}'? [y/N]: ", self.code); + eprint!("Delete probe '{code}'? [y/N]: "); let mut input = String::new(); std::io::stdin().read_line(&mut input)?; if !input.trim().eq_ignore_ascii_case("y") { @@ -28,7 +35,7 @@ impl DeleteGeoProbeCliCommand { let serviceability_globalstate_pk = client.get_serviceability_globalstate_pk(); let sig = client.delete_geo_probe(DeleteGeoProbeCommand { - code: self.code, + code, serviceability_globalstate_pk, })?; @@ -42,8 +49,10 @@ impl DeleteGeoProbeCliCommand { mod tests { use super::*; use crate::geoclicommand::MockGeoCliCommand; + use doublezero_geolocation::state::{accounttype::AccountType, geo_probe::GeoProbe}; use mockall::predicate; use solana_sdk::{pubkey::Pubkey, signature::Signature}; + use std::net::Ipv4Addr; #[test] fn test_cli_geo_probe_delete() { @@ -57,6 +66,29 @@ mod tests { 100, 221, 20, 137, 4, 5, ]); + client + .expect_get_geo_probe() + .with(predicate::eq(GetGeoProbeCommand { + pubkey_or_code: "ams-probe-01".to_string(), + })) + .returning(move |_| { + Ok(( + Pubkey::new_unique(), + GeoProbe { + account_type: AccountType::GeoProbe, + owner: Pubkey::new_unique(), + exchange_pk: Pubkey::new_unique(), + public_ip: Ipv4Addr::new(10, 0, 0, 1), + location_offset_port: 8923, + code: "ams-probe-01".to_string(), + parent_devices: vec![], + metrics_publisher_pk: Pubkey::new_unique(), + reference_count: 0, + target_update_count: 0, + }, + )) + }); + client .expect_get_serviceability_globalstate_pk() .returning(move || svc_gs_pk); @@ -71,7 +103,7 @@ mod tests { let mut output = Vec::new(); let res = DeleteGeoProbeCliCommand { - code: "ams-probe-01".to_string(), + probe: "ams-probe-01".to_string(), yes: true, } .execute(&client, &mut output); diff --git a/smartcontract/cli/src/geolocation/probe/get.rs b/smartcontract/cli/src/geolocation/probe/get.rs index a2c8094baa..37398f1713 100644 --- a/smartcontract/cli/src/geolocation/probe/get.rs +++ b/smartcontract/cli/src/geolocation/probe/get.rs @@ -20,9 +20,12 @@ pub struct GetGeoProbeCliCommand { /// Probe pubkey or code to retrieve #[arg(long, value_parser = validate_pubkey_or_code)] pub probe: String, - /// Output as JSON - #[arg(long)] + /// Output as pretty JSON + #[arg(long, default_value_t = false, conflicts_with = "json_compact")] pub json: bool, + /// Output as compact JSON + #[arg(long, default_value_t = false, conflicts_with = "json")] + pub json_compact: bool, } #[derive(Tabled, Serialize)] @@ -43,7 +46,7 @@ struct GeoProbeGetDisplay { #[tabled(rename = "parent_devices")] pub parent_devices_display: String, #[serde(serialize_with = "serializer::serialize_pubkey_as_string")] - pub signing_keypair: Pubkey, + pub signing_pubkey: Pubkey, pub reference_count: u32, } @@ -71,12 +74,16 @@ impl GetGeoProbeCliCommand { port: probe.location_offset_port, parent_devices: probe.parent_devices, parent_devices_display, - signing_keypair: probe.metrics_publisher_pk, + signing_pubkey: probe.metrics_publisher_pk, reference_count: probe.reference_count, }; - if self.json { - let json = serde_json::to_string_pretty(&display)?; + if self.json || self.json_compact { + let json = if self.json_compact { + serde_json::to_string(&display)? + } else { + serde_json::to_string_pretty(&display)? + }; writeln!(out, "{json}")?; } else { let headers = GeoProbeGetDisplay::headers(); @@ -149,6 +156,7 @@ mod tests { let res = GetGeoProbeCliCommand { probe: Pubkey::new_unique().to_string(), json: false, + json_compact: false, } .execute(&client, &mut output); assert!(res.is_err()); @@ -157,6 +165,7 @@ mod tests { let res = GetGeoProbeCliCommand { probe: probe_pk.to_string(), json: false, + json_compact: false, } .execute(&client, &mut output); assert!(res.is_ok()); @@ -172,7 +181,7 @@ mod tests { assert!(has_row("exchange", &exchange_pk.to_string())); assert!(has_row("public_ip", "10.0.0.1")); assert!(has_row("port", "8923")); - assert!(has_row("signing_keypair", &metrics_pk.to_string())); + assert!(has_row("signing_pubkey", &metrics_pk.to_string())); assert!(has_row("reference_count", "0")); } @@ -193,6 +202,7 @@ mod tests { let res = GetGeoProbeCliCommand { probe: probe_pk.to_string(), json: true, + json_compact: false, } .execute(&client, &mut output); assert!(res.is_ok()); @@ -208,7 +218,7 @@ mod tests { assert_eq!(parents.len(), 1); assert_eq!(parents[0].as_str().unwrap(), parent_pk.to_string()); assert_eq!( - json["signing_keypair"].as_str().unwrap(), + json["signing_pubkey"].as_str().unwrap(), metrics_pk.to_string() ); assert_eq!(json["reference_count"].as_u64().unwrap(), 0); diff --git a/smartcontract/cli/src/geolocation/probe/remove_parent.rs b/smartcontract/cli/src/geolocation/probe/remove_parent.rs index 8de7955591..949e4982f6 100644 --- a/smartcontract/cli/src/geolocation/probe/remove_parent.rs +++ b/smartcontract/cli/src/geolocation/probe/remove_parent.rs @@ -1,16 +1,15 @@ -use crate::{ - geoclicommand::GeoCliCommand, - validators::{validate_code, validate_pubkey_or_code}, -}; +use crate::{geoclicommand::GeoCliCommand, validators::validate_pubkey_or_code}; use clap::Args; -use doublezero_sdk::geolocation::geo_probe::remove_parent_device::RemoveParentDeviceCommand; +use doublezero_sdk::geolocation::geo_probe::{ + get::GetGeoProbeCommand, remove_parent_device::RemoveParentDeviceCommand, +}; use std::io::Write; #[derive(Args, Debug)] pub struct RemoveParentGeoProbeCliCommand { - /// Probe code - #[arg(long, value_name = "PROBE_CODE", value_parser = validate_code)] - pub code: String, + /// Probe pubkey or code + #[arg(long, value_parser = validate_pubkey_or_code)] + pub probe: String, /// Device pubkey or code to remove as parent #[arg(long, value_name = "PARENT_DEVICE", value_parser = validate_pubkey_or_code)] pub device: String, @@ -18,11 +17,16 @@ pub struct RemoveParentGeoProbeCliCommand { impl RemoveParentGeoProbeCliCommand { pub fn execute(self, client: &C, out: &mut W) -> eyre::Result<()> { + let (_, resolved_probe) = client.get_geo_probe(GetGeoProbeCommand { + pubkey_or_code: self.probe, + })?; + let code = resolved_probe.code; + let device_pk = client.resolve_device_pk(self.device)?; let serviceability_globalstate_pk = client.get_serviceability_globalstate_pk(); let sig = client.remove_parent_device(RemoveParentDeviceCommand { - code: self.code, + code, device_pk, serviceability_globalstate_pk, })?; @@ -37,8 +41,10 @@ impl RemoveParentGeoProbeCliCommand { mod tests { use super::*; use crate::geoclicommand::MockGeoCliCommand; + use doublezero_geolocation::state::{accounttype::AccountType, geo_probe::GeoProbe}; use mockall::predicate; use solana_sdk::{pubkey::Pubkey, signature::Signature}; + use std::net::Ipv4Addr; #[test] fn test_cli_geo_probe_remove_parent() { @@ -53,6 +59,29 @@ mod tests { 100, 221, 20, 137, 4, 5, ]); + client + .expect_get_geo_probe() + .with(predicate::eq(GetGeoProbeCommand { + pubkey_or_code: "ams-probe-01".to_string(), + })) + .returning(move |_| { + Ok(( + Pubkey::new_unique(), + GeoProbe { + account_type: AccountType::GeoProbe, + owner: Pubkey::new_unique(), + exchange_pk: Pubkey::new_unique(), + public_ip: Ipv4Addr::new(10, 0, 0, 1), + location_offset_port: 8923, + code: "ams-probe-01".to_string(), + parent_devices: vec![], + metrics_publisher_pk: Pubkey::new_unique(), + reference_count: 0, + target_update_count: 0, + }, + )) + }); + client .expect_resolve_device_pk() .with(predicate::eq(device_pk.to_string())) @@ -73,7 +102,7 @@ mod tests { let mut output = Vec::new(); let res = RemoveParentGeoProbeCliCommand { - code: "ams-probe-01".to_string(), + probe: "ams-probe-01".to_string(), device: device_pk.to_string(), } .execute(&client, &mut output); diff --git a/smartcontract/cli/src/geolocation/probe/update.rs b/smartcontract/cli/src/geolocation/probe/update.rs index 261618023d..f02c3cd086 100644 --- a/smartcontract/cli/src/geolocation/probe/update.rs +++ b/smartcontract/cli/src/geolocation/probe/update.rs @@ -1,48 +1,55 @@ use crate::{ geoclicommand::GeoCliCommand, - validators::{validate_code, validate_pubkey}, + validators::{validate_pubkey, validate_pubkey_or_code}, }; use clap::Args; -use doublezero_sdk::geolocation::geo_probe::update::UpdateGeoProbeCommand; +use doublezero_sdk::geolocation::geo_probe::{ + get::GetGeoProbeCommand, update::UpdateGeoProbeCommand, +}; use solana_sdk::pubkey::Pubkey; use std::{io::Write, net::Ipv4Addr}; #[derive(Args, Debug)] pub struct UpdateGeoProbeCliCommand { - /// Probe code to update - #[arg(long, value_name = "PROBE_CODE", value_parser = validate_code)] - pub code: String, + /// Probe pubkey or code to update + #[arg(long, value_parser = validate_pubkey_or_code)] + pub probe: String, /// Updated public IPv4 address #[arg(long)] pub public_ip: Option, /// Updated UDP listen port for location offsets #[arg(long)] pub port: Option, - /// Updated signing keypair public key + /// Updated signing public key #[arg(long, value_parser = validate_pubkey)] - pub signing_keypair: Option, + pub signing_pubkey: Option, } impl UpdateGeoProbeCliCommand { pub fn execute(self, client: &C, out: &mut W) -> eyre::Result<()> { - if self.public_ip.is_none() && self.port.is_none() && self.signing_keypair.is_none() { + if self.public_ip.is_none() && self.port.is_none() && self.signing_pubkey.is_none() { return Err(eyre::eyre!( - "At least one of --public-ip, --port, or --signing-keypair is required" + "At least one of --public-ip, --port, or --signing-pubkey is required" )); } + let (_, resolved_probe) = client.get_geo_probe(GetGeoProbeCommand { + pubkey_or_code: self.probe, + })?; + let code = resolved_probe.code; + let metrics_publisher_pk = self - .signing_keypair + .signing_pubkey .map(|mp| { mp.parse::() - .map_err(|_| eyre::eyre!("invalid signing keypair pubkey: {mp}")) + .map_err(|_| eyre::eyre!("invalid signing pubkey: {mp}")) }) .transpose()?; let serviceability_globalstate_pk = client.get_serviceability_globalstate_pk(); let sig = client.update_geo_probe(UpdateGeoProbeCommand { - code: self.code, + code, serviceability_globalstate_pk, public_ip: self.public_ip, location_offset_port: self.port, @@ -59,6 +66,7 @@ impl UpdateGeoProbeCliCommand { mod tests { use super::*; use crate::geoclicommand::MockGeoCliCommand; + use doublezero_geolocation::state::{accounttype::AccountType, geo_probe::GeoProbe}; use mockall::predicate; use solana_sdk::{pubkey::Pubkey, signature::Signature}; @@ -74,6 +82,29 @@ mod tests { 100, 221, 20, 137, 4, 5, ]); + client + .expect_get_geo_probe() + .with(predicate::eq(GetGeoProbeCommand { + pubkey_or_code: "ams-probe-01".to_string(), + })) + .returning(move |_| { + Ok(( + Pubkey::new_unique(), + GeoProbe { + account_type: AccountType::GeoProbe, + owner: Pubkey::new_unique(), + exchange_pk: Pubkey::new_unique(), + public_ip: Ipv4Addr::new(10, 0, 0, 1), + location_offset_port: 8923, + code: "ams-probe-01".to_string(), + parent_devices: vec![], + metrics_publisher_pk: Pubkey::new_unique(), + reference_count: 0, + target_update_count: 0, + }, + )) + }); + client .expect_get_serviceability_globalstate_pk() .returning(move || svc_gs_pk); @@ -91,10 +122,10 @@ mod tests { let mut output = Vec::new(); let res = UpdateGeoProbeCliCommand { - code: "ams-probe-01".to_string(), + probe: "ams-probe-01".to_string(), public_ip: Some(Ipv4Addr::new(192, 168, 1, 1)), port: None, - signing_keypair: None, + signing_pubkey: None, } .execute(&client, &mut output); assert!(res.is_ok()); diff --git a/smartcontract/cli/src/geolocation/user/add_target.rs b/smartcontract/cli/src/geolocation/user/add_target.rs index d120ac1ff6..2d75c3b398 100644 --- a/smartcontract/cli/src/geolocation/user/add_target.rs +++ b/smartcontract/cli/src/geolocation/user/add_target.rs @@ -1,12 +1,12 @@ use crate::{ geoclicommand::GeoCliCommand, - validators::{validate_code, validate_pubkey, validate_pubkey_or_code}, + validators::{validate_pubkey, validate_pubkey_or_code}, }; use clap::{Args, ValueEnum}; use doublezero_geolocation::state::geolocation_user::GeoLocationTargetType; use doublezero_sdk::geolocation::{ geo_probe::{get::GetGeoProbeCommand, list::ListGeoProbeCommand}, - geolocation_user::add_target::AddTargetCommand, + geolocation_user::{add_target::AddTargetCommand, get::GetGeolocationUserCommand}, }; use solana_sdk::pubkey::Pubkey; use std::{io::Write, net::Ipv4Addr}; @@ -21,8 +21,8 @@ pub enum TargetType { #[derive(Args, Debug)] #[command(group = clap::ArgGroup::new("probe_source").required(true))] pub struct AddTargetCliCommand { - /// User code - #[arg(long, value_parser = validate_code)] + /// User code or pubkey + #[arg(long, value_parser = validate_pubkey_or_code)] pub user: String, /// Target type #[arg(long = "type", value_enum)] @@ -35,7 +35,7 @@ pub struct AddTargetCliCommand { pub target_port: u16, /// Target signing pubkey (required for inbound) #[arg(long, value_parser = validate_pubkey)] - pub target_pk: Option, + pub target_signing_pubkey: Option, /// Probe code or pubkey #[arg(long, value_parser = validate_pubkey_or_code, group = "probe_source")] pub probe: Option, @@ -59,9 +59,9 @@ impl AddTargetCliCommand { ) } TargetType::Inbound => { - let pk_str = self - .target_pk - .ok_or_else(|| eyre::eyre!("--target-pk is required for inbound targets"))?; + let pk_str = self.target_signing_pubkey.ok_or_else(|| { + eyre::eyre!("--target-signing-pubkey is required for inbound targets") + })?; let pk: Pubkey = pk_str.parse().expect("validated by clap"); (GeoLocationTargetType::Inbound, Ipv4Addr::UNSPECIFIED, 0, pk) } @@ -78,10 +78,14 @@ impl AddTargetCliCommand { } }; + let (_, resolved_user) = client.get_geolocation_user(GetGeolocationUserCommand { + pubkey_or_code: self.user, + })?; + let probe_pk = resolve_probe(client, self.probe, self.exchange)?; let sig = client.add_target(AddTargetCommand { - code: self.user, + code: resolved_user.code, probe_pk, target_type, ip_address, @@ -134,11 +138,39 @@ pub(super) fn resolve_probe( mod tests { use super::*; use crate::geoclicommand::MockGeoCliCommand; - use doublezero_geolocation::state::{accounttype::AccountType, geo_probe::GeoProbe}; + use doublezero_geolocation::state::{ + accounttype::AccountType, + geo_probe::GeoProbe, + geolocation_user::{ + FlatPerEpochConfig, GeolocationBillingConfig, GeolocationPaymentStatus, + GeolocationUser, GeolocationUserStatus, + }, + }; use mockall::predicate; use solana_sdk::{pubkey::Pubkey, signature::Signature}; use std::collections::HashMap; + fn mock_get_geolocation_user(client: &mut MockGeoCliCommand) { + client.expect_get_geolocation_user().returning(move |cmd| { + Ok(( + Pubkey::new_unique(), + GeolocationUser { + account_type: AccountType::GeolocationUser, + owner: Pubkey::new_unique(), + code: cmd.pubkey_or_code.clone(), + token_account: Pubkey::new_unique(), + payment_status: GeolocationPaymentStatus::Paid, + billing: GeolocationBillingConfig::FlatPerEpoch(FlatPerEpochConfig { + rate: 1000, + last_deduction_dz_epoch: 42, + }), + status: GeolocationUserStatus::Activated, + targets: vec![], + }, + )) + }); + } + fn make_probe(exchange_pk: Pubkey) -> GeoProbe { GeoProbe { account_type: AccountType::GeoProbe, @@ -163,6 +195,8 @@ mod tests { let probe = make_probe(exchange_pk); let signature = Signature::new_unique(); + mock_get_geolocation_user(&mut client); + client .expect_get_geo_probe() .with(predicate::eq(GetGeoProbeCommand { @@ -188,7 +222,7 @@ mod tests { target_type: TargetType::Outbound, target_ip: Some(Ipv4Addr::new(8, 8, 8, 8)), target_port: 8923, - target_pk: None, + target_signing_pubkey: None, probe: Some("ams-probe-01".to_string()), exchange: None, } @@ -208,6 +242,8 @@ mod tests { let target_pk = Pubkey::from_str_const("HQ3UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcx"); let signature = Signature::new_unique(); + mock_get_geolocation_user(&mut client); + client .expect_get_geo_probe() .with(predicate::eq(GetGeoProbeCommand { @@ -233,7 +269,7 @@ mod tests { target_type: TargetType::Inbound, target_ip: None, target_port: 8923, - target_pk: Some(target_pk.to_string()), + target_signing_pubkey: Some(target_pk.to_string()), probe: Some(probe_pk.to_string()), exchange: None, } @@ -252,6 +288,8 @@ mod tests { let probe = make_probe(exchange_pk); let signature = Signature::new_unique(); + mock_get_geolocation_user(&mut client); + client .expect_resolve_exchange_pk() .with(predicate::eq(exchange_pk.to_string())) @@ -282,7 +320,7 @@ mod tests { target_type: TargetType::Outbound, target_ip: Some(Ipv4Addr::new(8, 8, 8, 8)), target_port: 8923, - target_pk: None, + target_signing_pubkey: None, probe: None, exchange: Some(exchange_pk.to_string()), } @@ -301,6 +339,8 @@ mod tests { let probe = make_probe(exchange_pk); let signature = Signature::new_unique(); + mock_get_geolocation_user(&mut client); + client .expect_resolve_exchange_pk() .with(predicate::eq("xams".to_string())) @@ -331,7 +371,7 @@ mod tests { target_type: TargetType::Outbound, target_ip: Some(Ipv4Addr::new(8, 8, 8, 8)), target_port: 8923, - target_pk: None, + target_signing_pubkey: None, probe: None, exchange: Some("xams".to_string()), } @@ -350,6 +390,8 @@ mod tests { let probe = make_probe(exchange_pk); let signature = Signature::new_unique(); + mock_get_geolocation_user(&mut client); + client .expect_get_geo_probe() .with(predicate::eq(GetGeoProbeCommand { @@ -375,7 +417,7 @@ mod tests { target_type: TargetType::OutboundIcmp, target_ip: Some(Ipv4Addr::new(8, 8, 8, 8)), target_port: 8923, - target_pk: None, + target_signing_pubkey: None, probe: Some("ams-probe-01".to_string()), exchange: None, } @@ -395,7 +437,7 @@ mod tests { target_type: TargetType::OutboundIcmp, target_ip: None, target_port: 8923, - target_pk: None, + target_signing_pubkey: None, probe: Some("ams-probe-01".to_string()), exchange: None, } @@ -422,7 +464,7 @@ mod tests { target_type: TargetType::Outbound, target_ip: None, target_port: 8923, - target_pk: None, + target_signing_pubkey: None, probe: Some("ams-probe-01".to_string()), exchange: None, } @@ -441,13 +483,16 @@ mod tests { target_type: TargetType::Inbound, target_ip: None, target_port: 8923, - target_pk: None, + target_signing_pubkey: None, probe: Some("ams-probe-01".to_string()), exchange: None, } .execute(&client, &mut output); assert!(res.is_err()); - assert!(res.unwrap_err().to_string().contains("--target-pk")); + assert!(res + .unwrap_err() + .to_string() + .contains("--target-signing-pubkey")); } #[test] @@ -458,6 +503,8 @@ mod tests { let probe_pk2 = Pubkey::from_str_const("HQ3UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcx"); let exchange_pk = Pubkey::from_str_const("GQ2UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcc"); + mock_get_geolocation_user(&mut client); + client .expect_resolve_exchange_pk() .with(predicate::eq(exchange_pk.to_string())) @@ -477,7 +524,7 @@ mod tests { target_type: TargetType::Outbound, target_ip: Some(Ipv4Addr::new(8, 8, 8, 8)), target_port: 8923, - target_pk: None, + target_signing_pubkey: None, probe: None, exchange: Some(exchange_pk.to_string()), } diff --git a/smartcontract/cli/src/geolocation/user/delete.rs b/smartcontract/cli/src/geolocation/user/delete.rs index 9cf55bac28..7e5d995627 100644 --- a/smartcontract/cli/src/geolocation/user/delete.rs +++ b/smartcontract/cli/src/geolocation/user/delete.rs @@ -1,12 +1,14 @@ -use crate::{geoclicommand::GeoCliCommand, validators::validate_code}; +use crate::{geoclicommand::GeoCliCommand, validators::validate_pubkey_or_code}; use clap::Args; -use doublezero_sdk::geolocation::geolocation_user::delete::DeleteGeolocationUserCommand; +use doublezero_sdk::geolocation::geolocation_user::{ + delete::DeleteGeolocationUserCommand, get::GetGeolocationUserCommand, +}; use std::io::Write; #[derive(Args, Debug)] pub struct DeleteGeolocationUserCliCommand { /// User code to delete - #[arg(long, value_parser = validate_code)] + #[arg(long, value_parser = validate_pubkey_or_code)] pub user: String, /// Skip confirmation prompt #[arg(long, default_value_t = false)] @@ -15,8 +17,13 @@ pub struct DeleteGeolocationUserCliCommand { impl DeleteGeolocationUserCliCommand { pub fn execute(self, client: &C, out: &mut W) -> eyre::Result<()> { + let (_, resolved_user) = client.get_geolocation_user(GetGeolocationUserCommand { + pubkey_or_code: self.user.clone(), + })?; + let code = resolved_user.code; + if !self.yes { - eprint!("Delete user '{}'? [y/N]: ", self.user); + eprint!("Delete user '{}'? [y/N]: ", &code); let mut input = String::new(); std::io::stdin().read_line(&mut input)?; if !input.trim().eq_ignore_ascii_case("y") { @@ -28,7 +35,7 @@ impl DeleteGeolocationUserCliCommand { let serviceability_globalstate_pk = client.get_serviceability_globalstate_pk(); let sig = client.delete_geolocation_user(DeleteGeolocationUserCommand { - code: self.user, + code, serviceability_globalstate_pk, })?; @@ -42,6 +49,13 @@ impl DeleteGeolocationUserCliCommand { mod tests { use super::*; use crate::geoclicommand::MockGeoCliCommand; + use doublezero_geolocation::state::{ + accounttype::AccountType, + geolocation_user::{ + FlatPerEpochConfig, GeolocationBillingConfig, GeolocationPaymentStatus, + GeolocationUser, GeolocationUserStatus, + }, + }; use mockall::predicate; use solana_sdk::{pubkey::Pubkey, signature::Signature}; @@ -57,6 +71,30 @@ mod tests { 100, 221, 20, 137, 4, 5, ]); + client + .expect_get_geolocation_user() + .with(predicate::eq(GetGeolocationUserCommand { + pubkey_or_code: "geo-user-01".to_string(), + })) + .returning(move |_| { + Ok(( + Pubkey::new_unique(), + GeolocationUser { + account_type: AccountType::GeolocationUser, + owner: Pubkey::new_unique(), + code: "geo-user-01".to_string(), + token_account: Pubkey::new_unique(), + payment_status: GeolocationPaymentStatus::Paid, + billing: GeolocationBillingConfig::FlatPerEpoch(FlatPerEpochConfig { + rate: 1000, + last_deduction_dz_epoch: 42, + }), + status: GeolocationUserStatus::Activated, + targets: vec![], + }, + )) + }); + client .expect_get_serviceability_globalstate_pk() .returning(move || svc_gs_pk); diff --git a/smartcontract/cli/src/geolocation/user/get.rs b/smartcontract/cli/src/geolocation/user/get.rs index a1e891e32f..11ea7ed215 100644 --- a/smartcontract/cli/src/geolocation/user/get.rs +++ b/smartcontract/cli/src/geolocation/user/get.rs @@ -15,9 +15,12 @@ pub struct GetGeolocationUserCliCommand { /// User pubkey or code to retrieve #[arg(long, value_parser = validate_pubkey_or_code)] pub user: String, - /// Output as JSON - #[arg(long)] + /// Output as pretty JSON + #[arg(long, default_value_t = false, conflicts_with = "json_compact")] pub json: bool, + /// Output as compact JSON + #[arg(long, default_value_t = false, conflicts_with = "json")] + pub json_compact: bool, } #[derive(Serialize)] @@ -44,7 +47,7 @@ struct TargetDisplay { pub ip: String, pub port: u16, #[serde(serialize_with = "serializer::serialize_pubkey_as_string")] - pub target_pk: Pubkey, + pub target_signing_pubkey: Pubkey, #[serde(serialize_with = "serializer::serialize_pubkey_as_string")] #[tabled(rename = "probe")] pub geoprobe_pk: Pubkey, @@ -70,7 +73,7 @@ impl GetGeolocationUserCliCommand { target_type: t.target_type.to_string(), ip, port, - target_pk: t.target_pk, + target_signing_pubkey: t.target_pk, geoprobe_pk: t.geoprobe_pk, } }) @@ -97,8 +100,12 @@ impl GetGeolocationUserCliCommand { targets, }; - if self.json { - let json = serde_json::to_string_pretty(&display)?; + if self.json || self.json_compact { + let json = if self.json_compact { + serde_json::to_string(&display)? + } else { + serde_json::to_string_pretty(&display)? + }; writeln!(out, "{json}")?; } else { let rows: Vec<(&str, String)> = vec![ @@ -189,6 +196,7 @@ mod tests { let res = GetGeolocationUserCliCommand { user: user_pk.to_string(), json: false, + json_compact: false, } .execute(&client, &mut output); assert!(res.is_ok()); @@ -225,6 +233,7 @@ mod tests { let res = GetGeolocationUserCliCommand { user: user_pk.to_string(), json: true, + json_compact: false, } .execute(&client, &mut output); assert!(res.is_ok()); @@ -256,6 +265,7 @@ mod tests { let res = GetGeolocationUserCliCommand { user: user_pk.to_string(), json: false, + json_compact: false, } .execute(&client, &mut output); assert!(res.is_ok()); diff --git a/smartcontract/cli/src/geolocation/user/remove_target.rs b/smartcontract/cli/src/geolocation/user/remove_target.rs index 2bf1d992da..6dc2237528 100644 --- a/smartcontract/cli/src/geolocation/user/remove_target.rs +++ b/smartcontract/cli/src/geolocation/user/remove_target.rs @@ -1,10 +1,12 @@ use crate::{ geoclicommand::GeoCliCommand, - validators::{validate_code, validate_pubkey, validate_pubkey_or_code}, + validators::{validate_pubkey, validate_pubkey_or_code}, }; use clap::Args; use doublezero_geolocation::state::geolocation_user::GeoLocationTargetType; -use doublezero_sdk::geolocation::geolocation_user::remove_target::RemoveTargetCommand; +use doublezero_sdk::geolocation::geolocation_user::{ + get::GetGeolocationUserCommand, remove_target::RemoveTargetCommand, +}; use solana_sdk::pubkey::Pubkey; use std::{io::Write, net::Ipv4Addr}; @@ -13,8 +15,8 @@ use super::add_target::TargetType; #[derive(Args, Debug)] #[command(group = clap::ArgGroup::new("probe_source").required(true))] pub struct RemoveTargetCliCommand { - /// User code - #[arg(long, value_parser = validate_code)] + /// User code or pubkey + #[arg(long, value_parser = validate_pubkey_or_code)] pub user: String, /// Target type #[arg(long = "type", value_enum)] @@ -24,7 +26,7 @@ pub struct RemoveTargetCliCommand { pub target_ip: Option, /// Target signing pubkey (required for inbound) #[arg(long, value_parser = validate_pubkey)] - pub target_pk: Option, + pub target_signing_pubkey: Option, /// Probe code or pubkey #[arg(long, value_parser = validate_pubkey_or_code, group = "probe_source")] pub probe: Option, @@ -43,9 +45,9 @@ impl RemoveTargetCliCommand { (GeoLocationTargetType::Outbound, ip, Pubkey::default()) } TargetType::Inbound => { - let pk_str = self - .target_pk - .ok_or_else(|| eyre::eyre!("--target-pk is required for inbound targets"))?; + let pk_str = self.target_signing_pubkey.ok_or_else(|| { + eyre::eyre!("--target-signing-pubkey is required for inbound targets") + })?; let pk: Pubkey = pk_str.parse().expect("validated by clap"); (GeoLocationTargetType::Inbound, Ipv4Addr::UNSPECIFIED, pk) } @@ -57,11 +59,15 @@ impl RemoveTargetCliCommand { } }; + let (_, resolved_user) = client.get_geolocation_user(GetGeolocationUserCommand { + pubkey_or_code: self.user, + })?; + let probe_pk = super::add_target::resolve_probe(client, self.probe, self.exchange)?; let serviceability_globalstate_pk = client.get_serviceability_globalstate_pk(); let sig = client.remove_target(RemoveTargetCommand { - code: self.user, + code: resolved_user.code, probe_pk, target_type, ip_address, @@ -79,11 +85,39 @@ impl RemoveTargetCliCommand { mod tests { use super::*; use crate::geoclicommand::MockGeoCliCommand; - use doublezero_geolocation::state::{accounttype::AccountType, geo_probe::GeoProbe}; + use doublezero_geolocation::state::{ + accounttype::AccountType, + geo_probe::GeoProbe, + geolocation_user::{ + FlatPerEpochConfig, GeolocationBillingConfig, GeolocationPaymentStatus, + GeolocationUser, GeolocationUserStatus, + }, + }; use doublezero_sdk::geolocation::geo_probe::get::GetGeoProbeCommand; use mockall::predicate; use solana_sdk::{pubkey::Pubkey, signature::Signature}; + fn mock_get_geolocation_user(client: &mut MockGeoCliCommand) { + client.expect_get_geolocation_user().returning(move |cmd| { + Ok(( + Pubkey::new_unique(), + GeolocationUser { + account_type: AccountType::GeolocationUser, + owner: Pubkey::new_unique(), + code: cmd.pubkey_or_code.clone(), + token_account: Pubkey::new_unique(), + payment_status: GeolocationPaymentStatus::Paid, + billing: GeolocationBillingConfig::FlatPerEpoch(FlatPerEpochConfig { + rate: 1000, + last_deduction_dz_epoch: 42, + }), + status: GeolocationUserStatus::Activated, + targets: vec![], + }, + )) + }); + } + fn make_probe(exchange_pk: Pubkey) -> GeoProbe { GeoProbe { account_type: AccountType::GeoProbe, @@ -109,6 +143,8 @@ mod tests { let probe = make_probe(exchange_pk); let signature = Signature::new_unique(); + mock_get_geolocation_user(&mut client); + client .expect_get_geo_probe() .with(predicate::eq(GetGeoProbeCommand { @@ -137,7 +173,7 @@ mod tests { user: "geo-user-01".to_string(), target_type: TargetType::Outbound, target_ip: Some(Ipv4Addr::new(8, 8, 8, 8)), - target_pk: None, + target_signing_pubkey: None, probe: Some("ams-probe-01".to_string()), exchange: None, } @@ -157,6 +193,8 @@ mod tests { let probe = make_probe(exchange_pk); let signature = Signature::new_unique(); + mock_get_geolocation_user(&mut client); + client .expect_get_geo_probe() .with(predicate::eq(GetGeoProbeCommand { @@ -185,7 +223,7 @@ mod tests { user: "geo-user-01".to_string(), target_type: TargetType::OutboundIcmp, target_ip: Some(Ipv4Addr::new(8, 8, 8, 8)), - target_pk: None, + target_signing_pubkey: None, probe: Some("ams-probe-01".to_string()), exchange: None, } @@ -206,6 +244,8 @@ mod tests { let target_pk = Pubkey::from_str_const("HQ3UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcx"); let signature = Signature::new_unique(); + mock_get_geolocation_user(&mut client); + client .expect_get_geo_probe() .with(predicate::eq(GetGeoProbeCommand { @@ -234,7 +274,7 @@ mod tests { user: "geo-user-01".to_string(), target_type: TargetType::Inbound, target_ip: None, - target_pk: Some(target_pk.to_string()), + target_signing_pubkey: Some(target_pk.to_string()), probe: Some(probe_pk.to_string()), exchange: None, } diff --git a/smartcontract/cli/src/geolocation/user/update_payment_status.rs b/smartcontract/cli/src/geolocation/user/update_payment_status.rs index 2cf850c51f..c3168e98ac 100644 --- a/smartcontract/cli/src/geolocation/user/update_payment_status.rs +++ b/smartcontract/cli/src/geolocation/user/update_payment_status.rs @@ -1,7 +1,9 @@ -use crate::{geoclicommand::GeoCliCommand, validators::validate_code}; +use crate::{geoclicommand::GeoCliCommand, validators::validate_pubkey_or_code}; use clap::{Args, ValueEnum}; use doublezero_geolocation::state::geolocation_user::GeolocationPaymentStatus; -use doublezero_sdk::geolocation::geolocation_user::update_payment_status::UpdatePaymentStatusCommand; +use doublezero_sdk::geolocation::geolocation_user::{ + get::GetGeolocationUserCommand, update_payment_status::UpdatePaymentStatusCommand, +}; use std::io::Write; #[derive(ValueEnum, Debug, Clone)] @@ -12,8 +14,8 @@ pub enum PaymentStatus { #[derive(Args, Debug)] pub struct UpdatePaymentStatusCliCommand { - /// User code - #[arg(long, value_parser = validate_code)] + /// User code or pubkey + #[arg(long, value_parser = validate_pubkey_or_code)] pub user: String, /// New payment status #[arg(long, value_enum)] @@ -30,10 +32,14 @@ impl UpdatePaymentStatusCliCommand { PaymentStatus::Delinquent => GeolocationPaymentStatus::Delinquent, }; + let (_, resolved_user) = client.get_geolocation_user(GetGeolocationUserCommand { + pubkey_or_code: self.user, + })?; + let serviceability_globalstate_pk = client.get_serviceability_globalstate_pk(); let sig = client.update_payment_status(UpdatePaymentStatusCommand { - code: self.user, + code: resolved_user.code, serviceability_globalstate_pk, payment_status, last_deduction_dz_epoch: self.last_deduction_epoch, @@ -49,9 +55,36 @@ impl UpdatePaymentStatusCliCommand { mod tests { use super::*; use crate::geoclicommand::MockGeoCliCommand; + use doublezero_geolocation::state::{ + accounttype::AccountType, + geolocation_user::{ + FlatPerEpochConfig, GeolocationBillingConfig, GeolocationUser, GeolocationUserStatus, + }, + }; use mockall::predicate; use solana_sdk::{pubkey::Pubkey, signature::Signature}; + fn mock_get_geolocation_user(client: &mut MockGeoCliCommand) { + client.expect_get_geolocation_user().returning(move |cmd| { + Ok(( + Pubkey::new_unique(), + GeolocationUser { + account_type: AccountType::GeolocationUser, + owner: Pubkey::new_unique(), + code: cmd.pubkey_or_code.clone(), + token_account: Pubkey::new_unique(), + payment_status: GeolocationPaymentStatus::Paid, + billing: GeolocationBillingConfig::FlatPerEpoch(FlatPerEpochConfig { + rate: 1000, + last_deduction_dz_epoch: 42, + }), + status: GeolocationUserStatus::Activated, + targets: vec![], + }, + )) + }); + } + #[test] fn test_cli_update_payment_status_paid() { let mut client = MockGeoCliCommand::new(); @@ -59,6 +92,8 @@ mod tests { let svc_gs_pk = Pubkey::from_str_const("HQ2UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcx"); let signature = Signature::new_unique(); + mock_get_geolocation_user(&mut client); + client .expect_get_serviceability_globalstate_pk() .returning(move || svc_gs_pk); @@ -92,6 +127,8 @@ mod tests { let svc_gs_pk = Pubkey::from_str_const("HQ2UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcx"); let signature = Signature::new_unique(); + mock_get_geolocation_user(&mut client); + client .expect_get_serviceability_globalstate_pk() .returning(move || svc_gs_pk); From 896d0bba70bc6c6c85ff681996d685dd8ab9b089 Mon Sep 17 00:00:00 2001 From: Ben Blier Date: Thu, 9 Apr 2026 10:19:17 -0400 Subject: [PATCH 2/2] Add json_compact tests. fix move instead of copy of user in delete. update comment to match pubkey-code optoins. --- .../cli/src/geolocation/probe/get.rs | 30 +++++++++++++ .../cli/src/geolocation/user/delete.rs | 4 +- smartcontract/cli/src/geolocation/user/get.rs | 43 +++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/smartcontract/cli/src/geolocation/probe/get.rs b/smartcontract/cli/src/geolocation/probe/get.rs index 37398f1713..7f0026375b 100644 --- a/smartcontract/cli/src/geolocation/probe/get.rs +++ b/smartcontract/cli/src/geolocation/probe/get.rs @@ -223,4 +223,34 @@ mod tests { ); assert_eq!(json["reference_count"].as_u64().unwrap(), 0); } + + #[test] + fn test_cli_geo_probe_get_json_compact() { + let (mut client, probe_pk, owner_pk, exchange_pk, metrics_pk) = setup_client(); + let parent_pk = Pubkey::from_str_const("AQ2UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcc"); + let probe = make_probe(owner_pk, exchange_pk, metrics_pk, vec![parent_pk]); + + client + .expect_get_geo_probe() + .with(predicate::eq(GetGeoProbeCommand { + pubkey_or_code: probe_pk.to_string(), + })) + .returning(move |_| Ok((probe_pk, probe.clone()))); + + let mut output = Vec::new(); + let res = GetGeoProbeCliCommand { + probe: probe_pk.to_string(), + json: false, + json_compact: true, + } + .execute(&client, &mut output); + assert!(res.is_ok()); + let output_str = String::from_utf8(output).unwrap(); + let trimmed = output_str.trim(); + assert!(!trimmed.contains('\n'), "compact JSON must be single-line"); + let json: serde_json::Value = serde_json::from_str(trimmed).unwrap(); + assert_eq!(json["account"].as_str().unwrap(), probe_pk.to_string()); + assert_eq!(json["code"].as_str().unwrap(), "ams-probe-01"); + assert_eq!(json["parent_devices"].as_array().unwrap().len(), 1); + } } diff --git a/smartcontract/cli/src/geolocation/user/delete.rs b/smartcontract/cli/src/geolocation/user/delete.rs index 7e5d995627..f4756b2ffc 100644 --- a/smartcontract/cli/src/geolocation/user/delete.rs +++ b/smartcontract/cli/src/geolocation/user/delete.rs @@ -7,7 +7,7 @@ use std::io::Write; #[derive(Args, Debug)] pub struct DeleteGeolocationUserCliCommand { - /// User code to delete + /// User pubkey or code to delete #[arg(long, value_parser = validate_pubkey_or_code)] pub user: String, /// Skip confirmation prompt @@ -18,7 +18,7 @@ pub struct DeleteGeolocationUserCliCommand { impl DeleteGeolocationUserCliCommand { pub fn execute(self, client: &C, out: &mut W) -> eyre::Result<()> { let (_, resolved_user) = client.get_geolocation_user(GetGeolocationUserCommand { - pubkey_or_code: self.user.clone(), + pubkey_or_code: self.user, })?; let code = resolved_user.code; diff --git a/smartcontract/cli/src/geolocation/user/get.rs b/smartcontract/cli/src/geolocation/user/get.rs index 11ea7ed215..016d0f1a14 100644 --- a/smartcontract/cli/src/geolocation/user/get.rs +++ b/smartcontract/cli/src/geolocation/user/get.rs @@ -272,4 +272,47 @@ mod tests { let output_str = String::from_utf8(output).unwrap(); assert!(!output_str.contains("Targets:")); } + + #[test] + fn test_cli_geolocation_user_get_json_compact() { + let mut client = MockGeoCliCommand::new(); + let user_pk = Pubkey::from_str_const("BmrLoL9jzYo4yiPUsFhYFU8hgE3CD3Npt8tgbqvneMyB"); + let probe_pk = Pubkey::from_str_const("HQ3UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcx"); + + let user = make_user( + "geo-user-01", + vec![GeolocationTarget { + target_type: GeoLocationTargetType::Outbound, + ip_address: Ipv4Addr::new(8, 8, 8, 8), + location_offset_port: 8923, + target_pk: Pubkey::default(), + geoprobe_pk: probe_pk, + }], + ); + + client + .expect_get_geolocation_user() + .with(predicate::eq(GetGeolocationUserCommand { + pubkey_or_code: user_pk.to_string(), + })) + .returning(move |_| Ok((user_pk, user.clone()))); + + let mut output = Vec::new(); + let res = GetGeolocationUserCliCommand { + user: user_pk.to_string(), + json: false, + json_compact: true, + } + .execute(&client, &mut output); + assert!(res.is_ok()); + let output_str = String::from_utf8(output).unwrap(); + let trimmed = output_str.trim(); + assert!(!trimmed.contains('\n'), "compact JSON must be single-line"); + let json: serde_json::Value = serde_json::from_str(trimmed).unwrap(); + assert_eq!(json["account"].as_str().unwrap(), user_pk.to_string()); + assert_eq!(json["code"].as_str().unwrap(), "geo-user-01"); + assert_eq!(json["target_count"].as_u64().unwrap(), 1); + assert_eq!(json["targets"].as_array().unwrap().len(), 1); + assert_eq!(json["targets"][0]["ip"].as_str().unwrap(), "8.8.8.8"); + } }