diff --git a/.vscode/cspell.json b/.vscode/cspell.json index f90978a195..0884f729dd 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -60,7 +60,8 @@ "upvote", "userdelegationkey", "versionid", - "virtualmachine" + "virtualmachine", + "worktree" ], "dictionaryDefinitions": [ { @@ -160,4 +161,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/Cargo.lock b/Cargo.lock index 3f5d0a5647..f26e258de5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,7 +369,9 @@ dependencies = [ "async-trait", "azure_core", "azure_core_test_macros", + "azure_identity", "clap", + "futures", "serde", "serde_json", "tokio", @@ -463,6 +465,7 @@ version = "0.1.0" dependencies = [ "async-std", "azure_core", + "azure_core_test", "azure_identity", "futures", "rand 0.8.5", diff --git a/sdk/core/azure_core_test/Cargo.toml b/sdk/core/azure_core_test/Cargo.toml index 44e7138201..f4fc4a7ee3 100644 --- a/sdk/core/azure_core_test/Cargo.toml +++ b/sdk/core/azure_core_test/Cargo.toml @@ -12,11 +12,14 @@ keywords = ["sdk", "azure", "rest", "iot", "cloud"] categories = ["development-tools::testing"] edition.workspace = true rust-version.workspace = true +publish = false [dependencies] async-trait.workspace = true azure_core = { workspace = true, features = ["test"] } azure_core_test_macros.workspace = true +azure_identity.workspace = true +futures.workspace = true serde.workspace = true serde_json.workspace = true tracing.workspace = true diff --git a/sdk/core/azure_core_test/src/lib.rs b/sdk/core/azure_core_test/src/lib.rs index d17f466e2e..766a52fe64 100644 --- a/sdk/core/azure_core_test/src/lib.rs +++ b/sdk/core/azure_core_test/src/lib.rs @@ -7,7 +7,8 @@ pub mod proxy; pub mod recorded; mod recording; -pub use azure_core::test::TestMode; +use azure_core::Error; +pub use azure_core::{error::ErrorKind, test::TestMode}; pub use proxy::{matchers::*, sanitizers::*}; pub use recording::*; use std::path::{Path, PathBuf}; @@ -21,19 +22,32 @@ const SPAN_TARGET: &str = "test-proxy"; #[derive(Debug)] pub struct TestContext { crate_dir: &'static Path, + service_directory: &'static str, + test_module: &'static str, test_name: &'static str, recording: Option, } impl TestContext { - /// Not intended for use outside the `azure_core` crates. - #[doc(hidden)] - pub fn new(crate_dir: &'static str, test_name: &'static str) -> Self { - Self { + pub(crate) fn new( + crate_dir: &'static str, + test_module: &'static str, + test_name: &'static str, + ) -> azure_core::Result { + let service_directory = parent_of(crate_dir, "sdk") + .ok_or_else(|| Error::message(ErrorKind::Other, "not under 'sdk' folder in repo"))?; + let test_module = Path::new(test_module) + .file_stem() + .ok_or_else(|| Error::message(ErrorKind::Other, "invalid test module"))? + .to_str() + .ok_or_else(|| Error::message(ErrorKind::Other, "invalid test module"))?; + Ok(Self { crate_dir: Path::new(crate_dir), + service_directory, + test_module, test_name, recording: None, - } + }) } /// Gets the root directory of the crate under test. @@ -52,15 +66,69 @@ impl TestContext { .expect("not recording or playback started") } + /// Gets the service directory containing the current test. + /// + /// This is the directory under `sdk/` within the repository e.g., "core" in `sdk/core`. + pub fn service_directory(&self) -> &'static str { + self.service_directory + } + /// Gets the test data directory under [`Self::crate_dir`]. pub fn test_data_dir(&self) -> PathBuf { self.crate_dir.join("tests/data") } + /// Gets the module name containing the current test. + pub fn test_module(&self) -> &'static str { + self.test_module + } + /// Gets the current test function name. pub fn test_name(&self) -> &'static str { self.test_name } + + /// Gets the recording assets file under the crate directory. + pub(crate) fn test_recording_assets_file(&self) -> Option { + #[cfg(target_arch = "wasm32")] + { + None + } + + #[cfg(not(target_arch = "wasm32"))] + { + let path = + find_ancestor(self.crate_dir, "assets.json").unwrap_or_else(|err| panic!("{err}")); + path.as_path().to_str().map(String::from) + } + } + + /// Gets the recording file of the current test. + pub(crate) fn test_recording_file(&self) -> String { + let path = self + .test_data_dir() + .join(self.test_module) + .join(self.test_name) + .as_path() + .with_extension("json"); + path.to_str() + .map(String::from) + .unwrap_or_else(|| panic!("{path:?} is invalid")) + } +} + +fn parent_of<'a>(dir: &'a str, name: &'static str) -> Option<&'a str> { + let mut child = None; + + let dir = Path::new(dir); + let components = dir.components().rev(); + for dir in components { + if dir.as_os_str() == name { + return child; + } + child = dir.as_os_str().to_str(); + } + None } #[cfg(not(target_arch = "wasm32"))] @@ -70,6 +138,15 @@ fn find_ancestor(dir: impl AsRef, name: &str) -> azure_core::Result( azure_core::error::ErrorKind::Io, @@ -83,7 +160,8 @@ mod tests { #[test] fn test_content_new() { - let ctx = TestContext::new(env!("CARGO_MANIFEST_DIR"), "test_content_new"); + let ctx = + TestContext::new(env!("CARGO_MANIFEST_DIR"), file!(), "test_content_new").unwrap(); assert!(ctx.recording.is_none()); assert!(ctx .crate_dir() @@ -91,6 +169,22 @@ mod tests { .unwrap() .replace("\\", "/") .ends_with("sdk/core/azure_core_test")); + assert_eq!(ctx.test_module(), "lib"); assert_eq!(ctx.test_name(), "test_content_new"); + assert!(ctx + .test_recording_file() + .as_str() + .replace("\\", "/") + .ends_with("sdk/core/azure_core_test/tests/data/lib/test_content_new.json")); + } + + #[test] + fn test_parent_of() { + assert_eq!( + parent_of("~/src/azure-sdk-for-rust/sdk/core", "sdk"), + Some("core"), + ); + assert!(parent_of("~/src/azure-sdk-for-rust/sdk/", "sdk").is_none()); + assert!(parent_of("~/src/azure-sdk-for-rust/sdk/core", "should_not_exist").is_none()); } } diff --git a/sdk/core/azure_core_test/src/proxy/client.rs b/sdk/core/azure_core_test/src/proxy/client.rs index 88667c6a32..61e3a86bd4 100644 --- a/sdk/core/azure_core_test/src/proxy/client.rs +++ b/sdk/core/azure_core_test/src/proxy/client.rs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +use std::{convert::Infallible, str::FromStr}; + use super::{ matchers::Matcher, models::{PlaybackStartResult, RecordStartResult, StartPayload, VariablePayload}, @@ -193,6 +195,19 @@ impl Header for &RecordingId { } } +impl AsRef for RecordingId { + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +impl FromStr for RecordingId { + type Err = Infallible; + fn from_str(value: &str) -> std::result::Result { + Ok(RecordingId(value.to_string())) + } +} + #[derive(Debug, Default)] pub struct ClientRecordStartOptions<'a> { pub method_options: ClientMethodOptions<'a>, diff --git a/sdk/core/azure_core_test/src/proxy/mod.rs b/sdk/core/azure_core_test/src/proxy/mod.rs index 6c4a5bcf0c..ab4521d1ca 100644 --- a/sdk/core/azure_core_test/src/proxy/mod.rs +++ b/sdk/core/azure_core_test/src/proxy/mod.rs @@ -4,7 +4,7 @@ //! Wrappers for the [Test Proxy](https://github.com/Azure/azure-sdk-tools/blob/main/tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md) service. pub(crate) mod client; pub(crate) mod matchers; -mod models; +pub(crate) mod models; pub(crate) mod policy; pub(crate) mod sanitizers; diff --git a/sdk/core/azure_core_test/src/proxy/models.rs b/sdk/core/azure_core_test/src/proxy/models.rs index fe5ef30c2e..afbb2094c7 100644 --- a/sdk/core/azure_core_test/src/proxy/models.rs +++ b/sdk/core/azure_core_test/src/proxy/models.rs @@ -82,6 +82,13 @@ pub struct VariablePayload { pub variables: HashMap, } +impl TryFrom for RequestContent { + type Error = azure_core::Error; + fn try_from(value: VariablePayload) -> Result { + RequestContent::try_from(to_json(&value)?) + } +} + #[derive(Debug, Deserialize)] pub struct PlaybackStartResult { #[serde(skip)] diff --git a/sdk/core/azure_core_test/src/recorded.rs b/sdk/core/azure_core_test/src/recorded.rs index cfe5b775f3..9b093ec5d0 100644 --- a/sdk/core/azure_core_test/src/recorded.rs +++ b/sdk/core/azure_core_test/src/recorded.rs @@ -2,9 +2,8 @@ // Licensed under the MIT License. //! Live recording and playing back of client library tests. -use crate::proxy::Proxy; use crate::{ - proxy::{client::Client, ProxyOptions}, + proxy::{client::Client, Proxy, ProxyOptions}, recording::Recording, TestContext, }; @@ -23,37 +22,57 @@ static TEST_PROXY: OnceCell>> = OnceCell::const_new(); /// The [Test Proxy](https://github.com/Azure/azure-sdk-tools/blob/main/tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md) service will be started as needed. /// Every `#[recorded::test]` will call this automatically, but it can also be called manually by any other test e.g., those attributed with `#[tokio::test]`. pub async fn start( - ctx: &mut TestContext, test_mode: TestMode, + crate_dir: &'static str, + test_module: &'static str, + test_name: &'static str, #[cfg_attr(target_arch = "wasm32", allow(unused_variables))] options: Option, -) -> Result<()> { +) -> Result { + let mut ctx = TestContext::new(crate_dir, test_module, test_name)?; + #[cfg(target_arch = "wasm32")] let proxy: Option> = None; #[cfg(not(target_arch = "wasm32"))] - let proxy = match test_mode { - TestMode::Live => None, - _ => Some( - TEST_PROXY - .get_or_init(|| async { - crate::proxy::start(ctx.test_data_dir(), options) - .await - .map(Arc::new) - }) - .await - .as_ref() - .map(|proxy| proxy.clone()) - .map_err(|err| azure_core::Error::new(err.kind().clone(), err))?, - ), + let proxy = { + let test_data_dir = ctx.test_data_dir(); + match test_mode { + TestMode::Live => None, + _ => Some( + TEST_PROXY + .get_or_init(|| async move { + crate::proxy::start(test_data_dir, options) + .await + .map(Arc::new) + }) + .await + .as_ref() + .map(|proxy| proxy.clone()) + .map_err(|err| azure_core::Error::new(err.kind().clone(), err))?, + ), + } }; + // TODO: Could we cache the client? Hypothetically, this function should only run once per `tests/*` file so it should be practical. let mut client = None; if let Some(proxy) = &proxy { client = Some(Client::new(proxy.endpoint().clone())?); } - let span = debug_span!(target: crate::SPAN_TARGET, "recording", mode = ?test_mode, test = ?ctx.test_name()); - ctx.recording = Some(Recording::new(test_mode, span, proxy, client)); + let span = + debug_span!(target: crate::SPAN_TARGET, "recording", mode = ?test_mode, test = ?test_name); + + let mut recording = Recording::new( + test_mode, + span.entered(), + proxy.clone(), + client, + ctx.service_directory(), + ctx.test_recording_file(), + ctx.test_recording_assets_file(), + ); + recording.start().await?; - Ok(()) + ctx.recording = Some(recording); + Ok(ctx) } diff --git a/sdk/core/azure_core_test/src/recording.rs b/sdk/core/azure_core_test/src/recording.rs index 985e1f6dbe..522edb2eff 100644 --- a/sdk/core/azure_core_test/src/recording.rs +++ b/sdk/core/azure_core_test/src/recording.rs @@ -6,30 +6,44 @@ use crate::{ proxy::{ client::{Client, ClientAddSanitizerOptions, ClientSetMatcherOptions, RecordingId}, + models::{StartPayload, VariablePayload}, policy::RecordingPolicy, Proxy, }, Matcher, Sanitizer, }; use azure_core::{ + credentials::TokenCredential, error::ErrorKind, headers::{AsHeaders, HeaderName, HeaderValue}, test::TestMode, ClientOptions, Header, }; -use std::{cell::OnceCell, sync::Arc}; -use tracing::Span; +use azure_identity::DefaultAzureCredential; +use std::{ + borrow::Cow, + cell::OnceCell, + collections::HashMap, + env, + sync::{Arc, RwLock}, +}; +use tracing::span::EnteredSpan; /// Represents a playback or recording session using the [`Proxy`]. #[derive(Debug)] pub struct Recording { test_mode: TestMode, // Keep the span open for our lifetime. - _span: Span, + #[allow(dead_code)] + span: EnteredSpan, _proxy: Option>, client: Option, policy: OnceCell>, + service_directory: String, + recording_file: String, + recording_assets_file: Option, id: Option, + variables: RwLock>, } impl Recording { @@ -50,6 +64,18 @@ impl Recording { client.add_sanitizer(_sanitizer, Some(options)).await } + /// Gets a [`TokenCredential`] you can use for testing. + /// + /// # Panics + /// + /// Panics if the [`TokenCredential`] could not be created. + pub fn credential(&self) -> Arc { + match DefaultAzureCredential::new() { + Ok(credential) => credential as Arc, + Err(err) => panic!("{err}"), + } + } + /// Instruments the [`ClientOptions`] to support recording and playing back of session records. /// /// # Examples @@ -110,7 +136,7 @@ impl Recording { /// /// This only affects [`TestMode::Record`] mode and is intended for cleanup. /// When [`Recording::test_mode()`] is [`TestMode::Playback`] you should avoid sending those requests. - pub fn skip(&mut self, skip: Skip) -> azure_core::Result> { + pub fn skip(&self, skip: Skip) -> azure_core::Result> { self.set_skip(Some(skip))?; Ok(SkipGuard(self)) } @@ -120,36 +146,76 @@ impl Recording { self.test_mode } - /// Gets the named variable from the environment or recording. - pub fn var(&self, name: impl AsRef) -> Option { + /// Gets a required variable from the environment or recording. + pub fn var(&self, key: K, options: Option) -> String + where + K: AsRef, + { + let key = key.as_ref(); + self.var_opt(key, options) + .unwrap_or_else(|| panic!("{key} is not set")) + } + + /// Gets an optional variable from the environment or recording. + pub fn var_opt(&self, key: K, options: Option) -> Option + where + K: AsRef, + { + let key = key.as_ref(); + if self.test_mode == TestMode::Playback { + let variables = self.variables.read().map_err(read_lock_error).ok()?; + return variables.get(key).map(Into::into); + } + + let value = self.env(key); if self.test_mode == TestMode::Live { - return std::env::var(name.as_ref()).ok(); + return value; } - // TODO: attempt to get it from the recording or fallthrough to the environment; or, do we need separate calls like .NET to fallthrough? - todo!() + let mut variables = self.variables.write().map_err(write_lock_error).ok()?; + variables.insert(key.into(), Value::from(value.as_ref(), options)); + value } } impl Recording { pub(crate) fn new( test_mode: TestMode, - span: Span, + span: EnteredSpan, proxy: Option>, client: Option, + service_directory: &'static str, + recording_file: String, + recording_assets_file: Option, ) -> Self { Self { test_mode, - _span: span, + span, _proxy: proxy, client, policy: OnceCell::new(), + service_directory: service_directory.into(), + recording_file, + recording_assets_file, id: None, + variables: RwLock::new(HashMap::new()), } } - fn set_skip(&mut self, skip: Option) -> azure_core::Result<()> { - let Some(policy) = self.policy.get_mut() else { + fn env(&self, key: K) -> Option + where + K: AsRef, + { + const AZURE_PREFIX: &str = "AZURE_"; + + env::var_os(self.service_directory.clone() + "_" + key.as_ref()) + .or_else(|| env::var_os(key.as_ref())) + .or_else(|| env::var_os(String::from(AZURE_PREFIX) + key.as_ref())) + .and_then(|v| v.into_string().ok()) + } + + fn set_skip(&self, skip: Option) -> azure_core::Result<()> { + let Some(policy) = self.policy.get() else { return Ok(()); }; @@ -161,6 +227,92 @@ impl Recording { Ok(()) } + + /// Starts recording or playback. + /// + /// If playing back a recording, environment variable that were recorded will be reloaded. + pub(crate) async fn start(&mut self) -> azure_core::Result<()> { + let Some(client) = &self.client else { + return Ok(()); + }; + + let payload = StartPayload { + recording_file: self.recording_file.clone(), + recording_assets_file: self.recording_assets_file.clone(), + }; + + // TODO: Should RecordingId be used everywhere and models implement AsHeaders and FromHeaders? + let recording_id = match self.test_mode { + TestMode::Playback => { + let result = client.playback_start(payload.try_into()?, None).await?; + let mut variables = self.variables.write().map_err(write_lock_error)?; + variables.extend(result.variables.into_iter().map(|(k, v)| (k, v.into()))); + + result.recording_id + } + TestMode::Record => { + client + .record_start(payload.try_into()?, None) + .await? + .recording_id + } + mode => panic!("{mode:?} not supported"), + }; + self.id = Some(recording_id.parse()?); + + Ok(()) + } + + /// Stops the recording or playback. + /// + /// If recording, environment variables that were retrieved will be recorded. + pub(crate) async fn stop(&self) -> azure_core::Result<()> { + let Some(client) = &self.client else { + return Ok(()); + }; + + let Some(recording_id) = self.id.as_ref() else { + tracing::error!(target: crate::SPAN_TARGET, parent: &self.span, "missing recording ID"); + + return Err(azure_core::Error::message( + ErrorKind::Other, + "missing recording ID", + )); + }; + + match self.test_mode { + TestMode::Playback => client.playback_stop(recording_id.as_ref(), None).await, + TestMode::Record => { + let payload = { + let variables = self.variables.read().map_err(read_lock_error)?; + VariablePayload { + variables: HashMap::from_iter( + variables.iter().map(|(k, v)| (k.clone(), v.into())), + ), + } + }; + client + .record_stop(recording_id.as_ref(), payload.try_into()?, None) + .await + } + mode => panic!("{mode:?} not supported"), + } + } +} + +impl Drop for Recording { + /// Stops the recording or playback. + fn drop(&mut self) { + futures::executor::block_on(self.stop()).unwrap_or_else(|err| panic!("{err}")); + } +} + +fn read_lock_error(_: impl std::error::Error) -> azure_core::Error { + azure_core::Error::message(ErrorKind::Other, "failed to lock variables for read") +} + +fn write_lock_error(_: impl std::error::Error) -> azure_core::Error { + azure_core::Error::message(ErrorKind::Other, "failed to lock variables for write") } /// What to skip when recording to a file. @@ -192,7 +344,7 @@ impl Header for Skip { /// When the `SkipGuard` is dropped, recording requests and responses will begin again. /// /// Returned from [`Recording::skip()`]. -pub struct SkipGuard<'a>(&'a mut Recording); +pub struct SkipGuard<'a>(&'a Recording); impl Drop for SkipGuard<'_> { fn drop(&mut self) { @@ -201,3 +353,63 @@ impl Drop for SkipGuard<'_> { } } } + +/// Options for getting variables from a [`Recording`]. +#[derive(Clone, Debug)] +pub struct VarOptions { + /// Whether to sanitize the variable value with [`VarOptions::sanitize_value`]. + pub sanitize: bool, + + /// The value to use for sanitized variables. + /// + /// The default is "Sanitized". + pub sanitize_value: Cow<'static, str>, +} + +impl Default for VarOptions { + fn default() -> Self { + Self { + sanitize: false, + sanitize_value: Cow::Borrowed(crate::SANITIZED_VALUE), + } + } +} + +#[derive(Debug)] +struct Value { + value: String, + sanitized: Option>, +} + +impl Value { + fn from(value: Option, options: Option) -> Self + where + S: Into, + { + Self { + value: value.map_or_else(String::new, Into::into), + sanitized: match options { + Some(options) if options.sanitize => Some(options.sanitize_value.clone()), + _ => None, + }, + } + } +} + +impl From for Value { + fn from(value: String) -> Self { + Self { + value, + sanitized: None, + } + } +} + +impl From<&Value> for String { + fn from(value: &Value) -> Self { + value + .sanitized + .as_ref() + .map_or_else(|| value.value.clone(), |v| v.to_string()) + } +} diff --git a/sdk/core/azure_core_test_macros/Cargo.toml b/sdk/core/azure_core_test_macros/Cargo.toml index e04c7ef0f0..df05a74a89 100644 --- a/sdk/core/azure_core_test_macros/Cargo.toml +++ b/sdk/core/azure_core_test_macros/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["azure", "cloud", "iot", "rest", "sdk"] categories = ["development-tools"] edition.workspace = true rust-version.workspace = true +publish = false [lib] proc-macro = true diff --git a/sdk/core/azure_core_test_macros/src/lib.rs b/sdk/core/azure_core_test_macros/src/lib.rs index 1c06c87c1b..d33049ceee 100644 --- a/sdk/core/azure_core_test_macros/src/lib.rs +++ b/sdk/core/azure_core_test_macros/src/lib.rs @@ -9,6 +9,32 @@ use proc_macro::TokenStream; /// Attribute client library tests to play back recordings, record sessions, or execute tests without recording. /// +/// # Examples +/// +/// For live or recorded tests, you must be async and accept a `TestContext`. +/// You may return a `Result`. +/// +/// ``` +/// use azure_core_test::{recorded, TestContext}; +/// +/// #[recorded::test] +/// async fn test(ctx: TestContext) { +/// todo!() +/// } +/// ``` +/// +/// For live-only tests, you must be async and may accept a `TestContext`. +/// You may return a `Result`. +/// +/// ``` +/// use azure_core_test::recorded; +/// +/// #[recorded::test(live)] +/// async fn test() -> Result<(), Box> { +/// todo!() +/// } +/// ``` +/// /// Read documentation for `azure_core_test` for more information and examples. #[proc_macro_attribute] pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream { diff --git a/sdk/core/azure_core_test_macros/src/test.rs b/sdk/core/azure_core_test_macros/src/test.rs index f0722f2a99..586fad8690 100644 --- a/sdk/core/azure_core_test_macros/src/test.rs +++ b/sdk/core/azure_core_test_macros/src/test.rs @@ -50,8 +50,13 @@ pub fn parse_test(attr: TokenStream, item: TokenStream) -> Result { let test_mode = test_mode_to_tokens(test_mode); quote! { #[allow(dead_code)] - let mut ctx = ::azure_core_test::TestContext::new(env!("CARGO_MANIFEST_DIR"), stringify!(#fn_name)); - ::azure_core_test::recorded::start(&mut ctx, #test_mode, ::std::option::Option::None).await?; + let mut ctx = ::azure_core_test::recorded::start( + #test_mode, + env!("CARGO_MANIFEST_DIR"), + file!(), + stringify!(#fn_name), + ::std::option::Option::None, + ).await?; #fn_name(ctx).await } } diff --git a/sdk/keyvault/azure_security_keyvault_secrets/Cargo.toml b/sdk/keyvault/azure_security_keyvault_secrets/Cargo.toml index 75162ff142..bfafaaed84 100644 --- a/sdk/keyvault/azure_security_keyvault_secrets/Cargo.toml +++ b/sdk/keyvault/azure_security_keyvault_secrets/Cargo.toml @@ -17,14 +17,15 @@ categories = ["api-bindings"] [dependencies] async-std = { workspace = true, features = ["attributes", "tokio1"] } -azure_core = { workspace = true} -futures = { workspace = true} +azure_core = { workspace = true } +futures = { workspace = true } serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true} -time = { workspace = true} +serde_json = { workspace = true } +time = { workspace = true } typespec_client_core = { workspace = true, features = ["derive"] } [dev-dependencies] +azure_core_test.workspace = true azure_identity.workspace = true rand.workspace = true tokio.workspace = true diff --git a/sdk/keyvault/azure_security_keyvault_secrets/src/generated/models.rs b/sdk/keyvault/azure_security_keyvault_secrets/src/generated/models.rs index 0f73aef004..1d364931d1 100644 --- a/sdk/keyvault/azure_security_keyvault_secrets/src/generated/models.rs +++ b/sdk/keyvault/azure_security_keyvault_secrets/src/generated/models.rs @@ -282,7 +282,6 @@ pub struct SecretRestoreParameters { /// The secret set parameters. #[derive(Clone, Debug, Default, Deserialize, Serialize, azure_core::Model)] -#[non_exhaustive] pub struct SecretSetParameters { /// Type of the secret value such as a password. #[serde(rename = "contentType", skip_serializing_if = "Option::is_none")] diff --git a/sdk/keyvault/azure_security_keyvault_secrets/tests/secret_client.rs b/sdk/keyvault/azure_security_keyvault_secrets/tests/secret_client.rs new file mode 100644 index 0000000000..f1041b3c77 --- /dev/null +++ b/sdk/keyvault/azure_security_keyvault_secrets/tests/secret_client.rs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#![cfg_attr(target_arch = "wasm32", allow(unused_imports))] + +use azure_core_test::{recorded, TestContext}; +use azure_security_keyvault_secrets::{ + models::SecretSetParameters, SecretClient, SecretClientOptions, +}; + +#[recorded::test] +async fn secret_roundtrip(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let mut options = SecretClientOptions::default(); + recording.instrument(&mut options.client_options); + + let client = SecretClient::new( + recording.var("AZURE_KEYVAULT_URL", None).as_str(), + recording.credential(), + Some(options), + )?; + + let body = SecretSetParameters { + value: Some("secret-value".into()), + ..Default::default() + }; + client + // TODO: https://github.com/Azure/typespec-rust/issues/223 + .set_secret("secret-name".into(), body.try_into()?, None) + .await?; + + let secret = client + // TODO: https://github.com/Azure/typespec-rust/issues/223 + .get_secret("secret-name".into(), "".into(), None) + .await? + .into_body() + .await?; + + assert_eq!("secret-value", secret.value.unwrap()); + Ok(()) +}