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

endoze / nysm / 15221124329

23 May 2025 11:50PM UTC coverage: 95.402% (+2.8%) from 92.593%
15221124329

push

github

web-flow
Merge pull request #8 from endoze/fix-yaml-parsing-with-lines-ending-in-spaces

Fix yaml parsing with lines ending in spaces

14 of 14 new or added lines in 2 files covered. (100.0%)

1 existing line in 1 file now uncovered.

83 of 87 relevant lines covered (95.4%)

1.69 hits per line

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

98.44
/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
}
34

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

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

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

75
/// Enum to describe the different data formats that can be used with Secrets
76
#[derive(Clone, Debug, Deserialize, Serialize, ValueEnum, PartialEq)]
77
#[serde(rename_all = "lowercase")]
78
pub enum DataFormat {
79
  /// Json format
80
  Json,
81
  /// Yaml format
82
  Yaml,
83
  /// Plaintext format
84
  Text,
85
}
86

87
impl std::fmt::Display for DataFormat {
88
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1✔
89
    std::fmt::Debug::fmt(self, f)
1✔
90
  }
91
}
92

93
impl ArgumentParser {
94
  /// Runs the given subcommand and uses the provided client
95
  ///
96
  /// # Arguments
97
  /// * `client` - Trait object that implements [QuerySecrets]
98
  ///
99
  #[cfg(not(tarpaulin_include))]
100
  pub async fn run_subcommand(&self, client: impl QuerySecrets) {
101
    let result = match &self.command {
102
      Commands::List(args) => {
103
        let result = list(client, args).await;
104

105
        match result {
106
          Ok(list) => println!("{}", list),
107
          Err(error) => println!("{}", error),
108
        }
109

110
        Ok(())
111
      }
112
      Commands::Edit(args) => edit(client, args).await,
113
      Commands::Show(args) => show(client, args).await,
114
    };
115

116
    if let Err(error) = result {
117
      println!("{}", error);
118
    }
119
  }
120
}
121

122
async fn list(client: impl QuerySecrets, _args: &List) -> Result<String, NysmError> {
6✔
123
  let secrets_list = client.secrets_list().await?;
2✔
124

125
  Ok(secrets_list.table_display())
2✔
126
}
127

128
async fn show(client: impl QuerySecrets, args: &Show) -> Result<(), NysmError> {
7✔
129
  let secret_value = client.secret_value(args.secret_id.clone()).await?;
5✔
130

131
  let formatted_secret = reformat_data(
132
    &secret_value.secret,
1✔
133
    &args.secret_format,
1✔
134
    &args.print_format,
1✔
135
  )?;
136

137
  let _ = pretty_print(formatted_secret, &args.print_format);
2✔
138

139
  Ok(())
1✔
140
}
141

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

145
  if let Ok(dir) = temporary_directory() {
3✔
146
    let update_contents = launch_editor(
147
      secret_value.secret,
1✔
UNCOV
148
      dir,
×
149
      &args.secret_format,
1✔
150
      &args.edit_format,
1✔
151
    )?;
152

153
    if let Some(contents) = update_contents {
1✔
154
      let _ = client
6✔
155
        .update_secret_value(args.secret_id.clone(), contents)
2✔
156
        .await?;
4✔
157
    }
158
  }
159

160
  Ok(())
1✔
161
}
162

163
fn strip_trailing_whitespace_from_block_scalars(content: &str) -> String {
1✔
164
  if content.contains(": |") {
1✔
165
    content
4✔
166
      .lines()
167
      .map(|line| line.trim_end())
4✔
168
      .collect::<Vec<_>>()
169
      .join("\n")
170
  } else {
171
    content.to_string()
1✔
172
  }
173
}
174

175
fn reformat_data(
1✔
176
  content: &str,
177
  source_format: &DataFormat,
178
  destination_format: &DataFormat,
179
) -> Result<String, NysmError> {
180
  Ok(match source_format {
2✔
181
    DataFormat::Json => {
182
      let json_value: serde_json::Value = serde_json::from_str(content)?;
2✔
183

184
      match destination_format {
1✔
185
        DataFormat::Json => serde_json::to_string_pretty(&json_value)?,
2✔
186
        DataFormat::Yaml => serde_yml::to_string(&json_value)?,
2✔
187
        DataFormat::Text => String::from(content),
2✔
188
      }
189
    }
190
    DataFormat::Yaml => match destination_format {
1✔
191
      DataFormat::Yaml => {
192
        serde_yml::from_str::<serde_yml::Value>(content)?;
2✔
193
        String::from(content)
1✔
194
      }
195
      DataFormat::Json => {
196
        let cleaned_content = strip_trailing_whitespace_from_block_scalars(content);
1✔
197
        let yaml_value: serde_yml::Value = serde_yml::from_str(&cleaned_content)?;
2✔
198
        serde_json::to_string_pretty(&yaml_value)?
2✔
199
      }
200
      DataFormat::Text => String::from(content),
1✔
201
    },
202
    DataFormat::Text => String::from(content),
1✔
203
  })
204
}
205

206
/// Pretty prints a string with bat.
207
///
208
/// # Arguments
209
/// * `content` - String to be pretty printed
210
/// * `print_format` - Format to print the string as
211
///
212
/// # Returns
213
/// Returns a result with either an empty tuple or a NysmError. This can error if
214
/// bat has trouble printing in the specified format.
215
#[cfg(not(tarpaulin_include))]
216
fn pretty_print(content: String, print_format: &DataFormat) -> Result<(), NysmError> {
217
  if std::io::stdout().is_terminal() {
218
    let language_string = print_format.to_string();
219
    let mut printer = PrettyPrinter::new();
220
    let _printer = match print_format {
221
      DataFormat::Yaml | DataFormat::Json => printer.language(&language_string),
222
      _ => &mut printer,
223
    };
224

225
    #[allow(unused)]
226
    #[cfg(not(test))]
227
    let _ = _printer
228
      .grid(true)
229
      .line_numbers(true)
230
      .paging_mode(bat::PagingMode::QuitIfOneScreen)
231
      .pager("less")
232
      .theme("OneHalfDark")
233
      .input_from_bytes(content.as_bytes())
234
      .print()?;
235
  } else {
236
    println!("{}", content);
237
  }
238

239
  Ok(())
240
}
241

242
/// This method is designed to open up an editor with contents from a secret.
243
///
244
/// # Arguments
245
/// * `contents` - String contents to open up in an editor
246
/// * `path` - Temporary directory to save the contents of the file to when editing a secret
247
/// * `secret_format` - Format of the secret as given by the secret provider
248
/// * `edit_format` - Format of the secret to use while editing the secret in an editor
249
///
250
/// # Returns
251
/// Returns a result containing the changes to the contents originally passed into the method.
252
/// Can error if any IO operation fails (read/write of the temporary file).
253
///
254
fn launch_editor<P>(
1✔
255
  contents: String,
256
  path: P,
257
  secret_format: &DataFormat,
258
  edit_format: &DataFormat,
259
) -> Result<Option<String>, NysmError>
260
where
261
  P: AsRef<std::path::Path>,
262
{
263
  let language_string = edit_format.to_string().to_lowercase();
2✔
264
  let file_path = path.as_ref().join("data").with_extension(language_string);
1✔
265

266
  let file_contents = reformat_data(&contents, secret_format, edit_format)?;
1✔
267
  std::fs::write(&file_path, file_contents)?;
2✔
268

269
  let mut editor = match std::env::var("EDITOR") {
1✔
270
    Ok(editor) => editor,
1✔
271
    Err(_) => String::from("vim"),
2✔
272
  };
273

274
  editor.push(' ');
1✔
275
  editor.push_str(&file_path.to_string_lossy());
1✔
276

277
  #[cfg(test)]
278
  editor.insert_str(0, "vim(){ :; }; ");
279

280
  std::process::Command::new("/usr/bin/env")
4✔
281
    .arg("sh")
282
    .arg("-c")
283
    .arg(editor)
1✔
284
    .spawn()
285
    .expect("Error: Failed to run editor")
286
    .wait()
287
    .expect("Error: Editor returned a non-zero status");
288

289
  let file_contents: String = std::fs::read_to_string(file_path)?;
1✔
290
  let json_data = reformat_data(&file_contents, edit_format, secret_format)?;
2✔
291

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

295
    Ok(None)
1✔
296
  } else {
297
    Ok(Some(json_data))
1✔
298
  }
299
}
300

301
fn temporary_directory() -> std::io::Result<TempDir> {
1✔
302
  TempDir::new()
1✔
303
}
304

305
#[cfg(test)]
306
mod tests {
307
  use super::*;
308
  use futures::FutureExt;
309
  use lazy_static::lazy_static;
310
  use serde_json::json;
311
  use std::env::VarError;
312
  use std::future::Future;
313
  use std::panic::AssertUnwindSafe;
314
  use std::panic::{RefUnwindSafe, UnwindSafe};
315
  use std::{env, panic};
316

317
  lazy_static! {
318
    static ref SERIAL_TEST: tokio::sync::Mutex<()> = Default::default();
319
  }
320

321
  /// Sets environment variables to the given value for the duration of the closure.
322
  /// Restores the previous values when the closure completes or panics, before unwinding the panic.
323
  pub async fn async_with_env_vars<F>(kvs: Vec<(&str, Option<&str>)>, closure: F)
324
  where
325
    F: Future<Output = ()> + UnwindSafe + RefUnwindSafe,
326
  {
327
    let guard = SERIAL_TEST.lock().await;
328
    let mut old_kvs: Vec<(&str, Result<String, VarError>)> = Vec::new();
329

330
    for (k, v) in kvs {
331
      let old_v = env::var(k);
332
      old_kvs.push((k, old_v));
333
      match v {
334
        None => unsafe { env::remove_var(k) },
335
        Some(v) => unsafe { env::set_var(k, v) },
336
      }
337
    }
338

339
    match closure.catch_unwind().await {
340
      Ok(_) => {
341
        for (k, v) in old_kvs {
342
          reset_env(k, v);
343
        }
344
      }
345
      Err(err) => {
346
        for (k, v) in old_kvs {
347
          reset_env(k, v);
348
        }
349
        drop(guard);
350
        panic::resume_unwind(err);
351
      }
352
    }
353
  }
354

355
  fn reset_env(k: &str, old: Result<String, VarError>) {
356
    if let Ok(v) = old {
357
      unsafe { env::set_var(k, v) };
358
    } else {
359
      unsafe { env::remove_var(k) };
360
    }
361
  }
362

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

365
  mod reformat_data {
366
    use super::*;
367

368
    #[test]
369
    fn from_json_to_yaml() -> TestResult {
370
      let data = r#"{"banana": true, "apple": false}"#;
371
      let expected = "apple: false\nbanana: true\n";
372

373
      let result = reformat_data(data, &DataFormat::Json, &DataFormat::Yaml)?;
374

375
      assert_eq!(expected, result);
376

377
      Ok(())
378
    }
379

380
    #[test]
381
    fn from_json_to_json() -> TestResult {
382
      let data = r#"{"banana": true, "apple": false}"#;
383
      let json_value = json!({
384
        "apple": false,
385
        "banana": true,
386
      });
387
      let expected = serde_json::to_string_pretty(&json_value)?;
388

389
      let result = reformat_data(data, &DataFormat::Json, &DataFormat::Json)?;
390

391
      assert_eq!(expected, result);
392

393
      Ok(())
394
    }
395

396
    #[test]
397
    fn from_json_to_text() -> TestResult {
398
      let data = r#"{"apple":false,"banana":true}"#;
399
      let expected = json!({
400
        "apple": false,
401
        "banana": true,
402
      })
403
      .to_string();
404

405
      let result = reformat_data(data, &DataFormat::Json, &DataFormat::Text)?;
406

407
      assert_eq!(expected, result);
408

409
      Ok(())
410
    }
411

412
    #[test]
413
    fn from_yaml_to_json() -> TestResult {
414
      let yaml_string = r#"apple: false
415
banana: true
416
"#;
417
      let json_value = json!({
418
        "apple": false,
419
        "banana": true,
420
      });
421
      let expected = serde_json::to_string_pretty(&json_value)?;
422

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

425
      assert_eq!(expected, result);
426

427
      Ok(())
428
    }
429

430
    #[test]
431
    fn from_yaml_to_yaml() -> TestResult {
432
      let yaml_string = r#"apple: false
433
banana: true
434
"#;
435
      let expected = "apple: false\nbanana: true\n";
436

437
      let result = reformat_data(yaml_string, &DataFormat::Yaml, &DataFormat::Yaml)?;
438

439
      assert_eq!(expected, result);
440

441
      Ok(())
442
    }
443

444
    #[test]
445
    fn from_yaml_to_text() -> TestResult {
446
      let yaml_string = r#"apple: false
447
banana: true
448
"#;
449
      let expected = "apple: false\nbanana: true\n";
450

451
      let result = reformat_data(yaml_string, &DataFormat::Yaml, &DataFormat::Text)?;
452

453
      assert_eq!(expected, result);
454

455
      Ok(())
456
    }
457

458
    #[test]
459
    fn from_yaml_with_trailing_whitespace_to_json() -> TestResult {
460
      let yaml_string = "application.yml: |-\n  banana: false \n  apple: true\n  flasdjfljasdlfjalsd: alsdkjflasjdflajdslf\n";
461

462
      let result = reformat_data(yaml_string, &DataFormat::Yaml, &DataFormat::Json)?;
463

464
      assert!(!result.contains("false \\n"));
465
      assert!(result.contains("false\\n"));
466

467
      Ok(())
468
    }
469

470
    #[test]
471
    fn from_text() -> TestResult {
472
      let text = "This is a plain string with no data structure.";
473
      let expected = "This is a plain string with no data structure.";
474

475
      let result = reformat_data(text, &DataFormat::Text, &DataFormat::Text)?;
476

477
      assert_eq!(expected, result);
478

479
      Ok(())
480
    }
481
  }
482

483
  #[test]
484
  fn data_format_display() -> TestResult {
485
    assert_eq!(format!("{}", DataFormat::Json), "Json");
486
    assert_eq!(format!("{}", DataFormat::Yaml), "Yaml");
487
    assert_eq!(format!("{}", DataFormat::Text), "Text");
488

489
    Ok(())
490
  }
491

492
  #[test]
493
  fn test_yaml_with_mixed_whitespace_fixture() -> TestResult {
494
    let fixture_path = "tests/fixtures/mixed_whitespace.yml";
495
    let problematic_yaml =
496
      std::fs::read_to_string(fixture_path).expect("Failed to read fixture file");
497

498
    let result = reformat_data(&problematic_yaml, &DataFormat::Yaml, &DataFormat::Json)?;
499

500
    assert!(result.contains("application.yml"));
501
    assert!(result.contains("banana: false"));
502
    assert!(!result.contains("false \\n"));
503

504
    Ok(())
505
  }
506

507
  mod argument_parsing {
508
    use super::*;
509

510
    #[test]
511
    fn accepts_region() -> TestResult {
512
      let args = "nysm -r us-west-2 list".split_whitespace();
513
      let arg_parser = ArgumentParser::try_parse_from(args)?;
514

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

517
      Ok(())
518
    }
519

520
    #[test]
521
    fn sets_list_subcommand() -> TestResult {
522
      let args = "nysm -r us-west-2 list".split_whitespace();
523
      let arg_parser = ArgumentParser::try_parse_from(args)?;
524

525
      assert_eq!(arg_parser.command, Commands::List(List {}));
526

527
      Ok(())
528
    }
529

530
    #[test]
531
    fn sets_show_subcommand() -> TestResult {
532
      let args = "nysm -r us-west-2 show testing-secrets".split_whitespace();
533
      let arg_parser = ArgumentParser::try_parse_from(args)?;
534

535
      assert_eq!(
536
        arg_parser.command,
537
        Commands::Show(Show {
538
          secret_id: "testing-secrets".into(),
539
          print_format: DataFormat::Yaml,
540
          secret_format: DataFormat::Json,
541
        })
542
      );
543

544
      Ok(())
545
    }
546

547
    #[test]
548
    fn sets_edit_subcommand() -> TestResult {
549
      let args = "nysm -r us-west-2 edit testing-secrets".split_whitespace();
550
      let arg_parser = ArgumentParser::try_parse_from(args)?;
551

552
      assert_eq!(
553
        arg_parser.command,
554
        Commands::Edit(Edit {
555
          secret_id: "testing-secrets".into(),
556
          edit_format: DataFormat::Yaml,
557
          secret_format: DataFormat::Json,
558
        })
559
      );
560

561
      Ok(())
562
    }
563
  }
564

565
  #[allow(clippy::field_reassign_with_default)]
566
  mod client {
567
    use super::*;
568
    use crate::client::{GetSecretValueResult, ListSecretsResult, Secret, UpdateSecretValueResult};
569
    use async_trait::async_trait;
570

571
    #[derive(Default)]
572
    pub struct TestClient {
573
      fails_on_list_secrets: bool,
574
      fails_on_get_secret_value: bool,
575
      fails_on_update_secret_value: bool,
576
    }
577

578
    #[async_trait]
579
    impl QuerySecrets for TestClient {
580
      async fn secrets_list(&self) -> Result<ListSecretsResult, NysmError> {
581
        if self.fails_on_list_secrets {
582
          return Err(NysmError::AwsListSecretsNoList);
583
        }
584

585
        Ok(ListSecretsResult {
586
          entries: vec![Secret {
587
            name: Some("secret-one".into()),
588
            uri: Some("some-unique-id-one".into()),
589
            description: Some("blah blah blah".into()),
590
          }],
591
        })
592
      }
593

594
      async fn secret_value(&self, _secret_id: String) -> Result<GetSecretValueResult, NysmError> {
595
        if self.fails_on_get_secret_value {
596
          return Err(NysmError::AwsSecretValueNoValueString);
597
        }
598

599
        let secret_value = json!({
600
          "apple": true,
601
          "banana": false,
602
        });
603

604
        let secret_value = serde_json::to_string_pretty(&secret_value)?;
605

606
        Ok(GetSecretValueResult {
607
          secret: secret_value,
608
        })
609
      }
610

611
      async fn update_secret_value(
612
        &self,
613
        _secret_id: String,
614
        _secret_value: String,
615
      ) -> Result<UpdateSecretValueResult, NysmError> {
616
        if self.fails_on_update_secret_value {
617
          return Err(NysmError::AwsSecretValueUpdate);
618
        }
619

620
        Ok(UpdateSecretValueResult {
621
          name: Some("testy-test-secret".into()),
622
          uri: Some("some-unique-id".into()),
623
          version_id: Some("definitely-a-new-version-id".into()),
624
        })
625
      }
626
    }
627

628
    mod list_output {
629
      use super::*;
630

631
      #[tokio::test]
632
      async fn error_when_api_list_call_fails() -> TestResult {
633
        let mut client = TestClient::default();
634
        client.fails_on_list_secrets = true;
635

636
        let result = list(client, &List {}).await;
637

638
        assert_eq!(result, Err(NysmError::AwsListSecretsNoList));
639

640
        Ok(())
641
      }
642

643
      #[tokio::test]
644
      async fn ok_when_list_api_call_succeeds() -> TestResult {
645
        let client = TestClient::default();
646

647
        let result = list(client, &List {}).await;
648

649
        assert!(result.is_ok());
650

651
        Ok(())
652
      }
653
    }
654

655
    mod show_output {
656
      use super::*;
657

658
      #[tokio::test]
659
      async fn error_when_api_show_call_fails() -> TestResult {
660
        let mut client = TestClient::default();
661
        client.fails_on_get_secret_value = true;
662

663
        let result = show(
664
          client,
665
          &Show {
666
            secret_id: "fake".into(),
667
            print_format: DataFormat::Json,
668
            secret_format: DataFormat::Json,
669
          },
670
        )
671
        .await;
672

673
        assert_eq!(result, Err(NysmError::AwsSecretValueNoValueString));
674

675
        Ok(())
676
      }
677

678
      #[tokio::test]
679
      async fn ok_when_api_show_call_succeeds() -> TestResult {
680
        let client = TestClient::default();
681

682
        let result = show(
683
          client,
684
          &Show {
685
            secret_id: "fake".into(),
686
            print_format: DataFormat::Json,
687
            secret_format: DataFormat::Json,
688
          },
689
        )
690
        .await;
691

692
        assert!(result.is_ok());
693

694
        Ok(())
695
      }
696
    }
697

698
    mod edit_output {
699
      use super::*;
700

701
      #[tokio::test]
702
      async fn error_when_api_update_call_fails() -> TestResult {
703
        async_with_env_vars(
704
          vec![("EDITOR", Some("echo 'another: true\n' >> "))],
705
          AssertUnwindSafe(async {
706
            let mut client = TestClient::default();
707
            client.fails_on_update_secret_value = true;
708

709
            let result = edit(
710
              client,
711
              &Edit {
712
                secret_id: "fake".into(),
713
                edit_format: DataFormat::Yaml,
714
                secret_format: DataFormat::Json,
715
              },
716
            )
717
            .await;
718

719
            assert_eq!(result, Err(NysmError::AwsSecretValueUpdate));
720
          }),
721
        )
722
        .await;
723

724
        Ok(())
725
      }
726

727
      #[tokio::test]
728
      async fn json_error_when_api_update_call_fails_due_to_syntax() -> TestResult {
729
        async_with_env_vars(
730
          vec![("EDITOR", Some("echo 'another: true\n' >> "))],
731
          AssertUnwindSafe(async {
732
            let client = TestClient::default();
733

734
            let result = edit(
735
              client,
736
              &Edit {
737
                secret_id: "fake".into(),
738
                edit_format: DataFormat::Json,
739
                secret_format: DataFormat::Json,
740
              },
741
            )
742
            .await;
743

744
            assert_eq!(
745
              result,
746
              Err(NysmError::SerdeJson(
747
                serde_json::from_str::<String>(";;;").unwrap_err()
748
              ))
749
            );
750
          }),
751
        )
752
        .await;
753

754
        Ok(())
755
      }
756

757
      #[tokio::test]
758
      async fn yaml_error_when_api_update_call_fails_due_to_syntax() -> TestResult {
759
        async_with_env_vars(
760
          vec![("EDITOR", Some("echo '@invalid_yaml' >> "))],
761
          AssertUnwindSafe(async {
762
            let client = TestClient::default();
763

764
            let result = edit(
765
              client,
766
              &Edit {
767
                secret_id: "fake".into(),
768
                edit_format: DataFormat::Yaml,
769
                secret_format: DataFormat::Yaml,
770
              },
771
            )
772
            .await;
773

774
            assert_eq!(
775
              result,
776
              Err(NysmError::SerdeYaml(
777
                serde_yml::from_str::<String>("::::").unwrap_err()
778
              ))
779
            );
780
          }),
781
        )
782
        .await;
783

784
        Ok(())
785
      }
786

787
      #[tokio::test]
788
      async fn error_when_api_get_call_fails() -> TestResult {
789
        async_with_env_vars(
790
          vec![("EDITOR", Some("echo >/dev/null 2>&1 <<<"))],
791
          AssertUnwindSafe(async {
792
            let mut client = TestClient::default();
793
            client.fails_on_get_secret_value = true;
794

795
            let result = edit(
796
              client,
797
              &Edit {
798
                secret_id: "fake".into(),
799
                edit_format: DataFormat::Json,
800
                secret_format: DataFormat::Json,
801
              },
802
            )
803
            .await;
804

805
            assert_eq!(result, Err(NysmError::AwsSecretValueNoValueString));
806
          }),
807
        )
808
        .await;
809

810
        Ok(())
811
      }
812

813
      #[tokio::test]
814
      async fn ok_when_api_get_calls_succeed() -> TestResult {
815
        async_with_env_vars(
816
          vec![("EDITOR", Some("echo >/dev/null 2>&1 <<<"))],
817
          AssertUnwindSafe(async {
818
            let client = TestClient::default();
819

820
            let result = edit(
821
              client,
822
              &Edit {
823
                secret_id: "fake".into(),
824
                edit_format: DataFormat::Json,
825
                secret_format: DataFormat::Json,
826
              },
827
            )
828
            .await;
829

830
            assert!(result.is_ok());
831
          }),
832
        )
833
        .await;
834

835
        Ok(())
836
      }
837

838
      #[tokio::test]
839
      async fn ok_when_no_editor_environment_variable() -> TestResult {
840
        async_with_env_vars(
841
          vec![("EDITOR", None)],
842
          AssertUnwindSafe(async {
843
            let client = TestClient::default();
844

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

855
            assert!(result.is_ok());
856
          }),
857
        )
858
        .await;
859

860
        Ok(())
861
      }
862

863
      #[tokio::test]
864
      async fn ok_when_api_get_calls_succeed_and_no_change() -> TestResult {
865
        async_with_env_vars(
866
          vec![("EDITOR", Some("echo >/dev/null 2>&1 <<<"))],
867
          AssertUnwindSafe(async {
868
            let client = TestClient::default();
869

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

880
            assert!(result.is_ok());
881
          }),
882
        )
883
        .await;
884

885
        Ok(())
886
      }
887
    }
888
  }
889
}
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