• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

endoze / nysm / 15221700177

24 May 2025 12:51AM UTC coverage: 93.269% (-2.1%) from 95.402%
15221700177

push

github

web-flow
Merge pull request #10 from endoze/feat-add-support-for-creating-secrets

Feat: Add support for creating secrets

14 of 16 new or added lines in 1 file covered. (87.5%)

2 existing lines in 2 files now uncovered.

97 of 104 relevant lines covered (93.27%)

1.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.25
/src/cli.rs
1
#![deny(missing_docs)]
2
use crate::client::QuerySecrets;
3
use crate::error::NysmError;
4
use bat::PrettyPrinter;
5
use clap::ValueEnum;
6
use clap::{Args, Parser, Subcommand};
7
use serde::{Deserialize, Serialize};
8
use std::io::IsTerminal;
9
use tempfile::TempDir;
10

11
/// This struct defines the main command line interface for Nysm.
12
#[derive(Parser)]
13
#[command(author, version, about, long_about = None)]
14
#[command(propagate_version = true)]
15
pub struct ArgumentParser {
16
  /// Which subcommand to use
17
  #[command(subcommand)]
18
  pub command: Commands,
19
  /// Region to retreive secrets from
20
  #[arg(short, long, global = true)]
21
  pub region: Option<String>,
22
}
23

24
/// This enum defines the main command line subcommands for Nysm.
25
#[derive(Subcommand, PartialEq, Debug)]
26
pub enum Commands {
27
  /// Retrieve a list of secrets
28
  List(List),
29
  /// Edit the value of a specific secret
30
  Edit(Edit),
31
  /// Show the value of a specific secret
32
  Show(Show),
33
  /// Create a new secret
34
  Create(Create),
35
}
36

37
/// Retrieve a list of secrets
38
#[derive(Args, PartialEq, Debug)]
39
pub struct List {}
40

41
/// Edit the value of a specific secret
42
#[derive(Args, PartialEq, Debug)]
43
pub struct Edit {
44
  /// ID of the secret to edit
45
  pub secret_id: String,
46
  #[clap(
47
    value_enum,
48
    short = 'f',
49
    long = "secret-format",
50
    default_value = "json"
51
  )]
52
  /// Format of the secret as stored by the provider
53
  pub secret_format: DataFormat,
54
  /// Format to edit the secret in
55
  #[clap(value_enum, short = 'e', long = "edit-format", default_value = "yaml")]
56
  pub edit_format: DataFormat,
57
}
58

59
/// Show the value of a specific secret
60
#[derive(Args, PartialEq, Debug)]
61
pub struct Show {
62
  /// ID of the secret to edit
63
  pub secret_id: String,
64
  /// Format to print the secret in
65
  #[clap(value_enum, short = 'p', long = "print-format", default_value = "yaml")]
66
  pub print_format: DataFormat,
67
  #[clap(
68
    value_enum,
69
    short = 'f',
70
    long = "secret-format",
71
    default_value = "json"
72
  )]
73
  /// Format of the secret as stored by the provider
74
  pub secret_format: DataFormat,
75
}
76

77
/// Create a new secret
78
#[derive(Args, PartialEq, Debug)]
79
pub struct Create {
80
  /// ID of the secret to create
81
  pub secret_id: String,
82
  /// Description of the secret
83
  #[clap(short = 'd', long = "description")]
84
  pub description: Option<String>,
85
  /// Format of the secret as stored by the provider
86
  #[clap(
87
    value_enum,
88
    short = 'f',
89
    long = "secret-format",
90
    default_value = "json"
91
  )]
92
  pub secret_format: DataFormat,
93
  /// Format to edit the secret in
94
  #[clap(value_enum, short = 'e', long = "edit-format", default_value = "yaml")]
95
  pub edit_format: DataFormat,
96
}
97

98
/// Enum to describe the different data formats that can be used with Secrets
99
#[derive(Clone, Debug, Deserialize, Serialize, ValueEnum, PartialEq)]
100
#[serde(rename_all = "lowercase")]
101
pub enum DataFormat {
102
  /// Json format
103
  Json,
104
  /// Yaml format
105
  Yaml,
106
  /// Plaintext format
107
  Text,
108
}
109

110
impl std::fmt::Display for DataFormat {
111
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1✔
112
    std::fmt::Debug::fmt(self, f)
1✔
113
  }
114
}
115

116
impl ArgumentParser {
117
  /// Runs the given subcommand and uses the provided client
118
  ///
119
  /// # Arguments
120
  /// * `client` - Trait object that implements [QuerySecrets]
121
  ///
122
  #[cfg(not(tarpaulin_include))]
123
  pub async fn run_subcommand(&self, client: impl QuerySecrets) {
124
    let result = match &self.command {
125
      Commands::List(args) => {
126
        let result = list(client, args).await;
127

128
        match result {
129
          Ok(list) => println!("{}", list),
130
          Err(error) => println!("{}", error),
131
        }
132

133
        Ok(())
134
      }
135
      Commands::Edit(args) => edit(client, args).await,
136
      Commands::Show(args) => show(client, args).await,
137
      Commands::Create(args) => create(client, args).await,
138
    };
139

140
    if let Err(error) = result {
141
      println!("{}", error);
142
    }
143
  }
144
}
145

146
async fn list(client: impl QuerySecrets, _args: &List) -> Result<String, NysmError> {
4✔
147
  let secrets_list = client.secrets_list().await?;
2✔
148

149
  Ok(secrets_list.table_display())
2✔
150
}
151

152
async fn show(client: impl QuerySecrets, args: &Show) -> Result<(), NysmError> {
4✔
153
  let secret_value = client.secret_value(args.secret_id.clone()).await?;
3✔
154

155
  let formatted_secret = reformat_data(
156
    &secret_value.secret,
1✔
157
    &args.secret_format,
1✔
158
    &args.print_format,
1✔
159
  )?;
160

161
  let _ = pretty_print(formatted_secret, &args.print_format);
2✔
162

163
  Ok(())
1✔
164
}
165

166
async fn edit(client: impl QuerySecrets, args: &Edit) -> Result<(), NysmError> {
4✔
167
  let secret_value = client.secret_value(args.secret_id.clone()).await?;
3✔
168

169
  if let Ok(dir) = temporary_directory() {
3✔
170
    let update_contents = launch_editor(
171
      secret_value.secret,
1✔
172
      dir,
×
173
      &args.secret_format,
1✔
174
      &args.edit_format,
1✔
175
    )?;
176

177
    if let Some(contents) = update_contents {
1✔
178
      let _ = client
6✔
179
        .update_secret_value(args.secret_id.clone(), contents)
2✔
180
        .await?;
5✔
181
    }
182
  }
183

184
  Ok(())
1✔
185
}
186

187
async fn create(client: impl QuerySecrets, args: &Create) -> Result<(), NysmError> {
4✔
188
  if let Ok(dir) = temporary_directory() {
3✔
189
    let initial_content = match args.edit_format {
1✔
190
      DataFormat::Json => "{}".to_string(),
2✔
191
      DataFormat::Yaml => "".to_string(),
2✔
NEW
192
      DataFormat::Text => "".to_string(),
×
193
    };
194

195
    let secret_contents =
2✔
NEW
196
      launch_editor(initial_content, dir, &args.edit_format, &args.edit_format)?;
×
197

198
    if let Some(contents) = secret_contents {
1✔
199
      let formatted_contents = reformat_data(&contents, &args.edit_format, &args.secret_format)?;
2✔
200
      let _ = client
7✔
201
        .create_secret(
202
          args.secret_id.clone(),
2✔
203
          formatted_contents,
1✔
204
          args.description.clone(),
1✔
205
        )
206
        .await?;
5✔
207
    }
208
  }
209

210
  Ok(())
1✔
211
}
212

213
fn strip_trailing_whitespace_from_block_scalars(content: &str) -> String {
1✔
214
  if content.contains(": |") {
1✔
215
    content
3✔
216
      .lines()
217
      .map(|line| line.trim_end())
4✔
218
      .collect::<Vec<_>>()
219
      .join("\n")
220
  } else {
221
    content.to_string()
1✔
222
  }
223
}
224

225
fn reformat_data(
1✔
226
  content: &str,
227
  source_format: &DataFormat,
228
  destination_format: &DataFormat,
229
) -> Result<String, NysmError> {
230
  Ok(match source_format {
2✔
231
    DataFormat::Json => {
232
      let json_value: serde_json::Value = serde_json::from_str(content)?;
2✔
233

234
      match destination_format {
1✔
235
        DataFormat::Json => serde_json::to_string_pretty(&json_value)?,
2✔
236
        DataFormat::Yaml => serde_yml::to_string(&json_value)?,
2✔
237
        DataFormat::Text => String::from(content),
2✔
238
      }
239
    }
240
    DataFormat::Yaml => match destination_format {
1✔
241
      DataFormat::Yaml => {
242
        serde_yml::from_str::<serde_yml::Value>(content)?;
2✔
243
        String::from(content)
1✔
244
      }
245
      DataFormat::Json => {
246
        let cleaned_content = strip_trailing_whitespace_from_block_scalars(content);
1✔
247
        let yaml_value: serde_yml::Value = serde_yml::from_str(&cleaned_content)?;
2✔
248
        serde_json::to_string_pretty(&yaml_value)?
2✔
249
      }
250
      DataFormat::Text => String::from(content),
1✔
251
    },
252
    DataFormat::Text => String::from(content),
1✔
253
  })
254
}
255

256
/// Pretty prints a string with bat.
257
///
258
/// # Arguments
259
/// * `content` - String to be pretty printed
260
/// * `print_format` - Format to print the string as
261
///
262
/// # Returns
263
/// Returns a result with either an empty tuple or a NysmError. This can error if
264
/// bat has trouble printing in the specified format.
265
#[cfg(not(tarpaulin_include))]
266
fn pretty_print(content: String, print_format: &DataFormat) -> Result<(), NysmError> {
267
  if std::io::stdout().is_terminal() {
268
    let language_string = print_format.to_string();
269
    let mut printer = PrettyPrinter::new();
270
    let _printer = match print_format {
271
      DataFormat::Yaml | DataFormat::Json => printer.language(&language_string),
272
      _ => &mut printer,
273
    };
274

275
    #[allow(unused)]
276
    #[cfg(not(test))]
277
    let _ = _printer
278
      .grid(true)
279
      .line_numbers(true)
280
      .paging_mode(bat::PagingMode::QuitIfOneScreen)
281
      .pager("less")
282
      .theme("OneHalfDark")
283
      .input_from_bytes(content.as_bytes())
284
      .print()?;
285
  } else {
286
    println!("{}", content);
287
  }
288

289
  Ok(())
290
}
291

292
/// This method is designed to open up an editor with contents from a secret.
293
///
294
/// # Arguments
295
/// * `contents` - String contents to open up in an editor
296
/// * `path` - Temporary directory to save the contents of the file to when editing a secret
297
/// * `secret_format` - Format of the secret as given by the secret provider
298
/// * `edit_format` - Format of the secret to use while editing the secret in an editor
299
///
300
/// # Returns
301
/// Returns a result containing the changes to the contents originally passed into the method.
302
/// Can error if any IO operation fails (read/write of the temporary file).
303
///
304
fn launch_editor<P>(
1✔
305
  contents: String,
306
  path: P,
307
  secret_format: &DataFormat,
308
  edit_format: &DataFormat,
309
) -> Result<Option<String>, NysmError>
310
where
311
  P: AsRef<std::path::Path>,
312
{
313
  let language_string = edit_format.to_string().to_lowercase();
2✔
314
  let file_path = path.as_ref().join("data").with_extension(language_string);
1✔
315

316
  let file_contents = reformat_data(&contents, secret_format, edit_format)?;
1✔
317
  std::fs::write(&file_path, file_contents)?;
2✔
318

319
  let mut editor = match std::env::var("EDITOR") {
1✔
320
    Ok(editor) => editor,
1✔
321
    Err(_) => String::from("vim"),
2✔
322
  };
323

324
  editor.push(' ');
1✔
325
  editor.push_str(&file_path.to_string_lossy());
1✔
326

327
  #[cfg(test)]
328
  editor.insert_str(0, "vim(){ :; }; ");
329

330
  std::process::Command::new("/usr/bin/env")
4✔
331
    .arg("sh")
332
    .arg("-c")
333
    .arg(editor)
1✔
334
    .spawn()
335
    .expect("Error: Failed to run editor")
336
    .wait()
337
    .expect("Error: Editor returned a non-zero status");
338

339
  let file_contents: String = std::fs::read_to_string(file_path)?;
1✔
340
  let json_data = reformat_data(&file_contents, edit_format, secret_format)?;
2✔
341

342
  if json_data.eq(&contents) {
4✔
343
    println!("It seems the file hasn't changed, not persisting back to AWS Secrets Manager.");
2✔
344

345
    Ok(None)
1✔
346
  } else {
347
    Ok(Some(json_data))
1✔
348
  }
349
}
350

351
fn temporary_directory() -> std::io::Result<TempDir> {
1✔
352
  TempDir::new()
1✔
353
}
354

355
#[cfg(test)]
356
mod tests {
357
  use super::*;
358
  use futures::FutureExt;
359
  use lazy_static::lazy_static;
360
  use serde_json::json;
361
  use std::env::VarError;
362
  use std::future::Future;
363
  use std::panic::AssertUnwindSafe;
364
  use std::panic::{RefUnwindSafe, UnwindSafe};
365
  use std::{env, panic};
366

367
  lazy_static! {
368
    static ref SERIAL_TEST: tokio::sync::Mutex<()> = Default::default();
369
  }
370

371
  /// Sets environment variables to the given value for the duration of the closure.
372
  /// Restores the previous values when the closure completes or panics, before unwinding the panic.
373
  pub async fn async_with_env_vars<F>(kvs: Vec<(&str, Option<&str>)>, closure: F)
374
  where
375
    F: Future<Output = ()> + UnwindSafe + RefUnwindSafe,
376
  {
377
    let guard = SERIAL_TEST.lock().await;
378
    let mut old_kvs: Vec<(&str, Result<String, VarError>)> = Vec::new();
379

380
    for (k, v) in kvs {
381
      let old_v = env::var(k);
382
      old_kvs.push((k, old_v));
383
      match v {
384
        None => unsafe { env::remove_var(k) },
385
        Some(v) => unsafe { env::set_var(k, v) },
386
      }
387
    }
388

389
    match closure.catch_unwind().await {
390
      Ok(_) => {
391
        for (k, v) in old_kvs {
392
          reset_env(k, v);
393
        }
394
      }
395
      Err(err) => {
396
        for (k, v) in old_kvs {
397
          reset_env(k, v);
398
        }
399
        drop(guard);
400
        panic::resume_unwind(err);
401
      }
402
    }
403
  }
404

405
  fn reset_env(k: &str, old: Result<String, VarError>) {
406
    if let Ok(v) = old {
407
      unsafe { env::set_var(k, v) };
408
    } else {
409
      unsafe { env::remove_var(k) };
410
    }
411
  }
412

413
  type TestResult = Result<(), Box<dyn std::error::Error>>;
414

415
  mod reformat_data {
416
    use super::*;
417

418
    #[test]
419
    fn from_json_to_yaml() -> TestResult {
420
      let data = r#"{"banana": true, "apple": false}"#;
421
      let expected = "apple: false\nbanana: true\n";
422

423
      let result = reformat_data(data, &DataFormat::Json, &DataFormat::Yaml)?;
424

425
      assert_eq!(expected, result);
426

427
      Ok(())
428
    }
429

430
    #[test]
431
    fn from_json_to_json() -> TestResult {
432
      let data = r#"{"banana": true, "apple": false}"#;
433
      let json_value = json!({
434
        "apple": false,
435
        "banana": true,
436
      });
437
      let expected = serde_json::to_string_pretty(&json_value)?;
438

439
      let result = reformat_data(data, &DataFormat::Json, &DataFormat::Json)?;
440

441
      assert_eq!(expected, result);
442

443
      Ok(())
444
    }
445

446
    #[test]
447
    fn from_json_to_text() -> TestResult {
448
      let data = r#"{"apple":false,"banana":true}"#;
449
      let expected = json!({
450
        "apple": false,
451
        "banana": true,
452
      })
453
      .to_string();
454

455
      let result = reformat_data(data, &DataFormat::Json, &DataFormat::Text)?;
456

457
      assert_eq!(expected, result);
458

459
      Ok(())
460
    }
461

462
    #[test]
463
    fn from_yaml_to_json() -> TestResult {
464
      let yaml_string = r#"apple: false
465
banana: true
466
"#;
467
      let json_value = json!({
468
        "apple": false,
469
        "banana": true,
470
      });
471
      let expected = serde_json::to_string_pretty(&json_value)?;
472

473
      let result = reformat_data(yaml_string, &DataFormat::Yaml, &DataFormat::Json)?;
474

475
      assert_eq!(expected, result);
476

477
      Ok(())
478
    }
479

480
    #[test]
481
    fn from_yaml_to_yaml() -> TestResult {
482
      let yaml_string = r#"apple: false
483
banana: true
484
"#;
485
      let expected = "apple: false\nbanana: true\n";
486

487
      let result = reformat_data(yaml_string, &DataFormat::Yaml, &DataFormat::Yaml)?;
488

489
      assert_eq!(expected, result);
490

491
      Ok(())
492
    }
493

494
    #[test]
495
    fn from_yaml_to_text() -> TestResult {
496
      let yaml_string = r#"apple: false
497
banana: true
498
"#;
499
      let expected = "apple: false\nbanana: true\n";
500

501
      let result = reformat_data(yaml_string, &DataFormat::Yaml, &DataFormat::Text)?;
502

503
      assert_eq!(expected, result);
504

505
      Ok(())
506
    }
507

508
    #[test]
509
    fn from_yaml_with_trailing_whitespace_to_json() -> TestResult {
510
      let yaml_string = "application.yml: |-\n  banana: false \n  apple: true\n  flasdjfljasdlfjalsd: alsdkjflasjdflajdslf\n";
511

512
      let result = reformat_data(yaml_string, &DataFormat::Yaml, &DataFormat::Json)?;
513

514
      assert!(!result.contains("false \\n"));
515
      assert!(result.contains("false\\n"));
516

517
      Ok(())
518
    }
519

520
    #[test]
521
    fn from_text() -> TestResult {
522
      let text = "This is a plain string with no data structure.";
523
      let expected = "This is a plain string with no data structure.";
524

525
      let result = reformat_data(text, &DataFormat::Text, &DataFormat::Text)?;
526

527
      assert_eq!(expected, result);
528

529
      Ok(())
530
    }
531
  }
532

533
  #[test]
534
  fn data_format_display() -> TestResult {
535
    assert_eq!(format!("{}", DataFormat::Json), "Json");
536
    assert_eq!(format!("{}", DataFormat::Yaml), "Yaml");
537
    assert_eq!(format!("{}", DataFormat::Text), "Text");
538

539
    Ok(())
540
  }
541

542
  #[test]
543
  fn test_yaml_with_mixed_whitespace_fixture() -> TestResult {
544
    let fixture_path = "tests/fixtures/mixed_whitespace.yml";
545
    let problematic_yaml =
546
      std::fs::read_to_string(fixture_path).expect("Failed to read fixture file");
547

548
    let result = reformat_data(&problematic_yaml, &DataFormat::Yaml, &DataFormat::Json)?;
549

550
    assert!(result.contains("application.yml"));
551
    assert!(result.contains("banana: false"));
552
    assert!(!result.contains("false \\n"));
553

554
    Ok(())
555
  }
556

557
  mod argument_parsing {
558
    use super::*;
559

560
    #[test]
561
    fn accepts_region() -> TestResult {
562
      let args = "nysm -r us-west-2 list".split_whitespace();
563
      let arg_parser = ArgumentParser::try_parse_from(args)?;
564

565
      assert_eq!(arg_parser.region.unwrap(), "us-west-2".to_string());
566

567
      Ok(())
568
    }
569

570
    #[test]
571
    fn sets_list_subcommand() -> TestResult {
572
      let args = "nysm -r us-west-2 list".split_whitespace();
573
      let arg_parser = ArgumentParser::try_parse_from(args)?;
574

575
      assert_eq!(arg_parser.command, Commands::List(List {}));
576

577
      Ok(())
578
    }
579

580
    #[test]
581
    fn sets_show_subcommand() -> TestResult {
582
      let args = "nysm -r us-west-2 show testing-secrets".split_whitespace();
583
      let arg_parser = ArgumentParser::try_parse_from(args)?;
584

585
      assert_eq!(
586
        arg_parser.command,
587
        Commands::Show(Show {
588
          secret_id: "testing-secrets".into(),
589
          print_format: DataFormat::Yaml,
590
          secret_format: DataFormat::Json,
591
        })
592
      );
593

594
      Ok(())
595
    }
596

597
    #[test]
598
    fn sets_edit_subcommand() -> TestResult {
599
      let args = "nysm -r us-west-2 edit testing-secrets".split_whitespace();
600
      let arg_parser = ArgumentParser::try_parse_from(args)?;
601

602
      assert_eq!(
603
        arg_parser.command,
604
        Commands::Edit(Edit {
605
          secret_id: "testing-secrets".into(),
606
          edit_format: DataFormat::Yaml,
607
          secret_format: DataFormat::Json,
608
        })
609
      );
610

611
      Ok(())
612
    }
613

614
    #[test]
615
    fn sets_create_subcommand() -> TestResult {
616
      let args = "nysm -r us-west-2 create new-secret".split_whitespace();
617
      let arg_parser = ArgumentParser::try_parse_from(args)?;
618

619
      assert_eq!(
620
        arg_parser.command,
621
        Commands::Create(Create {
622
          secret_id: "new-secret".into(),
623
          description: None,
624
          edit_format: DataFormat::Yaml,
625
          secret_format: DataFormat::Json,
626
        })
627
      );
628

629
      Ok(())
630
    }
631

632
    #[test]
633
    fn sets_create_subcommand_with_description() -> TestResult {
634
      let args = vec![
635
        "nysm",
636
        "-r",
637
        "us-west-2",
638
        "create",
639
        "new-secret",
640
        "-d",
641
        "Test secret",
642
      ];
643
      let arg_parser = ArgumentParser::try_parse_from(args)?;
644

645
      assert_eq!(
646
        arg_parser.command,
647
        Commands::Create(Create {
648
          secret_id: "new-secret".into(),
649
          description: Some("Test secret".into()),
650
          edit_format: DataFormat::Yaml,
651
          secret_format: DataFormat::Json,
652
        })
653
      );
654

655
      Ok(())
656
    }
657
  }
658

659
  #[allow(clippy::field_reassign_with_default)]
660
  mod client {
661
    use super::*;
662
    use crate::client::{
663
      CreateSecretResult, GetSecretValueResult, ListSecretsResult, Secret, UpdateSecretValueResult,
664
    };
665
    use async_trait::async_trait;
666

667
    pub struct TestClient {
668
      fails_on_list_secrets: bool,
669
      fails_on_get_secret_value: bool,
670
      fails_on_update_secret_value: bool,
671
      fails_on_create_secret: bool,
672
      on_create_secret: Option<Box<dyn Fn(&str) + Send + Sync>>,
673
      on_update_secret: Option<Box<dyn Fn(&str) + Send + Sync>>,
674
    }
675

676
    impl Default for TestClient {
677
      fn default() -> Self {
678
        Self {
679
          fails_on_list_secrets: false,
680
          fails_on_get_secret_value: false,
681
          fails_on_update_secret_value: false,
682
          fails_on_create_secret: false,
683
          on_create_secret: None,
684
          on_update_secret: None,
685
        }
686
      }
687
    }
688

689
    #[async_trait]
690
    impl QuerySecrets for TestClient {
691
      async fn secrets_list(&self) -> Result<ListSecretsResult, NysmError> {
692
        if self.fails_on_list_secrets {
693
          return Err(NysmError::AwsListSecretsNoList);
694
        }
695

696
        Ok(ListSecretsResult {
697
          entries: vec![Secret {
698
            name: Some("secret-one".into()),
699
            uri: Some("some-unique-id-one".into()),
700
            description: Some("blah blah blah".into()),
701
          }],
702
        })
703
      }
704

705
      async fn secret_value(&self, _secret_id: String) -> Result<GetSecretValueResult, NysmError> {
706
        if self.fails_on_get_secret_value {
707
          return Err(NysmError::AwsSecretValueNoValueString);
708
        }
709

710
        let secret_value = json!({
711
          "apple": true,
712
          "banana": false,
713
        });
714

715
        let secret_value = serde_json::to_string_pretty(&secret_value)?;
716

717
        Ok(GetSecretValueResult {
718
          secret: secret_value,
719
        })
720
      }
721

722
      async fn update_secret_value(
723
        &self,
724
        _secret_id: String,
725
        secret_value: String,
726
      ) -> Result<UpdateSecretValueResult, NysmError> {
727
        if self.fails_on_update_secret_value {
728
          return Err(NysmError::AwsSecretValueUpdate);
729
        }
730

731
        if let Some(callback) = &self.on_update_secret {
732
          callback(&secret_value);
733
        }
734

735
        Ok(UpdateSecretValueResult {
736
          name: Some("testy-test-secret".into()),
737
          uri: Some("some-unique-id".into()),
738
          version_id: Some("definitely-a-new-version-id".into()),
739
        })
740
      }
741

742
      async fn create_secret(
743
        &self,
744
        _secret_id: String,
745
        secret_value: String,
746
        _description: Option<String>,
747
      ) -> Result<CreateSecretResult, NysmError> {
748
        if self.fails_on_create_secret {
749
          return Err(NysmError::AwsSecretValueCreate);
750
        }
751

752
        if let Some(callback) = &self.on_create_secret {
753
          callback(&secret_value);
754
        }
755

756
        Ok(CreateSecretResult {
757
          name: Some("new-test-secret".into()),
758
          uri: Some("some-new-unique-id".into()),
759
          version_id: Some("new-secret-version-id".into()),
760
        })
761
      }
762
    }
763

764
    mod list_output {
765
      use super::*;
766

767
      #[tokio::test]
768
      async fn error_when_api_list_call_fails() -> TestResult {
769
        let mut client = TestClient::default();
770
        client.fails_on_list_secrets = true;
771

772
        let result = list(client, &List {}).await;
773

774
        assert_eq!(result, Err(NysmError::AwsListSecretsNoList));
775

776
        Ok(())
777
      }
778

779
      #[tokio::test]
780
      async fn ok_when_list_api_call_succeeds() -> TestResult {
781
        let client = TestClient::default();
782

783
        let result = list(client, &List {}).await;
784

785
        assert!(result.is_ok());
786

787
        Ok(())
788
      }
789
    }
790

791
    mod show_output {
792
      use super::*;
793

794
      #[tokio::test]
795
      async fn error_when_api_show_call_fails() -> TestResult {
796
        let mut client = TestClient::default();
797
        client.fails_on_get_secret_value = true;
798

799
        let result = show(
800
          client,
801
          &Show {
802
            secret_id: "fake".into(),
803
            print_format: DataFormat::Json,
804
            secret_format: DataFormat::Json,
805
          },
806
        )
807
        .await;
808

809
        assert_eq!(result, Err(NysmError::AwsSecretValueNoValueString));
810

811
        Ok(())
812
      }
813

814
      #[tokio::test]
815
      async fn ok_when_api_show_call_succeeds() -> TestResult {
816
        let client = TestClient::default();
817

818
        let result = show(
819
          client,
820
          &Show {
821
            secret_id: "fake".into(),
822
            print_format: DataFormat::Json,
823
            secret_format: DataFormat::Json,
824
          },
825
        )
826
        .await;
827

828
        assert!(result.is_ok());
829

830
        Ok(())
831
      }
832
    }
833

834
    mod edit_output {
835
      use super::*;
836

837
      #[tokio::test]
838
      async fn error_when_api_update_call_fails() -> TestResult {
839
        async_with_env_vars(
840
          vec![("EDITOR", Some("echo 'another: true\n' >> "))],
841
          AssertUnwindSafe(async {
842
            let mut client = TestClient::default();
843
            client.fails_on_update_secret_value = true;
844

845
            let result = edit(
846
              client,
847
              &Edit {
848
                secret_id: "fake".into(),
849
                edit_format: DataFormat::Yaml,
850
                secret_format: DataFormat::Json,
851
              },
852
            )
853
            .await;
854

855
            assert_eq!(result, Err(NysmError::AwsSecretValueUpdate));
856
          }),
857
        )
858
        .await;
859

860
        Ok(())
861
      }
862

863
      #[tokio::test]
864
      async fn json_error_when_api_update_call_fails_due_to_syntax() -> TestResult {
865
        async_with_env_vars(
866
          vec![("EDITOR", Some("echo 'another: true\n' >> "))],
867
          AssertUnwindSafe(async {
868
            let client = TestClient::default();
869

870
            let result = edit(
871
              client,
872
              &Edit {
873
                secret_id: "fake".into(),
874
                edit_format: DataFormat::Json,
875
                secret_format: DataFormat::Json,
876
              },
877
            )
878
            .await;
879

880
            assert_eq!(
881
              result,
882
              Err(NysmError::SerdeJson(
883
                serde_json::from_str::<String>(";;;").unwrap_err()
884
              ))
885
            );
886
          }),
887
        )
888
        .await;
889

890
        Ok(())
891
      }
892

893
      #[tokio::test]
894
      async fn yaml_error_when_api_update_call_fails_due_to_syntax() -> TestResult {
895
        async_with_env_vars(
896
          vec![("EDITOR", Some("echo '@invalid_yaml' >> "))],
897
          AssertUnwindSafe(async {
898
            let client = TestClient::default();
899

900
            let result = edit(
901
              client,
902
              &Edit {
903
                secret_id: "fake".into(),
904
                edit_format: DataFormat::Yaml,
905
                secret_format: DataFormat::Yaml,
906
              },
907
            )
908
            .await;
909

910
            assert_eq!(
911
              result,
912
              Err(NysmError::SerdeYaml(
913
                serde_yml::from_str::<String>("::::").unwrap_err()
914
              ))
915
            );
916
          }),
917
        )
918
        .await;
919

920
        Ok(())
921
      }
922

923
      #[tokio::test]
924
      async fn error_when_api_get_call_fails() -> TestResult {
925
        async_with_env_vars(
926
          vec![("EDITOR", Some("echo >/dev/null 2>&1 <<<"))],
927
          AssertUnwindSafe(async {
928
            let mut client = TestClient::default();
929
            client.fails_on_get_secret_value = true;
930

931
            let result = edit(
932
              client,
933
              &Edit {
934
                secret_id: "fake".into(),
935
                edit_format: DataFormat::Json,
936
                secret_format: DataFormat::Json,
937
              },
938
            )
939
            .await;
940

941
            assert_eq!(result, Err(NysmError::AwsSecretValueNoValueString));
942
          }),
943
        )
944
        .await;
945

946
        Ok(())
947
      }
948

949
      #[tokio::test]
950
      async fn ok_when_api_get_calls_succeed() -> TestResult {
951
        async_with_env_vars(
952
          vec![("EDITOR", Some("echo >/dev/null 2>&1 <<<"))],
953
          AssertUnwindSafe(async {
954
            let client = TestClient::default();
955

956
            let result = edit(
957
              client,
958
              &Edit {
959
                secret_id: "fake".into(),
960
                edit_format: DataFormat::Json,
961
                secret_format: DataFormat::Json,
962
              },
963
            )
964
            .await;
965

966
            assert!(result.is_ok());
967
          }),
968
        )
969
        .await;
970

971
        Ok(())
972
      }
973

974
      #[tokio::test]
975
      async fn ok_when_no_editor_environment_variable() -> TestResult {
976
        async_with_env_vars(
977
          vec![("EDITOR", None)],
978
          AssertUnwindSafe(async {
979
            let client = TestClient::default();
980

981
            let result = edit(
982
              client,
983
              &Edit {
984
                secret_id: "fake".into(),
985
                edit_format: DataFormat::Json,
986
                secret_format: DataFormat::Json,
987
              },
988
            )
989
            .await;
990

991
            assert!(result.is_ok());
992
          }),
993
        )
994
        .await;
995

996
        Ok(())
997
      }
998

999
      #[tokio::test]
1000
      async fn ok_when_api_get_calls_succeed_and_no_change() -> TestResult {
1001
        async_with_env_vars(
1002
          vec![("EDITOR", Some("echo >/dev/null 2>&1 <<<"))],
1003
          AssertUnwindSafe(async {
1004
            let client = TestClient::default();
1005

1006
            let result = edit(
1007
              client,
1008
              &Edit {
1009
                secret_id: "secret-one".into(),
1010
                edit_format: DataFormat::Json,
1011
                secret_format: DataFormat::Json,
1012
              },
1013
            )
1014
            .await;
1015

1016
            assert!(result.is_ok());
1017
          }),
1018
        )
1019
        .await;
1020

1021
        Ok(())
1022
      }
1023

1024
      #[tokio::test]
1025
      async fn uses_correct_formats_for_editing() -> TestResult {
1026
        async_with_env_vars(
1027
          vec![("EDITOR", Some("echo 'updated_key: yaml_value\n' > "))],
1028
          AssertUnwindSafe(async {
1029
            let mut client = TestClient::default();
1030

1031
            client.on_update_secret = Some(Box::new(|secret_string| {
1032
              let parsed: serde_json::Value =
1033
                serde_json::from_str(secret_string).expect("Should be valid JSON");
1034
              assert_eq!(parsed["updated_key"], "yaml_value");
1035
            }));
1036

1037
            let result = edit(
1038
              client,
1039
              &Edit {
1040
                secret_id: "secret-one".into(),
1041
                edit_format: DataFormat::Yaml,
1042
                secret_format: DataFormat::Json,
1043
              },
1044
            )
1045
            .await;
1046

1047
            assert!(result.is_ok());
1048
          }),
1049
        )
1050
        .await;
1051

1052
        Ok(())
1053
      }
1054
    }
1055

1056
    mod create_output {
1057
      use super::*;
1058

1059
      #[tokio::test]
1060
      async fn error_when_api_create_call_fails() -> TestResult {
1061
        async_with_env_vars(
1062
          vec![("EDITOR", Some("echo 'test: value\n' >> "))],
1063
          AssertUnwindSafe(async {
1064
            let mut client = TestClient::default();
1065
            client.fails_on_create_secret = true;
1066

1067
            let result = create(
1068
              client,
1069
              &Create {
1070
                secret_id: "fake".into(),
1071
                description: None,
1072
                edit_format: DataFormat::Yaml,
1073
                secret_format: DataFormat::Json,
1074
              },
1075
            )
1076
            .await;
1077

1078
            assert_eq!(result, Err(NysmError::AwsSecretValueCreate));
1079
          }),
1080
        )
1081
        .await;
1082

1083
        Ok(())
1084
      }
1085

1086
      #[tokio::test]
1087
      async fn ok_when_api_create_call_succeeds() -> TestResult {
1088
        async_with_env_vars(
1089
          vec![("EDITOR", Some("echo 'test: value\n' >> "))],
1090
          AssertUnwindSafe(async {
1091
            let client = TestClient::default();
1092

1093
            let result = create(
1094
              client,
1095
              &Create {
1096
                secret_id: "new-secret".into(),
1097
                description: Some("Test description".into()),
1098
                edit_format: DataFormat::Yaml,
1099
                secret_format: DataFormat::Json,
1100
              },
1101
            )
1102
            .await;
1103

1104
            assert!(result.is_ok());
1105
          }),
1106
        )
1107
        .await;
1108

1109
        Ok(())
1110
      }
1111

1112
      #[tokio::test]
1113
      async fn ok_when_no_changes_made_in_editor() -> TestResult {
1114
        async_with_env_vars(
1115
          vec![("EDITOR", Some("echo >/dev/null 2>&1 <<<"))],
1116
          AssertUnwindSafe(async {
1117
            let client = TestClient::default();
1118

1119
            let result = create(
1120
              client,
1121
              &Create {
1122
                secret_id: "new-secret".into(),
1123
                description: None,
1124
                edit_format: DataFormat::Json,
1125
                secret_format: DataFormat::Json,
1126
              },
1127
            )
1128
            .await;
1129

1130
            assert!(result.is_ok());
1131
          }),
1132
        )
1133
        .await;
1134

1135
        Ok(())
1136
      }
1137

1138
      #[tokio::test]
1139
      async fn uses_correct_formats_for_editing() -> TestResult {
1140
        async_with_env_vars(
1141
          vec![("EDITOR", Some("echo 'key: yaml_value\n' > "))],
1142
          AssertUnwindSafe(async {
1143
            let mut client = TestClient::default();
1144

1145
            client.on_create_secret = Some(Box::new(|secret_string| {
1146
              let parsed: serde_json::Value =
1147
                serde_json::from_str(secret_string).expect("Should be valid JSON");
1148
              assert_eq!(parsed["key"], "yaml_value");
1149
            }));
1150

1151
            let result = create(
1152
              client,
1153
              &Create {
1154
                secret_id: "new-secret".into(),
1155
                description: None,
1156
                edit_format: DataFormat::Yaml,
1157
                secret_format: DataFormat::Json,
1158
              },
1159
            )
1160
            .await;
1161

1162
            assert!(result.is_ok());
1163
          }),
1164
        )
1165
        .await;
1166

1167
        Ok(())
1168
      }
1169
    }
1170
  }
1171
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc