Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file.

- CLI
- `doublezero resource verify` command will now suggest creating resources or create them with --fix
- Improve `tenant get` output: display config and administrators as formatted tables; add `--json` flag for machine-readable output
- SDK
- Fix multicast group deserialization in `smartcontract/sdk/go` to correctly read publisher and subscriber counts and align status enum with onchain definition
- Smartcontract
Expand Down
24 changes: 14 additions & 10 deletions e2e/multi_tenant_vrf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
package e2e_test

import (
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"testing"
Expand Down Expand Up @@ -597,15 +597,19 @@ func TestE2E_TenantDeletionLifecycle(t *testing.T) {
// `doublezero tenant get --code <code>`.
func getTenantVrfID(t *testing.T, dn *devnet.Devnet, tenantCode string) uint16 {
t.Helper()
output, err := dn.Manager.Exec(t.Context(), []string{"doublezero", "tenant", "get", "--code", tenantCode})
output, err := dn.Manager.Exec(t.Context(), []string{"doublezero", "tenant", "get", "--code", tenantCode, "--json"})
require.NoError(t, err)
for _, line := range strings.Split(string(output), "\n") {
if strings.HasPrefix(line, "vrf_id: ") {
val, err := strconv.ParseUint(strings.TrimPrefix(line, "vrf_id: "), 10, 16)
require.NoError(t, err)
return uint16(val)
}
type config struct {
VrfID uint16 `json:"vrf_id"`
}
type response struct {
Config config `json:"config"`
}
var resp response
err = json.Unmarshal(output, &resp)
require.NoError(t, err, "failed to parse JSON output: %s", string(output))
if resp.Config.VrfID == 0 {
t.Fatalf("vrf_id not found in tenant get output for %s: %s", tenantCode, string(output))
}
t.Fatalf("vrf_id not found in tenant get output for %s: %s", tenantCode, string(output))
return 0
return resp.Config.VrfID
}
154 changes: 140 additions & 14 deletions smartcontract/cli/src/tenant/get.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,79 @@
use crate::{doublezerocommand::CliCommand, validators::validate_pubkey_or_code};
use clap::Args;
use doublezero_sdk::commands::tenant::get::GetTenantCommand;
use serde::Serialize;
use std::io::Write;
use tabled::Tabled;

#[derive(Args, Debug)]
pub struct GetTenantCliCommand {
/// Tenant pubkey or code
#[arg(long, value_parser = validate_pubkey_or_code)]
pub code: String,
/// Output as JSON
#[arg(long)]
pub json: bool,
}

#[derive(Tabled, Serialize)]
struct ConfigDisplay {
pub account: String,
pub code: String,
pub vrf_id: u16,
pub metro_routing: bool,
pub route_liveness: bool,
pub reference_count: u32,
pub owner: String,
}

#[derive(Tabled, Serialize)]
struct AdminDisplay {
pub administrator: String,
}

impl GetTenantCliCommand {
pub fn execute<C: CliCommand, W: Write>(self, client: &C, out: &mut W) -> eyre::Result<()> {
let (pubkey, tenant) = client.get_tenant(GetTenantCommand {
pubkey_or_code: self.code,
pubkey_or_code: self.code.clone(),
})?;

writeln!(out, "account: {pubkey}")?;
writeln!(out, "code: {}", tenant.code)?;
writeln!(out, "vrf_id: {}", tenant.vrf_id)?;
writeln!(out, "metro_routing: {}", tenant.metro_routing)?;
writeln!(out, "route_liveness: {}", tenant.route_liveness)?;
writeln!(out, "reference_count: {}", tenant.reference_count)?;
writeln!(out, "owner: {}", tenant.owner)?;
let config = ConfigDisplay {
account: pubkey.to_string(),
code: tenant.code.clone(),
vrf_id: tenant.vrf_id,
metro_routing: tenant.metro_routing,
route_liveness: tenant.route_liveness,
reference_count: tenant.reference_count,
owner: tenant.owner.to_string(),
};
let admin_rows: Vec<AdminDisplay> = tenant
.administrators
.iter()
.map(|a| AdminDisplay {
administrator: a.to_string(),
})
.collect();

if self.json {
#[derive(Serialize)]
struct Output {
config: ConfigDisplay,
administrators: Vec<AdminDisplay>,
}
let output = Output {
config,
administrators: admin_rows,
};
let json = serde_json::to_string_pretty(&output)?;
writeln!(out, "{}", json)?;
} else {
let table = tabled::Table::new([config]);
writeln!(out, "{}", table)?;
if !admin_rows.is_empty() {
let admin_table = tabled::Table::new(admin_rows);
writeln!(out, "\n{}", admin_table)?;
}
}
Ok(())
}
}
Expand Down Expand Up @@ -58,6 +108,13 @@ mod tests {
billing: TenantBillingConfig::default(),
};

let tenant_with_admins = Tenant {
administrators: vec![Pubkey::from_str_const(
"FposHWrkvPP3VErBAWCd4ELWGuh2mgx2Wx6cuNEA4X2S",
)],
..tenant.clone()
};

let tenant_cloned = tenant.clone();
client
.expect_get_tenant()
Expand All @@ -72,38 +129,107 @@ mod tests {
pubkey_or_code: "test-tenant".to_string(),
}))
.returning(move |_| Ok((tenant_pubkey, tenant_cloned2.clone())));
let tenant_with_admins_cloned = tenant_with_admins.clone();
client
.expect_get_tenant()
.with(predicate::eq(GetTenantCommand {
pubkey_or_code: "test-tenant-admin".to_string(),
}))
.returning(move |_| Ok((tenant_pubkey, tenant_with_admins_cloned.clone())));
client
.expect_get_tenant()
.returning(move |_| Err(eyre::eyre!("not found")));

/*****************************************************************************************************/
// Expected failure
let mut output = Vec::new();
let res = GetTenantCliCommand {
code: Pubkey::new_unique().to_string(),
json: false,
}
.execute(&client, &mut output);
assert!(res.is_err(), "I shouldn't find anything.");

// Expected success by pubkey
// Expected success by pubkey (no admins, table output)
let mut output = Vec::new();
let res = GetTenantCliCommand {
code: tenant_pubkey.to_string(),
json: false,
}
.execute(&client, &mut output);
assert!(res.is_ok(), "I should find a item by pubkey");
let output_str = String::from_utf8(output).unwrap();
assert_eq!(
output_str,
"account: BmrLoL9jzYo4yiPUsFhYFU8hgE3CD3Npt8tgbqvneMyB\ncode: test-tenant\nvrf_id: 100\nmetro_routing: true\nroute_liveness: false\nreference_count: 0\nowner: BmrLoL9jzYo4yiPUsFhYFU8hgE3CD3Npt8tgbqvneMyB\n"
assert!(
output_str.contains("account"),
"Should contain table header"
);
assert!(
output_str.contains("test-tenant"),
"Should contain tenant code"
);
assert!(
!output_str.contains("administrator"),
"Should not contain admin table"
);

// Expected success by code
// Expected success by code (no admins, JSON output)
let mut output = Vec::new();
let res = GetTenantCliCommand {
code: "test-tenant".to_string(),
json: true,
}
.execute(&client, &mut output);
assert!(res.is_ok(), "I should find a item by code");
let output_str = String::from_utf8(output).unwrap();
assert!(
output_str.contains("\"config\""),
"Should contain config key in JSON"
);
assert!(
output_str.contains("\"administrators\""),
"Should contain administrators key in JSON"
);
assert!(
output_str.contains("test-tenant"),
"Should contain tenant code in JSON"
);

// Expected success with admins (table output)
let mut output = Vec::new();
let res = GetTenantCliCommand {
code: "test-tenant-admin".to_string(),
json: false,
}
.execute(&client, &mut output);
assert!(res.is_ok(), "I should find a item by code with admins");
let output_str = String::from_utf8(output).unwrap();
assert!(
output_str.contains("administrator"),
"Should contain admin table header"
);
assert!(
output_str.contains("FposHWrkvPP3VErBAWCd4ELWGuh2mgx2Wx6cuNEA4X2S"),
"Should contain admin pubkey"
);

// Expected success with admins (JSON output)
let mut output = Vec::new();
let res = GetTenantCliCommand {
code: "test-tenant-admin".to_string(),
json: true,
}
.execute(&client, &mut output);
assert!(
res.is_ok(),
"I should find a item by code with admins (json)"
);
let output_str = String::from_utf8(output).unwrap();
assert!(
output_str.contains("\"administrator\""),
"Should contain admin key in JSON"
);
assert!(
output_str.contains("FposHWrkvPP3VErBAWCd4ELWGuh2mgx2Wx6cuNEA4X2S"),
"Should contain admin pubkey in JSON"
);
}
}
Loading