diff --git a/.editorconfig b/.editorconfig index 823d0dac3f..704728d305 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,9 @@ indent_size = 4 indent_style = space trim_trailing_whitespace = true +[*.bicep] +indent_size = 2 + [*.json] indent_size = 2 diff --git a/.vscode/cspell.json b/.vscode/cspell.json index f90978a195..ea3634fcd3 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -41,12 +41,14 @@ "hmac", "iothub", "keyvault", + "lldb", "maxresults", "maxsize", "msrc", "newtonsoft", "oidc", "pageable", + "pageables", "pkce", "pkcs", "posix", @@ -60,7 +62,8 @@ "upvote", "userdelegationkey", "versionid", - "virtualmachine" + "virtualmachine", + "worktree" ], "dictionaryDefinitions": [ { @@ -160,4 +163,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd13d47de0..eb3b8d9552 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ When you run `cargo build`, toolchain version [1.80](https://releases.rs/docs/1.80.0/) and necessary components will be installed automatically. -- (Recommended) If you use [Visual Studio Code](https://code.visualstudio.com), install recommended extensions to improve your development experience. +- (Recommended) If you use [Visual Studio Code], install recommended extensions to improve your development experience. ## Generated code @@ -21,6 +21,77 @@ To build any library in the Azure SDK for Rust navigate to the library's project [TODO] Add instructions on how to run tests for a specific project. [TODO] Add instructions for write new tests. +### Debugging with Visual Studio Code + +[Visual Studio Code] with recommended extensions installed can be used to run and debug tests for a module or individual tests. + +If you need to debug a test, you can use the LLDB extension and set environment variables as needed. For example, to debug recording a specific test, +your `.vscode/launch.json` file might look something like: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Record secret_roundtrip", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=secret_client", + "--package=azure_security_keyvault_secrets", + "secret_roundtrip" + ], + "filter": { + "name": "secret_client", + "kind": "test" + }, + "env": { + } + }, + "cwd": "${workspaceFolder}", + "env": { + "AZURE_KEYVAULT_URL": "https://my-vault.vault.azure.net/", + "PROXY_MANUAL_START": "true", + "RUST_LOG": "trace" + } + }, + { + "type": "lldb", + "request": "launch", + "name": "Play back secret_roundtrip", + "cargo": { + "args": [ + "test", + "--no-run", + "--test=secret_client", + "--package=azure_security_keyvault_secrets", + "secret_roundtrip" + ], + "filter": { + "name": "secret_client", + "kind": "test" + }, + "env": { + "AZURE_TEST_MODE": "playback" + } + }, + "cwd": "${workspaceFolder}", + "env": { + "RUST_LOG": "trace" + } + } + ] +} +``` + +You can also start the [Test Proxy] manually, in which can you add to the outer `env` above to `"PROXY_MANUAL_START": "true"`. + +To enable tracing, you can add the `RUST_LOG` environment variable as shown above using the [same format supported by `env_logger`](https://docs.rs/env_logger/latest/env_logger/#enabling-logging). +The targets are the crate names if you want to trace more or less for specific targets e.g., `RUST_LOG=info,azure_core=trace` to trace information messages by default but detailed traces for the `azure_core` crate. + ## Code Review Process Before a pull request will be considered by the Azure SDK team, the following requirements must be met: @@ -94,3 +165,6 @@ Samples may take the following categories of dependencies: - **Tiered licensed**: Offerings that enable readers to use the license tier that corresponds to their characteristics. For example, tiers may be available for students, hobbyists, or companies with defined revenue thresholds. For offerings with tiered licenses, strive to limit our use in tutorials to the features available in the lowest tier. This policy enables the widest audience for the article. [Docker](https://www.docker.com/), [IdentityServer](https://duendesoftware.com/products/identityserver), [ImageSharp](https://sixlabors.com/products/imagesharp/), and [Visual Studio](https://visualstudio.com) are examples of this license type. In general, we prefer taking dependencies on licensed components in the order of the listed categories. In cases where the category may not be well known, we'll document the category so that readers understand the choice that they're making by using that dependency. + +[Test Proxy]: https://github.com/Azure/azure-sdk-tools/blob/main/tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md +[Visual Studio Code]: https://code.visualstudio.com diff --git a/Cargo.lock b/Cargo.lock index 49ec632e85..3ada5ee246 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,13 +369,16 @@ dependencies = [ "async-trait", "azure_core", "azure_core_test_macros", + "azure_identity", "clap", + "futures", "serde", "serde_json", "tokio", "tracing", "tracing-subscriber", "typespec_client_core", + "url", ] [[package]] @@ -463,6 +466,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/src/test.rs b/sdk/core/azure_core/src/test.rs index 00e743b25c..b46ca5928e 100644 --- a/sdk/core/azure_core/src/test.rs +++ b/sdk/core/azure_core/src/test.rs @@ -36,8 +36,8 @@ impl fmt::Debug for TestMode { } } -impl From<&TestMode> for &'static str { - fn from(mode: &TestMode) -> Self { +impl From for &'static str { + fn from(mode: TestMode) -> Self { match mode { TestMode::Playback => "playback", TestMode::Record => "record", @@ -46,6 +46,12 @@ impl From<&TestMode> for &'static str { } } +impl From<&TestMode> for &'static str { + fn from(mode: &TestMode) -> Self { + TestMode::into(*mode) + } +} + impl FromStr for TestMode { type Err = Error; diff --git a/sdk/core/azure_core_test/Cargo.toml b/sdk/core/azure_core_test/Cargo.toml index 44e7138201..10189c4cfb 100644 --- a/sdk/core/azure_core_test/Cargo.toml +++ b/sdk/core/azure_core_test/Cargo.toml @@ -12,18 +12,36 @@ keywords = ["sdk", "azure", "rest", "iot", "cloud"] categories = ["development-tools::testing"] edition.workspace = true rust-version.workspace = true +publish = false + +[features] +default = [] +tracing = ["tracing-subscriber"] [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 +tracing-subscriber = { workspace = true, features = [ + "env-filter", + "fmt", +], optional = true } typespec_client_core = { workspace = true, features = ["derive"] } +url.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tokio = { workspace = true, features = ["io-util", "process", "sync", "time"] } +tokio = { workspace = true, features = [ + "io-util", + "process", + "rt-multi-thread", + "sync", + "time", +] } [dev-dependencies] clap.workspace = true diff --git a/sdk/core/azure_core_test/README.md b/sdk/core/azure_core_test/README.md index f26d5ce05b..12d06de3ad 100644 --- a/sdk/core/azure_core_test/README.md +++ b/sdk/core/azure_core_test/README.md @@ -4,12 +4,12 @@ The types and functions in this crate help test client libraries built on `azure ## Client methods -To test client methods using our [Test Proxy], you can attribute both synchronous and asynchronous (recommend) tests +To test client methods using our [Test Proxy] or run against live resources, you can attribute asynchronous tests using the `#[recorded::test]` attribute: ```rust -use azure_core_test::{recorded, TestContext}; use azure_core::Result; +use azure_core_test::{recorded, TestContext}; #[recorded::test] async fn get_secret(ctx: TestContext) -> Result<()> { @@ -17,9 +17,11 @@ async fn get_secret(ctx: TestContext) -> Result<()> { } ``` -The `TestContext` parameter is required unless your test function is attribute as `#[recorded::test(live)]` (live-only), -in which case it is optional. You can name the parameter whatever you want. +The `TestContext` parameter is required unless your test function is attributed as `#[recorded::test(live)]` (live-only). +You can name the parameter whatever you want. The `TestContext` parameter is used to initialize an HTTP client to play back or record tests and provides other information to test functions that may be useful. +These tests must also return a `std::result::Result`, which can be redefined e.g., `azure_core::Result`. + [Test Proxy]: https://github.com/Azure/azure-sdk-tools/blob/main/tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md diff --git a/sdk/core/azure_core_test/src/lib.rs b/sdk/core/azure_core_test/src/lib.rs index d17f466e2e..00caeaa276 100644 --- a/sdk/core/azure_core_test/src/lib.rs +++ b/sdk/core/azure_core_test/src/lib.rs @@ -7,11 +7,14 @@ 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}; +#[cfg_attr(target_arch = "wasm32", allow(dead_code))] +const ASSETS_FILE: &str = "assets.json"; const SPAN_TARGET: &str = "test-proxy"; /// Context information required by recorded client library tests. @@ -20,20 +23,35 @@ const SPAN_TARGET: &str = "test-proxy"; /// to setup up the HTTP client to record or play back session records. #[derive(Debug)] pub struct TestContext { + repo_dir: &'static Path, crate_dir: &'static Path, + service_dir: &'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_dir = 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 { + repo_dir: find_ancestor_of(crate_dir, ".git")?, crate_dir: Path::new(crate_dir), + service_dir, + test_module, test_name, recording: None, - } + }) } /// Gets the root directory of the crate under test. @@ -52,24 +70,147 @@ impl TestContext { .expect("not recording or playback started") } + /// Gets the repository root. + pub fn repo_dir(&self) -> &'static Path { + self.repo_dir + } + + /// 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_dir(&self) -> &'static str { + self.service_dir + } + /// Gets the test data directory under [`Self::crate_dir`]. + /// + /// The path is relative to the repository root e.g., `sdk/core/azure_core/tests/data`. + /// + /// # Panics + /// + /// Panics if the [`TestContext::crate_dir()`] is not rooted within a Git repository. pub fn test_data_dir(&self) -> PathBuf { - self.crate_dir.join("tests/data") + self.crate_dir + .join("tests/data") + .strip_prefix(self.repo_dir) + .expect("not rooted within repo") + .to_path_buf() + } + + /// 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. + /// + /// The path is relative to the repository root e.g., `sdk/core/assets.json`. + /// + /// # Panics + /// + /// Panics if the [`TestContext::crate_dir()`] is not rooted within a Git repository. + pub(crate) fn test_recording_assets_file( + &self, + #[cfg_attr(target_arch = "wasm32", allow(unused_variables))] mode: TestMode, + ) -> Option { + #[cfg(target_arch = "wasm32")] + { + None + } + + #[cfg(not(target_arch = "wasm32"))] + { + let path = match find_ancestor_file(self.crate_dir, ASSETS_FILE) { + Ok(path) => path, + Err(_) if mode == TestMode::Record => { + return Path::new("sdk") + .join(self.service_dir) + .join(ASSETS_FILE) + .as_path() + .to_str() + .map(String::from); + } + Err(err) => panic!("{err}"), + }; + path.strip_prefix(self.repo_dir) + .expect("not rooted within repo") + .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")) + } } +/// Finds `name` under `dir` and returns the path to the parent `dir`. +/// +/// This function does *not* check the file system. +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 +} + +/// Finds `name` under `dir` and returns the path to the named entry. +/// +/// This function does check the file system. #[cfg(not(target_arch = "wasm32"))] -fn find_ancestor(dir: impl AsRef, name: &str) -> azure_core::Result { +fn find_ancestor_file(dir: impl AsRef, name: &str) -> azure_core::Result { for dir in dir.as_ref().ancestors() { let path = dir.join(name); if path.exists() { return Ok(path); } + + // Keep looking until we get to the repo root where `.git` is either a directory (primary repo) or file (worktree). + let path = dir.join(".git"); + if path.exists() { + return Err(azure_core::Error::message( + ErrorKind::Other, + format!("{name} not found under repo {}", dir.display()), + )); + } + } + Err(azure_core::Error::new::( + azure_core::error::ErrorKind::Io, + std::io::ErrorKind::NotFound.into(), + )) +} + +/// Finds `name` under `dir` and returns the path to the parent `dir`. +/// +/// This function does check the file system. +fn find_ancestor_of(dir: &'static str, name: &'static str) -> azure_core::Result<&'static Path> { + let dir = Path::new(dir); + for dir in dir.ancestors() { + let path = dir.join(name); + if path.exists() { + return Ok(dir); + } } Err(azure_core::Error::new::( azure_core::error::ErrorKind::Io, @@ -82,8 +223,9 @@ mod tests { use super::*; #[test] - fn test_content_new() { - let ctx = TestContext::new(env!("CARGO_MANIFEST_DIR"), "test_content_new"); + fn test_context_new() { + let ctx = + TestContext::new(env!("CARGO_MANIFEST_DIR"), file!(), "test_context_new").unwrap(); assert!(ctx.recording.is_none()); assert!(ctx .crate_dir() @@ -91,6 +233,21 @@ mod tests { .unwrap() .replace("\\", "/") .ends_with("sdk/core/azure_core_test")); - assert_eq!(ctx.test_name(), "test_content_new"); + assert_eq!(ctx.test_module(), "lib"); + assert_eq!(ctx.test_name(), "test_context_new"); + assert_eq!( + ctx.test_recording_file().replace("\\", "/"), + "sdk/core/azure_core_test/tests/data/lib/test_context_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..21bd5f858c 100644 --- a/sdk/core/azure_core_test/src/proxy/client.rs +++ b/sdk/core/azure_core_test/src/proxy/client.rs @@ -5,15 +5,14 @@ use super::{ matchers::Matcher, models::{PlaybackStartResult, RecordStartResult, StartPayload, VariablePayload}, sanitizers::Sanitizer, + RecordingId, RECORDING_ID, }; use azure_core::{ - headers::{AsHeaders, HeaderName, HeaderValue, ACCEPT, CONTENT_TYPE}, - ClientMethodOptions, ClientOptions, Context, Header, Method, Pipeline, Request, RequestContent, - Result, Url, + headers::{AsHeaders, ACCEPT, CONTENT_TYPE}, + ClientMethodOptions, ClientOptions, Context, Method, Pipeline, Request, RequestContent, Result, + Url, }; -const X_RECORDING_ID: HeaderName = HeaderName::from_static("x-recording-id"); - /// The test-proxy client. /// /// See for usage. @@ -53,17 +52,15 @@ impl Client { let mut url = self.endpoint.clone(); url = url.join("/Record/Start")?; let mut request = Request::new(url, Method::Post); - request.insert_header("accept", "application/json"); - request.insert_header("content-type", "application/json"); + request.insert_header(ACCEPT, "application/json"); + request.insert_header(CONTENT_TYPE, "application/json"); request.set_body(body); let resp = self .pipeline .send::(&ctx, &mut request) .await?; - let recording_id = resp.headers().get_str(&X_RECORDING_ID)?.to_string(); - let mut result: RecordStartResult = resp.into_json_body().await?; - result.recording_id = recording_id; - Ok(result) + let recording_id = resp.headers().get_str(&RECORDING_ID)?.to_string(); + Ok(RecordStartResult { recording_id }) } pub async fn record_stop( @@ -77,9 +74,9 @@ impl Client { let mut url = self.endpoint.clone(); url = url.join("/Record/Stop")?; let mut request = Request::new(url, Method::Post); - request.insert_header("accept", "application/json"); - request.insert_header("content-type", "application/json"); - request.insert_header(X_RECORDING_ID, recording_id.to_string()); + request.insert_header(ACCEPT, "application/json"); + request.insert_header(CONTENT_TYPE, "application/json"); + request.insert_header(RECORDING_ID, recording_id.to_string()); request.set_body(body); self.pipeline.send::<()>(&ctx, &mut request).await?; Ok(()) @@ -95,15 +92,15 @@ impl Client { let mut url = self.endpoint.clone(); url = url.join("/Playback/Start")?; let mut request = Request::new(url, Method::Post); - request.insert_header("accept", "application/json"); - request.insert_header("content-type", "application/json"); + request.insert_header(ACCEPT, "application/json"); + request.insert_header(CONTENT_TYPE, "application/json"); request.add_optional_header(&options.recording_id); request.set_body(body); let resp = self .pipeline - .send::(&ctx, &mut request) + .send::(&ctx, &mut request) .await?; - let recording_id = resp.headers().get_str(&X_RECORDING_ID)?.to_string(); + let recording_id = resp.headers().get_str(&RECORDING_ID)?.to_string(); let mut result: PlaybackStartResult = resp.into_json_body().await?; result.recording_id = recording_id; Ok(result) @@ -119,9 +116,9 @@ impl Client { let mut url = self.endpoint.clone(); url = url.join("/Playback/Stop")?; let mut request = Request::new(url, Method::Post); - request.insert_header("accept", "application/json"); - request.insert_header("content-type", "application/json"); - request.insert_header(X_RECORDING_ID, recording_id.to_string()); + request.insert_header(ACCEPT, "application/json"); + request.insert_header(CONTENT_TYPE, "application/json"); + request.insert_header(RECORDING_ID, recording_id.to_string()); self.pipeline.send::<()>(&ctx, &mut request).await?; Ok(()) } @@ -180,19 +177,6 @@ impl Client { } } -#[derive(Debug)] -pub struct RecordingId(String); - -impl Header for &RecordingId { - fn name(&self) -> HeaderName { - X_RECORDING_ID - } - - fn value(&self) -> HeaderValue { - self.0.clone().into() - } -} - #[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..8eb7eba67a 100644 --- a/sdk/core/azure_core_test/src/proxy/mod.rs +++ b/sdk/core/azure_core_test/src/proxy/mod.rs @@ -4,13 +4,17 @@ //! 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; #[cfg(not(target_arch = "wasm32"))] use azure_core::Result; -use azure_core::{error::ErrorKind, headers::HeaderName, Url}; +use azure_core::{ + error::ErrorKind, + headers::{HeaderName, HeaderValue}, + Header, Url, +}; use serde::Serializer; #[cfg(not(target_arch = "wasm32"))] use std::process::ExitStatus; @@ -21,6 +25,10 @@ use tokio::process::Child; use tracing::Level; const ABSTRACTION_IDENTIFIER: HeaderName = HeaderName::from_static("x-abstraction-identifier"); +const RECORDING_ID: HeaderName = HeaderName::from_static("x-recording-id"); +const RECORDING_MODE: HeaderName = HeaderName::from_static("x-recording-mode"); +const RECORDING_UPSTREAM_BASE_URI: HeaderName = + HeaderName::from_static("x-recording-upstream-base-uri"); #[cfg(not(target_arch = "wasm32"))] pub use bootstrap::start; @@ -51,7 +59,7 @@ mod bootstrap { /// /// This is intended for internal use only and should not be called directly in tests. pub async fn start( - test_data_dir: impl AsRef, + crate_dir: impl AsRef, options: Option, ) -> Result { if env::var(PROXY_MANUAL_START).is_ok_and(|v| v.eq_ignore_ascii_case("true")) { @@ -60,7 +68,7 @@ mod bootstrap { } // Find root of git repo or work tree: a ".git" directory or file will exist either way. - let git_dir = crate::find_ancestor(test_data_dir, ".git")?; + let git_dir = crate::find_ancestor_file(crate_dir, ".git")?; let git_dir = git_dir.parent().ok_or_else(|| { io::Error::new(io::ErrorKind::NotFound, "parent git repository not found") })?; @@ -96,7 +104,7 @@ mod bootstrap { v = wait_till_listening(&mut stdout) => { v? }, _ = tokio::time::sleep(max_seconds) => { command.kill().await?; - return Err(azure_core::Error::message(ErrorKind::Other, "timeout waiting for test-proxy to start")); + return Err(azure_core::Error::message(ErrorKind::Other, "timed out waiting for test-proxy to start")); }, }; @@ -212,7 +220,7 @@ impl Drop for Proxy { } /// Options for starting the [`Proxy`]. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct ProxyOptions { /// Allow insecure upstream SSL certs. pub insecure: bool, @@ -235,6 +243,51 @@ impl ProxyOptions { } } +impl Default for ProxyOptions { + fn default() -> Self { + Self { + insecure: false, + auto_shutdown_in_seconds: 300, + } + } +} + +#[derive(Clone, Debug)] +pub struct RecordingId(String); + +impl Header for RecordingId { + fn name(&self) -> HeaderName { + RECORDING_ID + } + + fn value(&self) -> HeaderValue { + self.0.clone().into() + } +} + +impl Header for &RecordingId { + fn name(&self) -> HeaderName { + RECORDING_ID + } + + fn value(&self) -> HeaderValue { + self.0.clone().into() + } +} + +impl AsRef for RecordingId { + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +impl FromStr for RecordingId { + type Err = std::convert::Infallible; + fn from_str(value: &str) -> std::result::Result { + Ok(RecordingId(value.to_string())) + } +} + #[derive(Debug, Default, Eq, PartialEq, Ord, PartialOrd)] struct Version { major: i32, diff --git a/sdk/core/azure_core_test/src/proxy/models.rs b/sdk/core/azure_core_test/src/proxy/models.rs index fe5ef30c2e..502edd116a 100644 --- a/sdk/core/azure_core_test/src/proxy/models.rs +++ b/sdk/core/azure_core_test/src/proxy/models.rs @@ -78,10 +78,17 @@ pub struct RecordStartResult { #[derive(Debug, Default, Serialize)] pub struct VariablePayload { - #[serde(rename = "Variables")] + #[serde(flatten)] 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/proxy/policy.rs b/sdk/core/azure_core_test/src/proxy/policy.rs index 9d6ba95444..e25ba899e4 100644 --- a/sdk/core/azure_core_test/src/proxy/policy.rs +++ b/sdk/core/azure_core_test/src/proxy/policy.rs @@ -1,23 +1,31 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +use crate::{ + proxy::{RecordingId, RECORDING_UPSTREAM_BASE_URI}, + Skip, +}; use async_trait::async_trait; use azure_core::{ + error::ErrorKind, headers::{AsHeaders, HeaderName, HeaderValue}, test::TestMode, - Context, Policy, PolicyResult, Request, + Context, Policy, PolicyResult, Request, Url, }; use std::{ convert::Infallible, sync::{Arc, RwLock}, }; use tracing::{debug_span, Instrument}; +use url::Origin; -use crate::Skip; +use super::RECORDING_MODE; #[derive(Debug, Default)] pub struct RecordingPolicy { pub test_mode: TestMode, + pub host: Option, + pub recording_id: Option, pub options: RwLock, } @@ -31,14 +39,62 @@ impl Policy for RecordingPolicy { next: &[Arc], ) -> PolicyResult { let span = debug_span!(target: crate::SPAN_TARGET, "request", mode = ?self.test_mode); + + // Replace the upstream host with the test-proxy host, which will make and record the upstream call. + let mut origin = None; + if let Some(host) = &self.host { + let url = request.url_mut(); + origin = Some(url.origin()); + + url.set_scheme(host.scheme()).map_err(|_| { + azure_core::Error::message(ErrorKind::Other, "failed to set recording url scheme") + })?; + url.set_host(host.host_str()).map_err(|_| { + azure_core::Error::message(ErrorKind::Other, "failed to set recording url host") + })?; + url.set_port(host.port()).map_err(|_| { + azure_core::Error::message(ErrorKind::Other, "failed to set recording url port") + })?; + } + + if let Some(origin) = &origin { + request.insert_header(RECORDING_UPSTREAM_BASE_URI, origin.ascii_serialization()); + } + + request.insert_headers(&self.recording_id)?; + request.insert_header( + RECORDING_MODE, + HeaderValue::from_static(self.test_mode.into()), + ); if let Ok(options) = self.options.read() { request.insert_headers(&*options)?; } - next[0] - .send(ctx, request, &next[1..]) - .instrument(span) - .await + async move { + let resp = next[0].send(ctx, request, &next[1..]).await?; + + // Restore the upstream host to support pageables and pollers that may need the original URL. + if let Some(Origin::Tuple(scheme, host, port)) = origin { + let url = request.url_mut(); + + url.set_scheme(scheme.as_ref()).map_err(|_| { + azure_core::Error::message( + ErrorKind::Other, + "failed to set recording url scheme", + ) + })?; + url.set_host(Some(host.to_string().as_ref())).map_err(|_| { + azure_core::Error::message(ErrorKind::Other, "failed to set recording url host") + })?; + url.set_port(Some(port)).map_err(|_| { + azure_core::Error::message(ErrorKind::Other, "failed to set recording url port") + })?; + } + + Ok(resp) + } + .instrument(span) + .await } } diff --git a/sdk/core/azure_core_test/src/recorded.rs b/sdk/core/azure_core_test/src/recorded.rs index cfe5b775f3..39e538bc47 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,63 @@ 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 = { + match test_mode { + TestMode::Live => None, + _ => Some( + TEST_PROXY + .get_or_init(|| async move { + #[cfg(feature = "tracing")] + { + use tracing_subscriber::EnvFilter; + + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .init(); + } + + crate::proxy::start(crate_dir, options).await.map(Arc::new) + }) + .await + .as_ref() + .map(Clone::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 { + if let Some(proxy) = proxy.as_ref() { 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_dir(), + ctx.test_recording_file(), + ctx.test_recording_assets_file(test_mode), + ); + 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..edfd6ced81 100644 --- a/sdk/core/azure_core_test/src/recording.rs +++ b/sdk/core/azure_core_test/src/recording.rs @@ -5,31 +5,45 @@ use crate::{ proxy::{ - client::{Client, ClientAddSanitizerOptions, ClientSetMatcherOptions, RecordingId}, + client::{Client, ClientAddSanitizerOptions, ClientSetMatcherOptions}, + models::{StartPayload, VariablePayload}, policy::RecordingPolicy, - Proxy, + Proxy, RecordingId, }, 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 @@ -85,6 +111,8 @@ impl Recording { .get_or_init(|| { Arc::new(RecordingPolicy { test_mode: self.test_mode, + host: self.client.as_ref().map(|c| c.endpoint().clone()), + recording_id: self.id.clone(), ..Default::default() }) }) @@ -110,7 +138,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 +148,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 +229,90 @@ 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.as_ref() else { + // Assumes running live test. + 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.as_ref() else { + // Assumes running live test. + return Ok(()); + }; + + let Some(recording_id) = self.id.as_ref() else { + // Do not return an error or we hide any test-proxy client or client under test error. + return Ok(()); + }; + + 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 71e0eff5ef..7d555ac588 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/README.md b/sdk/core/azure_core_test_macros/README.md index 96f49e90b7..d4da31a5d6 100644 --- a/sdk/core/azure_core_test_macros/README.md +++ b/sdk/core/azure_core_test_macros/README.md @@ -4,12 +4,12 @@ Macros for testing client libraries built on `azure_core`. ## Client methods -To test client methods using our [Test Proxy], you can attribute both synchronous and asynchronous (recommend) tests +To test client methods using our [Test Proxy] or run against live resources, you can attribute asynchronous tests using the `#[recorded::test]` attribute: ```rust -use azure_core_test::{recorded, TestContext}; use azure_core::Result; +use azure_core_test::{recorded, TestContext}; #[recorded::test] async fn get_secret(ctx: TestContext) -> Result<()> { @@ -17,9 +17,11 @@ async fn get_secret(ctx: TestContext) -> Result<()> { } ``` -The `TestContext` parameter is required unless your test function is attribute as `#[recorded::test(live)]` (live-only), -in which case it is optional. You can name the parameter whatever you want. +The `TestContext` parameter is required unless your test function is attribute as `#[recorded::test(live)]` (live-only). +You can name the parameter whatever you want. The `TestContext` parameter is used to initialize an HTTP client to play back or record tests and provides other information to test functions that may be useful. +These tests must also return a `std::result::Result`, which can be redefined e.g., `azure_core::Result`. + [Test Proxy]: https://github.com/Azure/azure-sdk-tools/blob/main/tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md diff --git a/sdk/core/azure_core_test_macros/src/lib.rs b/sdk/core/azure_core_test_macros/src/lib.rs index 1c06c87c1b..82cca6f702 100644 --- a/sdk/core/azure_core_test_macros/src/lib.rs +++ b/sdk/core/azure_core_test_macros/src/lib.rs @@ -9,6 +9,31 @@ 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 declare an async function that accepts a `TestContext` and returns a `Result`. +/// +/// ``` +/// use azure_core::Result; +/// use azure_core_test::{recorded, TestContext}; +/// +/// #[recorded::test] +/// async fn test(ctx: TestContext) -> Result<()> { +/// todo!() +/// } +/// ``` +/// +/// For live-only tests, you must declare an async function that may accept a `TestContext` and must 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..4f572cb1f2 100644 --- a/sdk/core/azure_core_test_macros/src/test.rs +++ b/sdk/core/azure_core_test_macros/src/test.rs @@ -5,12 +5,14 @@ use azure_core::test::TestMode; use proc_macro2::TokenStream; use quote::quote; use std::sync::LazyLock; -use syn::{parse::Parse, spanned::Spanned, FnArg, ItemFn, Meta, PatType, Result, Token}; +use syn::{ + parse::Parse, spanned::Spanned, FnArg, ItemFn, Meta, PatType, Result, ReturnType, Token, +}; const INVALID_RECORDED_ATTRIBUTE_MESSAGE: &str = "expected `#[recorded::test]` or `#[recorded::test(live)]`"; const INVALID_RECORDED_FUNCTION_MESSAGE: &str = - "expected `async fn(TestContext)` function signature with optional `Result` return"; + "expected `async fn(TestContext)` function signature with `Result` return"; // cspell:ignore asyncness pub fn parse_test(attr: TokenStream, item: TokenStream) -> Result { @@ -23,7 +25,7 @@ pub fn parse_test(attr: TokenStream, item: TokenStream) -> Result { } = syn::parse2(item)?; let mut test_attr: TokenStream = match original_sig.asyncness { - Some(_) => quote! { #[::tokio::test] }, + Some(_) => quote! { #[::tokio::test(flavor = "multi_thread")] }, None => { return Err(syn::Error::new( original_sig.span(), @@ -32,6 +34,14 @@ pub fn parse_test(attr: TokenStream, item: TokenStream) -> Result { } }; + // Assumes the return type is a `Result` since that's all `#[test]`s support currently. + if let ReturnType::Default = original_sig.output { + return Err(syn::Error::new( + original_sig.output.span(), + INVALID_RECORDED_FUNCTION_MESSAGE, + )); + } + // Ignore live-only tests if not running live tests. let test_mode = *TEST_MODE; if recorded_attrs.live && test_mode < TestMode::Live { @@ -50,8 +60,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 } } @@ -209,7 +224,7 @@ mod tests { fn parse_recorded_playback() { let attr = TokenStream::new(); let item = quote! { - async fn recorded() { + async fn recorded() -> azure_core::Result<()> { todo!() } }; @@ -220,7 +235,7 @@ mod tests { fn parse_recorded_playback_with_context() { let attr = TokenStream::new(); let item = quote! { - async fn recorded(ctx: TestContext) { + async fn recorded(ctx: TestContext) -> azure_core::Result<()> { todo!() } }; @@ -231,7 +246,7 @@ mod tests { fn parse_recorded_playback_with_multiple() { let attr = TokenStream::new(); let item = quote! { - async fn recorded(ctx: TestContext, name: &'static str) { + async fn recorded(ctx: TestContext, name: &'static str)- > azure_core::Result<()> { todo!() } }; @@ -242,7 +257,7 @@ mod tests { fn parse_recorded_live() { let attr = quote! { live }; let item = quote! { - async fn live_only() { + async fn live_only() -> azure_core::Result<()> { todo!() } }; @@ -253,7 +268,7 @@ mod tests { fn parse_recorded_live_with_context() { let attr = quote! { live }; let item = quote! { - async fn live_only(ctx: TestContext) { + async fn live_only(ctx: TestContext) -> azure_core::Result<()> { todo!() } }; diff --git a/sdk/eventhubs/azure_messaging_eventhubs/tests/consumer.rs b/sdk/eventhubs/azure_messaging_eventhubs/tests/consumer.rs index d033e1c398..ec66cc0241 100644 --- a/sdk/eventhubs/azure_messaging_eventhubs/tests/consumer.rs +++ b/sdk/eventhubs/azure_messaging_eventhubs/tests/consumer.rs @@ -10,38 +10,40 @@ use azure_messaging_eventhubs::consumer::{ ConsumerClient, ConsumerClientOptions, ReceiveOptions, StartPosition, }; use futures::{pin_mut, StreamExt}; -use std::{env, time::Duration}; +use std::{env, error::Error, time::Duration}; use tracing::{info, trace}; mod common; #[recorded::test(live)] -async fn test_new() { +async fn test_new() -> Result<(), Box> { common::setup(); - let host = env::var("EVENTHUBS_HOST").unwrap(); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let host = env::var("EVENTHUBS_HOST")?; + let eventhub = env::var("EVENTHUB_NAME")?; let _client = ConsumerClient::new( host, eventhub, None, - DefaultAzureCredential::new().unwrap(), + DefaultAzureCredential::new()?, Some(ConsumerClientOptions { application_id: Some("test_new".to_string()), ..Default::default() }), ); + + Ok(()) } #[recorded::test(live)] -async fn test_new_with_error() { +async fn test_new_with_error() -> Result<(), Box> { common::setup(); trace!("test_new_with_error"); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let eventhub = env::var("EVENTHUB_NAME")?; let consumer = ConsumerClient::new( "invalid_host".into(), eventhub, None, - DefaultAzureCredential::new().unwrap(), + DefaultAzureCredential::new()?, Some(ConsumerClientOptions { application_id: Some("test_new".to_string()), ..Default::default() @@ -50,51 +52,57 @@ async fn test_new_with_error() { let result = consumer.open().await; assert!(result.is_err()); info!("Error: {:?}", result); + + Ok(()) } #[recorded::test(live)] -async fn test_open() { +async fn test_open() -> Result<(), Box> { common::setup(); - let host = env::var("EVENTHUBS_HOST").unwrap(); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let host = env::var("EVENTHUBS_HOST")?; + let eventhub = env::var("EVENTHUB_NAME")?; let client = ConsumerClient::new( host, eventhub, None, - azure_identity::DefaultAzureCredential::new().unwrap(), + azure_identity::DefaultAzureCredential::new()?, Some(ConsumerClientOptions { application_id: Some("test_open".to_string()), ..Default::default() }), ); - client.open().await.unwrap(); + client.open().await?; + + Ok(()) } #[recorded::test(live)] -async fn test_close() { +async fn test_close() -> Result<(), Box> { common::setup(); - let host = env::var("EVENTHUBS_HOST").unwrap(); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let host = env::var("EVENTHUBS_HOST")?; + let eventhub = env::var("EVENTHUB_NAME")?; let client = ConsumerClient::new( host, eventhub, None, - azure_identity::DefaultAzureCredential::new().unwrap(), + azure_identity::DefaultAzureCredential::new()?, Some(ConsumerClientOptions { application_id: Some("test_open".to_string()), ..Default::default() }), ); - client.open().await.unwrap(); - client.close().await.unwrap(); + client.open().await?; + client.close().await?; + + Ok(()) } #[recorded::test(live)] -async fn test_get_properties() { +async fn test_get_properties() -> Result<(), Box> { common::setup(); - let host = env::var("EVENTHUBS_HOST").unwrap(); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let host = env::var("EVENTHUBS_HOST")?; + let eventhub = env::var("EVENTHUB_NAME")?; - let credential = DefaultAzureCredential::new().unwrap(); + let credential = DefaultAzureCredential::new()?; let client = ConsumerClient::new( host, @@ -106,19 +114,21 @@ async fn test_get_properties() { ..Default::default() }), ); - client.open().await.unwrap(); - let properties = client.get_eventhub_properties().await.unwrap(); + client.open().await?; + let properties = client.get_eventhub_properties().await?; info!("Properties: {:?}", properties); assert_eq!(properties.name, eventhub); + + Ok(()) } #[recorded::test(live)] -async fn test_get_partition_properties() { +async fn test_get_partition_properties() -> Result<(), Box> { common::setup(); - let host = env::var("EVENTHUBS_HOST").unwrap(); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let host = env::var("EVENTHUBS_HOST")?; + let eventhub = env::var("EVENTHUB_NAME")?; - let credential = DefaultAzureCredential::new().unwrap(); + let credential = DefaultAzureCredential::new()?; let client = ConsumerClient::new( host, @@ -130,29 +140,30 @@ async fn test_get_partition_properties() { ..Default::default() }), ); - client.open().await.unwrap(); - let properties = client.get_eventhub_properties().await.unwrap(); + client.open().await?; + let properties = client.get_eventhub_properties().await?; for partition_id in properties.partition_ids { let partition_properties = client .get_partition_properties(partition_id.clone()) - .await - .unwrap(); + .await?; info!("Partition properties: {:?}", partition_properties); assert_eq!(partition_properties.id, partition_id); } + + Ok(()) } #[recorded::test(live)] -async fn receive_lots_of_events() { +async fn receive_lots_of_events() -> Result<(), Box> { common::setup(); - let host = env::var("EVENTHUBS_HOST").unwrap(); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let host = env::var("EVENTHUBS_HOST")?; + let eventhub = env::var("EVENTHUB_NAME")?; info!("Establishing credentials."); - let credential = DefaultAzureCredential::new().unwrap(); + let credential = DefaultAzureCredential::new()?; info!("Creating client."); let client = ConsumerClient::new( @@ -167,7 +178,7 @@ async fn receive_lots_of_events() { ); info!("Opening client."); - client.open().await.unwrap(); + client.open().await?; info!("Creating event receive stream."); let event_stream = client @@ -207,4 +218,6 @@ async fn receive_lots_of_events() { assert!(result.is_err()); info!("Received {count} messages."); + + Ok(()) } diff --git a/sdk/eventhubs/azure_messaging_eventhubs/tests/producer.rs b/sdk/eventhubs/azure_messaging_eventhubs/tests/producer.rs index 46e82ee585..52b2b1efa4 100644 --- a/sdk/eventhubs/azure_messaging_eventhubs/tests/producer.rs +++ b/sdk/eventhubs/azure_messaging_eventhubs/tests/producer.rs @@ -12,35 +12,37 @@ use azure_identity::DefaultAzureCredential; use azure_messaging_eventhubs::producer::{ batch::EventDataBatchOptions, ProducerClient, ProducerClientOptions, }; -use std::env; +use std::{env, error::Error}; use tracing::{info, trace}; mod common; #[recorded::test(live)] -async fn test_new() { +async fn test_new() -> Result<(), Box> { common::setup(); - let host = env::var("EVENTHUBS_HOST").unwrap(); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let host = env::var("EVENTHUBS_HOST")?; + let eventhub = env::var("EVENTHUB_NAME")?; let _client = ProducerClient::new( host, eventhub, - DefaultAzureCredential::new().unwrap(), + DefaultAzureCredential::new()?, Some(ProducerClientOptions { application_id: Some("test_new".to_string()), ..Default::default() }), ); + + Ok(()) } #[recorded::test(live)] -async fn test_new_with_error() { +async fn test_new_with_error() -> Result<(), Box> { common::setup(); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let eventhub = env::var("EVENTHUB_NAME")?; let producer = ProducerClient::new( "invalid_host".to_string(), eventhub, - azure_identity::DefaultAzureCredential::new().unwrap(), + azure_identity::DefaultAzureCredential::new()?, Some(ProducerClientOptions { application_id: Some("test_new_with_error".to_string()), ..Default::default() @@ -49,49 +51,55 @@ async fn test_new_with_error() { let result = producer.open().await; assert!(result.is_err()); info!("Error: {:?}", result); + + Ok(()) } #[recorded::test(live)] -async fn test_open() { +async fn test_open() -> Result<(), Box> { common::setup(); - let host = env::var("EVENTHUBS_HOST").unwrap(); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let host = env::var("EVENTHUBS_HOST")?; + let eventhub = env::var("EVENTHUB_NAME")?; let client = ProducerClient::new( host, eventhub, - azure_identity::DefaultAzureCredential::new().unwrap(), + azure_identity::DefaultAzureCredential::new()?, Some(ProducerClientOptions { application_id: Some("test_open".to_string()), ..Default::default() }), ); - client.open().await.unwrap(); + client.open().await?; + + Ok(()) } #[recorded::test(live)] -async fn test_close() { +async fn test_close() -> Result<(), Box> { common::setup(); - let host = env::var("EVENTHUBS_HOST").unwrap(); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let host = env::var("EVENTHUBS_HOST")?; + let eventhub = env::var("EVENTHUB_NAME")?; let client = ProducerClient::new( host, eventhub, - azure_identity::DefaultAzureCredential::new().unwrap(), + azure_identity::DefaultAzureCredential::new()?, Some(ProducerClientOptions { application_id: Some("test_close".to_string()), ..Default::default() }), ); - client.open().await.unwrap(); - client.close().await.unwrap(); + client.open().await?; + client.close().await?; + + Ok(()) } #[recorded::test(live)] -async fn test_get_properties() { +async fn test_get_properties() -> Result<(), Box> { common::setup(); - let host = env::var("EVENTHUBS_HOST").unwrap(); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let host = env::var("EVENTHUBS_HOST")?; + let eventhub = env::var("EVENTHUB_NAME")?; - let credential = DefaultAzureCredential::new().unwrap(); + let credential = DefaultAzureCredential::new()?; let client = ProducerClient::new( host, @@ -102,19 +110,21 @@ async fn test_get_properties() { ..Default::default() }), ); - client.open().await.unwrap(); - let properties = client.get_eventhub_properties().await.unwrap(); + client.open().await?; + let properties = client.get_eventhub_properties().await?; info!("Properties: {:?}", properties); assert_eq!(properties.name, eventhub); + + Ok(()) } #[recorded::test(live)] -async fn test_get_partition_properties() { +async fn test_get_partition_properties() -> Result<(), Box> { common::setup(); - let host = env::var("EVENTHUBS_HOST").unwrap(); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let host = env::var("EVENTHUBS_HOST")?; + let eventhub = env::var("EVENTHUB_NAME")?; - let credential = DefaultAzureCredential::new().unwrap(); + let credential = DefaultAzureCredential::new()?; let client = ProducerClient::new( host, @@ -125,21 +135,22 @@ async fn test_get_partition_properties() { ..Default::default() }), ); - client.open().await.unwrap(); - let properties = client.get_eventhub_properties().await.unwrap(); + client.open().await?; + let properties = client.get_eventhub_properties().await?; for partition_id in properties.partition_ids { let partition_properties = client .get_partition_properties(partition_id.clone()) - .await - .unwrap(); + .await?; info!("Partition properties: {:?}", partition_properties); assert_eq!(partition_properties.id, partition_id); } + + Ok(()) } #[recorded::test(live)] -async fn test_create_eventdata() { +async fn test_create_eventdata() -> Result<(), Box> { common::setup(); let data = b"hello world"; let ed1 = azure_messaging_eventhubs::models::EventData::builder() @@ -160,15 +171,17 @@ async fn test_create_eventdata() { .with_message_id(35u64) .add_property("key".to_string(), "value") .build(); + + Ok(()) } #[recorded::test(live)] -async fn test_create_batch() { +async fn test_create_batch() -> Result<(), Box> { common::setup(); - let host = env::var("EVENTHUBS_HOST").unwrap(); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let host = env::var("EVENTHUBS_HOST")?; + let eventhub = env::var("EVENTHUB_NAME")?; - let credential = DefaultAzureCredential::new().unwrap(); + let credential = DefaultAzureCredential::new()?; let client = ProducerClient::new( host, @@ -179,20 +192,22 @@ async fn test_create_batch() { ..Default::default() }), ); - client.open().await.unwrap(); + client.open().await?; { - let batch = client.create_batch(None).await.unwrap(); + let batch = client.create_batch(None).await?; assert_eq!(batch.len(), 0); } + + Ok(()) } #[recorded::test(live)] -async fn test_create_and_send_batch() { +async fn test_create_and_send_batch() -> Result<(), Box> { common::setup(); - let host = env::var("EVENTHUBS_HOST").unwrap(); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let host = env::var("EVENTHUBS_HOST")?; + let eventhub = env::var("EVENTHUB_NAME")?; - let credential = DefaultAzureCredential::new().unwrap(); + let credential = DefaultAzureCredential::new()?; let client = ProducerClient::new( host, @@ -203,11 +218,11 @@ async fn test_create_and_send_batch() { ..Default::default() }), ); - client.open().await.unwrap(); + client.open().await?; { - let mut batch = client.create_batch(None).await.unwrap(); + let mut batch = client.create_batch(None).await?; assert_eq!(batch.len(), 0); - assert!(batch.try_add_event_data(vec![1, 2, 3, 4], None).unwrap()); + assert!(batch.try_add_event_data(vec![1, 2, 3, 4], None)?); let res = client.submit_batch(&batch).await; assert!(res.is_ok()); @@ -218,33 +233,34 @@ async fn test_create_and_send_batch() { partition_id: Some("0".to_string()), ..Default::default() })) - .await - .unwrap(); + .await?; for i in 0..10 { - let res = batch.try_add_event_data(vec![i as u8], None).unwrap(); + let res = batch.try_add_event_data(vec![i as u8], None)?; assert!(res); } - assert!(batch.try_add_event_data("This is data", None).unwrap()); - assert!(batch.try_add_event_data([23], None).unwrap()); - assert!(batch.try_add_event_data(vec![1, 2, 4, 8], None).unwrap()); - assert!(batch.try_add_event_data("&data", None).unwrap()); - assert!(batch.try_add_event_data("&data", None).unwrap()); - assert!(batch.try_add_event_data("&data", None).unwrap()); - assert!(batch.try_add_event_data("&data", None).unwrap()); - assert!(batch.try_add_event_data("&data", None).unwrap()); + assert!(batch.try_add_event_data("This is data", None)?); + assert!(batch.try_add_event_data([23], None)?); + assert!(batch.try_add_event_data(vec![1, 2, 4, 8], None)?); + assert!(batch.try_add_event_data("&data", None)?); + assert!(batch.try_add_event_data("&data", None)?); + assert!(batch.try_add_event_data("&data", None)?); + assert!(batch.try_add_event_data("&data", None)?); + assert!(batch.try_add_event_data("&data", None)?); let res = client.submit_batch(&batch).await; assert!(res.is_ok()); } + + Ok(()) } #[recorded::test(live)] async fn test_add_amqp_messages_to_batch() -> Result<(), Box> { common::setup(); - let host = env::var("EVENTHUBS_HOST").unwrap(); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let host = env::var("EVENTHUBS_HOST")?; + let eventhub = env::var("EVENTHUB_NAME")?; - let credential = DefaultAzureCredential::new().unwrap(); + let credential = DefaultAzureCredential::new()?; let client = ProducerClient::new( host, @@ -269,9 +285,7 @@ async fn test_add_amqp_messages_to_batch() -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { common::setup(); - let host = env::var("EVENTHUBS_HOST").unwrap(); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let host = env::var("EVENTHUBS_HOST")?; + let eventhub = env::var("EVENTHUB_NAME")?; - let credential = DefaultAzureCredential::new().unwrap(); + let credential = DefaultAzureCredential::new()?; info!("Create producer client..."); @@ -337,7 +349,7 @@ async fn test_overload_batch() { ); info!("Open producer client..."); - client.open().await.unwrap(); + client.open().await?; info!("Client is open."); { @@ -346,18 +358,14 @@ async fn test_overload_batch() { partition_id: Some("0".to_string()), ..Default::default() })) - .await - .unwrap(); + .await?; trace!("Batch created."); for i in 0..25_000 { if i % 5_000 == 0 { info!("Add event data, now at {}", i); info!("Batch size: {}", batch.size()); } - if !batch - .try_add_event_data(format!("Message {i}"), None) - .unwrap() - { + if !batch.try_add_event_data(format!("Message {i}"), None)? { info!( "Batch is full at {i} ({} bytes), sending batch", batch.size() @@ -375,4 +383,6 @@ async fn test_overload_batch() { } assert!(result.is_ok()); } + + Ok(()) } diff --git a/sdk/eventhubs/azure_messaging_eventhubs/tests/round_trip.rs b/sdk/eventhubs/azure_messaging_eventhubs/tests/round_trip.rs index 81de55bc55..6fac28b6f1 100644 --- a/sdk/eventhubs/azure_messaging_eventhubs/tests/round_trip.rs +++ b/sdk/eventhubs/azure_messaging_eventhubs/tests/round_trip.rs @@ -18,22 +18,22 @@ use azure_messaging_eventhubs::{ producer::{batch::EventDataBatchOptions, ProducerClient, ProducerClientOptions}, }; use futures::pin_mut; -use std::env; +use std::{env, error::Error}; use tracing::info; mod common; #[recorded::test(live)] -async fn test_round_trip_batch() { +async fn test_round_trip_batch() -> Result<(), Box> { const EVENTHUB_PARTITION: &str = "1"; const TEST_NAME: &str = "test_round_trip_batch"; common::setup(); - let host = env::var("EVENTHUBS_HOST").unwrap(); - let eventhub = env::var("EVENTHUB_NAME").unwrap(); + let host = env::var("EVENTHUBS_HOST")?; + let eventhub = env::var("EVENTHUB_NAME")?; let producer = ProducerClient::new( host.clone(), eventhub.clone(), - DefaultAzureCredential::new().unwrap(), + DefaultAzureCredential::new()?, Some(ProducerClientOptions { application_id: Some(TEST_NAME.to_string()), ..Default::default() @@ -44,8 +44,7 @@ async fn test_round_trip_batch() { let partition_properties = producer .get_partition_properties(EVENTHUB_PARTITION.to_string()) - .await - .unwrap(); + .await?; info!( "Start receiving messages from sequence: {:?}", @@ -59,64 +58,55 @@ async fn test_round_trip_batch() { partition_key: Some("My Partition Key.".to_string()), ..Default::default() })) - .await - .unwrap(); - - assert!(batch - .try_add_event_data( - EventData::builder() - .with_body(b"Hello, World!") - .add_property("Message#".to_string(), 1) - .with_message_id(1) - .build(), - None - ) - .unwrap()); - - assert!(batch - .try_add_amqp_message( - AmqpMessage::builder() - .with_body(AmqpValue::from("Hello, World!")) - .add_application_property("Message#".to_string(), 2) - .with_properties(AmqpMessageProperties { - message_id: Some(2.into()), - ..Default::default() - }) - .build(), - None, - ) - .unwrap()); - - assert!(batch - .try_add_amqp_message( - AmqpMessage::builder() - .with_body(AmqpList::from(vec![ - AmqpValue::from("Hello, World!"), - 3.into(), - 5.into() - ])) - .add_application_property("Message#".to_string(), 3) - .with_properties(AmqpMessageProperties { - message_id: Some(3.into()), - ..Default::default() - }) - .build(), - None, - ) - .unwrap()); - - assert!(batch - .try_add_amqp_message( - AmqpMessage::builder() - .add_application_property("Message#".to_string(), 4) - .with_properties(AmqpMessageProperties { - message_id: Some(4.into()), - ..Default::default() - }) - .build(), - None - ) - .unwrap()); + .await?; + + assert!(batch.try_add_event_data( + EventData::builder() + .with_body(b"Hello, World!") + .add_property("Message#".to_string(), 1) + .with_message_id(1) + .build(), + None + )?); + + assert!(batch.try_add_amqp_message( + AmqpMessage::builder() + .with_body(AmqpValue::from("Hello, World!")) + .add_application_property("Message#".to_string(), 2) + .with_properties(AmqpMessageProperties { + message_id: Some(2.into()), + ..Default::default() + }) + .build(), + None, + )?); + + assert!(batch.try_add_amqp_message( + AmqpMessage::builder() + .with_body(AmqpList::from(vec![ + AmqpValue::from("Hello, World!"), + 3.into(), + 5.into() + ])) + .add_application_property("Message#".to_string(), 3) + .with_properties(AmqpMessageProperties { + message_id: Some(3.into()), + ..Default::default() + }) + .build(), + None, + )?); + + assert!(batch.try_add_amqp_message( + AmqpMessage::builder() + .add_application_property("Message#".to_string(), 4) + .with_properties(AmqpMessageProperties { + message_id: Some(4.into()), + ..Default::default() + }) + .build(), + None + )?); assert!(producer.submit_batch(&batch).await.is_ok()); @@ -124,7 +114,7 @@ async fn test_round_trip_batch() { host, eventhub, None, - DefaultAzureCredential::new().unwrap(), + DefaultAzureCredential::new()?, Some(ConsumerClientOptions { application_id: Some(TEST_NAME.to_string()), ..Default::default() @@ -176,4 +166,6 @@ async fn test_round_trip_batch() { } }) .await; + + Ok(()) } diff --git a/sdk/keyvault/assets.json b/sdk/keyvault/assets.json new file mode 100644 index 0000000000..d52b217292 --- /dev/null +++ b/sdk/keyvault/assets.json @@ -0,0 +1,6 @@ +{ + "AssetsRepo": "Azure/azure-sdk-assets", + "AssetsRepoPrefixPath": "rust", + "TagPrefix": "rust/keyvault", + "Tag": "rust/keyvault_0b4c5acf34" +} diff --git a/sdk/keyvault/azure_security_keyvault_secrets/Cargo.toml b/sdk/keyvault/azure_security_keyvault_secrets/Cargo.toml index 53cad55707..1a693d05af 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, features = ["tracing"] } azure_identity.path = "../../identity/azure_identity" 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..984792f3fc --- /dev/null +++ b/sdk/keyvault/azure_security_keyvault_secrets/tests/secret_client.rs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#![cfg_attr(target_arch = "wasm32", allow(unused_imports))] + +use azure_core::Result; +use azure_core_test::{recorded, TestContext}; +use azure_security_keyvault_secrets::{ + models::SecretSetParameters, SecretClient, SecretClientOptions, +}; + +#[recorded::test] +async fn secret_roundtrip(ctx: TestContext) -> Result<()> { + 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(()) +} diff --git a/sdk/keyvault/test-resources.bicep b/sdk/keyvault/test-resources.bicep new file mode 100644 index 0000000000..83adc49023 --- /dev/null +++ b/sdk/keyvault/test-resources.bicep @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +param baseName string = resourceGroup().name +param tenantId string = '72f988bf-86f1-41af-91ab-2d7cd011db47' +param testApplicationOid string +param location string = resourceGroup().location +@allowed(['standard', 'premium']) +param keyVaultSku string = 'premium' + +var kvAdminDefinitionId = '00482a5a-887f-4fb3-b363-3b7fe8e74483' +var kvAdminAssignmentName = guid(resourceGroup().id, kvAdminDefinitionId, testApplicationOid) + +resource kv 'Microsoft.KeyVault/vaults@2023-07-01' = { + name: baseName + location: location + properties: { + sku: { + family: 'A' + name: keyVaultSku + } + tenantId: tenantId + enableRbacAuthorization: true + softDeleteRetentionInDays: 7 + } +} + +resource kvAdmin 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: kvAdminAssignmentName + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', kvAdminDefinitionId) + principalId: testApplicationOid + } +} + +output AZURE_KEYVAULT_URL string = kv.properties.vaultUri diff --git a/sdk/typespec/typespec_client_core/src/http/clients/reqwest.rs b/sdk/typespec/typespec_client_core/src/http/clients/reqwest.rs index 82add9d58e..7507fe4cb3 100644 --- a/sdk/typespec/typespec_client_core/src/http/clients/reqwest.rs +++ b/sdk/typespec/typespec_client_core/src/http/clients/reqwest.rs @@ -15,7 +15,7 @@ use typespec::error::{Error, ErrorKind, Result, ResultExt}; /// Create a new [`HttpClient`] with the `reqwest` backend. pub fn new_reqwest_client() -> Arc { - debug!("instantiating an http client using the reqwest backend"); + debug!("creating an http client using `reqwest`"); // Set `pool_max_idle_per_host` to `0` to avoid an issue in the underlying // `hyper` library that causes the `reqwest` client to hang in some cases. diff --git a/sdk/typespec/typespec_client_core/src/http/headers/mod.rs b/sdk/typespec/typespec_client_core/src/http/headers/mod.rs index 73a0990410..024f18709c 100644 --- a/sdk/typespec/typespec_client_core/src/http/headers/mod.rs +++ b/sdk/typespec/typespec_client_core/src/http/headers/mod.rs @@ -222,8 +222,13 @@ impl Headers { impl fmt::Debug for Headers { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // TODO: Sanitize headers. - write!(f, "Headers(len: {})", self.0.len()) + #[allow(dead_code)] + const SANITIZED_VALUE: &str = "*****"; + + // TODO: Sanitize all bug safe headers. + f.debug_map() + .entries(self.0.keys().map(|k| (k.as_str(), SANITIZED_VALUE))) + .finish() } } diff --git a/sdk/typespec/typespec_client_core/src/http/policies/retry/mod.rs b/sdk/typespec/typespec_client_core/src/http/policies/retry/mod.rs index 5d909bc874..fbe49a5ac3 100644 --- a/sdk/typespec/typespec_client_core/src/http/policies/retry/mod.rs +++ b/sdk/typespec/typespec_client_core/src/http/policies/retry/mod.rs @@ -137,9 +137,10 @@ where let (last_error, retry_after) = match result { Ok(response) if response.status().is_success() => { trace!( - "Successful response. Request={:?} response={:?}", - request, - response + ?request, + ?response, + "server returned success status {}", + response.status(), ); return Ok(response); } diff --git a/sdk/typespec/typespec_client_core/src/http/policies/transport.rs b/sdk/typespec/typespec_client_core/src/http/policies/transport.rs index 0e09f89f78..be067d0be0 100644 --- a/sdk/typespec/typespec_client_core/src/http/policies/transport.rs +++ b/sdk/typespec/typespec_client_core/src/http/policies/transport.rs @@ -33,7 +33,7 @@ impl Policy for TransportPolicy { // there must be no more policies assert_eq!(0, next.len()); - debug!("the following request will be passed to the transport policy: {request:#?}"); + debug!(?request, "sending request '{}'", request.url); let response = { self.transport_options.send(ctx, request) }; response.await diff --git a/sdk/typespec/typespec_client_core/src/http/request/mod.rs b/sdk/typespec/typespec_client_core/src/http/request/mod.rs index 40146b4d1f..112dd88c8c 100644 --- a/sdk/typespec/typespec_client_core/src/http/request/mod.rs +++ b/sdk/typespec/typespec_client_core/src/http/request/mod.rs @@ -14,7 +14,7 @@ use crate::{ }; use bytes::Bytes; use serde::Serialize; -use std::{fmt::Debug, marker::PhantomData, str::FromStr}; +use std::{fmt, marker::PhantomData, str::FromStr}; /// An HTTP Body. #[derive(Clone)] @@ -53,12 +53,15 @@ impl Body { } } -impl Debug for Body { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Debug for Body { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Bytes(v) => write!(f, "Bytes(len: {})", v.len()), + Self::Bytes(v) if !v.is_empty() => f.write_str("Bytes { ... }"), + Self::Bytes(_) => f.write_str("Bytes {}"), #[cfg(not(target_arch = "wasm32"))] - Self::SeekableStream(v) => write!(f, "SeekableStream(len: {})", v.len()), + Self::SeekableStream(v) if !v.is_empty() => f.write_str("SeekableStream { ... }"), + #[cfg(not(target_arch = "wasm32"))] + Self::SeekableStream(_) => f.write_str("SeekableStream {}"), } } } @@ -95,7 +98,7 @@ impl PartialEq for Body { /// /// A pipeline request is composed by a destination (uri), a method, a collection of headers and a /// body. Policies are expected to enrich the request by mutating it. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct Request { pub(crate) url: Url, pub(crate) method: Method, @@ -181,6 +184,18 @@ impl Request { } } +impl fmt::Debug for Request { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Request") + // Format URL as simple string instead of struct. + .field("url", &self.url.as_str()) + .field("method", &self.method) + .field("headers", &self.headers) + .field("body", &self.body) + .finish() + } +} + /// The body content of a service client request. /// This allows callers to pass a model to serialize or raw content to client methods. #[derive(Clone, Debug)]