Skip to content

Commit c112380

Browse files
authored
Add delete + purge test to Key Vault Secrets (Azure#2069)
1 parent 31dbecf commit c112380

File tree

3 files changed

+169
-3
lines changed

3 files changed

+169
-3
lines changed

sdk/keyvault/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "rust",
44
"TagPrefix": "rust/keyvault",
5-
"Tag": "rust/keyvault_9528e38b39"
5+
"Tag": "rust/keyvault_84345f715b"
66
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
use std::{ops::Mul, pin::Pin, time::Duration};
5+
use tokio::time::{sleep, Sleep};
6+
7+
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60 * 3);
8+
const MAX_DURATION: Duration = Duration::from_secs(30);
9+
10+
/// Use in a retry loop.
11+
#[derive(Debug)]
12+
pub struct Retry(RetryImpl);
13+
14+
#[derive(Debug)]
15+
enum RetryImpl {
16+
Immediate,
17+
Progressive {
18+
duration: Duration,
19+
timeout: Pin<Box<Sleep>>,
20+
},
21+
}
22+
23+
impl Retry {
24+
/// Creates a retry that returns immediately.
25+
///
26+
/// Useful when playing back recorded tests.
27+
pub fn immediate() -> Self {
28+
Self(RetryImpl::Immediate)
29+
}
30+
31+
/// Creates a progressive retry that starts at 500 milliseconds and doubles up until 30 seconds.
32+
///
33+
/// Useful when recording tests.
34+
///
35+
/// # Arguments
36+
///
37+
/// * `timeout` - How long until the retry times out. If `None`, the default of 3 minutes is used.
38+
/// This only affects calls to [`Retry::next()`]. Actual timeout may be a little longer depending on your workload.
39+
pub fn progressive(timeout: Option<Duration>) -> Self {
40+
let timeout = Box::pin(sleep(timeout.unwrap_or(DEFAULT_TIMEOUT)));
41+
Self(RetryImpl::Progressive {
42+
duration: Duration::from_millis(500),
43+
timeout,
44+
})
45+
}
46+
47+
/// Waits on the next retry interval or returns `None` if timed out.
48+
pub async fn next(&mut self) -> Option<()> {
49+
match &mut self.0 {
50+
RetryImpl::Immediate => Some(()),
51+
RetryImpl::Progressive { duration, timeout } => {
52+
tokio::select! {
53+
_ = sleep(*duration) => {},
54+
_ = timeout => {
55+
return None;
56+
},
57+
};
58+
59+
*duration = duration.mul(2);
60+
if *duration > MAX_DURATION {
61+
*duration = MAX_DURATION;
62+
}
63+
Some(())
64+
}
65+
}
66+
}
67+
68+
/// Gets the current [`Duration`] for the next call to [`Retry::next()`].
69+
pub fn duration(&self) -> Option<Duration> {
70+
match &self.0 {
71+
RetryImpl::Immediate => None,
72+
RetryImpl::Progressive { duration, .. } => Some(*duration),
73+
}
74+
}
75+
}
76+
77+
#[tokio::test]
78+
async fn test_retry_immediate() {
79+
let mut i = 0;
80+
let mut retry = Retry::immediate();
81+
while (retry.next().await).is_some() {
82+
println!("Attempt {i}");
83+
i += 1;
84+
if i >= 5 {
85+
break;
86+
}
87+
}
88+
}
89+
90+
#[ignore = "literal waste of time normally"]
91+
#[tokio::test]
92+
async fn test_retry_progressive() {
93+
let mut i = 0;
94+
let mut retry = Retry::progressive(Some(Duration::from_secs(2)));
95+
while (retry.next().await).is_some() {
96+
println!("Attempt {i}");
97+
i += 1;
98+
if i >= 5 {
99+
break;
100+
}
101+
}
102+
}

sdk/keyvault/azure_security_keyvault_secrets/tests/secret_client.rs

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33

44
#![cfg_attr(target_arch = "wasm32", allow(unused_imports))]
55

6-
use azure_core::Result;
7-
use azure_core_test::{recorded, TestContext};
6+
mod common;
7+
8+
use azure_core::{Result, StatusCode};
9+
use azure_core_test::{recorded, TestContext, TestMode};
810
use azure_security_keyvault_secrets::{
911
models::{SecretSetParameters, SecretUpdateParameters},
1012
ResourceExt as _, SecretClient, SecretClientOptions,
1113
};
14+
use common::Retry;
1215
use futures::TryStreamExt;
1316
use std::collections::HashMap;
1417

@@ -153,3 +156,64 @@ async fn list_secrets(ctx: TestContext) -> Result<()> {
153156

154157
Ok(())
155158
}
159+
160+
#[recorded::test]
161+
async fn purge_secret(ctx: TestContext) -> Result<()> {
162+
let recording = ctx.recording();
163+
recording.remove_sanitizers(REMOVE_SANITIZERS).await?;
164+
165+
let mut options = SecretClientOptions::default();
166+
recording.instrument(&mut options.client_options);
167+
168+
let client = SecretClient::new(
169+
recording.var("AZURE_KEYVAULT_URL", None).as_str(),
170+
recording.credential(),
171+
Some(options),
172+
)?;
173+
174+
// Create a secret.
175+
let secret = client
176+
.set_secret(
177+
"purge-secret",
178+
SecretSetParameters {
179+
value: Some("secret-value".into()),
180+
..Default::default()
181+
}
182+
.try_into()?,
183+
None,
184+
)
185+
.await?
186+
.into_body()
187+
.await?;
188+
189+
// Delete the secret.
190+
let name = secret.resource_id()?.name;
191+
client.delete_secret(name.as_ref(), None).await?;
192+
193+
// Because deletes may not happen right away, try purging in a loop.
194+
let mut retry = match recording.test_mode() {
195+
TestMode::Playback => Retry::immediate(),
196+
_ => Retry::progressive(None),
197+
};
198+
199+
loop {
200+
match client.purge_deleted_secret(name.as_ref(), None).await {
201+
Ok(_) => {
202+
println!("{name} has been purged");
203+
break;
204+
}
205+
Err(err) if matches!(err.http_status(), Some(StatusCode::Conflict)) => {
206+
println!(
207+
"Retrying in {} seconds",
208+
retry.duration().unwrap_or_default().as_secs_f32()
209+
);
210+
if retry.next().await.is_none() {
211+
return Err(err);
212+
}
213+
}
214+
Err(err) => return Err(err),
215+
}
216+
}
217+
218+
Ok(())
219+
}

0 commit comments

Comments
 (0)