use crate::{
client::build_client,
config::turnkey::Config,
errors::MissingResource,
local_operator_key::LocalOperatorSeedSource,
operator::{OperatorCtx, ResolvedOperator, resolve_operator},
outcome::Outcome,
output::StdCtx,
pair::HexSeed,
prompts::{self, bail_required_in_non_interactive, stdin_can_prompt},
shell_print, shell_println,
util::{read_file_to_string, write_file},
};
use anyhow::{Context, anyhow, bail};
use clap::{ArgGroup, Args as ClapArgs};
use qos_core::protocol::services::boot::{
Approval, ManifestSet, Namespace, NitroConfig, QuorumMember, ShareSet, VersionedManifest,
};
use serde::{Serialize, Serializer};
use std::collections::HashSet;
use std::fmt::{self, Display, Formatter, Write};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::debug;
use turnkey_client::generated::external::data::v1::TvcDeployment;
use turnkey_client::generated::{
CreateTvcManifestApprovalsIntent, GetTvcDeploymentRequest, TvcManifestApproval,
};
use uuid::Uuid;
const QUORUM_REACHED_MESSAGE: &str =
"Manifest approval quorum reached. Your deployment will be available soon.";
#[derive(Debug, ClapArgs)]
#[command(about, long_about = None)]
#[command(group(ArgGroup::new("manifest-source").args(["manifest", "deploy_id"])))]
pub struct Args {
#[arg(
short,
long,
value_name = "PATH",
env = "TVC_MANIFEST",
help_heading = "Manifest source (pick one)"
)]
pub manifest: Option<PathBuf>,
#[arg(
short,
long,
env = "TVC_DEPLOY_ID",
help_heading = "Manifest source (pick one)"
)]
pub deploy_id: Option<Uuid>,
#[arg(long, env = "TVC_MANIFEST_ID")]
pub manifest_id: Option<String>,
#[arg(long, env = "TVC_OPERATOR_ID")]
pub operator_id: Option<Uuid>,
#[arg(
long,
value_name = "HEX_SEED",
env = "TVC_OPERATOR_SEED",
help_heading = "Operator seed (pick one)"
)]
pub operator_seed: Option<HexSeed>,
#[arg(
long,
value_name = "PATH",
env = "TVC_OPERATOR_SEED_PATH",
help_heading = "Operator seed (pick one)"
)]
pub operator_seed_path: Option<PathBuf>,
#[arg(long, env = "TVC_DRY_RUN")]
pub dry_run: bool,
#[arg(long, env = "TVC_DANGEROUS_SKIP_INTERACTIVE")]
pub dangerous_skip_interactive: bool,
#[arg(short = 'o', long, value_name = "PATH", env = "TVC_APPROVAL_OUT")]
pub approval_out: Option<PathBuf>,
#[arg(long, env = "TVC_SKIP_POST")]
pub skip_post: bool,
}
struct PostApprovalPlan<'a> {
manifest_id: &'a str,
operator_id: &'a Uuid,
deploy_id: Option<&'a str>,
}
struct PostedApproval {
approval_ids: Vec<String>,
quorum_reached: Option<bool>,
}
pub enum ApproveOutcome {
Posted(ApprovalPosted),
NotPosted(ApprovalGenerated),
DryRun(ApprovalDryRun),
}
impl Serialize for ApproveOutcome {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
ApproveOutcome::Posted(msg) => msg.serialize(serializer),
ApproveOutcome::NotPosted(msg) => msg.serialize(serializer),
ApproveOutcome::DryRun(msg) => msg.serialize(serializer),
}
}
}
impl Display for ApproveOutcome {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
ApproveOutcome::Posted(msg) => msg.fmt(f),
ApproveOutcome::NotPosted(msg) => msg.fmt(f),
ApproveOutcome::DryRun(msg) => msg.fmt(f),
}
}
}
fn approval_payload_human_message(
approval: &Option<Approval>,
written_to: &Option<String>,
) -> String {
match (approval, written_to) {
(Some(approval), _) => serde_json::to_string_pretty(approval)
.expect("serializing manifest approval should not fail"),
(None, Some(path)) => format!("Approval written to: {path}"),
(None, None) => String::new(),
}
}
#[derive(Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ApprovalPosted {
#[serde(skip_serializing_if = "Option::is_none")]
approval: Option<Approval>,
#[serde(skip_serializing_if = "Option::is_none")]
written_to: Option<String>,
manifest_id: String,
operator_id: String,
approval_ids: Vec<String>,
quorum_reached: Option<bool>,
}
impl Display for ApprovalPosted {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(&approval_payload_human_message(
&self.approval,
&self.written_to,
))?;
write!(
f,
r#"
Approval posted successfully!
Approval IDs: {:?}
Manifest ID: {}
Operator ID: {}"#,
self.approval_ids, self.manifest_id, self.operator_id
)?;
if let Some(reached) = self.quorum_reached {
let quorum_line = if reached {
QUORUM_REACHED_MESSAGE
} else {
"Your approval has been posted. Deployment requires additional manifest approvals before it can be deployed on TVC."
};
write!(f, "\n\n{quorum_line}")?;
}
Ok(())
}
}
#[derive(Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ApprovalGenerated {
#[serde(skip_serializing_if = "Option::is_none")]
approval: Option<Approval>,
#[serde(skip_serializing_if = "Option::is_none")]
written_to: Option<String>,
}
impl Display for ApprovalGenerated {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(&approval_payload_human_message(
&self.approval,
&self.written_to,
))
}
}
#[derive(Default, Serialize)]
pub struct ApprovalDryRun {}
impl Display for ApprovalDryRun {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("Dry run complete. No approval generated.")
}
}
struct PostTarget {
manifest_id: String,
deploy_id: Option<String>,
}
struct ResolvedApproveInputs {
manifest: VersionedManifest,
operator_seed_source: Option<LocalOperatorSeedSource>,
operator_id: Option<Uuid>,
approval_out: Option<PathBuf>,
dry_run: bool,
skip_post: bool,
post_target: Option<PostTarget>,
}
pub async fn run(ctx: &mut StdCtx, mut args: Args) -> anyhow::Result<Outcome> {
let operator_seed_source = LocalOperatorSeedSource::from_args(
args.operator_seed.take(),
args.operator_seed_path.take(),
)?;
let inputs = if ctx.is_non_interactive() {
build_inputs_non_interactive(ctx, args, operator_seed_source).await?
} else {
build_inputs_interactive(ctx, args, operator_seed_source).await?
};
let outcome = run_with_resolved_inputs(ctx, inputs).await?;
Ok(Outcome::DeployApprove(outcome))
}
async fn build_inputs_interactive(
ctx: &mut StdCtx,
args: Args,
operator_seed_source: Option<LocalOperatorSeedSource>,
) -> anyhow::Result<ResolvedApproveInputs> {
let do_prompt_user = !args.dangerous_skip_interactive;
if do_prompt_user && !stdin_can_prompt() {
bail_required_in_non_interactive("--dangerous-skip-interactive")?;
}
let (manifest, fetched_manifest_id) = load_manifest(ctx, &args).await?;
if do_prompt_user {
interactive_approve(ctx, &manifest)?;
}
let mut operator_id = args.operator_id;
let post_target = if !args.dry_run && !args.skip_post {
let manifest_id = resolve_manifest_id(&args, fetched_manifest_id.as_deref())?;
if operator_id.is_none() {
operator_id = Some({
let saved_ids = load_saved_operator_ids().await?;
match &*saved_ids {
[] => bail!(
"--operator-id is required to post approval to API. \
No saved operator IDs found. \
Use --skip-post to only generate the approval locally."
),
[id] => *id,
_ if stdin_can_prompt() => {
prompts::select("Select approving operator", saved_ids)?
}
_ => bail!(
"--operator-id is required to post approval to API when multiple saved operator IDs are available"
),
}
});
}
Some(PostTarget {
manifest_id,
deploy_id: args.deploy_id.map(|id| id.to_string()),
})
} else {
None
};
Ok(ResolvedApproveInputs {
manifest,
operator_seed_source,
operator_id,
approval_out: args.approval_out,
dry_run: args.dry_run,
skip_post: args.skip_post,
post_target,
})
}
async fn build_inputs_non_interactive(
ctx: &mut StdCtx,
args: Args,
operator_seed_source: Option<LocalOperatorSeedSource>,
) -> anyhow::Result<ResolvedApproveInputs> {
if !args.dangerous_skip_interactive {
bail_required_in_non_interactive("--dangerous-skip-interactive")?;
}
let (manifest, fetched_manifest_id) = load_manifest(ctx, &args).await?;
let mut operator_id = args.operator_id;
let post_target = if !args.dry_run && !args.skip_post {
let manifest_id = resolve_manifest_id(&args, fetched_manifest_id.as_deref())?;
if operator_id.is_none() {
operator_id = Some({
let saved_ids = load_saved_operator_ids().await?;
match &*saved_ids {
[] => bail!(
"--operator-id is required to post approval to API. \
No saved operator IDs found. \
Use --skip-post to only generate the approval locally."
),
[id] => *id,
_ => bail!(
"--operator-id is required to post approval to API when multiple saved operator IDs are available"
),
}
});
}
Some(PostTarget {
manifest_id,
deploy_id: args.deploy_id.map(|id| id.to_string()),
})
} else {
None
};
Ok(ResolvedApproveInputs {
manifest,
operator_seed_source,
operator_id,
approval_out: args.approval_out,
dry_run: args.dry_run,
skip_post: args.skip_post,
post_target,
})
}
async fn run_with_resolved_inputs(
ctx: &mut StdCtx,
inputs: ResolvedApproveInputs,
) -> anyhow::Result<ApproveOutcome> {
if inputs.dry_run {
return Ok(ApproveOutcome::DryRun(ApprovalDryRun {}));
}
let operator = resolve_operator(inputs.operator_seed_source, inputs.operator_id).await?;
if inputs.skip_post && operator.is_hosted() {
bail!("--skip-post is only supported for local operators");
}
let auth = if operator.is_hosted() {
Some(build_client().await?)
} else {
None
};
let approval = sign_and_write_approval(
&operator,
&OperatorCtx {
auth: auth.as_ref(),
},
inputs.approval_out.as_deref(),
&inputs.manifest,
)
.await?;
let written_to = inputs
.approval_out
.as_ref()
.map(|path| path.display().to_string());
let inline_approval = if written_to.is_none() {
Some(approval.clone())
} else {
None
};
match inputs.post_target {
Some(target) => {
let operator_id = operator
.id()
.context("resolved operator ID required to post approval")?;
let plan = PostApprovalPlan {
manifest_id: target.manifest_id.as_str(),
operator_id: &operator_id,
deploy_id: target.deploy_id.as_deref(),
};
let posted = post_approval_to_api(ctx, plan, &approval).await?;
Ok(ApproveOutcome::Posted(ApprovalPosted {
approval: inline_approval,
written_to,
manifest_id: target.manifest_id,
operator_id: operator_id.to_string(),
approval_ids: posted.approval_ids,
quorum_reached: posted.quorum_reached,
}))
}
None => Ok(ApproveOutcome::NotPosted(ApprovalGenerated {
approval: inline_approval,
written_to,
})),
}
}
async fn load_manifest(
ctx: &mut StdCtx,
args: &Args,
) -> anyhow::Result<(VersionedManifest, Option<String>)> {
match (&args.manifest, &args.deploy_id) {
(Some(path), _) => Ok((read_manifest_from_path(path).await?, None)),
(_, Some(deploy_id)) => {
let (manifest, manifest_id) =
fetch_manifest_from_deploy(ctx, &deploy_id.to_string()).await?;
Ok((manifest, Some(manifest_id)))
}
(None, None) => bail!("a manifest source is required"),
}
}
async fn sign_and_write_approval(
operator: &ResolvedOperator,
operator_ctx: &OperatorCtx<'_>,
approval_out: Option<&Path>,
manifest: &VersionedManifest,
) -> anyhow::Result<Approval> {
let approval = operator.approve_manifest(operator_ctx, manifest).await?;
if let Some(path) = approval_out {
let json = serde_json::to_string_pretty(&approval)?;
write_file(path, &json).await?;
}
Ok(approval)
}
fn resolve_manifest_id(args: &Args, fetched_manifest_id: Option<&str>) -> anyhow::Result<String> {
fetched_manifest_id
.map(|s| s.to_string())
.or_else(|| args.manifest_id.clone())
.ok_or_else(|| {
anyhow!(
"--manifest-id is required to post approval to API (or use --deploy-id). \
Use --skip-post to only generate the approval locally."
)
})
}
async fn load_saved_operator_ids() -> anyhow::Result<Vec<Uuid>> {
let config = Config::load().await?;
config
.get_last_operator_ids()
.unwrap_or_default()
.into_iter()
.map(|id| {
Uuid::parse_str(&id).with_context(|| format!("saved operator ID '{id}' is not a UUID"))
})
.collect()
}
async fn post_approval_to_api(
ctx: &mut StdCtx,
plan: PostApprovalPlan<'_>,
approval: &Approval,
) -> anyhow::Result<PostedApproval> {
shell_println!(ctx)?;
shell_println!(ctx, "Posting approval to Turnkey...")?;
let auth = crate::client::build_client().await?;
let tvc_approval = TvcManifestApproval {
operator_id: plan.operator_id.to_string(),
signature: hex::encode(&approval.signature),
};
let intent = CreateTvcManifestApprovalsIntent {
manifest_id: plan.manifest_id.into(),
approvals: vec![tvc_approval],
};
let timestamp_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("system time before unix epoch")?
.as_millis();
let result = auth
.client
.create_tvc_manifest_approvals(auth.org_id.clone(), timestamp_ms, intent)
.await
.context("failed to post manifest approval")?;
let quorum_reached = match plan.deploy_id {
Some(deploy_id) => {
let request = GetTvcDeploymentRequest {
organization_id: auth.org_id.clone(),
deployment_id: deploy_id.to_string(),
};
match auth.client.get_tvc_deployment(request).await {
Ok(response) => Some(
response
.tvc_deployment
.as_ref()
.is_some_and(manifest_approval_quorum_reached),
),
Err(error) => {
debug!(
deploy_id,
%error,
"failed to fetch deployment after posting manifest approval"
);
None
}
}
}
None => None,
};
Ok(PostedApproval {
approval_ids: result.result.approval_ids,
quorum_reached,
})
}
fn manifest_approval_quorum_reached(deployment: &TvcDeployment) -> bool {
let Some(manifest_set) = &deployment.manifest_set else {
return false;
};
if manifest_set.threshold == 0 {
return false;
}
let mut operator_ids = HashSet::new();
for approval in &deployment.manifest_approvals {
let Some(operator) = &approval.operator else {
return false;
};
if operator.id.is_empty() {
return false;
}
operator_ids.insert(operator.id.as_str());
}
operator_ids.len() >= manifest_set.threshold as usize
}
fn interactive_approve(ctx: &mut StdCtx, manifest: &VersionedManifest) -> anyhow::Result<()> {
shell_println!(ctx, "\n========================================")?;
shell_println!(ctx, " MANIFEST APPROVAL")?;
shell_println!(ctx, "========================================\n")?;
review_namespace(ctx, manifest.namespace())?;
review_enclave(ctx, manifest.enclave())?;
review_pivot(ctx, manifest)?;
review_manifest_set(ctx, manifest.manifest_set())?;
review_share_set(ctx, manifest.share_set())?;
shell_println!(ctx, "\n========================================")?;
shell_println!(ctx, " ALL SECTIONS APPROVED")?;
shell_println!(ctx, "========================================\n")?;
Ok(())
}
fn render_namespace(namespace: &Namespace) -> String {
let mut s = String::new();
let _ = writeln!(s, "NAMESPACE");
let _ = writeln!(s, "─────────────────────────────────────");
let _ = writeln!(s, " Name: {}", namespace.name);
let _ = writeln!(s, " Nonce: {}", namespace.nonce);
let _ = writeln!(s, " Quorum Key: {}", hex::encode(&namespace.quorum_key));
let _ = writeln!(s);
s
}
fn review_namespace(ctx: &mut StdCtx, namespace: &Namespace) -> anyhow::Result<()> {
shell_print!(ctx, "{}", render_namespace(namespace))?;
prompts::confirm_or_bail("Approve namespace?", "approval")
}
fn render_enclave(enclave: &NitroConfig) -> String {
let mut s = String::new();
let _ = writeln!(s, "ENCLAVE (AWS Nitro)");
let _ = writeln!(s, "─────────────────────────────────────");
let _ = writeln!(s, " PCR0 (image): {}", hex::encode(&enclave.pcr0));
let _ = writeln!(s, " PCR1 (kernel): {}", hex::encode(&enclave.pcr1));
let _ = writeln!(s, " PCR2 (app): {}", hex::encode(&enclave.pcr2));
let _ = writeln!(s, " PCR3 (IAM role): {}", hex::encode(&enclave.pcr3));
let _ = writeln!(s);
s
}
fn review_enclave(ctx: &mut StdCtx, enclave: &NitroConfig) -> anyhow::Result<()> {
shell_print!(ctx, "{}", render_enclave(enclave))?;
prompts::confirm_or_bail("Approve enclave configuration?", "approval")
}
fn render_pivot(manifest: &VersionedManifest) -> String {
let mut s = String::new();
let _ = writeln!(s, "PIVOT BINARY");
let _ = writeln!(s, "─────────────────────────────────────");
let _ = writeln!(
s,
" Pivot Binary Hash: {}",
hex::encode(manifest.pivot_hash())
);
if manifest.args().is_empty() {
let _ = writeln!(s, " CLI Args: (none)");
} else {
let _ = writeln!(s, " CLI Args:\n {}", manifest.args().join("\n "));
}
let _ = writeln!(s);
s
}
fn review_pivot(ctx: &mut StdCtx, manifest: &VersionedManifest) -> anyhow::Result<()> {
shell_print!(ctx, "{}", render_pivot(manifest))?;
prompts::confirm_or_bail("Approve pivot binary?", "approval")
}
fn render_quorum_members(members: &[QuorumMember]) -> String {
let mut s = String::new();
for member in members.iter() {
let _ = writeln!(s, " {} ({})", member.alias, hex::encode(&member.pub_key));
}
s
}
fn render_manifest_set(set: &ManifestSet) -> String {
let mut s = String::new();
let _ = writeln!(s, "MANIFEST SET");
let _ = writeln!(s, "─────────────────────────────────────");
let _ = writeln!(s, " Threshold: {} of {}", set.threshold, set.members.len());
let _ = writeln!(s, " Members:");
s.push_str(&render_quorum_members(&set.members));
let _ = writeln!(s);
s
}
fn review_manifest_set(ctx: &mut StdCtx, set: &ManifestSet) -> anyhow::Result<()> {
shell_print!(ctx, "{}", render_manifest_set(set))?;
prompts::confirm_or_bail("Approve manifest set?", "approval")
}
fn render_share_set(set: &ShareSet) -> String {
let mut s = String::new();
let _ = writeln!(s, "SHARE SET");
let _ = writeln!(s, "─────────────────────────────────────");
let _ = writeln!(s, " Threshold: {} of {}", set.threshold, set.members.len());
let _ = writeln!(s, " Members:");
s.push_str(&render_quorum_members(&set.members));
let _ = writeln!(s);
s
}
fn review_share_set(ctx: &mut StdCtx, set: &ShareSet) -> anyhow::Result<()> {
shell_print!(ctx, "{}", render_share_set(set))?;
prompts::confirm_or_bail("Approve share set?", "approval")
}
async fn read_manifest_from_path(path: &Path) -> anyhow::Result<VersionedManifest> {
let content = read_file_to_string(path).await?;
let manifest = VersionedManifest::try_from_slice_compat(content.as_bytes())
.with_context(|| format!("failed to parse manifest JSON from: {}", path.display()))?;
Ok(manifest)
}
async fn fetch_manifest_from_deploy(
ctx: &mut StdCtx,
deploy_id: &str,
) -> anyhow::Result<(VersionedManifest, String)> {
shell_println!(ctx, "Fetching deployment {deploy_id}...")?;
let auth = build_client().await?;
let request = GetTvcDeploymentRequest {
organization_id: auth.org_id.clone(),
deployment_id: deploy_id.to_string(),
};
let response = auth
.client
.get_tvc_deployment(request)
.await
.with_context(|| format!("failed to fetch deployment {deploy_id}"))?;
let deployment = response
.tvc_deployment
.ok_or_else(|| MissingResource::new("deployment", deploy_id.to_string()))?;
let tvc_manifest = deployment
.manifest
.ok_or_else(|| MissingResource::new("manifest", format!("deployment {deploy_id}")))?;
let manifest = VersionedManifest::try_from_slice_compat(&tvc_manifest.manifest)
.context("failed to parse manifest from deployment")?;
shell_println!(ctx, "✓ Manifest loaded (manifest_id: {})", tvc_manifest.id)?;
Ok((manifest, tvc_manifest.id))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::Message;
use turnkey_client::generated::external::data::v1::{
TvcOperator, TvcOperatorApproval, TvcOperatorSet,
};
fn fixture_manifest() -> VersionedManifest {
VersionedManifest::try_from_slice_compat(include_bytes!("../../../fixtures/manifest.json"))
.expect("fixture manifest should parse")
}
fn test_operator(id: &str) -> TvcOperator {
TvcOperator {
id: id.to_string(),
name: format!("operator-{id}"),
public_key: format!("public-key-{id}"),
created_at: None,
updated_at: None,
}
}
fn test_approval(index: usize, operator_id: Option<&str>) -> TvcOperatorApproval {
TvcOperatorApproval {
id: format!("approval-{index}"),
manifest_id: "manifest-123".to_string(),
operator: operator_id.map(test_operator),
approval: vec![],
created_at: None,
updated_at: None,
}
}
fn test_deployment(
manifest_threshold: Option<u32>,
approval_operator_ids: &[Option<&str>],
) -> TvcDeployment {
let operators = approval_operator_ids
.iter()
.filter_map(|operator_id| operator_id.map(test_operator))
.collect();
TvcDeployment {
id: "deployment-123".to_string(),
organization_id: "org-123".to_string(),
app_id: "app-123".to_string(),
manifest_set: manifest_threshold.map(|threshold| TvcOperatorSet {
id: "manifest-set-123".to_string(),
name: "manifest-set".to_string(),
organization_id: "org-123".to_string(),
operators,
threshold,
created_at: None,
updated_at: None,
}),
share_set: None,
manifest: None,
manifest_approvals: approval_operator_ids
.iter()
.enumerate()
.map(|(index, operator_id)| test_approval(index, *operator_id))
.collect(),
qos_version: "qos-v1".to_string(),
pivot_container: None,
debug_mode: false,
created_at: None,
updated_at: None,
delete: false,
}
}
#[test]
fn render_namespace_includes_name_nonce_and_quorum_key() {
let manifest = fixture_manifest();
let rendered = render_namespace(manifest.namespace());
assert!(rendered.contains("NAMESPACE"));
assert!(rendered.contains("turnkey-prod"));
assert!(rendered.contains("Nonce:"));
assert!(rendered.contains("Quorum Key:"));
}
#[test]
fn render_enclave_includes_all_four_pcrs() {
let manifest = fixture_manifest();
let rendered = render_enclave(manifest.enclave());
assert!(rendered.contains("ENCLAVE (AWS Nitro)"));
assert!(rendered.contains("PCR0"));
assert!(rendered.contains("PCR1"));
assert!(rendered.contains("PCR2"));
assert!(rendered.contains("PCR3"));
}
#[test]
fn render_pivot_includes_header_and_args() {
let manifest = fixture_manifest();
let rendered = render_pivot(&manifest);
assert!(rendered.contains("PIVOT BINARY"));
assert!(rendered.contains("Pivot Binary Hash:"));
assert!(rendered.contains("--flag"));
assert!(rendered.contains("positional"));
}
#[test]
fn render_manifest_set_includes_threshold_and_each_member() {
let manifest = fixture_manifest();
let rendered = render_manifest_set(manifest.manifest_set());
assert!(rendered.contains("MANIFEST SET"));
assert!(rendered.contains("Threshold: 2 of 3"));
assert!(rendered.contains("operator-alice"));
assert!(rendered.contains("operator-bob"));
assert!(rendered.contains("operator-charlie"));
}
#[test]
fn manifest_approval_quorum_reached_is_false_below_threshold() {
let deployment = test_deployment(Some(2), &[Some("operator-1")]);
assert!(!manifest_approval_quorum_reached(&deployment));
}
#[test]
fn manifest_approval_quorum_reached_is_true_at_threshold() {
let deployment = test_deployment(Some(2), &[Some("operator-1"), Some("operator-2")]);
assert!(manifest_approval_quorum_reached(&deployment));
}
#[test]
fn manifest_approval_quorum_counts_distinct_operators() {
let deployment = test_deployment(Some(2), &[Some("operator-1"), Some("operator-1")]);
assert!(!manifest_approval_quorum_reached(&deployment));
}
#[test]
fn manifest_approval_quorum_is_false_without_manifest_set() {
let deployment = test_deployment(None, &[Some("operator-1"), Some("operator-2")]);
assert!(!manifest_approval_quorum_reached(&deployment));
}
#[test]
fn manifest_approval_quorum_is_false_with_zero_threshold() {
let deployment = test_deployment(Some(0), &[Some("operator-1")]);
assert!(!manifest_approval_quorum_reached(&deployment));
}
#[test]
fn manifest_approval_quorum_is_false_when_approval_lacks_operator() {
let deployment = test_deployment(Some(2), &[Some("operator-1"), None]);
assert!(!manifest_approval_quorum_reached(&deployment));
}
#[test]
fn manifest_approval_quorum_is_false_when_operator_id_is_empty() {
let deployment = test_deployment(Some(1), &[Some("")]);
assert!(!manifest_approval_quorum_reached(&deployment));
}
fn posted_to_file() -> ApprovalPosted {
ApprovalPosted {
approval: None,
written_to: Some("approval.json".to_string()),
manifest_id: "manifest-123".to_string(),
operator_id: "operator-456".to_string(),
approval_ids: vec!["approval-1".to_string()],
quorum_reached: Some(true),
}
}
#[test]
fn approval_posted_serializes_expected_json() {
let value: serde_json::Value = serde_json::from_str(
&Outcome::DeployApprove(ApproveOutcome::Posted(posted_to_file())).to_json_string(),
)
.unwrap();
assert_eq!(
value,
serde_json::json!({
"reason": "manifest_approval_posted",
"writtenTo": "approval.json",
"manifestId": "manifest-123",
"operatorId": "operator-456",
"approvalIds": ["approval-1"],
"quorumReached": true,
})
);
}
#[test]
fn approval_generated_serializes_expected_json() {
let generated = ApprovalGenerated {
approval: Some(Approval {
signature: vec![0xde, 0xad],
member: QuorumMember {
alias: "operator-alice".to_string(),
pub_key: vec![0xaa],
},
}),
written_to: None,
};
let value: serde_json::Value = serde_json::from_str(
&Outcome::DeployApprove(ApproveOutcome::NotPosted(generated)).to_json_string(),
)
.unwrap();
assert_eq!(
value,
serde_json::json!({
"reason": "manifest_approval_generated",
"approval": {
"signature": "dead",
"member": {
"alias": "operator-alice",
"pubKey": "aa",
},
},
})
);
}
#[test]
fn approval_dry_run_serializes_reason_only() {
let value: serde_json::Value = serde_json::from_str(
&Outcome::DeployApprove(ApproveOutcome::DryRun(ApprovalDryRun {})).to_json_string(),
)
.unwrap();
assert_eq!(
value,
serde_json::json!({ "reason": "manifest_approval_dry_run" })
);
}
#[test]
fn approval_posted_human_message_includes_quorum_line_only_when_known() {
let mut posted = posted_to_file();
posted.quorum_reached = Some(true);
assert!(
posted
.to_string()
.contains("Manifest approval quorum reached.")
);
posted.quorum_reached = Some(false);
assert!(
posted
.to_string()
.contains("requires additional manifest approvals")
);
posted.quorum_reached = None;
let human = posted.to_string();
assert!(!human.contains("quorum"));
assert!(human.ends_with("Operator ID: operator-456"));
}
}