diff --git a/.vscode/cspell.json b/.vscode/cspell.json index d620b7d977..c1ad861cea 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -124,6 +124,11 @@ "path": "../sdk/keyvault/.dict.txt", "noSuggest": true }, + { + "name": "servicebus", + "path": "../sdk/servicebus/.dict.txt", + "noSuggest": true + }, { "name": "storage", "path": "../sdk/storage/.dict.txt", @@ -202,6 +207,14 @@ "cosmos" ] }, + { + "filename": "sdk/servicebus/**", + "dictionaries": [ + "crates", + "rust-custom", + "servicebus" + ] + }, { "filename": "sdk/storage/**", "dictionaries": [ @@ -211,4 +224,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/Cargo.lock b/Cargo.lock index f769de8d4e..ad85f2f2ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,6 +381,31 @@ dependencies = [ "uuid", ] +[[package]] +name = "azure_messaging_servicebus" +version = "0.1.0" +dependencies = [ + "async-lock", + "async-stream", + "async-trait", + "azure_core", + "azure_core_amqp", + "azure_core_test", + "azure_identity", + "azure_messaging_servicebus", + "futures", + "rand 0.9.2", + "rand_chacha 0.9.0", + "serde", + "serde_json", + "time", + "tokio", + "tracing", + "tracing-subscriber", + "url", + "uuid", +] + [[package]] name = "azure_security_keyvault_certificates" version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index d5fae13115..17eca861e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "sdk/keyvault/azure_security_keyvault_certificates", "sdk/keyvault/azure_security_keyvault_keys", "sdk/keyvault/azure_security_keyvault_secrets", + "sdk/servicebus/azure_messaging_servicebus", "sdk/template/azure_template_core", "sdk/template/azure_template", "sdk/storage/azure_storage_common", diff --git a/sdk/servicebus/.dict.txt b/sdk/servicebus/.dict.txt new file mode 100644 index 0000000000..c0485c4110 --- /dev/null +++ b/sdk/servicebus/.dict.txt @@ -0,0 +1,13 @@ +amqps +azuremessagingservicebus +backoff +mybus +myqueue +myservicebus +retryable +Retryable +SubQueue +subqueues +testqueue +testtopic +testsubscription diff --git a/sdk/servicebus/azure_messaging_servicebus/CHANGELOG.md b/sdk/servicebus/azure_messaging_servicebus/CHANGELOG.md new file mode 100644 index 0000000000..7b23c80f50 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/CHANGELOG.md @@ -0,0 +1,17 @@ +# Release History + +## 0.1.0 (Unreleased) + +### Features Added + +- Initial release of Azure Service Bus client library for Rust +- Support for sending and receiving messages from Service Bus queues and topics +- Support for session-enabled entities +- Support for dead letter queues +- AMQP-based implementation using azure_core_amqp + +### Breaking Changes + +### Bugs Fixed + +### Other Changes diff --git a/sdk/servicebus/azure_messaging_servicebus/CONTRIBUTING.md b/sdk/servicebus/azure_messaging_servicebus/CONTRIBUTING.md new file mode 100644 index 0000000000..4f5150e59d --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/CONTRIBUTING.md @@ -0,0 +1,120 @@ +# Running Azure Service Bus Live Tests + +This guide explains how to set up and run live integration tests for the Azure Service Bus SDK for Rust using the Azure SDK test infrastructure. + +## Prerequisites + +1. **Azure Subscription**: Access to the targeted subscription +2. **Azure CLI**: Install and configure Azure CLI (`az login`) +3. **Azure PowerShell**: Install PowerShell modules (`Install-Module Az -Force`) +4. **Bicep**: For infrastructure deployment (`az bicep install`) +5. **Permissions**: Contributor access to create resources in your subscription + +## Automated Test Infrastructure Setup + +The Azure SDK provides a standardized test infrastructure using the `New-TestResources.ps1` script with Bicep templates to automatically create and configure all required Azure Service Bus resources. + +### Deploy Test Resources + +From the repository root, run: + +```powershell +# Set subscription +az account set --subscription "" + +# Deploy test infrastructure +./eng/common/TestResources/New-TestResources.ps1 servicebus/azure_messaging_servicebus -SubscriptionId +``` + +This will automatically create: + +- Azure Service Bus namespace with Standard tier +- Test queue named `testqueue` +- Test topic named `testtopic` with subscription `testsubscription` +- RBAC permissions for TokenCredential testing +- All required environment variables + +The script will output PowerShell commands to set environment variables. + +### Environment Variables + +After deployment completes successfully, the script will output commands like: + +```powershell +$env:SERVICEBUS_NAMESPACE = "sb-your-deployment-name.servicebus.windows.net" +$env:SERVICEBUS_QUEUE_NAME = "testqueue" +$env:SERVICEBUS_TOPIC_NAME = "testtopic" +$env:SERVICEBUS_SUBSCRIPTION_NAME = "testsubscription" +``` + +## Run Live Tests + +Once the environment variables are set, run the tests: + +**All Live Tests:** + +```powershell +cd sdk\servicebus\azure_messaging_servicebus +cargo test +``` + +## Debugging + +Enable debug logging for detailed information: + +```powershell +$env:RUST_LOG = "debug" +cargo test +``` + +For detailed AMQP protocol debugging: + +```powershell +$env:RUST_LOG = "azure_messaging_servicebus=debug,azure_core_amqp=debug" +cargo test +``` + +## Expected Test Behavior + +- **TokenCredential Tests**: Will skip if not properly configured (not a failure) +- **Test Duration**: Each test typically takes 10-30 seconds due to network operations +- **Resource Cleanup**: Tests automatically complete/delete sent messages + +## Troubleshooting + +### Deployment Issues + +- Ensure you're logged into Azure CLI: `az login` +- Verify you have Contributor permissions on the subscription +- Check that the resource group name doesn't already exist +- Ensure Service Bus is available in your selected region + +### Authentication Issues + +- For TokenCredential tests, ensure you're logged in via Azure CLI +- RBAC permissions are automatically configured during deployment +- Service principal credentials can be set via environment variables if needed + +### Test Execution Issues + +- Verify all environment variables are set correctly +- Check that Service Bus resources are in "Active" status +- Ensure network connectivity to Azure Service Bus +- Review test output for specific error messages + +## Resource Cleanup + +When you're finished testing, clean up the resources: + +```powershell +# Clean up test resources +./eng/common/TestResources/Remove-TestResources.ps1 servicebus/azure_messaging_servicebus +``` + +## Performance Notes + +- Live tests create real network connections +- Messages are sent to actual Azure Service Bus queues +- Consider using a dedicated test namespace +- Tests include cleanup operations (complete/delete messages) +- Some tests may have delays for timing validation diff --git a/sdk/servicebus/azure_messaging_servicebus/Cargo.toml b/sdk/servicebus/azure_messaging_servicebus/Cargo.toml new file mode 100644 index 0000000000..50909e61cc --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/Cargo.toml @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corp. All Rights Reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. + +[package] +name = "azure_messaging_servicebus" +version = "0.1.0" +description = "Rust client for Azure Service Bus" +readme = "README.md" +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage = "https://github.com/azure/azure-sdk-for-rust" +documentation = "https://docs.rs/azure_messaging_servicebus" + +keywords = ["sdk", "azure", "messaging", "cloud", "servicebus"] +categories = ["api-bindings"] + +edition.workspace = true + +[dependencies] +async-lock.workspace = true +async-stream.workspace = true +async-trait.workspace = true +azure_core.workspace = true +azure_core_amqp.workspace = true +futures.workspace = true +rand.workspace = true +rand_chacha.workspace = true +serde = { workspace = true, features = ["derive"] } +time.workspace = true +tracing.workspace = true +url.workspace = true +uuid.workspace = true + +[dev-dependencies] +azure_core_amqp = { workspace = true, features = ["test"] } +azure_core_test = { workspace = true, features = ["tracing"] } +azure_identity.workspace = true +azure_messaging_servicebus = { path = "." } +serde_json.workspace = true +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } + +[lints] +workspace = true diff --git a/sdk/servicebus/azure_messaging_servicebus/README.md b/sdk/servicebus/azure_messaging_servicebus/README.md new file mode 100644 index 0000000000..c384405a78 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/README.md @@ -0,0 +1,185 @@ + + +# Azure Service Bus client library for Rust + +Azure Service Bus is a fully managed enterprise message broker with message queues and publish-subscribe topics. Service Bus is used to decouple applications and services from each other, providing the following benefits: + +- Load-balancing work across competing workers +- Safely routing and transferring data and control across service and application boundaries +- Coordinating transactional work that requires a high-degree of reliability + +[Source code] | [Package (crates.io)] | [API reference documentation] | [Product documentation] + +## WARNING - Not production ready crate + +This crate is in early development, it **SHOULD NOT** be used in production. + +## Getting started + +### Prerequisites + +- Rust 1.85.0 or later +- An Azure subscription +- A Service Bus namespace + +### Install dependencies + +Add the following crates to your project: + +```sh +cargo add azure_identity tokio +``` + +### Authenticate the client + +In order to interact with the Azure Service Bus service, you'll need to create an instance of a client class. To create a client object, you'll need the Service Bus namespace and a credential object. + +#### Using Azure Identity + +```rust,no_run +# async fn example() -> Result<(), Box> { +use azure_messaging_servicebus::ServiceBusClient; +use azure_identity::DeveloperToolsCredential; + +let credential = DeveloperToolsCredential::new(None)?; +let client = ServiceBusClient::builder() + .open("your_namespace.servicebus.windows.net", credential.clone()) + .await?; +# Ok(()) +# } +``` + +The `ServiceBusClient` supports various credential types from the `azure_identity` crate: + +- **DeveloperToolsCredential** (Recommended): Automatically tries multiple authentication methods +- **ClientSecretCredential**: For service principals with client secrets +- **ManagedIdentityCredential**: For Azure resources with managed identity +- **AzureCliCredential**: For development using Azure CLI authentication + +All credentials handle token acquisition, caching, and automatic refresh automatically. + +For a comprehensive example showing all credential types, see [token_credential_auth.rs](https://github.com/Azure/azure-sdk-for-rust/blob/main/sdk/servicebus/azure_messaging_servicebus/examples/token_credential_auth.rs). + +## Key concepts + +- **Namespace**: A Service Bus namespace is a scoping container for all messaging components. +- **Queue**: A queue allows storage of messages until the receiving application is available to receive and process them. +- **Topic**: A topic provides a one-to-many form of communication using a publish/subscribe pattern. +- **Subscription**: A subscription is used to receive messages from a topic. +- **Message**: A message is a package of information that contains both data and metadata. + +## Examples + +### Samples + +See our [samples] + +### Send a message to a queue + +```rust,no_run +# async fn example() -> Result<(), Box> { +use azure_messaging_servicebus::{ServiceBusClient, Message, CreateSenderOptions, SendMessageOptions}; +use azure_identity::DeveloperToolsCredential; + +let credential = DeveloperToolsCredential::new(None)?; +let client = ServiceBusClient::builder() + .open("your_namespace.servicebus.windows.net", credential.clone()) + .await?; +let sender = client.create_sender("my_queue", None).await?; + +let message = Message::from_string("Hello, Service Bus!"); +sender.send_message(message, None).await?; +# Ok(()) +# } +``` + +### Receive messages from a queue + +```rust,no_run +# async fn example() -> Result<(), Box> { +use azure_messaging_servicebus::{ServiceBusClient, CreateReceiverOptions, ReceiveMessageOptions, CompleteMessageOptions}; +use azure_identity::DeveloperToolsCredential; + +let credential = DeveloperToolsCredential::new(None)?; +let client = ServiceBusClient::builder() + .open("your_namespace.servicebus.windows.net", credential.clone()) + .await?; +let receiver = client.create_receiver("my_queue", None).await?; + +let messages = receiver.receive_messages(5, None).await?; +for message in messages { + println!("Received: {}", message.body_as_string()?); + receiver.complete_message(&message, None).await?; +} +# Ok(()) +# } +``` + +### Send a message to a topic + +```rust,no_run +# async fn example() -> Result<(), Box> { +use azure_messaging_servicebus::{ServiceBusClient, Message, CreateSenderOptions, SendMessageOptions}; +use azure_identity::DeveloperToolsCredential; + +let credential = DeveloperToolsCredential::new(None)?; +let client = ServiceBusClient::builder() + .open("your_namespace.servicebus.windows.net", credential.clone()) + .await?; +let sender = client.create_sender("my_topic", None).await?; + +let message = Message::from_string("Hello, Topic subscribers!"); +sender.send_message(message, None).await?; +# Ok(()) +# } +``` + +### Receive messages from a subscription + +```rust,no_run +# async fn example() -> Result<(), Box> { +use azure_messaging_servicebus::{ServiceBusClient, CreateReceiverOptions, ReceiveMessageOptions, CompleteMessageOptions}; +use azure_identity::DeveloperToolsCredential; + +let credential = DeveloperToolsCredential::new(None)?; +let client = ServiceBusClient::builder() + .open("your_namespace.servicebus.windows.net", credential.clone()) + .await?; +let receiver = client.create_receiver_for_subscription("my_topic", "my_subscription", None).await?; + +let messages = receiver.receive_messages(5, None).await?; +for message in messages { + println!("Received: {}", message.body_as_string()?); + receiver.complete_message(&message, None).await?; +} +# Ok(()) +# } +``` + +## Troubleshooting + +- Read about the different [Service Bus messaging patterns] + +## Contributing + +See the [CONTRIBUTING.md] for details on building, testing, and contributing to these libraries. + +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit . + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct]. For more information see the [Code of Conduct FAQ] or contact with any additional questions or comments. + +### Reporting security issues and security bugs + +Security issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC) . You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Further information, including the MSRC PGP key, can be found in the [Security TechCenter](https://www.microsoft.com/msrc/faqs-report-an-issue). + +### License + +Azure SDK for Rust is licensed under the [MIT](https://github.com/Azure/azure-sdk-for-rust/blob/main/LICENSE.txt) license. + +[CONTRIBUTING.md]: https://github.com/Azure/azure-sdk-for-rust/blob/main/CONTRIBUTING.md +[samples]: https://github.com/Azure/azure-sdk-for-rust/tree/main/sdk/servicebus/azure_messaging_servicebus/examples +[Service Bus messaging patterns]: https://docs.microsoft.com/azure/service-bus-messaging/ +[Microsoft Open Source Code of Conduct]: https://opensource.microsoft.com/codeofconduct/ +[Code of Conduct FAQ]: https://opensource.microsoft.com/codeofconduct/faq/ diff --git a/sdk/servicebus/azure_messaging_servicebus/examples/builder_pattern.rs b/sdk/servicebus/azure_messaging_servicebus/examples/builder_pattern.rs new file mode 100644 index 0000000000..73ef47f9e1 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/examples/builder_pattern.rs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! This example demonstrates how to create a Service Bus client using the builder pattern. + +use azure_identity::DeveloperToolsCredential; + +use azure_messaging_servicebus::{ + CompleteMessageOptions, CreateReceiverOptions, CreateSenderOptions, Message, + ReceiveMessageOptions, SendMessageOptions, ServiceBusClient, +}; +use std::env; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing for logging + tracing_subscriber::fmt::init(); + + // Get the namespace from environment variable + let namespace = env::var("SERVICEBUS_NAMESPACE") + .expect("SERVICEBUS_NAMESPACE environment variable must be set"); + + // Get the queue name from environment variable + let queue_name = env::var("SERVICEBUS_QUEUE_NAME") + .expect("SERVICEBUS_QUEUE_NAME environment variable must be set"); + + println!("Creating Service Bus client using builder pattern..."); + + // Create a credential + let credential = DeveloperToolsCredential::new(None)?; + + // Create client using builder pattern with custom retry options + let client = ServiceBusClient::builder() + .open(&namespace, credential.clone()) + .await?; + + println!("Creating sender for queue: {}", queue_name); + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + // Create a message + let mut message = Message::from_string("Hello, Service Bus from Rust Builder Pattern!"); + message.set_message_id("builder-example-message-1"); + message.set_property("language", "rust"); + message.set_property("example", "builder_pattern"); + message.set_property("pattern", "builder"); + + println!("Sending message to queue: {}", queue_name); + let send_options = SendMessageOptions::default(); + sender.send_message(message, Some(send_options)).await?; + + println!("Message sent successfully using builder pattern!"); + + // Create a receiver to verify the message was sent + println!("Creating receiver for queue: {}", queue_name); + let receiver = client + .create_receiver(&queue_name, Some(CreateReceiverOptions::default())) + .await?; + + println!("Receiving message..."); + let receive_options = ReceiveMessageOptions::default(); + if let Some(received_message) = receiver.receive_message(Some(receive_options)).await? { + println!("Received message: {}", received_message.body_as_string()?); + println!("Message ID: {:?}", received_message.message_id()); + + // Print custom properties + for (key, value) in received_message.properties() { + println!("Property {}: {}", key, value); + } + + // Complete the message + let complete_options = CompleteMessageOptions::default(); + receiver + .complete_message(&received_message, Some(complete_options)) + .await?; + println!("Message completed successfully!"); + } else { + println!("No message received"); + } + + // Close the sender and receiver + sender.close().await?; + receiver.close().await?; + + // Close the client + client.close().await?; + + Ok(()) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/examples/dead_letter_queue.rs b/sdk/servicebus/azure_messaging_servicebus/examples/dead_letter_queue.rs new file mode 100644 index 0000000000..b35dae19fd --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/examples/dead_letter_queue.rs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! This example demonstrates how to work with dead letter queues in Service Bus using the SubQueue enum. +//! It shows how to: +//! 1. Send a message to a queue +//! 2. Receive and dead letter the message +//! 3. Use SubQueue::DeadLetter to receive the dead lettered message from the dead letter queue + +use azure_identity::DeveloperToolsCredential; +use azure_messaging_servicebus::{ + CreateReceiverOptions, CreateSenderOptions, DeadLetterMessageOptions, Message, ReceiveMode, + ServiceBusClient, SubQueue, +}; +use std::env; +use tokio::time::{sleep, Duration}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing for logging + tracing_subscriber::fmt::init(); + + // Get the queue name from environment variable + let queue_name = env::var("SERVICEBUS_QUEUE_NAME") + .expect("SERVICEBUS_QUEUE_NAME environment variable must be set"); + + println!("Creating Service Bus client..."); + let credential = DeveloperToolsCredential::new(None)?; + let client = ServiceBusClient::builder() + .open("myservicebus.servicebus.windows.net", credential.clone()) + .await?; + + // Step 1: Send a message to the queue + println!("Creating sender for queue: {}", queue_name); + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let mut message = Message::from_string("This message will be dead lettered"); + message.set_message_id("dead-letter-example-1"); + message.set_property("example", "dead_letter_queue"); + + println!("Sending message to queue: {}", queue_name); + sender.send_message(message, None).await?; + println!("Message sent successfully!"); + + // Step 2: Receive the message and dead letter it + println!("Creating receiver for queue: {}", queue_name); + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + + println!("Receiving message from queue..."); + if let Some(received_message) = receiver.receive_message(None).await? { + println!("Received message: {}", received_message.body_as_string()?); + println!("Message ID: {:?}", received_message.message_id()); + + // Dead letter the message with a reason + println!("Dead lettering the message..."); + let dead_letter_options = DeadLetterMessageOptions { + reason: Some("ProcessingFailed".to_string()), + error_description: Some( + "Message could not be processed due to invalid format".to_string(), + ), + properties_to_modify: None, + }; + receiver + .dead_letter_message(&received_message, Some(dead_letter_options)) + .await?; + println!("Message dead lettered successfully!"); + } else { + println!("No message received"); + } + + // Step 3: Receive the dead lettered message from the dead letter queue using SubQueue + println!("Creating dead letter receiver for queue: {}", queue_name); + let dead_letter_receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: Some(SubQueue::DeadLetter), + }), + ) + .await?; + + println!("Checking dead letter queue for messages..."); + // Wait a bit for the message to appear in the dead letter queue + sleep(Duration::from_secs(2)).await; + + match dead_letter_receiver.receive_message(None).await? { + Some(dead_letter_message) => { + println!("Received dead lettered message!"); + println!("Message body: {}", dead_letter_message.body_as_string()?); + println!("Message ID: {:?}", dead_letter_message.message_id()); + println!( + "Dead letter reason: {:?}", + dead_letter_message.system_properties().dead_letter_reason + ); + println!( + "Dead letter description: {:?}", + dead_letter_message + .system_properties() + .dead_letter_error_description + ); + + // Complete the dead lettered message to remove it from the dead letter queue + println!("Completing dead lettered message..."); + dead_letter_receiver + .complete_message(&dead_letter_message, None) + .await?; + println!("Dead lettered message completed successfully!"); + } + None => { + println!("No dead lettered messages found"); + } + } + + // Clean up + sender.close().await?; + receiver.close().await?; + dead_letter_receiver.close().await?; + client.close().await?; + + Ok(()) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/examples/message_batching.rs b/sdk/servicebus/azure_messaging_servicebus/examples/message_batching.rs new file mode 100644 index 0000000000..f610ec7259 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/examples/message_batching.rs @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! This example demonstrates how to use message batching with Azure Service Bus +//! to efficiently send multiple messages in a single operation. + +use azure_identity::DeveloperToolsCredential; +use azure_messaging_servicebus::{ + CreateMessageBatchOptions, CreateSenderOptions, Message, ServiceBusClient, +}; +use std::env; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing for better debugging + tracing_subscriber::fmt::init(); + + let queue_name = env::var("SERVICEBUS_QUEUE_NAME") + .expect("SERVICEBUS_QUEUE_NAME environment variable is required"); + + println!("šŸš€ Service Bus Message Batching Example"); + println!("Queue: {}", queue_name); + + // Create Service Bus client + let credential = DeveloperToolsCredential::new(None)?; + let client = ServiceBusClient::builder() + .open("myservicebus.servicebus.windows.net", credential.clone()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + // Example 1: Basic message batching + println!("\nšŸ“¦ Example 1: Basic Message Batching"); + basic_message_batching(&sender).await?; + + // Example 2: Size-limited batching + println!("\nšŸ“ Example 2: Size-Limited Batching"); + size_limited_batching(&sender).await?; + + // Example 3: Handling batch overflow + println!("\nšŸ”„ Example 3: Handling Batch Overflow"); + batch_overflow_handling(&sender).await?; + + // Example 4: Batching with message properties + println!("\nšŸ·ļø Example 4: Batching with Message Properties"); + batching_with_properties(&sender).await?; + + // Clean up + sender.close().await?; + client.close().await?; + + println!("\nāœ… All batching examples completed successfully!"); + Ok(()) +} + +/// Demonstrates basic message batching functionality +async fn basic_message_batching( + sender: &azure_messaging_servicebus::Sender, +) -> Result<(), Box> { + // Create a batch with default settings + let mut batch = sender.create_message_batch(None).await?; + + println!( + " šŸ“Š Created batch with max size: {} bytes", + batch.maximum_size_in_bytes() + ); + + // Add multiple messages to the batch + let messages = [ + "Hello, batch message 1!", + "Hello, batch message 2!", + "Hello, batch message 3!", + "Hello, batch message 4!", + "Hello, batch message 5!", + ]; + + for (i, message_text) in messages.iter().enumerate() { + let message = Message::from_string(*message_text); + + if batch.try_add_message(message) { + println!(" āœ… Added message {}: '{}'", i + 1, message_text); + } else { + println!(" āš ļø Failed to add message {}: batch is full", i + 1); + break; + } + } + + println!( + " šŸ“ˆ Batch contains {} messages, total size: {} bytes", + batch.count(), + batch.size_in_bytes() + ); + + // Send the entire batch in one operation + sender.send_message_batch(batch, None).await?; + println!(" šŸš€ Batch sent successfully!"); + + Ok(()) +} + +/// Demonstrates size-limited batching +async fn size_limited_batching( + sender: &azure_messaging_servicebus::Sender, +) -> Result<(), Box> { + // Create a batch with a small size limit for demonstration + let options = CreateMessageBatchOptions { + maximum_size_in_bytes: Some(1024), + }; + let mut batch = sender.create_message_batch(Some(options)).await?; + + println!( + " šŸ“ Created size-limited batch: {} bytes max", + batch.maximum_size_in_bytes() + ); + + // Try to add messages until the batch is full + let mut message_count = 0; + for i in 0..50 { + let message_text = format!( + "Size-limited message {} with some additional content to use more space", + i + ); + let message = Message::from_string(&message_text); + + if batch.try_add_message(message) { + message_count += 1; + println!( + " āœ… Added message {} (batch size: {} bytes)", + i, + batch.size_in_bytes() + ); + } else { + println!( + " šŸ›‘ Batch full after {} messages at {} bytes", + message_count, + batch.size_in_bytes() + ); + break; + } + } + + if !batch.is_empty() { + sender.send_message_batch(batch, None).await?; + println!(" šŸš€ Size-limited batch sent successfully!"); + } + + Ok(()) +} + +/// Demonstrates handling batch overflow by creating multiple batches +async fn batch_overflow_handling( + sender: &azure_messaging_servicebus::Sender, +) -> Result<(), Box> { + let total_messages = 20; + let batch_options = CreateMessageBatchOptions { + maximum_size_in_bytes: Some(2048), + }; + + let mut current_batch = sender + .create_message_batch(Some(batch_options.clone())) + .await?; + let mut batch_count = 1; + let mut messages_sent = 0; + + println!( + " šŸ“¦ Sending {} messages using automatic batch overflow handling", + total_messages + ); + + for i in 0..total_messages { + let message_text = format!("Overflow handling message {} with content", i); + let message = Message::from_string(&message_text); + + if !current_batch.try_add_message(message.clone()) { + // Current batch is full, send it and create a new one + if !current_batch.is_empty() { + let batch_message_count = current_batch.count(); + sender.send_message_batch(current_batch, None).await?; + println!( + " šŸš€ Sent batch {} with {} messages", + batch_count, batch_message_count + ); + messages_sent += batch_message_count; + batch_count += 1; + } + + // Create a new batch and add the message that didn't fit + current_batch = sender + .create_message_batch(Some(batch_options.clone())) + .await?; + + if !current_batch.try_add_message(message) { + println!(" āŒ Message {} is too large for an empty batch!", i); + continue; + } + } + + println!(" āœ… Added message {} to batch {}", i, batch_count); + } + + // Send the final batch if it has messages + if !current_batch.is_empty() { + let final_batch_count = current_batch.count(); + messages_sent += final_batch_count; + sender.send_message_batch(current_batch, None).await?; + println!( + " šŸš€ Sent final batch {} with {} messages", + batch_count, final_batch_count + ); + } + + println!( + " šŸ“Š Total: {} batches sent with {} messages", + batch_count, messages_sent + ); + Ok(()) +} + +/// Demonstrates batching messages with properties +async fn batching_with_properties( + sender: &azure_messaging_servicebus::Sender, +) -> Result<(), Box> { + let mut batch = sender.create_message_batch(None).await?; + let batch_id = uuid::Uuid::new_v4().to_string(); + + println!(" šŸ·ļø Creating batch with ID: {}", batch_id); + + // Add messages with various properties + for i in 0..3 { + let message_text = format!("Property message {} content", i); + let mut message = Message::from_string(&message_text); + + // Set standard properties + message.set_message_id(format!("prop-msg-{}-{}", batch_id, i)); + message.set_correlation_id(&batch_id); + message.set_content_type("text/plain"); + message.set_subject(format!("Batch Message {}", i)); + + // Set custom properties + message.set_property("batch_id", &batch_id); + message.set_property("sequence", i.to_string()); + message.set_property("priority", if i % 2 == 0 { "high" } else { "normal" }); + message.set_property("category", "demo"); + + if batch.try_add_message(message) { + println!(" āœ… Added message {} with properties", i); + } else { + println!(" āš ļø Failed to add message {}: batch is full", i); + break; + } + } + + println!( + " šŸ“ˆ Batch with properties contains {} messages, size: {} bytes", + batch.count(), + batch.size_in_bytes() + ); + + sender.send_message_batch(batch, None).await?; + println!(" šŸš€ Batch with properties sent successfully!"); + + Ok(()) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/examples/message_deferral.rs b/sdk/servicebus/azure_messaging_servicebus/examples/message_deferral.rs new file mode 100644 index 0000000000..93b9e195fe --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/examples/message_deferral.rs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! This example demonstrates how to defer and receive deferred messages in Service Bus. +//! It shows how to: +//! 1. Send a message to a queue +//! 2. Receive and defer the message +//! 3. Attempt to receive the deferred message using its sequence number + +use azure_identity::DeveloperToolsCredential; +use azure_messaging_servicebus::{ + CompleteMessageOptions, CreateReceiverOptions, CreateSenderOptions, DeferMessageOptions, + Message, ReceiveDeferredMessagesOptions, ReceiveMessageOptions, ReceiveMode, + SendMessageOptions, ServiceBusClient, +}; +use std::env; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing for logging + tracing_subscriber::fmt::init(); + + // Get the queue name from environment variable + let queue_name = env::var("SERVICEBUS_QUEUE_NAME") + .expect("SERVICEBUS_QUEUE_NAME environment variable must be set"); + + println!("Creating Service Bus client..."); + let credential = DeveloperToolsCredential::new(None)?; + let client = ServiceBusClient::builder() + .open("myservicebus.servicebus.windows.net", credential.clone()) + .await?; + + // Step 1: Send a message to the queue + println!("Creating sender for queue: {}", queue_name); + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let mut message = Message::from_string("This message will be deferred"); + message.set_message_id("defer-example-1"); + message.set_property("example", "message_deferral"); + + println!("Sending message to queue: {}", queue_name); + let send_options = SendMessageOptions::default(); + sender.send_message(message, Some(send_options)).await?; + println!("Message sent successfully!"); + + // Step 2: Receive the message and defer it + println!("Creating receiver for queue: {}", queue_name); + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + + println!("Receiving message from queue..."); + let receive_options = ReceiveMessageOptions::default(); + if let Some(received_message) = receiver.receive_message(Some(receive_options)).await? { + println!("Received message: {}", received_message.body_as_string()?); + println!("Message ID: {:?}", received_message.message_id()); + + let sequence_number = received_message.sequence_number(); + println!("Sequence number: {:?}", sequence_number); + + // Defer the message + println!("Deferring the message..."); + let defer_options = DeferMessageOptions::default(); + receiver + .defer_message(&received_message, Some(defer_options)) + .await?; + println!("Message deferred successfully!"); + + // Step 3: Try to receive the deferred message using its sequence number + if let Some(seq_num) = sequence_number { + println!( + "Attempting to receive deferred message with sequence number: {}", + seq_num + ); + + match receiver + .receive_deferred_message(seq_num, Some(ReceiveDeferredMessagesOptions::default())) + .await? + { + Some(deferred_message) => { + println!("Received deferred message!"); + println!("Message body: {}", deferred_message.body_as_string()?); + println!("Message ID: {:?}", deferred_message.message_id()); + + // Complete the deferred message + println!("Completing deferred message..."); + let complete_options = CompleteMessageOptions::default(); + receiver + .complete_message(&deferred_message, Some(complete_options)) + .await?; + println!("Deferred message completed successfully!"); + } + None => { + println!("Deferred message not found (implementation incomplete)"); + println!("Note: Deferred message retrieval is not yet fully implemented"); + } + } + } else { + println!("Message did not have a sequence number"); + } + + // Example of receiving multiple deferred messages + let sequence_numbers = vec![123, 456, 789]; // Example sequence numbers + println!("Attempting to receive multiple deferred messages..."); + let deferred_messages = receiver + .receive_deferred_messages( + &sequence_numbers, + Some(ReceiveDeferredMessagesOptions::default()), + ) + .await?; + println!( + "Received {} deferred messages (implementation incomplete)", + deferred_messages.len() + ); + } else { + println!("No message received"); + } + + // Clean up + sender.close().await?; + receiver.close().await?; + client.close().await?; + + Ok(()) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/examples/receive_from_subscription.rs b/sdk/servicebus/azure_messaging_servicebus/examples/receive_from_subscription.rs new file mode 100644 index 0000000000..43bb059ab2 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/examples/receive_from_subscription.rs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! This example demonstrates how to receive messages from a Service Bus topic subscription. + +use azure_identity::DeveloperToolsCredential; +use azure_messaging_servicebus::{CreateReceiverOptions, ReceiveMode, ServiceBusClient}; +use std::env; +use tokio::time::{sleep, Duration}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing for logging + tracing_subscriber::fmt::init(); + + // Get the topic and subscription names from environment variables + let topic_name = env::var("SERVICEBUS_TOPIC_NAME") + .expect("SERVICEBUS_TOPIC_NAME environment variable must be set"); + let subscription_name = env::var("SERVICEBUS_SUBSCRIPTION_NAME") + .expect("SERVICEBUS_SUBSCRIPTION_NAME environment variable must be set"); + + println!("Creating Service Bus client..."); + let credential = DeveloperToolsCredential::new(None)?; + let client = ServiceBusClient::builder() + .open("myservicebus.servicebus.windows.net", credential.clone()) + .await?; + + println!( + "Creating receiver for topic: {} subscription: {}", + topic_name, subscription_name + ); + let receiver = client + .create_receiver_for_subscription( + &topic_name, + &subscription_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + + println!( + "Listening for messages on topic: {} subscription: {}", + topic_name, subscription_name + ); + println!("Press Ctrl+C to stop..."); + + // Receive messages in a loop + loop { + match receiver.receive_messages(10, None).await { + Ok(messages) => { + if messages.is_empty() { + println!("No messages received, waiting..."); + sleep(Duration::from_secs(5)).await; + continue; + } + + println!("Received {} messages from subscription", messages.len()); + + for message in messages { + println!("Message ID: {:?}", message.message_id()); + println!("Subject: {:?}", message.system_properties().subject); + println!("Message body: {}", message.body_as_string()?); + println!("Sequence number: {:?}", message.sequence_number()); + println!("Enqueued time: {:?}", message.enqueued_time_utc()); + + // Print custom properties + for (key, value) in message.properties() { + println!("Property {}: {}", key, value); + } + + // Complete the message to remove it from the subscription + println!("Completing message..."); + receiver.complete_message(&message, None).await?; + println!("Message completed successfully"); + println!("---"); + } + } + Err(e) => { + eprintln!("Error receiving messages: {}", e); + sleep(Duration::from_secs(5)).await; + } + } + } +} diff --git a/sdk/servicebus/azure_messaging_servicebus/examples/receive_messages.rs b/sdk/servicebus/azure_messaging_servicebus/examples/receive_messages.rs new file mode 100644 index 0000000000..d9ae7622ba --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/examples/receive_messages.rs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! This example demonstrates how to receive messages from a Service Bus queue. + +use azure_identity::DeveloperToolsCredential; +use azure_messaging_servicebus::{CreateReceiverOptions, ReceiveMode, ServiceBusClient}; +use std::env; +use tokio::time::{sleep, Duration}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing for logging + tracing_subscriber::fmt::init(); + + // Get the queue name from environment variable + let queue_name = env::var("SERVICEBUS_QUEUE_NAME") + .expect("SERVICEBUS_QUEUE_NAME environment variable must be set"); + + println!("Creating Service Bus client..."); + let credential = DeveloperToolsCredential::new(None)?; + let client = ServiceBusClient::builder() + .open("myservicebus.servicebus.windows.net", credential.clone()) + .await?; + + println!("Creating receiver for queue: {}", queue_name); + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + + println!("Listening for messages on queue: {}", queue_name); + println!("Press Ctrl+C to stop..."); + + // Receive messages in a loop + loop { + match receiver.receive_messages(5, None).await { + Ok(messages) => { + if messages.is_empty() { + println!("No messages received, waiting..."); + sleep(Duration::from_secs(5)).await; + continue; + } + + println!("Received {} messages", messages.len()); + + for message in messages { + println!("Message ID: {:?}", message.message_id()); + println!("Message body: {}", message.body_as_string()?); + println!("Sequence number: {:?}", message.sequence_number()); + println!("Enqueued time: {:?}", message.enqueued_time_utc()); + + // Print custom properties + for (key, value) in message.properties() { + println!("Property {}: {}", key, value); + } + + // Complete the message to remove it from the queue + println!("Completing message..."); + receiver.complete_message(&message, None).await?; + println!("Message completed successfully"); + println!("---"); + } + } + Err(e) => { + eprintln!("Error receiving messages: {}", e); + sleep(Duration::from_secs(5)).await; + } + } + } +} diff --git a/sdk/servicebus/azure_messaging_servicebus/examples/send_message.rs b/sdk/servicebus/azure_messaging_servicebus/examples/send_message.rs new file mode 100644 index 0000000000..fe91f22f6a --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/examples/send_message.rs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! This example demonstrates how to send a message to a Service Bus queue. + +use azure_identity::DeveloperToolsCredential; +use azure_messaging_servicebus::{Message, ServiceBusClient}; +use std::env; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing for logging + tracing_subscriber::fmt::init(); + + // Get the queue name from environment variable + let queue_name = env::var("SERVICEBUS_QUEUE_NAME") + .expect("SERVICEBUS_QUEUE_NAME environment variable must be set"); + + println!("Creating Service Bus client..."); + let credential = DeveloperToolsCredential::new(None)?; + let client = ServiceBusClient::builder() + .open("myservicebus.servicebus.windows.net", credential.clone()) + .await?; + + println!("Creating sender for queue: {}", queue_name); + let sender = client.create_sender(&queue_name, None).await?; + + // Create a message + let mut message = Message::from_string("Hello, Service Bus from Rust!"); + message.set_message_id("example-message-1"); + message.set_property("language", "rust"); + message.set_property("example", "send_message"); + + println!("Sending message to queue: {}", queue_name); + sender.send_message(message, None).await?; + + println!("Message sent successfully!"); + + // Close the sender + sender.close().await?; + + // Close the client + client.close().await?; + + Ok(()) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/examples/send_to_topic.rs b/sdk/servicebus/azure_messaging_servicebus/examples/send_to_topic.rs new file mode 100644 index 0000000000..b6959719f3 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/examples/send_to_topic.rs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! This example demonstrates how to send messages to a Service Bus topic. + +use azure_identity::DeveloperToolsCredential; +use azure_messaging_servicebus::{ + CreateSenderOptions, Message, SendMessageOptions, ServiceBusClient, +}; +use std::env; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing for logging + tracing_subscriber::fmt::init(); + + // Get the topic name from environment variable + let topic_name = env::var("SERVICEBUS_TOPIC_NAME") + .expect("SERVICEBUS_TOPIC_NAME environment variable must be set"); + + println!("Creating Service Bus client..."); + let credential = DeveloperToolsCredential::new(None)?; + let client = ServiceBusClient::builder() + .open("myservicebus.servicebus.windows.net", credential.clone()) + .await?; + + println!("Creating sender for topic: {}", topic_name); + let sender = client + .create_sender(&topic_name, Some(CreateSenderOptions::default())) + .await?; + + // Send multiple messages + for i in 1..=5 { + let mut message = Message::from_string(format!("Message {} to topic", i)); + message.set_message_id(format!("topic-message-{}", i)); + message.set_property("message_number", i.to_string()); + message.set_property("example", "send_to_topic"); + message.set_subject("example-message"); + + println!("Sending message {} to topic: {}", i, topic_name); + let send_options = SendMessageOptions::default(); + sender.send_message(message, Some(send_options)).await?; + } + + println!("All messages sent successfully to topic!"); + + // Close the sender + sender.close().await?; + + // Close the client + client.close().await?; + + Ok(()) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/examples/simple_token_auth.rs b/sdk/servicebus/azure_messaging_servicebus/examples/simple_token_auth.rs new file mode 100644 index 0000000000..a0252af9ed --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/examples/simple_token_auth.rs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! Simple example showing TokenCredential authentication with Azure Service Bus. +//! +//! This example demonstrates the most basic usage of TokenCredential authentication +//! using DeveloperToolsCredential to send a message to a Service Bus queue. + +use azure_identity::DeveloperToolsCredential; +use azure_messaging_servicebus::{CreateSenderOptions, Message, ServiceBusClient}; +use std::env; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing for logging + tracing_subscriber::fmt::init(); + + // Get the Service Bus namespace from environment variable + let namespace = env::var("SERVICEBUS_NAMESPACE") + .expect("SERVICEBUS_NAMESPACE environment variable must be set (e.g., 'mybus.servicebus.windows.net')"); + + // Get the queue name from environment variable + let queue_name = env::var("SERVICEBUS_QUEUE_NAME") + .expect("SERVICEBUS_QUEUE_NAME environment variable must be set"); + + println!("Service Bus namespace: {}", namespace); + println!("Queue name: {}", queue_name); + + // Create a DeveloperToolsCredential (tries multiple auth methods automatically) + let credential = DeveloperToolsCredential::new(None)?; + + // Create the Service Bus client with TokenCredential authentication + let client = ServiceBusClient::builder() + .open(&namespace, credential.clone()) + .await?; + + println!("āœ… Successfully created Service Bus client with TokenCredential!"); + + // Create a sender for the queue + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + println!("āœ… Successfully created sender for queue: {}", queue_name); + + // Create and send a message + let mut message = Message::from_string("Hello from Azure Service Bus with TokenCredential!"); + message.set_message_id("simple-auth-example"); + message.set_property("authentication", "TokenCredential"); + message.set_property("example", "simple_auth"); + + println!("Sending message..."); + sender.send_message(message, None).await?; + + println!("āœ… Message sent successfully!"); + + // Clean up + sender.close().await?; + client.close().await?; + + println!("šŸŽ‰ Example completed successfully!"); + println!("\nNote: This example uses DeveloperToolsCredential which automatically tries:"); + println!(" 1. Environment variables (AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, etc.)"); + println!(" 2. Managed Identity (if running on Azure)"); + println!(" 3. Azure CLI authentication (if logged in with 'az login')"); + println!(" 4. Azure PowerShell authentication"); + println!(" 5. Interactive browser authentication (as fallback)"); + + Ok(()) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/examples/subqueue_example.rs b/sdk/servicebus/azure_messaging_servicebus/examples/subqueue_example.rs new file mode 100644 index 0000000000..d03ff8910e --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/examples/subqueue_example.rs @@ -0,0 +1,263 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! This example demonstrates how to work with sub-queues in Service Bus using the SubQueue enum. +//! It shows how to: +//! 1. Send a message to a queue +//! 2. Receive and dead letter the message +//! 3. Use SubQueue enum to receive from the dead letter queue +//! 4. Demonstrate both queue and subscription sub-queues + +use azure_identity::DeveloperToolsCredential; +use azure_messaging_servicebus::{ + CreateReceiverOptions, CreateSenderOptions, DeadLetterMessageOptions, Message, ReceiveMode, + ServiceBusClient, SubQueue, +}; +use std::env; +use tokio::time::{sleep, Duration}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing for logging + tracing_subscriber::fmt::init(); + + // Get the queue name from environment variable + let queue_name = env::var("SERVICEBUS_QUEUE_NAME") + .expect("SERVICEBUS_QUEUE_NAME environment variable must be set"); + + println!("Creating Service Bus client..."); + let credential = DeveloperToolsCredential::new(None)?; + let client = ServiceBusClient::builder() + .open("myservicebus.servicebus.windows.net", credential.clone()) + .await?; + + // Demonstrate queue sub-queues + demonstrate_queue_subqueues(&client, &queue_name).await?; + + // Uncomment the following lines if you have topic/subscription configured + // let topic_name = env::var("SERVICEBUS_TOPIC_NAME").unwrap_or_default(); + // let subscription_name = env::var("SERVICEBUS_SUBSCRIPTION_NAME").unwrap_or_default(); + // if !topic_name.is_empty() && !subscription_name.is_empty() { + // demonstrate_subscription_subqueues(&client, &topic_name, &subscription_name).await?; + // } + + client.close().await?; + Ok(()) +} + +async fn demonstrate_queue_subqueues( + client: &ServiceBusClient, + queue_name: &str, +) -> Result<(), Box> { + println!("\n=== Demonstrating Queue Sub-Queues ==="); + + // Step 1: Send a message to the queue + println!("Creating sender for queue: {}", queue_name); + let sender = client + .create_sender(queue_name, Some(CreateSenderOptions::default())) + .await?; + + let mut message = Message::from_string("This message will demonstrate SubQueue enum usage"); + message.set_message_id("subqueue-example-1"); + message.set_property("example", "subqueue_demo"); + + println!("Sending message to queue: {}", queue_name); + sender.send_message(message, None).await?; + println!("Message sent successfully!"); + + // Step 2: Receive the message from the main queue + println!("Creating receiver for main queue: {}", queue_name); + let receiver = client + .create_receiver( + queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, // Main queue (no sub-queue) + }), + ) + .await?; + + println!("Receiving message from main queue..."); + if let Some(received_message) = receiver.receive_message(None).await? { + println!("Received message: {}", received_message.body_as_string()?); + println!("Message ID: {:?}", received_message.message_id()); + + // Dead letter the message with a reason + println!("Dead lettering the message..."); + let dead_letter_options = DeadLetterMessageOptions { + reason: Some("SubQueueDemo".to_string()), + error_description: Some( + "Message dead lettered to demonstrate SubQueue enum".to_string(), + ), + properties_to_modify: None, + }; + receiver + .dead_letter_message(&received_message, Some(dead_letter_options)) + .await?; + println!("Message dead lettered successfully!"); + } else { + println!("No message received from main queue"); + } + + // Step 3: Receive the message from the dead letter queue using SubQueue enum + println!("Creating receiver for dead letter queue using SubQueue::DeadLetter..."); + let dead_letter_receiver = client + .create_receiver( + queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: Some(SubQueue::DeadLetter), // Using SubQueue enum! + }), + ) + .await?; + + println!("Checking dead letter queue for messages..."); + // Wait a bit for the message to appear in the dead letter queue + sleep(Duration::from_secs(2)).await; + + match dead_letter_receiver.receive_message(None).await? { + Some(dead_letter_message) => { + println!("Successfully received dead lettered message using SubQueue::DeadLetter!"); + println!("Message body: {}", dead_letter_message.body_as_string()?); + println!("Message ID: {:?}", dead_letter_message.message_id()); + println!( + "Dead letter reason: {:?}", + dead_letter_message.system_properties().dead_letter_reason + ); + println!( + "Dead letter description: {:?}", + dead_letter_message + .system_properties() + .dead_letter_error_description + ); + + // Complete the dead lettered message to remove it from the dead letter queue + println!("Completing dead lettered message..."); + dead_letter_receiver + .complete_message(&dead_letter_message, None) + .await?; + println!("Dead lettered message completed successfully!"); + } + None => { + println!("No dead lettered messages found"); + } + } + + // Clean up + sender.close().await?; + receiver.close().await?; + dead_letter_receiver.close().await?; + + Ok(()) +} + +#[allow(dead_code)] +async fn demonstrate_subscription_subqueues( + client: &ServiceBusClient, + topic_name: &str, + subscription_name: &str, +) -> Result<(), Box> { + println!("\n=== Demonstrating Subscription Sub-Queues ==="); + + // Step 1: Send a message to the topic + println!("Creating sender for topic: {}", topic_name); + let sender = client + .create_sender(topic_name, Some(CreateSenderOptions::default())) + .await?; + + let mut message = Message::from_string("This message demonstrates subscription sub-queues"); + message.set_message_id("subqueue-subscription-example-1"); + message.set_property("example", "subscription_subqueue_demo"); + + println!("Sending message to topic: {}", topic_name); + sender.send_message(message, None).await?; + println!("Message sent successfully!"); + + // Step 2: Receive the message from the subscription + println!( + "Creating receiver for subscription: {}/{}", + topic_name, subscription_name + ); + let receiver = client + .create_receiver_for_subscription( + topic_name, + subscription_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, // Main subscription (no sub-queue) + }), + ) + .await?; + + println!("Receiving message from subscription..."); + if let Some(received_message) = receiver.receive_message(None).await? { + println!("Received message: {}", received_message.body_as_string()?); + println!("Message ID: {:?}", received_message.message_id()); + + // Dead letter the message + println!("Dead lettering the subscription message..."); + let dead_letter_options = DeadLetterMessageOptions { + reason: Some("SubscriptionSubQueueDemo".to_string()), + error_description: Some( + "Subscription message dead lettered to demonstrate SubQueue enum".to_string(), + ), + properties_to_modify: None, + }; + receiver + .dead_letter_message(&received_message, Some(dead_letter_options)) + .await?; + println!("Subscription message dead lettered successfully!"); + } else { + println!("No message received from subscription"); + } + + // Step 3: Receive from the subscription's dead letter queue using SubQueue enum + println!("Creating receiver for subscription dead letter queue using SubQueue::DeadLetter..."); + let subscription_dead_letter_receiver = client + .create_receiver_for_subscription( + topic_name, + subscription_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: Some(SubQueue::DeadLetter), // Using SubQueue enum for subscription! + }), + ) + .await?; + + println!("Checking subscription dead letter queue for messages..."); + sleep(Duration::from_secs(2)).await; + + match subscription_dead_letter_receiver + .receive_message(None) + .await? + { + Some(dead_letter_message) => { + println!( + "Successfully received subscription dead lettered message using SubQueue::DeadLetter!" + ); + println!("Message body: {}", dead_letter_message.body_as_string()?); + println!("Message ID: {:?}", dead_letter_message.message_id()); + println!( + "Dead letter reason: {:?}", + dead_letter_message.system_properties().dead_letter_reason + ); + + // Complete the message + println!("Completing subscription dead lettered message..."); + subscription_dead_letter_receiver + .complete_message(&dead_letter_message, None) + .await?; + println!("Subscription dead lettered message completed successfully!"); + } + None => { + println!("No subscription dead lettered messages found"); + } + } + + // Clean up + sender.close().await?; + receiver.close().await?; + subscription_dead_letter_receiver.close().await?; + + Ok(()) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/examples/token_credential_auth.rs b/sdk/servicebus/azure_messaging_servicebus/examples/token_credential_auth.rs new file mode 100644 index 0000000000..38f873ce70 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/examples/token_credential_auth.rs @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! This example demonstrates how to authenticate with Service Bus using TokenCredential. +//! +//! This example shows various ways to create a Service Bus client using different +//! credential types from the azure_identity crate, including: +//! - DeveloperToolsCredential (recommended for production) +//! - ClientSecretCredential (for service principals) +//! - ManagedIdentityCredential (for Azure resources) +//! - AzureCliCredential (for development with Azure CLI) +//! +//! The TokenCredential authentication automatically handles: +//! - Token acquisition and caching +//! - Automatic token refresh before expiration +//! - Claims-Based Security (CBS) authorization for AMQP +//! - Path-specific authorization for queues and topics + +use azure_identity::{ + AzureCliCredential, ClientSecretCredential, DeveloperToolsCredential, ManagedIdentityCredential, +}; + +use azure_messaging_servicebus::{Message, ServiceBusClient}; +use std::env; +use time::OffsetDateTime; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing for logging + tracing_subscriber::fmt::init(); + + // Get the Service Bus namespace from environment variable + let namespace = env::var("SERVICEBUS_NAMESPACE") + .expect("SERVICEBUS_NAMESPACE environment variable must be set (e.g., 'mybus.servicebus.windows.net')"); + + // Get the queue name from environment variable + let queue_name = env::var("SERVICEBUS_QUEUE_NAME") + .expect("SERVICEBUS_QUEUE_NAME environment variable must be set"); + + println!("Service Bus namespace: {}", namespace); + println!("Queue name: {}", queue_name); + + // Example 1: DeveloperToolsCredential (Recommended for production) + // This credential type tries multiple authentication methods in order: + // 1. Environment variables (client secret, certificate, username/password) + // 2. Managed Identity (if running on Azure) + // 3. Azure CLI credentials (if logged in) + // 4. Azure PowerShell credentials (if logged in) + // 5. Interactive browser authentication (as fallback) + println!("\n=== Example 1: DeveloperToolsCredential (Builder Pattern) ==="); + let default_credential = DeveloperToolsCredential::new(None)?; + let client = ServiceBusClient::builder() + .open(&namespace, default_credential.clone()) + .await?; + + match send_test_message(&client, &queue_name, "DeveloperToolsCredential").await { + Ok(_) => println!("āœ… DeveloperToolsCredential authentication successful!"), + Err(e) => println!("āŒ DeveloperToolsCredential authentication failed: {}", e), + } + + // Close the client + client.close().await?; + + // Example 2: ClientSecretCredential (For service principals) + // This is useful when you have a registered application in Azure AD + // and want to authenticate using client ID and secret + if let (Ok(tenant_id), Ok(client_id), Ok(client_secret)) = ( + env::var("AZURE_TENANT_ID"), + env::var("AZURE_CLIENT_ID"), + env::var("AZURE_CLIENT_SECRET"), + ) { + println!("\n=== Example 2: ClientSecretCredential (Builder Pattern) ==="); + let client_secret_credential = + ClientSecretCredential::new(&tenant_id, client_id, client_secret.into(), None)?; + let client = ServiceBusClient::builder() + .open(&namespace, client_secret_credential.clone()) + .await?; + + match send_test_message(&client, &queue_name, "ClientSecretCredential").await { + Ok(_) => println!("āœ… ClientSecretCredential authentication successful!"), + Err(e) => println!("āŒ ClientSecretCredential authentication failed: {}", e), + } + + client.close().await?; + } else { + println!("\n=== Example 2: ClientSecretCredential (Skipped) ==="); + println!("Set AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET to test ClientSecretCredential"); + } + + // Example 3: ManagedIdentityCredential (For Azure resources) + // This is useful when running on Azure resources like VMs, App Service, or AKS + // that have a managed identity assigned + println!( + " +=== Example 3: ManagedIdentityCredential (Builder Pattern) ===" + ); + let managed_identity_credential = ManagedIdentityCredential::new(None)?; + let client = ServiceBusClient::builder() + .open(&namespace, managed_identity_credential.clone()) + .await?; + + match send_test_message(&client, &queue_name, "ManagedIdentityCredential").await { + Ok(_) => println!("āœ… ManagedIdentityCredential authentication successful!"), + Err(e) => { + println!("āŒ ManagedIdentityCredential authentication failed: {}", e); + println!( + " This is expected if not running on an Azure resource with managed identity" + ); + } + } + + client.close().await?; + + // Example 4: AzureCliCredential (For development) + // This uses the credentials from Azure CLI (az login) + // Useful for local development when you're logged in via Azure CLI + println!("\n=== Example 4: AzureCliCredential (Builder Pattern) ==="); + let cli_credential = AzureCliCredential::new(None)?; + let client = ServiceBusClient::builder() + .open(&namespace, cli_credential.clone()) + .await?; + match send_test_message(&client, &queue_name, "AzureCliCredential").await { + Ok(_) => println!("āœ… AzureCliCredential authentication successful!"), + Err(e) => { + println!("āŒ AzureCliCredential authentication failed: {}", e); + println!(" Make sure you're logged in with 'az login'"); + } + } + + client.close().await?; + + // Example 5: Receiving messages with TokenCredential + println!( + " +=== Example 5: DeveloperToolsCredential (Builder Pattern) ===" + ); + let credential = DeveloperToolsCredential::new(None)?; + let client = ServiceBusClient::builder() + .open(&namespace, credential.clone()) + .await?; + match receive_test_messages(&client, &queue_name).await { + Ok(count) => println!("āœ… Received {} messages successfully!", count), + Err(e) => println!("āŒ Failed to receive messages: {}", e), + } + + client.close().await?; + + println!("\nšŸŽ‰ TokenCredential authentication examples completed!"); + println!( + "\nNote: Different credential types may succeed or fail depending on your environment:" + ); + println!("- DeveloperToolsCredential: Should work in most environments"); + println!("- ClientSecretCredential: Requires service principal setup"); + println!("- ManagedIdentityCredential: Only works on Azure resources"); + println!("- AzureCliCredential: Requires 'az login'"); + + Ok(()) +} + +/// Helper function to send a test message using the provided client +async fn send_test_message( + client: &ServiceBusClient, + queue_name: &str, + credential_type: &str, +) -> Result<(), Box> { + println!("Creating sender for queue: {}", queue_name); + let sender = client.create_sender(queue_name, None).await?; + + // Create a message with credential type information + let mut message = Message::from_string(format!( + "Hello from Service Bus Rust SDK using {}!", + credential_type + )); + message.set_message_id(format!( + "example-{}-{}", + credential_type.to_lowercase(), + OffsetDateTime::now_utc().unix_timestamp() + )); + message.set_property("credential_type", credential_type); + message.set_property("example", "token_credential_auth"); + message.set_property("timestamp", OffsetDateTime::now_utc().to_string()); + + println!("Sending message using {}...", credential_type); + sender.send_message(message, None).await?; + + println!("Message sent successfully using {}!", credential_type); + + // Close the sender + sender.close().await?; + + Ok(()) +} + +/// Helper function to receive test messages using the provided client +async fn receive_test_messages( + client: &ServiceBusClient, + queue_name: &str, +) -> Result> { + println!("Creating receiver for queue: {}", queue_name); + let receiver = client.create_receiver(queue_name, None).await?; + + println!("Receiving messages..."); + let messages = receiver.receive_messages(5, None).await?; + let count = messages.len() as u32; + + for (i, message) in messages.iter().enumerate() { + println!("Message {}: {}", i + 1, message.body_as_string()?); + + // Get custom properties if they exist + if let Some(credential_type) = message.property("credential_type") { + println!(" - Credential type: {}", credential_type); + } + if let Some(timestamp) = message.property("timestamp") { + println!(" - Timestamp: {}", timestamp); + } + } + + // Complete all messages (remove them from the queue) + for message in messages { + receiver.complete_message(&message, None).await?; + } + + println!("Completed {} messages", count); + + // Close the receiver + receiver.close().await?; + + Ok(count) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/src/client.rs b/sdk/servicebus/azure_messaging_servicebus/src/client.rs new file mode 100644 index 0000000000..8f3a0cca3b --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/src/client.rs @@ -0,0 +1,552 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +use crate::{ + common::authorizer::Authorizer, ErrorKind, ReceiveMode, Receiver, Result, Sender, + ServiceBusError, +}; +use azure_core::{credentials::TokenCredential, fmt::SafeDebug, http::ClientOptions}; +use azure_core_amqp::{ + AmqpConnection, AmqpConnectionApis, AmqpConnectionOptions, AmqpOrderedMap, AmqpSymbol, + AmqpValue, +}; +use std::sync::Arc; +use url::Url; + +/// SubQueue allows you to target a subqueue of a queue or subscription. +/// +/// For example, the dead letter queue (SubQueueDeadLetter) or transfer dead letter queue (SubQueueTransfer). +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SubQueue { + /// Targets the dead letter queue for a queue or subscription. + DeadLetter, + /// Targets the transfer dead letter queue for a queue or subscription. + Transfer, +} + +impl SubQueue { + /// Returns the path suffix for the sub-queue. + pub fn as_path_suffix(&self) -> &'static str { + match self { + SubQueue::DeadLetter => "/$DeadLetterQueue", + SubQueue::Transfer => "/$Transfer/$DeadLetterQueue", + } + } +} + +/// Options for configuring a Service Bus client. +#[derive(Clone, SafeDebug)] +pub struct ServiceBusClientOptions { + /// The API version to use when communicating with the Service Bus service. + pub api_version: String, + + /// Core client configuration options. + pub client_options: ClientOptions, + + /// Application ID that will be passed to the namespace. + /// + /// This optional identifier is passed to the Service Bus namespace during connection establishment + /// and can be used for diagnostic purposes. It follows the same pattern as the Go SDK's ApplicationID. + pub application_id: Option, +} + +impl Default for ServiceBusClientOptions { + fn default() -> Self { + Self { + api_version: "2017-04".to_string(), // Default Service Bus API version + client_options: ClientOptions::default(), + application_id: None, + } + } +} + +/// Options for creating a sender. +#[derive(Clone, Default)] +pub struct CreateSenderOptions { + // Place holder for future options +} + +/// Options for creating a receiver. +#[derive(Clone)] +pub struct CreateReceiverOptions { + /// The receive mode for the receiver. + pub receive_mode: ReceiveMode, + /// The sub-queue to target (e.g., dead letter queue). + pub sub_queue: Option, +} + +impl Default for CreateReceiverOptions { + fn default() -> Self { + Self { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + } + } +} + +/// A client for interacting with Azure Service Bus. +pub struct ServiceBusClient { + connection: Arc, + namespace: String, + options: ServiceBusClientOptions, + authorizer: Option>, +} + +impl ServiceBusClient { + /// Creates a helper function to build AmqpConnectionOptions from ServiceBusClientOptions. + fn build_connection_options( + options: &ServiceBusClientOptions, + ) -> Option { + if let Some(application_id) = &options.application_id { + let mut properties = AmqpOrderedMap::new(); + properties.insert( + AmqpSymbol::from("user-agent"), + AmqpValue::from(application_id.clone()), + ); + Some(AmqpConnectionOptions { + properties: Some(properties), + ..Default::default() + }) + } else { + None + } + } + /// Returns a builder which can be used to create a new instance of [`ServiceBusClient`]. + /// + /// # Examples + /// + /// ```no_run + /// use azure_messaging_servicebus::ServiceBusClient; + /// use azure_identity::DeveloperToolsCredential; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// let credential = DeveloperToolsCredential::new(None)?; + /// let client = ServiceBusClient::builder() + /// .open("my-servicebus.servicebus.windows.net", credential.clone()).await?; + /// Ok(()) + /// } + /// ``` + pub fn builder() -> ServiceBusClientBuilder { + ServiceBusClientBuilder::new() + } + + /// Creates a new client internally using the provided options. + pub(crate) async fn new_internal( + fully_qualified_namespace: &str, + credential: Arc, + options: ServiceBusClientOptions, + ) -> Result { + let endpoint = format!("amqps://{}:5671", fully_qualified_namespace); + let endpoint_url = Url::parse(&endpoint).map_err(|e| { + ServiceBusError::new( + ErrorKind::InvalidRequest, + format!("Invalid endpoint URL: {}", e), + ) + })?; + let namespace = fully_qualified_namespace.to_string(); + + let connection = Arc::new(AmqpConnection::new()); + + // Create authorizer with the credential for token-based authentication + let authorizer = Arc::new(Authorizer::new(Arc::downgrade(&connection), credential)); + + let connection_options = Self::build_connection_options(&options); + connection + .open( + "servicebus-client".to_string(), + endpoint_url, + connection_options, + ) + .await?; + + Ok(Self { + connection, + namespace, + options, + authorizer: Some(authorizer), + }) + } + + /// Creates a sender for the specified queue or topic. + pub async fn create_sender( + &self, + queue_or_topic_name: &str, + _options: Option, + ) -> Result { + // Authorize the path if we have a credential-based client + self.authorize_path(queue_or_topic_name).await?; + + Sender::new( + self.connection.clone(), + queue_or_topic_name.to_string(), + self.options.clone(), + ) + .await + } + + /// Creates a receiver for the specified queue with options. + /// + /// # Examples + /// + /// ```no_run + /// use azure_identity::DeveloperToolsCredential; + /// use azure_messaging_servicebus::{ServiceBusClient, CreateReceiverOptions, ReceiveMode, SubQueue}; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// // ... create client ... + /// let credential = DeveloperToolsCredential::new(None)?; + /// let client = ServiceBusClient::builder() + /// .open("my-servicebus.servicebus.windows.net", credential.clone()).await?; + /// + /// // Create receiver with default PeekLock mode + /// let receiver = client.create_receiver("my-queue", None).await?; + /// + /// // Create receiver with ReceiveAndDelete mode + /// let receiver = client.create_receiver("my-queue", Some(CreateReceiverOptions { + /// receive_mode: ReceiveMode::ReceiveAndDelete, + /// sub_queue: None, + /// })).await?; + /// + /// // Create receiver for dead letter queue + /// let receiver = client.create_receiver("my-queue", Some(CreateReceiverOptions { + /// receive_mode: ReceiveMode::PeekLock, + /// sub_queue: Some(SubQueue::DeadLetter), + /// })).await?; + /// Ok(()) + /// } + /// ``` + pub async fn create_receiver( + &self, + queue_name: &str, + options: Option, + ) -> Result { + // Build the entity path based on sub_queue option + let options = options.unwrap_or_default(); + let entity_path = if let Some(ref sub_queue) = options.sub_queue { + format!("{}{}", queue_name, sub_queue.as_path_suffix()) + } else { + queue_name.to_string() + }; + + // Authorize the path if we have a credential-based client + self.authorize_path(&entity_path).await?; + + Receiver::new( + self.connection.clone(), + entity_path, + None, + options.receive_mode, + self.options.clone(), + ) + .await + } + + /// Creates a receiver for the specified topic and subscription with options. + /// + /// # Examples + /// + /// ```no_run + /// use azure_identity::DeveloperToolsCredential; + /// use azure_messaging_servicebus::{ServiceBusClient, CreateReceiverOptions, SubQueue}; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// // ... create client ... + /// let credential = DeveloperToolsCredential::new(None)?; + /// let client = ServiceBusClient::builder() + /// .open("my-servicebus.servicebus.windows.net", credential.clone()).await?; + /// + /// // Create regular subscription receiver + /// let receiver = client.create_receiver_for_subscription( + /// "my-topic", + /// "my-subscription", + /// None + /// ).await?; + /// + /// // Create receiver for subscription's dead letter queue + /// let receiver = client.create_receiver_for_subscription( + /// "my-topic", + /// "my-subscription", + /// Some(CreateReceiverOptions { + /// sub_queue: Some(SubQueue::DeadLetter), + /// ..Default::default() + /// }) + /// ).await?; + /// Ok(()) + /// } + /// ``` + pub async fn create_receiver_for_subscription( + &self, + topic_name: &str, + subscription_name: &str, + options: Option, + ) -> Result { + // For topic subscriptions, the base path is topic/subscriptions/subscription + let base_path = format!("{}/subscriptions/{}", topic_name, subscription_name); + + // Build the entity path based on sub_queue option + let options = options.unwrap_or_default(); + let entity_path = if let Some(ref sub_queue) = options.sub_queue { + format!("{}{}", base_path, sub_queue.as_path_suffix()) + } else { + base_path + }; + + self.authorize_path(&entity_path).await?; + + Receiver::new( + self.connection.clone(), + entity_path, + None, + options.receive_mode, + self.options.clone(), + ) + .await + } + + /// Gets the fully qualified namespace. + pub fn fully_qualified_namespace(&self) -> &str { + &self.namespace + } + + /// Authorizes access to a Service Bus entity path using the configured credential. + /// This method is used internally by senders and receivers when authentication is required. + pub(crate) async fn authorize_path(&self, entity_path: &str) -> azure_core::Result<()> { + if let Some(ref authorizer) = self.authorizer { + let entity_url = azure_core::http::Url::parse(&format!( + "amqps://{}:5671/{}", + self.namespace, entity_path + ))?; + + authorizer + .authorize_path(&self.connection, &entity_url) + .await?; + } + // If no authorizer is configured (e.g., connection string auth), no additional authorization is needed + Ok(()) + } + + /// Closes the client and all associated senders and receivers. + pub async fn close(&self) -> Result<()> { + self.connection.close().await?; + Ok(()) + } +} + +/// A builder for creating a [`ServiceBusClient`]. +/// +/// This builder is used to create a new [`ServiceBusClient`] with the specified parameters. +/// It follows the same pattern as other Azure SDK client builders. +/// +/// # Examples +/// +/// ```no_run +/// use azure_messaging_servicebus::ServiceBusClient; +/// use azure_identity::DeveloperToolsCredential; +/// +/// #[tokio::main] +/// async fn main() -> Result<(), Box> { +/// let credential = DeveloperToolsCredential::new(None)?; +/// let client = ServiceBusClient::builder() +/// .open("my-servicebus.servicebus.windows.net", credential.clone()).await?; +/// Ok(()) +/// } +/// ``` +#[derive(Default)] +pub struct ServiceBusClientBuilder { + /// Application ID for diagnostic purposes. + application_id: Option, +} + +impl ServiceBusClientBuilder { + /// Creates a new [`ServiceBusClientBuilder`] with default options. + pub fn new() -> Self { + Self::default() + } + + /// Sets the application ID for the client. + /// + /// This identifier is passed to the Service Bus namespace during connection establishment + /// and can be used for diagnostic purposes. + /// + /// # Arguments + /// + /// * `application_id` - A string identifier for the application. + /// + /// # Examples + /// + /// ```no_run + /// use azure_messaging_servicebus::ServiceBusClient; + /// use azure_identity::DeveloperToolsCredential; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// let credential = DeveloperToolsCredential::new(None)?; + /// let client = ServiceBusClient::builder() + /// .with_application_id("my-application".to_string()) + /// .open("my-servicebus.servicebus.windows.net", credential.clone()).await?; + /// Ok(()) + /// } + /// ``` + pub fn with_application_id(mut self, application_id: String) -> Self { + self.application_id = Some(application_id); + self + } + + /// Opens a connection to the Service Bus namespace. + /// + /// # Arguments + /// + /// * `fully_qualified_namespace` - The fully qualified namespace of the Service Bus (e.g., "my-servicebus.servicebus.windows.net"). + /// * `credential` - The token credential to be used for authorization. + /// + /// # Returns + /// + /// A new instance of [`ServiceBusClient`]. + /// + /// # Examples + /// + /// ```no_run + /// use azure_messaging_servicebus::{ServiceBusClient, CreateSenderOptions, CreateReceiverOptions}; + /// use azure_identity::DeveloperToolsCredential; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// let credential = DeveloperToolsCredential::new(None)?; + /// let client = ServiceBusClient::builder() + /// .open("my-servicebus.servicebus.windows.net", credential.clone()).await?; + /// + /// // Use the client to create senders and receivers + /// let sender = client.create_sender("my-queue", None).await?; + /// let receiver = client.create_receiver("my-queue", None).await?; + /// + /// Ok(()) + /// } + /// ``` + pub async fn open( + self, + fully_qualified_namespace: &str, + credential: Arc, + ) -> Result { + let options = ServiceBusClientOptions { + application_id: self.application_id, + ..Default::default() + }; + + ServiceBusClient::new_internal(fully_qualified_namespace, credential, options).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_subqueue_path_suffixes() { + assert_eq!(SubQueue::DeadLetter.as_path_suffix(), "/$DeadLetterQueue"); + assert_eq!( + SubQueue::Transfer.as_path_suffix(), + "/$Transfer/$DeadLetterQueue" + ); + } + + #[test] + fn test_create_receiver_options_with_subqueue() { + // Test default options + let default_options = CreateReceiverOptions::default(); + assert_eq!(default_options.receive_mode, ReceiveMode::PeekLock); + assert_eq!(default_options.sub_queue, None); + + // Test options with dead letter queue + let dlq_options = CreateReceiverOptions { + receive_mode: ReceiveMode::ReceiveAndDelete, + sub_queue: Some(SubQueue::DeadLetter), + }; + assert_eq!(dlq_options.receive_mode, ReceiveMode::ReceiveAndDelete); + assert_eq!(dlq_options.sub_queue, Some(SubQueue::DeadLetter)); + + // Test options with transfer queue + let transfer_options = CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: Some(SubQueue::Transfer), + }; + assert_eq!(transfer_options.receive_mode, ReceiveMode::PeekLock); + assert_eq!(transfer_options.sub_queue, Some(SubQueue::Transfer)); + } + + #[test] + fn test_entity_path_with_subqueue() { + // Test queue path with dead letter queue + let queue_name = "my-queue"; + let dlq_path = format!("{}{}", queue_name, SubQueue::DeadLetter.as_path_suffix()); + assert_eq!(dlq_path, "my-queue/$DeadLetterQueue"); + + // Test subscription path with dead letter queue + let topic_name = "my-topic"; + let subscription_name = "my-subscription"; + let base_path = format!("{}/subscriptions/{}", topic_name, subscription_name); + let sub_dlq_path = format!("{}{}", base_path, SubQueue::DeadLetter.as_path_suffix()); + assert_eq!( + sub_dlq_path, + "my-topic/subscriptions/my-subscription/$DeadLetterQueue" + ); + + // Test transfer queue path + let transfer_path = format!("{}{}", queue_name, SubQueue::Transfer.as_path_suffix()); + assert_eq!(transfer_path, "my-queue/$Transfer/$DeadLetterQueue"); + } + + #[test] + fn test_servicebus_client_options_with_application_id() { + let options = ServiceBusClientOptions { + application_id: Some("test-application".to_string()), + ..Default::default() + }; + + assert_eq!(options.application_id, Some("test-application".to_string())); + } + + #[test] + fn test_servicebus_client_builder_with_application_id() { + let client_builder = + ServiceBusClientBuilder::new().with_application_id("my-rust-app".to_string()); + + assert_eq!( + client_builder.application_id, + Some("my-rust-app".to_string()) + ); + } + + #[test] + fn test_build_connection_options_with_application_id() { + let options = ServiceBusClientOptions { + application_id: Some("test-app-id".to_string()), + ..Default::default() + }; + + let connection_options = ServiceBusClient::build_connection_options(&options); + assert!(connection_options.is_some()); + + let conn_opts = connection_options.unwrap(); + assert!(conn_opts.properties.is_some()); + + let properties = conn_opts.properties.unwrap(); + assert_eq!( + properties.get(&AmqpSymbol::from("user-agent")), + Some(&AmqpValue::from("test-app-id")) + ); + } + + #[test] + fn test_build_connection_options_without_application_id() { + let options = ServiceBusClientOptions { + application_id: None, + ..Default::default() + }; + + let connection_options = ServiceBusClient::build_connection_options(&options); + assert!(connection_options.is_none()); + } +} diff --git a/sdk/servicebus/azure_messaging_servicebus/src/common/authorizer.rs b/sdk/servicebus/azure_messaging_servicebus/src/common/authorizer.rs new file mode 100644 index 0000000000..92679ba068 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/src/common/authorizer.rs @@ -0,0 +1,487 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +use async_lock::Mutex as AsyncMutex; +use azure_core::{ + async_runtime::{get_async_runtime, SpawnedTask}, + credentials::{AccessToken, TokenCredential}, + error::ErrorKind as AzureErrorKind, + fmt::SafeDebug, + http::Url, + time::{Duration, OffsetDateTime}, + Result as AzureResult, +}; +use azure_core_amqp::{AmqpClaimsBasedSecurityApis as _, AmqpConnection, AmqpSessionApis as _}; +use rand::{rng, Rng}; +use std::{ + collections::HashMap, + sync::{Arc, Mutex as SyncMutex, OnceLock, Weak}, +}; +use tracing::{debug, trace, warn}; + +// The number of seconds before token expiration that we wake up to refresh the token. +const TOKEN_REFRESH_BIAS: Duration = Duration::minutes(6); // By default, we refresh tokens 6 minutes before they expire. +const TOKEN_REFRESH_JITTER_MIN: Duration = Duration::seconds(-5); // Minimum jitter (added from the bias, so a negative number means we refresh before the bias) +const TOKEN_REFRESH_JITTER_MAX: Duration = Duration::seconds(5); // Maximum jitter (added to the bias) + +const SERVICEBUS_AUTHORIZATION_SCOPE: &str = "https://servicebus.azure.net/.default"; + +#[derive(SafeDebug)] +struct TokenRefreshTimes { + before_expiration_refresh_time: Duration, + jitter_min: Duration, + jitter_max: Duration, +} + +impl Default for TokenRefreshTimes { + fn default() -> Self { + Self { + before_expiration_refresh_time: TOKEN_REFRESH_BIAS, + jitter_min: TOKEN_REFRESH_JITTER_MIN, + jitter_max: TOKEN_REFRESH_JITTER_MAX, + } + } +} + +pub(crate) struct Authorizer { + authorization_scopes: AsyncMutex>, + authorization_refresher: OnceLock, + /// Bias to apply to token refresh time. This determines how much time we will refresh the token before it expires. + token_refresh_bias: SyncMutex, + credential: Arc, + connection: Weak, + /// This is used to disable authorization for testing purposes. + #[cfg(test)] + disable_authorization: SyncMutex, +} + +unsafe impl Send for Authorizer {} +unsafe impl Sync for Authorizer {} + +impl Authorizer { + pub fn new(connection: Weak, credential: Arc) -> Self { + Self { + authorization_refresher: OnceLock::new(), + authorization_scopes: AsyncMutex::new(HashMap::new()), + token_refresh_bias: SyncMutex::new(TokenRefreshTimes::default()), + credential, + connection, + #[cfg(test)] + disable_authorization: SyncMutex::new(false), + } + } + + #[cfg(test)] + fn disable_authorization(&self) -> AzureResult<()> { + let mut disable_authorization = self.disable_authorization.lock().map_err(|e| { + azure_core::Error::message(azure_core::error::ErrorKind::Other, e.to_string()) + })?; + *disable_authorization = true; + Ok(()) + } + + pub(crate) async fn authorize_path( + self: &Arc, + connection: &Arc, + path: &Url, + ) -> AzureResult { + debug!("Authorizing path: {path}"); + let mut scopes = self.authorization_scopes.lock().await; + + if !scopes.contains_key(path) { + debug!("Creating new authorization scope for path: {path}"); + + debug!("Get Token."); + let token = self + .credential + .get_token(&[SERVICEBUS_AUTHORIZATION_SCOPE], None) + .await?; + + debug!("Token for path {path} expires at {}", token.expires_on); + + self.perform_authorization(connection, path, &token).await?; + + // insert returns some if it *fails* to insert, None if it succeeded. + let present = scopes.insert(path.clone(), token); + if present.is_some() { + return Err(azure_core::Error::message( + AzureErrorKind::Other, + "Unable to add authentication token", + )); + } + + debug!("Token verified."); + self.authorization_refresher.get_or_init(|| { + debug!("Starting authorization refresh task."); + let self_clone = self.clone(); + let async_runtime = get_async_runtime(); + async_runtime.spawn(Box::pin(self_clone.refresh_tokens_task())) + }); + } else { + debug!("Token already exists for path: {path}"); + } + Ok(scopes + .get(path) + .ok_or_else(|| { + azure_core::Error::message( + AzureErrorKind::Other, + "Unable to add authentication token", + ) + })? + .clone()) + } + + /// Actually perform an authorization against the Service Bus service. + /// + /// This method establishes a connection to the Service Bus service and + /// performs the necessary authorization steps using the provided token. + /// + /// # Arguments + /// + /// * `connection` - The AMQP connection to use for the authorization. + /// * `url` - The URL of the resource being authorized. + /// * `new_token` - The new access token to use for authorization. + /// + async fn perform_authorization( + self: &Arc, + connection: &Arc, + url: &Url, + new_token: &AccessToken, + ) -> AzureResult<()> { + // Test Hook: Disable interacting with Service Bus service if the test doesn't want it. + #[cfg(test)] + { + let disable_authorization = self.disable_authorization.lock().map_err(|e| { + azure_core::Error::message( + azure_core::error::ErrorKind::Other, + format!("Unable to grab disable mutex: {}", e), + ) + })?; + if *disable_authorization { + debug!("Authorization disabled for testing."); + return Ok(()); + } + } + + debug!("Performing authorization for {url}"); + + // Create a session for CBS operations + let session = azure_core_amqp::AmqpSession::new(); + session.begin(connection.as_ref(), None).await?; + + // Create CBS client and authorize the path + let cbs_client = azure_core_amqp::AmqpClaimsBasedSecurity::new(session)?; + cbs_client.attach().await?; + + cbs_client + .authorize_path( + url.to_string(), + None, + &new_token.token, + new_token.expires_on, + ) + .await?; + + cbs_client.detach().await?; + + Ok(()) + } + + async fn refresh_tokens_task(self: Arc) { + let result = self.refresh_tokens().await; + if let Err(e) = result { + warn!(err=?e, "Error refreshing tokens: {e}"); + } + debug!("Token refresher task completed."); + } + + /// Refresh the authorization tokens associated with this connection manager. + /// + /// Each connection manager maintains an authorization token for each + /// resource it accesses, and this method ensures that all tokens are + /// refreshed before their expiration. + /// + /// This method is designed to be called periodically to ensure that + /// tokens are kept up to date. + /// + /// The first step in the refresh process is to gather the expiration times + /// of all tokens. This allows us to determine when to refresh each token + /// based on its expiration time. + /// + /// We calculate the first token to expire and sleep until it expires (with a bit of + /// jitter in the sleep). + /// + /// After we wake up, we iterate over all the authorized paths and refresh their tokens with + /// the Service Bus service. + async fn refresh_tokens(self: &Arc) -> AzureResult<()> { + debug!("Refreshing tokens."); + loop { + let mut expiration_times = vec![]; + { + let scopes = self.authorization_scopes.lock().await; + for (path, token) in scopes.iter() { + debug!( + "Token expiration time for path {}: {}", + path, token.expires_on + ); + expiration_times.push(token.expires_on); + } + } + expiration_times.sort(); + debug!("Found expiration times: {:?}", expiration_times); + if expiration_times.is_empty() { + debug!("No tokens to refresh. Sleeping for {TOKEN_REFRESH_BIAS:?}."); + azure_core::sleep::sleep(TOKEN_REFRESH_BIAS).await; + continue; + } + + // Calculate duration until we should refresh (6 minutes before expiration, + // with added random jitter) + + let mut now = OffsetDateTime::now_utc(); + trace!("refresh_tokens: Start pass for: {now}"); + let most_recent_refresh = expiration_times.first().ok_or_else(|| { + azure_core::Error::message(AzureErrorKind::Other, "No tokens to refresh?") + })?; + + debug!( + "Nearest token refresh time: {most_recent_refresh}, in {}", + *most_recent_refresh - now + ); + + let refresh_time: OffsetDateTime; + let token_refresh_bias: Duration; + { + let token_refresh_times = self.token_refresh_bias.lock().map_err(|e| { + azure_core::Error::message( + azure_core::error::ErrorKind::Other, + format!("Unable to grab token refresh bias mutex: {}", e), + ) + })?; + + debug!("Token refresh times: {token_refresh_times:?}"); + + let jitter_min = token_refresh_times.jitter_min.whole_milliseconds() as i64; + let jitter_max = token_refresh_times.jitter_max.whole_milliseconds() as i64; + let expiration_jitter = + Duration::milliseconds(rng().random_range(jitter_min..jitter_max)); + debug!("Expiration jitter: {expiration_jitter:?}"); + + token_refresh_bias = token_refresh_times + .before_expiration_refresh_time + .checked_add(expiration_jitter) + .ok_or_else(|| { + azure_core::Error::message( + AzureErrorKind::Other, + "Unable to calculate token refresh bias - overflow", + ) + })?; + debug!("Token refresh bias with jitter: {token_refresh_bias:?}"); + + refresh_time = most_recent_refresh + .checked_sub(token_refresh_bias) + .ok_or_else(|| { + azure_core::Error::message( + AzureErrorKind::Other, + "Unable to calculate token refresh bias - underflow", + ) + })?; + } + debug!("refresh_tokens: Refresh time: {refresh_time}"); + + // Convert to a duration if refresh time is in the future and sleep until it's time + // to refresh the token. + if refresh_time > now { + let sleep_duration = refresh_time - now; + debug!( + "refresh_tokens: Sleeping for {sleep_duration:?} until {:?}", + now + sleep_duration + ); + azure_core::sleep::sleep(sleep_duration).await; + now = OffsetDateTime::now_utc(); + } else { + debug!("Not sleeping because refresh time ({refresh_time}) is in the past (now = {now})."); + } + + // Refresh the tokens. + // First, collect the tokens that need refreshing while holding the lock briefly + let tokens_to_refresh = { + let scopes = self.authorization_scopes.lock().await; + let mut to_refresh = Vec::new(); + for (url, token) in scopes.iter() { + if token.expires_on >= now + (token_refresh_bias) { + debug!( + "Token not expired for {url}: ExpiresOn: {}, Now: {now}, Bias: {token_refresh_bias:?}", + token.expires_on + ); + continue; + } + + debug!( + "Token about to be expired for {url}: ExpiresOn: {}, Now: {now}, Bias: {token_refresh_bias:?}", + token.expires_on + ); + to_refresh.push(url.clone()); + } + to_refresh + }; + + // Now refresh tokens without holding the lock to avoid deadlocks + let mut updated_tokens = HashMap::new(); + for url in tokens_to_refresh { + let new_token = self + .credential + .get_token(&[SERVICEBUS_AUTHORIZATION_SCOPE], None) + .await?; + + // Create an ephemeral connection to host the authentication. + let connection = self.connection.upgrade().ok_or_else(|| { + azure_core::Error::message(AzureErrorKind::Other, "Connection has been dropped") + })?; + self.perform_authorization(&connection, &url, &new_token) + .await?; + + debug!( + "Token refreshed for {url}, new expiration time: {}", + new_token.expires_on + ); + updated_tokens.insert(url.clone(), new_token); + } + + // Finally, update the scopes map with the new tokens + if !updated_tokens.is_empty() { + let mut scopes = self.authorization_scopes.lock().await; + for (url, token) in updated_tokens.into_iter() { + scopes.insert(url.clone(), token); + } + debug!("Updated tokens."); + } + } + } + + #[cfg(test)] + fn set_token_refresh_times(&self, refresh_times: TokenRefreshTimes) -> AzureResult<()> { + let mut token_refresh_bias = self.token_refresh_bias.lock().map_err(|e| { + azure_core::Error::message(azure_core::error::ErrorKind::Other, e.to_string()) + })?; + *token_refresh_bias = refresh_times; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use azure_core::credentials::TokenRequestOptions; + use azure_core::{http::Url, time::OffsetDateTime, Result}; + use std::sync::Arc; + + // Helper struct to mock token credential + #[derive(Debug)] + struct MockTokenCredential { + /// Duration in seconds until the token expires + token_duration: i64, + + /// The token itself + /// This is a mock token, so we don't need to worry about the actual value + token: SyncMutex, + + /// Count of how many times the token has been requested + /// This is used to verify that the token is being refreshed correctly + /// in the tests + get_token_count: SyncMutex, + } + + impl MockTokenCredential { + fn new(expires_in_seconds: i64) -> Arc { + let expires_on = OffsetDateTime::now_utc() + Duration::seconds(expires_in_seconds); + Arc::new(Self { + token_duration: expires_in_seconds, + token: SyncMutex::new(AccessToken::new( + azure_core::credentials::Secret::new("mock_token"), + expires_on, + )), + get_token_count: SyncMutex::new(0), + }) + } + + #[allow(dead_code)] + fn get_token_get_count(&self) -> usize { + *self.get_token_count.lock().unwrap() + } + } + + #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] + #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] + impl TokenCredential for MockTokenCredential { + async fn get_token( + &self, + _scopes: &[&str], + _options: Option, + ) -> Result { + // Simulate a token refresh by incrementing the token get count + // and updating the token expiration time + { + let mut count = self.get_token_count.lock().unwrap(); + *count += 1; + } + + let expires_on = OffsetDateTime::now_utc() + Duration::seconds(self.token_duration); + { + let mut token = self.token.lock().unwrap(); + *token = AccessToken::new( + azure_core::credentials::Secret::new("mock_token"), + expires_on, + ); + Ok(token.clone()) + } + } + } + + // When a token is created, it needs to have a proper expiration time. + // This test verifies that the expiration time of tokens is set correctly when + // authorizing a path. It also confirms that tokens are properly stored for reuse + // and that their expiration times are within the expected range. + // + #[tokio::test] + async fn token_credential_expiration() { + let _url = Url::parse("amqps://example.com").unwrap(); + let path = Url::parse("amqps://example.com/test_token_credential_expiration").unwrap(); + + // Create a mock token credential that expires in 15 seconds + let mock_credential = MockTokenCredential::new(15); + + let connection = Arc::new(azure_core_amqp::AmqpConnection::new()); + let authorizer = Arc::new(Authorizer::new( + Arc::downgrade(&connection), + mock_credential.clone(), + )); + + // Disable actual authorization for testing + authorizer.disable_authorization().unwrap(); + + // Expire tokens 10 seconds before they would normally expire. + // The token in question expires in 15 seconds, so we want to refresh it before then. + authorizer + .set_token_refresh_times(TokenRefreshTimes { + before_expiration_refresh_time: Duration::seconds(10), + ..Default::default() + }) + .unwrap(); + + // This should succeed and store the token in the authorization scopes + let result = authorizer.authorize_path(&connection, &path).await; + println!("Result: {:?}", result); + assert!(result.is_ok()); + + // Verify token is stored + let scopes = authorizer.authorization_scopes.lock().await; + assert!(scopes.contains_key(&path)); + + // Verify expiration time + let stored_token = scopes.get(&path).unwrap(); + let now = OffsetDateTime::now_utc(); + assert!(stored_token.expires_on > now); + assert!(stored_token.expires_on < now + Duration::seconds(15)); // Should be less than now + 15 seconds + } +} diff --git a/sdk/servicebus/azure_messaging_servicebus/src/common/mod.rs b/sdk/servicebus/azure_messaging_servicebus/src/common/mod.rs new file mode 100644 index 0000000000..3a7fbda6c7 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/src/common/mod.rs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! Common functionality for Service Bus. + +/// Authorization functionality for Service Bus operations. +/// +/// Handles Entra ID authentication tokens and automatic token refresh for Service Bus resources. +pub mod authorizer; diff --git a/sdk/servicebus/azure_messaging_servicebus/src/error.rs b/sdk/servicebus/azure_messaging_servicebus/src/error.rs new file mode 100644 index 0000000000..e6247292c4 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/src/error.rs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +use azure_core::{error::ErrorKind as CoreErrorKind, fmt::SafeDebug}; +use std::fmt; + +/// The kind of Service Bus error. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ErrorKind { + /// An error occurred in the underlying AMQP transport. + Amqp, + /// The operation was cancelled. + Cancelled, + /// The entity (queue, topic, or subscription) was not found. + EntityNotFound, + /// The request was invalid or malformed. + InvalidRequest, + /// A message lock was lost. + MessageLockLost, + /// A message was not found. + MessageNotFound, + /// The message size exceeds the maximum allowed size. + MessageSizeExceeded, + /// A quota was exceeded. + QuotaExceeded, + /// The request timed out. + RequestTimeout, + /// The sender or receiver has been closed. + ServiceBusClosed, + /// A session lock was lost. + SessionLockLost, + /// An unknown error occurred. + Unknown, +} + +impl fmt::Display for ErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ErrorKind::Amqp => write!(f, "AMQP error"), + ErrorKind::Cancelled => write!(f, "Operation was cancelled"), + ErrorKind::EntityNotFound => write!(f, "Entity not found"), + ErrorKind::InvalidRequest => write!(f, "Invalid request"), + ErrorKind::MessageLockLost => write!(f, "Message lock lost"), + ErrorKind::MessageNotFound => write!(f, "Message not found"), + ErrorKind::MessageSizeExceeded => write!(f, "Message size exceeded"), + ErrorKind::QuotaExceeded => write!(f, "Quota exceeded"), + ErrorKind::RequestTimeout => write!(f, "Request timeout"), + ErrorKind::ServiceBusClosed => write!(f, "Service Bus client closed"), + ErrorKind::SessionLockLost => write!(f, "Session lock lost"), + ErrorKind::Unknown => write!(f, "Unknown error"), + } + } +} + +/// A Service Bus specific error. +#[derive(SafeDebug, Clone, PartialEq, Eq)] +pub struct ServiceBusError { + kind: ErrorKind, + message: String, + source: Option>, +} + +impl ServiceBusError { + /// Creates a new Service Bus error. + pub fn new(kind: ErrorKind, message: impl Into) -> Self { + Self { + kind, + message: message.into(), + source: None, + } + } + + /// Creates a new Service Bus error with a source error. + pub fn with_source( + kind: ErrorKind, + message: impl Into, + source: ServiceBusError, + ) -> Self { + Self { + kind, + message: message.into(), + source: Some(Box::new(source)), + } + } + + /// Returns the error kind. + pub fn kind(&self) -> &ErrorKind { + &self.kind + } + + /// Returns the error message. + pub fn message(&self) -> &str { + &self.message + } + + /// Returns the source error, if any. + pub fn source(&self) -> Option<&ServiceBusError> { + self.source.as_deref() + } +} + +impl fmt::Display for ServiceBusError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", self.kind, self.message) + } +} + +impl std::error::Error for ServiceBusError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.source.as_ref().map(|e| e as &dyn std::error::Error) + } +} + +impl From for ServiceBusError { + fn from(error: azure_core::error::Error) -> Self { + let kind = match error.kind() { + CoreErrorKind::Io => ErrorKind::Amqp, + CoreErrorKind::HttpResponse { + status: _, + error_code: _, + } => ErrorKind::InvalidRequest, + CoreErrorKind::Other => ErrorKind::Unknown, + _ => ErrorKind::Unknown, + }; + + ServiceBusError::new(kind, error.to_string()) + } +} + +impl From for ServiceBusError { + fn from(error: azure_core_amqp::AmqpError) -> Self { + ServiceBusError::new(ErrorKind::Amqp, error.to_string()) + } +} diff --git a/sdk/servicebus/azure_messaging_servicebus/src/lib.rs b/sdk/servicebus/azure_messaging_servicebus/src/lib.rs new file mode 100644 index 0000000000..27f75807a3 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/src/lib.rs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +#![recursion_limit = "128"] +#![warn(missing_docs)] +#![doc = include_str!("../README.md")] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +/// Service Bus client +pub mod client; +mod error; +mod message; +/// Service Bus message receiving functionality and options. +pub mod receiver; + +/// Service Bus message sending functionality and options. +pub mod sender; + +/// Models and types used throughout the Service Bus client. +pub mod models; + +/// Common types and utilities. +mod common; + +pub use client::{ + CreateReceiverOptions, CreateSenderOptions, ServiceBusClient, ServiceBusClientBuilder, + ServiceBusClientOptions, SubQueue, +}; +pub use error::{ErrorKind, ServiceBusError}; +pub use message::{Message, MessageBatch, ReceivedMessage}; +pub use receiver::{ + AbandonMessageOptions, CompleteMessageOptions, DeadLetterMessageOptions, DeferMessageOptions, + PeekMessagesOptions, ReceiveDeferredMessagesOptions, ReceiveMessageOptions, ReceiveMode, + Receiver, RenewMessageLockOptions, +}; +pub use sender::{ + CancelScheduledMessagesOptions, CreateMessageBatchOptions, ScheduleMessageOptions, + ScheduleMessagesOptions, SendMessageBatchOptions, SendMessageOptions, SendMessagesOptions, + Sender, +}; + +/// Result type used throughout the Service Bus client. +pub type Result = std::result::Result; diff --git a/sdk/servicebus/azure_messaging_servicebus/src/message.rs b/sdk/servicebus/azure_messaging_servicebus/src/message.rs new file mode 100644 index 0000000000..5f4437a620 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/src/message.rs @@ -0,0 +1,592 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +use crate::{ErrorKind, Result, ServiceBusError}; +use azure_core::fmt::SafeDebug; +use azure_core::time::Duration; +use azure_core_amqp::{ + message::{AmqpApplicationProperties, AmqpMessageBody, AmqpMessageId, AmqpMessageProperties}, + AmqpMessage, AmqpSimpleValue, AmqpSymbol, +}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use time::OffsetDateTime; +use uuid::Uuid; + +/// A message to be sent to Service Bus. +#[derive(SafeDebug, Clone)] +pub struct Message { + body: Vec, + properties: HashMap, + session_id: Option, + message_id: Option, + correlation_id: Option, + content_type: Option, + reply_to: Option, + reply_to_session_id: Option, + subject: Option, + time_to_live: Option, + scheduled_enqueue_time: Option, +} + +impl Message { + /// Creates a new message with the specified body. + pub fn new>>(body: T) -> Self { + Self { + body: body.into(), + properties: HashMap::new(), + session_id: None, + message_id: None, + correlation_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + time_to_live: None, + scheduled_enqueue_time: None, + } + } + + /// Creates a new message with a string body. + pub fn from_string(body: impl Into) -> Self { + Self::new(body.into().into_bytes()) + } + + /// Gets the message body as bytes. + pub fn body(&self) -> &[u8] { + &self.body + } + + /// Gets the message body as a string, if it's valid UTF-8. + pub fn body_as_string(&self) -> Result { + String::from_utf8(self.body.clone()) + .map_err(|_| ServiceBusError::new(ErrorKind::InvalidRequest, "Body is not valid UTF-8")) + } + + /// Sets a custom property on the message. + pub fn set_property(&mut self, key: impl Into, value: impl Into) { + self.properties.insert(key.into(), value.into()); + } + + /// Gets a custom property from the message. + pub fn property(&self, key: &str) -> Option<&String> { + self.properties.get(key) + } + + /// Gets all custom properties. + pub fn properties(&self) -> &HashMap { + &self.properties + } + + /// Sets the session ID for the message. + pub fn set_session_id(&mut self, session_id: impl Into) { + self.session_id = Some(session_id.into()); + } + + /// Gets the session ID. + pub fn session_id(&self) -> Option<&String> { + self.session_id.as_ref() + } + + /// Sets the message ID. + pub fn set_message_id(&mut self, message_id: impl Into) { + self.message_id = Some(message_id.into()); + } + + /// Gets the message ID. + pub fn message_id(&self) -> Option<&String> { + self.message_id.as_ref() + } + + /// Sets the correlation ID. + pub fn set_correlation_id(&mut self, correlation_id: impl Into) { + self.correlation_id = Some(correlation_id.into()); + } + + /// Gets the correlation ID. + pub fn correlation_id(&self) -> Option<&String> { + self.correlation_id.as_ref() + } + + /// Sets the content type. + pub fn set_content_type(&mut self, content_type: impl Into) { + self.content_type = Some(content_type.into()); + } + + /// Gets the content type. + pub fn content_type(&self) -> Option<&String> { + self.content_type.as_ref() + } + + /// Sets the reply-to address. + pub fn set_reply_to(&mut self, reply_to: impl Into) { + self.reply_to = Some(reply_to.into()); + } + + /// Gets the reply-to address. + pub fn reply_to(&self) -> Option<&String> { + self.reply_to.as_ref() + } + + /// Sets the reply-to session ID. + pub fn set_reply_to_session_id(&mut self, reply_to_session_id: impl Into) { + self.reply_to_session_id = Some(reply_to_session_id.into()); + } + + /// Gets the reply-to session ID. + pub fn reply_to_session_id(&self) -> Option<&String> { + self.reply_to_session_id.as_ref() + } + + /// Sets the message subject (label). + pub fn set_subject(&mut self, subject: impl Into) { + self.subject = Some(subject.into()); + } + + /// Gets the message subject (label). + pub fn subject(&self) -> Option<&String> { + self.subject.as_ref() + } + + /// Sets the time-to-live for the message. + pub fn set_time_to_live(&mut self, time_to_live: Duration) { + self.time_to_live = Some(time_to_live); + } + + /// Gets the time-to-live. + pub fn time_to_live(&self) -> Option { + self.time_to_live + } + + /// Sets the scheduled enqueue time. + pub fn set_scheduled_enqueue_time(&mut self, scheduled_enqueue_time: OffsetDateTime) { + self.scheduled_enqueue_time = Some(scheduled_enqueue_time); + } + + /// Gets the scheduled enqueue time. + pub fn scheduled_enqueue_time(&self) -> Option { + self.scheduled_enqueue_time + } +} + +/// A message received from Service Bus. +#[derive(SafeDebug, Clone)] +pub struct ReceivedMessage { + body: Vec, + properties: HashMap, + system_properties: SystemProperties, + lock_token: Option, +} + +/// System properties of a received message. +#[derive(SafeDebug, Clone, Default, Serialize, Deserialize)] +pub struct SystemProperties { + /// The message ID. + pub message_id: Option, + /// The correlation ID. + pub correlation_id: Option, + /// The session ID. + pub session_id: Option, + /// The content type. + pub content_type: Option, + /// The reply-to address. + pub reply_to: Option, + /// The reply-to session ID. + pub reply_to_session_id: Option, + /// The message subject (label). + pub subject: Option, + /// The enqueued time in UTC. + pub enqueued_time_utc: Option, + /// The sequence number. + pub sequence_number: Option, + /// The delivery count. + pub delivery_count: Option, + /// The time-to-live. + pub time_to_live: Option, + /// The dead letter source. + pub dead_letter_source: Option, + /// The dead letter reason. + pub dead_letter_reason: Option, + /// The dead letter error description. + pub dead_letter_error_description: Option, +} + +impl ReceivedMessage { + /// Creates a new received message. + pub(crate) fn new( + body: Vec, + properties: HashMap, + system_properties: SystemProperties, + lock_token: Option, + ) -> Self { + Self { + body, + properties, + system_properties, + lock_token, + } + } + + /// Gets the message body as bytes. + pub fn body(&self) -> &[u8] { + &self.body + } + + /// Gets the message body as a string, if it's valid UTF-8. + pub fn body_as_string(&self) -> Result { + String::from_utf8(self.body.clone()) + .map_err(|_| ServiceBusError::new(ErrorKind::InvalidRequest, "Body is not valid UTF-8")) + } + + /// Gets a custom property from the message. + pub fn property(&self, key: &str) -> Option<&String> { + self.properties.get(key) + } + + /// Gets all custom properties. + pub fn properties(&self) -> &HashMap { + &self.properties + } + + /// Gets the system properties. + pub fn system_properties(&self) -> &SystemProperties { + &self.system_properties + } + + /// Gets the lock token for this message. + pub fn lock_token(&self) -> Option { + self.lock_token + } + + /// Gets the message ID. + pub fn message_id(&self) -> Option<&String> { + self.system_properties.message_id.as_ref() + } + + /// Gets the correlation ID. + pub fn correlation_id(&self) -> Option<&String> { + self.system_properties.correlation_id.as_ref() + } + + /// Gets the session ID. + pub fn session_id(&self) -> Option<&String> { + self.system_properties.session_id.as_ref() + } + + /// Gets the sequence number. + pub fn sequence_number(&self) -> Option { + self.system_properties.sequence_number + } + + /// Gets the enqueued time in UTC. + pub fn enqueued_time_utc(&self) -> Option { + self.system_properties.enqueued_time_utc + } + + /// Gets the delivery count. + pub fn delivery_count(&self) -> Option { + self.system_properties.delivery_count + } +} + +impl From for AmqpMessage { + fn from(message: Message) -> Self { + let mut amqp_message_builder = AmqpMessage::builder(); + + // Set the body as binary data + amqp_message_builder = + amqp_message_builder.with_body(AmqpMessageBody::Binary(vec![message.body])); + + // Set message properties + let mut properties = AmqpMessageProperties::default(); + + if let Some(message_id) = message.message_id { + properties.message_id = Some(AmqpMessageId::String(message_id)); + } + + if let Some(correlation_id) = message.correlation_id { + properties.correlation_id = Some(AmqpMessageId::String(correlation_id)); + } + + if let Some(content_type) = message.content_type { + properties.content_type = Some(AmqpSymbol::from(content_type)); + } + + if let Some(reply_to) = message.reply_to { + properties.reply_to = Some(reply_to); + } + + if let Some(subject) = message.subject { + properties.subject = Some(subject); + } + + amqp_message_builder = amqp_message_builder.with_properties(properties); + + // Add application properties + if !message.properties.is_empty() { + let mut app_props = AmqpApplicationProperties::new(); + for (key, value) in message.properties { + app_props.insert(key, AmqpSimpleValue::String(value)); + } + amqp_message_builder = amqp_message_builder.with_application_properties(app_props); + } + + amqp_message_builder.build() + } +} + +/// Options for creating a message batch. +#[derive(SafeDebug, Clone, Default)] +pub struct CreateMessageBatchOptions { + /// The maximum size of the batch in bytes. + /// If not specified, the default Service Bus limit will be used. + #[allow(dead_code)] + pub maximum_size_in_bytes: Option, +} + +impl CreateMessageBatchOptions { + /// Creates new batch options with default values. + #[allow(dead_code)] + pub fn new() -> Self { + Self::default() + } + + /// Sets the maximum size in bytes for the batch. + #[allow(dead_code)] + pub fn with_maximum_size_in_bytes(mut self, size: usize) -> Self { + self.maximum_size_in_bytes = Some(size); + self + } +} + +/// A batch of Service Bus messages that can be sent efficiently in a single operation. +/// +/// This provides better performance than sending messages individually by reducing +/// the number of network round trips and AMQP operations required. +/// +/// Use `Sender::create_message_batch()` to create an instance. +#[derive(SafeDebug)] +pub struct MessageBatch { + messages: Vec, + current_size_in_bytes: usize, + maximum_size_in_bytes: usize, +} + +impl MessageBatch { + /// The default maximum size in bytes for a Service Bus message batch. + /// This is based on the Service Bus limit of 1MB minus overhead for headers. + pub const DEFAULT_MAX_SIZE_BYTES: usize = 1024 * 1024 - 64 * 1024; // 960KB + + /// Creates a new message batch with the specified maximum size. + pub(crate) fn new(maximum_size_in_bytes: Option) -> Self { + Self { + messages: Vec::new(), + current_size_in_bytes: 0, + maximum_size_in_bytes: maximum_size_in_bytes.unwrap_or(Self::DEFAULT_MAX_SIZE_BYTES), + } + } + + /// Attempts to add a message to the batch. + /// + /// Returns `true` if the message was successfully added, `false` if adding + /// the message would exceed the batch size limit. + /// + /// # Arguments + /// + /// * `message` - The message to add to the batch + /// + /// # Examples + /// + /// ```rust,no_run + /// # use azure_messaging_servicebus::{ServiceBusClient, Message, CreateSenderOptions, CreateMessageBatchOptions}; + /// # use azure_identity::DeveloperToolsCredential; + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let credential = DeveloperToolsCredential::new(None)?; + /// let client = ServiceBusClient::builder().open("myservicebus.servicebus.windows.net", credential.clone()).await?; + /// let sender = client.create_sender("myqueue", None).await?; + /// let mut batch = sender.create_message_batch(None).await?; + /// let message = Message::from_string("Hello, world!"); + /// + /// if batch.try_add_message(message) { + /// println!("Message added to batch"); + /// } else { + /// println!("Batch is full, cannot add message"); + /// } + /// # Ok(()) + /// # } + /// ``` + pub fn try_add_message(&mut self, message: Message) -> bool { + let estimated_message_size = self.estimate_message_size(&message); + + if self.current_size_in_bytes + estimated_message_size > self.maximum_size_in_bytes { + return false; + } + + self.current_size_in_bytes += estimated_message_size; + self.messages.push(message); + true + } + + /// Gets the number of messages in the batch. + pub fn count(&self) -> usize { + self.messages.len() + } + + /// Returns `true` if the batch is empty. + pub fn is_empty(&self) -> bool { + self.messages.is_empty() + } + + /// Gets the current size of the batch in bytes. + pub fn size_in_bytes(&self) -> usize { + self.current_size_in_bytes + } + + /// Gets the maximum size limit for the batch in bytes. + pub fn maximum_size_in_bytes(&self) -> usize { + self.maximum_size_in_bytes + } + + /// Consumes the batch and returns the messages contained within it. + /// + /// This is used internally by the sender to extract the messages for sending. + pub(crate) fn into_messages(self) -> Vec { + self.messages + } + + /// Gets a reference to the messages in the batch. + /// + /// This is used internally by the sender. + #[allow(dead_code)] + pub(crate) fn messages(&self) -> &[Message] { + &self.messages + } + + /// Estimates the size of a message in bytes for batching purposes. + /// + /// This is an approximation that includes the message body, properties, + /// and AMQP overhead. The actual wire size may be slightly different. + fn estimate_message_size(&self, message: &Message) -> usize { + let mut size = 0; + + // Message body size + size += message.body.len(); + + // Message properties overhead (estimated) + if let Some(id) = &message.message_id { + size += id.len() + 16; // Property overhead + } + if let Some(correlation_id) = &message.correlation_id { + size += correlation_id.len() + 16; + } + if let Some(content_type) = &message.content_type { + size += content_type.len() + 16; + } + if let Some(reply_to) = &message.reply_to { + size += reply_to.len() + 16; + } + if let Some(subject) = &message.subject { + size += subject.len() + 16; + } + + // Custom properties + for (key, value) in &message.properties { + size += key.len() + value.len() + 32; // Key-value pair overhead + } + + // AMQP frame overhead (estimated) + size += 128; + + size + } +} + +#[cfg(test)] +mod batch_tests { + use super::*; + + #[test] + fn new_batch_is_empty() { + let batch = MessageBatch::new(None); + assert!(batch.is_empty()); + assert_eq!(batch.count(), 0); + assert_eq!(batch.size_in_bytes(), 0); + } + + #[test] + fn try_add_message_success() { + let mut batch = MessageBatch::new(Some(1024)); + let message = Message::new("Small message"); + + let result = batch.try_add_message(message); + assert!(result); + assert_eq!(batch.count(), 1); + assert!(!batch.is_empty()); + assert!(batch.size_in_bytes() > 0); + } + + #[test] + fn try_add_message_exceeds_size_limit() { + let mut batch = MessageBatch::new(Some(100)); // Very small limit + let message = Message::new( + "This message is definitely too large for the tiny batch size limit that was set", + ); + + let result = batch.try_add_message(message); + assert!(!result); + assert_eq!(batch.count(), 0); + assert!(batch.is_empty()); + assert_eq!(batch.size_in_bytes(), 0); + } + + #[test] + fn batch_respects_maximum_size() { + let max_size = 500; + let batch = MessageBatch::new(Some(max_size)); + assert_eq!(batch.maximum_size_in_bytes(), max_size); + } + + #[test] + fn default_batch_options() { + let options = CreateMessageBatchOptions::default(); + assert!(options.maximum_size_in_bytes.is_none()); + } + + #[test] + fn batch_options_with_size() { + let size = 2048; + let options = CreateMessageBatchOptions::new().with_maximum_size_in_bytes(size); + assert_eq!(options.maximum_size_in_bytes, Some(size)); + } + + #[test] + fn estimate_message_size() { + let batch = MessageBatch::new(None); + let mut message = Message::new("Hello, world!"); + message.set_message_id("test-id"); + message.set_correlation_id("correlation-123"); + message.set_property("custom-key", "custom-value"); + + let estimated_size = batch.estimate_message_size(&message); + + // Should include body, properties, and overhead + assert!(estimated_size > "Hello, world!".len()); + assert!(estimated_size > 100); // At minimum should have overhead + } + + #[test] + fn into_messages_consumes_batch() { + let mut batch = MessageBatch::new(None); + let message1 = Message::new("Message 1"); + let message2 = Message::new("Message 2"); + + batch.try_add_message(message1); + batch.try_add_message(message2); + + let messages = batch.into_messages(); + assert_eq!(messages.len(), 2); + assert_eq!(messages[0].body_as_string().unwrap(), "Message 1"); + assert_eq!(messages[1].body_as_string().unwrap(), "Message 2"); + } +} diff --git a/sdk/servicebus/azure_messaging_servicebus/src/models/mod.rs b/sdk/servicebus/azure_messaging_servicebus/src/models/mod.rs new file mode 100644 index 0000000000..35f958aa84 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/src/models/mod.rs @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! Models and types used throughout the Service Bus client. + +use azure_core::fmt::SafeDebug; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +/// Represents the state of a Service Bus entity. +#[derive(Clone, Copy, SafeDebug, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] +pub enum EntityState { + /// The entity is active and can send/receive messages. + #[serde(rename = "Active")] + Active, + /// The entity is disabled and cannot send/receive messages. + #[serde(rename = "Disabled")] + Disabled, + /// The entity is temporarily disabled due to an error. + #[serde(rename = "SendDisabled")] + SendDisabled, + /// The entity is temporarily disabled for receiving. + #[serde(rename = "ReceiveDisabled")] + ReceiveDisabled, +} + +/// Statistics about a Service Bus queue. +#[derive(SafeDebug, Clone, Default, Serialize, Deserialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct QueueRuntimeProperties { + /// The name of the queue. + #[serde(skip_serializing_if = "Option::is_none")] + pub queue_name: Option, + /// The current size of the queue in bytes. + #[serde(skip_serializing_if = "Option::is_none")] + pub size_in_bytes: Option, + /// The total number of messages in the queue. + #[serde(skip_serializing_if = "Option::is_none")] + pub total_message_count: Option, + /// The number of active messages in the queue. + #[serde(skip_serializing_if = "Option::is_none")] + pub active_message_count: Option, + /// The number of messages in the dead letter queue. + #[serde(skip_serializing_if = "Option::is_none")] + pub dead_letter_message_count: Option, + /// The number of scheduled messages. + #[serde(skip_serializing_if = "Option::is_none")] + pub scheduled_message_count: Option, + /// The number of messages transferred to another queue/topic. + #[serde(skip_serializing_if = "Option::is_none")] + pub transfer_message_count: Option, + /// The number of messages transferred to the dead letter queue. + #[serde(skip_serializing_if = "Option::is_none")] + pub transfer_dead_letter_message_count: Option, + /// The time when the queue was created. + #[serde(skip_serializing_if = "Option::is_none")] + pub created_at: Option, + /// The time when the queue was last updated. + #[serde(skip_serializing_if = "Option::is_none")] + pub updated_at: Option, + /// The time when the queue was last accessed. + #[serde(skip_serializing_if = "Option::is_none")] + pub accessed_at: Option, +} + +/// Statistics about a Service Bus topic. +#[derive(SafeDebug, Clone, Default, Serialize, Deserialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct TopicRuntimeProperties { + /// The name of the topic. + #[serde(skip_serializing_if = "Option::is_none")] + pub topic_name: Option, + /// The current size of the topic in bytes. + #[serde(skip_serializing_if = "Option::is_none")] + pub size_in_bytes: Option, + /// The number of subscriptions. + #[serde(skip_serializing_if = "Option::is_none")] + pub subscription_count: Option, + /// The number of scheduled messages. + #[serde(skip_serializing_if = "Option::is_none")] + pub scheduled_message_count: Option, + /// The time when the topic was created. + #[serde(skip_serializing_if = "Option::is_none")] + pub created_at: Option, + /// The time when the topic was last updated. + #[serde(skip_serializing_if = "Option::is_none")] + pub updated_at: Option, + /// The time when the topic was last accessed. + #[serde(skip_serializing_if = "Option::is_none")] + pub accessed_at: Option, +} + +/// Statistics about a Service Bus subscription. +#[derive(SafeDebug, Clone, Default, Serialize, Deserialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct SubscriptionRuntimeProperties { + /// The name of the topic. + #[serde(skip_serializing_if = "Option::is_none")] + pub topic_name: Option, + /// The name of the subscription. + #[serde(skip_serializing_if = "Option::is_none")] + pub subscription_name: Option, + /// The total number of messages in the subscription. + #[serde(skip_serializing_if = "Option::is_none")] + pub total_message_count: Option, + /// The number of active messages in the subscription. + #[serde(skip_serializing_if = "Option::is_none")] + pub active_message_count: Option, + /// The number of messages in the dead letter queue. + #[serde(skip_serializing_if = "Option::is_none")] + pub dead_letter_message_count: Option, + /// The number of messages transferred to another queue/topic. + #[serde(skip_serializing_if = "Option::is_none")] + pub transfer_message_count: Option, + /// The number of messages transferred to the dead letter queue. + #[serde(skip_serializing_if = "Option::is_none")] + pub transfer_dead_letter_message_count: Option, + /// The time when the subscription was created. + #[serde(skip_serializing_if = "Option::is_none")] + pub created_at: Option, + /// The time when the subscription was last updated. + #[serde(skip_serializing_if = "Option::is_none")] + pub updated_at: Option, + /// The time when the subscription was last accessed. + #[serde(skip_serializing_if = "Option::is_none")] + pub accessed_at: Option, +} + +/// Represents the status of a Service Bus entity. +#[derive(Clone, Copy, SafeDebug, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] +pub enum EntityStatus { + /// The entity is active. + #[serde(rename = "Active")] + Active, + /// The entity is creating. + #[serde(rename = "Creating")] + Creating, + /// The entity is deleting. + #[serde(rename = "Deleting")] + Deleting, + /// The entity is disabled. + #[serde(rename = "Disabled")] + Disabled, + /// The entity is receiving disabled. + #[serde(rename = "ReceiveDisabled")] + ReceiveDisabled, + /// The entity is renaming. + #[serde(rename = "Renaming")] + Renaming, + /// The entity is restoring. + #[serde(rename = "Restoring")] + Restoring, + /// The entity is sending disabled. + #[serde(rename = "SendDisabled")] + SendDisabled, + /// The entity status is unknown. + #[serde(rename = "Unknown")] + Unknown, +} + +/// Represents access rights for a Service Bus entity. +#[derive(Clone, Copy, SafeDebug, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] +pub enum AccessRights { + /// Permission to manage the entity. + #[serde(rename = "Manage")] + Manage, + /// Permission to send messages to the entity. + #[serde(rename = "Send")] + Send, + /// Permission to receive messages from the entity. + #[serde(rename = "Listen")] + Listen, +} + +/// Information about a Service Bus namespace. +#[derive(SafeDebug, Clone, Default, Serialize, Deserialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct NamespaceProperties { + /// The name of the namespace. + #[serde(skip_serializing_if = "Option::is_none")] + pub namespace_name: Option, + /// The type of the namespace. + #[serde(skip_serializing_if = "Option::is_none")] + pub namespace_type: Option, + /// The time when the namespace was created. + #[serde(skip_serializing_if = "Option::is_none")] + pub created_at: Option, + /// The time when the namespace was last modified. + #[serde(skip_serializing_if = "Option::is_none")] + pub modified_at: Option, + /// The messaging SKU of the namespace. + #[serde(skip_serializing_if = "Option::is_none")] + pub messaging_sku: Option, + /// The number of messaging units for premium namespaces. + #[serde(skip_serializing_if = "Option::is_none")] + pub messaging_units: Option, +} + +/// The type of Service Bus namespace. +#[derive(Clone, Copy, SafeDebug, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] +pub enum NamespaceType { + /// Messaging namespace. + #[serde(rename = "Messaging")] + Messaging, + /// Mixed namespace (deprecated). + #[serde(rename = "Mixed")] + Mixed, + /// Notification Hub namespace. + #[serde(rename = "NotificationHub")] + NotificationHub, + /// Relay namespace. + #[serde(rename = "Relay")] + Relay, +} + +/// The messaging SKU (pricing tier) of a Service Bus namespace. +#[derive(Clone, Copy, SafeDebug, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] +pub enum MessagingSku { + /// Basic tier. + #[serde(rename = "Basic")] + Basic, + /// Standard tier. + #[serde(rename = "Standard")] + Standard, + /// Premium tier. + #[serde(rename = "Premium")] + Premium, +} diff --git a/sdk/servicebus/azure_messaging_servicebus/src/receiver.rs b/sdk/servicebus/azure_messaging_servicebus/src/receiver.rs new file mode 100644 index 0000000000..564060778a --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/src/receiver.rs @@ -0,0 +1,3758 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! Service Bus message receiver functionality. +//! +//! This module provides the [`Receiver`] struct and related types for receiving messages +//! from Azure Service Bus queues and topic subscriptions. The receiver supports two modes +//! of operation and provides comprehensive message settlement operations. +//! +//! # Core Types +//! +//! - [`Receiver`] - The main receiver for consuming messages from Service Bus entities +//! - [`ReceiveMode`] - Enumeration defining how messages are handled when received +//! - [`ReceiveMessageOptions`] - Configuration options for message receive operations +//! +//! # Receive Modes +//! +//! ## PeekLock Mode (Default) +//! +//! In [`ReceiveMode::PeekLock`] mode, messages are locked when received and must be +//! explicitly settled using one of the settlement operations: +//! +//! - [`Receiver::complete_message`] - Successfully processed, remove from queue +//! - [`Receiver::abandon_message`] - Release lock, allow redelivery +//! - [`Receiver::dead_letter_message`] - Move to dead letter queue for manual inspection +//! - [`Receiver::defer_message`] - Defer for later processing by sequence number +//! +//! ## ReceiveAndDelete Mode +//! +//! In [`ReceiveMode::ReceiveAndDelete`] mode, messages are automatically deleted when +//! received, providing better performance but no delivery guarantees. +//! +//! # Examples +//! +//! ## Basic Message Receiving +//! +//! ```rust,no_run +//! use azure_messaging_servicebus::{ServiceBusClient, ReceiveMessageOptions, ServiceBusClientOptions}; +//! use azure_core::time::Duration; +//! +//! use azure_identity::DeveloperToolsCredential; +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let credential = DeveloperToolsCredential::new(None)?; +//! let client = ServiceBusClient::builder().open("myservicebus.servicebus.windows.net", credential.clone()).await?; +//! let receiver = client.create_receiver("my-queue", None).await?; +//! +//! let options = ReceiveMessageOptions { +//! max_message_count: 10, +//! max_wait_time: Some(Duration::seconds(30)), +//! }; +//! +//! let messages = receiver.receive_messages(10, Some(options)).await?; +//! for message in &messages { +//! println!("Received: {:?}", String::from_utf8_lossy(message.body())); +//! receiver.complete_message(message, None).await?; +//! } +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Error Handling with Settlement +//! +//! ```rust,no_run +//! use azure_messaging_servicebus::{ +//! ServiceBusClient, +//! ReceiveMessageOptions, DeadLetterMessageOptions, AbandonMessageOptions +//! , ServiceBusClientOptions}; +//! +//! use azure_identity::DeveloperToolsCredential; +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let credential = DeveloperToolsCredential::new(None)?; +//! let client = ServiceBusClient::builder().open("myservicebus.servicebus.windows.net", credential.clone()).await?; +//! let receiver = client.create_receiver("my-queue", None).await?; +//! +//! if let Some(message) = receiver.receive_message(None).await? { +//! match process_message(&message).await { +//! Ok(_) => { +//! receiver.complete_message(&message, None).await?; +//! } +//! Err(ProcessingError::Retryable) => { +//! receiver.abandon_message(&message, None).await?; +//! } +//! Err(ProcessingError::Fatal) => { +//! let dead_letter_options = DeadLetterMessageOptions { +//! reason: Some("ProcessingFailed".to_string()), +//! error_description: Some("Unrecoverable processing error".to_string()), +//! properties_to_modify: None, +//! }; +//! receiver.dead_letter_message(&message, Some(dead_letter_options)).await?; +//! } +//! } +//! } +//! # Ok(()) +//! # } +//! # async fn process_message(message: &azure_messaging_servicebus::ReceivedMessage) -> Result<(), ProcessingError> { Ok(()) } +//! # #[derive(Debug)] enum ProcessingError { Retryable, Fatal } +//! ``` + +use crate::{ + client::ServiceBusClientOptions, message::SystemProperties, ErrorKind, ReceivedMessage, Result, + ServiceBusError, +}; +use async_lock::{Mutex, OnceCell}; +use azure_core::{fmt::SafeDebug, time::Duration, time::OffsetDateTime, Uuid}; +use azure_core_amqp::{ + message::{AmqpMessageBody, AmqpMessageId}, + AmqpConnection, AmqpDelivery, AmqpDeliveryApis, AmqpManagementApis, AmqpReceiver, + AmqpReceiverApis, AmqpSession, AmqpSessionApis, AmqpSimpleValue, AmqpSource, AmqpValue, +}; +use futures::{select, FutureExt}; +use std::{collections::HashMap, sync::Arc}; +use tracing::{debug, trace, warn}; + +/// Represents the lock style to use for a receiver - either `PeekLock` or `ReceiveAndDelete`. +/// +/// This enum controls when a message is deleted from Service Bus and determines how message +/// settlement works for received messages. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ReceiveMode { + /// Messages are locked for processing and can be settled using one of the settlement methods. + /// + /// In `PeekLock` mode (the default), messages are locked when received, preventing multiple + /// receivers from processing the same message simultaneously. You control the lock state of + /// the message using one of the message settlement functions: + /// - `complete_message()` - removes the message from Service Bus + /// - `abandon_message()` - makes the message available again for processing + /// - `dead_letter_message()` - moves the message to the dead letter queue + /// - `defer_message()` - defers the message for later processing + /// + /// Messages have a lock timeout period, after which they automatically become available + /// for other receivers if not settled. The lock can be renewed using `renew_message_lock()`. + /// + /// This mode provides "at-least-once" delivery semantics and allows for reliable message + /// processing patterns where messages can be retried on failure. + PeekLock, + + /// Messages are automatically removed from the entity when received. + /// + /// In `ReceiveAndDelete` mode, Service Bus removes the message as soon as it's received + /// by the client, before any processing occurs. This provides "at-most-once" delivery + /// semantics - messages are delivered once or not at all. + /// + /// **Note**: When using `ReceiveAndDelete` mode, you can call `receive_messages()` even + /// after the receiver has been closed. This allows you to continue reading from the + /// receiver's internal cache until it is empty. When all cached messages have been + /// consumed, `receive_messages()` will return an error. + /// + /// Settlement methods like `complete_message()`, `abandon_message()`, etc. are not + /// applicable in this mode since messages are already removed from Service Bus. + /// + /// This mode offers better performance but sacrifices reliability - if message processing + /// fails after the message is received, the message is lost and cannot be recovered. + ReceiveAndDelete, +} + +/// Options for configuring message receive operations. +/// +/// This struct provides configuration options for controlling how messages are received +/// from a Service Bus entity, including batch size and timeout behavior. +/// +/// # Examples +/// +/// ```rust +/// use azure_messaging_servicebus::receiver::ReceiveMessageOptions; +/// use azure_core::time::Duration; +/// +/// // Custom options - receive up to 10 messages with 30 second timeout +/// let custom_options = ReceiveMessageOptions { +/// max_message_count: 10, +/// max_wait_time: Some(Duration::seconds(30)), +/// }; +/// +/// // No timeout - block indefinitely until messages arrive +/// let blocking_options = ReceiveMessageOptions { +/// max_message_count: 5, +/// max_wait_time: None, +/// }; +/// ``` +#[derive(SafeDebug, Clone)] +pub struct ReceiveMessageOptions { + /// The maximum number of messages to receive in a single operation. + /// + /// This controls the upper bound on how many messages will be returned. + /// The actual number returned may be less if fewer messages are available + /// or if the timeout is reached. + pub max_message_count: u32, + + /// The maximum amount of time to wait for messages to arrive. + /// + /// - `Some(duration)` - Wait up to the specified duration for messages + /// - `None` - Block indefinitely until at least one message arrives + /// + /// If a timeout is specified and no messages arrive within that time, + /// the operation will return an empty result rather than an error. + pub max_wait_time: Option, +} + +impl Default for ReceiveMessageOptions { + fn default() -> Self { + Self { + max_message_count: 1, + max_wait_time: Some(Duration::seconds(60)), + } + } +} + +/// Options for configuring deferred message receive operations. +/// +/// This struct provides configuration options for receiving messages that were +/// previously deferred using [`Receiver::defer_message`]. Deferred messages can +/// only be retrieved by their sequence number. +/// +/// Currently, this struct is defined for future extensibility but contains no +/// configuration options. Additional options may be added in future versions. +/// +#[derive(SafeDebug, Clone, Default)] +pub struct ReceiveDeferredMessagesOptions { + // Currently no specific options, but structure is ready for future expansion +} + +/// Options for configuring message peek operations. +/// +/// This struct provides configuration options for peeking at messages without +/// removing them from the queue or subscription. Peeked messages cannot be +/// settled (completed, abandoned, etc.) as they are not locked. +/// +/// # Examples +/// +/// ```rust +/// use azure_messaging_servicebus::receiver::PeekMessagesOptions; +/// +/// // Peek from the beginning +/// let default_options = PeekMessagesOptions::default(); +/// +/// // Peek starting from a specific sequence number +/// let options_from_sequence = PeekMessagesOptions { +/// from_sequence_number: Some(12345), +/// }; +/// ``` +#[derive(SafeDebug, Clone, Default)] +pub struct PeekMessagesOptions { + /// The sequence number to start peeking from. + /// + /// - `Some(sequence_number)` - Start peeking from the specified sequence number + /// - `None` - Start peeking from the next available message + /// + /// The receiver maintains an internal cursor for peek operations. If this field + /// is `None`, peeking will continue from where the last peek operation left off. + /// Setting a specific sequence number overrides this cursor. + pub from_sequence_number: Option, +} + +/// Options for configuring message completion operations. +/// +/// This struct provides configuration options for completing messages, which +/// removes them from the Service Bus entity and marks them as successfully processed. +/// Message completion is only available in [`ReceiveMode::PeekLock`] mode. +/// +/// Currently, this struct is defined for future extensibility but contains no +/// configuration options. Additional options may be added in future versions. +/// +#[derive(SafeDebug, Clone, Default)] +pub struct CompleteMessageOptions {} + +/// Options for configuring message abandon operations. +/// +/// This struct provides configuration options for abandoning messages, which +/// releases the message lock and makes the message available for redelivery. +/// Message abandoning is only available in [`ReceiveMode::PeekLock`] mode. +/// +/// When a message is abandoned, its delivery count is incremented. If the +/// delivery count exceeds the maximum delivery count configured on the entity, +/// the message will be automatically dead-lettered. +/// +/// # Examples +/// +/// ```rust +/// use azure_messaging_servicebus::receiver::AbandonMessageOptions; +/// use std::collections::HashMap; +/// +/// // Basic abandon without property modifications +/// let basic_options = AbandonMessageOptions::default(); +/// +/// // Abandon with custom properties +/// let mut properties = HashMap::new(); +/// properties.insert("reason".to_string(), "processing_failed".to_string()); +/// properties.insert("retry_count".to_string(), "3".to_string()); +/// +/// let options_with_properties = AbandonMessageOptions { +/// properties_to_modify: Some(properties), +/// }; +/// ``` +#[derive(SafeDebug, Clone, Default)] +pub struct AbandonMessageOptions { + /// Properties to modify in the message when it is abandoned. + /// + /// These properties will be added to or updated in the message's application + /// properties. This can be useful for tracking abandon reasons, retry counts, + /// or other metadata that might be helpful for subsequent processing attempts. + /// + /// - `Some(properties)` - Modify the specified properties + /// - `None` - Abandon without modifying any properties + pub properties_to_modify: Option>, +} + +/// Options for configuring message dead letter operations. +/// +/// This struct provides configuration options for dead lettering messages, which +/// moves them to the dead letter queue where they can be examined and potentially +/// reprocessed. Dead lettering is only available in [`ReceiveMode::PeekLock`] mode. +/// +/// Dead lettered messages are moved to a special sub-queue and will not be delivered +/// to regular consumers. They can be retrieved from the dead letter queue using a +/// receiver configured with the appropriate sub-queue option. +/// +/// # Examples +/// +/// ```rust +/// use azure_messaging_servicebus::receiver::DeadLetterMessageOptions; +/// use std::collections::HashMap; +/// +/// // Dead letter with reason and description +/// let detailed_options = DeadLetterMessageOptions { +/// reason: Some("ValidationFailed".to_string()), +/// error_description: Some("Message schema validation failed".to_string()), +/// properties_to_modify: None, +/// }; +/// +/// // Dead letter with custom properties for diagnostics +/// let mut properties = HashMap::new(); +/// properties.insert("validation_error".to_string(), "missing_required_field".to_string()); +/// properties.insert("processor_version".to_string(), "1.2.3".to_string()); +/// +/// let options_with_properties = DeadLetterMessageOptions { +/// reason: Some("ProcessingError".to_string()), +/// error_description: Some("Failed to process after 3 retries".to_string()), +/// properties_to_modify: Some(properties), +/// }; +/// ``` +#[derive(SafeDebug, Clone, Default)] +pub struct DeadLetterMessageOptions { + /// The reason for dead lettering the message. + /// + /// This is a short, descriptive string that categorizes why the message + /// was dead lettered. Common examples include "ValidationFailed", + /// "ProcessingTimeout", "MaxRetryExceeded", etc. + pub reason: Option, + + /// A detailed description of the error that caused the message to be dead lettered. + /// + /// This field provides additional context about the specific error condition + /// that led to dead lettering. It can include technical details, error messages, + /// or other diagnostic information. + pub error_description: Option, + + /// Properties to modify in the message when it is dead lettered. + /// + /// These properties will be added to or updated in the message's application + /// properties. This can be useful for adding diagnostic information, tracking + /// processing attempts, or storing other metadata that might be helpful for + /// troubleshooting or reprocessing. + pub properties_to_modify: Option>, +} + +/// Options for configuring message defer operations. +/// +/// This struct provides configuration options for deferring messages, which +/// removes them from the normal message flow and makes them only retrievable +/// by their sequence number. Deferring is only available in [`ReceiveMode::PeekLock`] mode. +/// +/// Deferred messages are not delivered to regular consumers and must be explicitly +/// retrieved using [`Receiver::receive_deferred_message`] or +/// [`Receiver::receive_deferred_messages`] with their sequence numbers. +/// +/// # Examples +/// +/// ```rust +/// use azure_messaging_servicebus::receiver::DeferMessageOptions; +/// use std::collections::HashMap; +/// +/// // Basic defer without property modifications +/// let basic_options = DeferMessageOptions::default(); +/// +/// // Defer with custom properties for tracking +/// let mut properties = HashMap::new(); +/// properties.insert("defer_reason".to_string(), "waiting_for_dependency".to_string()); +/// properties.insert("defer_timestamp".to_string(), "2023-10-15T10:30:00Z".to_string()); +/// +/// let options_with_properties = DeferMessageOptions { +/// properties_to_modify: Some(properties), +/// }; +/// ``` +#[derive(SafeDebug, Clone, Default)] +pub struct DeferMessageOptions { + /// Properties to modify in the message when it is deferred. + /// + /// These properties will be added to or updated in the message's application + /// properties. This can be useful for tracking why the message was deferred, + /// when it should be processed, or other metadata that might be helpful for + /// later retrieval and processing. + pub properties_to_modify: Option>, +} + +/// Options for configuring message lock renewal operations. +/// +/// This struct provides configuration options for renewing the lock on a message +/// to extend the time available for processing. Lock renewal is only available +/// in [`ReceiveMode::PeekLock`] mode and can help prevent messages from being +/// automatically released due to lock timeout during long processing operations. +/// +/// Currently, this struct is defined for future extensibility but contains no +/// configuration options. Additional options may be added in future versions. +/// +#[derive(SafeDebug, Clone, Default)] +pub struct RenewMessageLockOptions {} + +/// A receiver for receiving messages from a Service Bus queue or subscription. +/// +/// `Receiver` provides methods to receive messages from Service Bus entities (queues or topic subscriptions) +/// and perform message settlement operations. The receiver supports two modes of operation: +/// - [`ReceiveMode::PeekLock`] - Messages are locked for processing and must be explicitly settled +/// - [`ReceiveMode::ReceiveAndDelete`] - Messages are automatically deleted when received +/// +/// # Message Settlement +/// +/// In `PeekLock` mode, received messages must be settled using one of the settlement methods: +/// - [`complete_message`](Receiver::complete_message) - Marks the message as successfully processed and removes it +/// - [`abandon_message`](Receiver::abandon_message) - Releases the lock and makes the message available for redelivery +/// - [`dead_letter_message`](Receiver::dead_letter_message) - Moves the message to the dead letter queue +/// - [`defer_message`](Receiver::defer_message) - Defers the message for later retrieval by sequence number +/// +/// # Examples +/// +/// ## Basic Message Receiving +/// +/// ```rust,no_run +/// use azure_messaging_servicebus::{ServiceBusClient, ReceiveMessageOptions, CreateReceiverOptions, ServiceBusClientOptions}; +/// use azure_core::time::Duration; +/// +/// use azure_identity::DeveloperToolsCredential; +/// # #[tokio::main] +/// # async fn main() -> Result<(), Box> { +/// let credential = DeveloperToolsCredential::new(None)?; +/// let client = ServiceBusClient::builder().open("myservicebus.servicebus.windows.net", credential.clone()).await?; +/// let receiver = client.create_receiver("my-queue", None).await?; +/// +/// if let Some(message) = receiver.receive_message(None).await? { +/// println!("Received message: {:?}", String::from_utf8_lossy(message.body())); +/// +/// // Complete the message to remove it from the queue +/// receiver.complete_message(&message, None).await?; +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// ## Batch Message Processing +/// +/// ```rust,no_run +/// use azure_messaging_servicebus::{ServiceBusClient, ReceiveMessageOptions, CreateReceiverOptions, ServiceBusClientOptions}; +/// use azure_core::time::Duration; +/// +/// use azure_identity::DeveloperToolsCredential; +/// # #[tokio::main] +/// # async fn main() -> Result<(), Box> { +/// let credential = DeveloperToolsCredential::new(None)?; +/// let client = ServiceBusClient::builder().open("myservicebus.servicebus.windows.net", credential.clone()).await?; +/// let receiver = client.create_receiver("my-queue", None).await?; +/// +/// // Receive up to 10 messages with 30 second timeout +/// let options = ReceiveMessageOptions { +/// max_message_count: 10, +/// max_wait_time: Some(Duration::seconds(30)), +/// }; +/// +/// let messages = receiver.receive_messages(10, Some(options)).await?; +/// for message in messages { +/// match process_message(&message).await { +/// Ok(_) => { +/// receiver.complete_message(&message, None).await?; +/// } +/// Err(e) => { +/// println!("Processing failed: {}", e); +/// receiver.abandon_message(&message, None).await?; +/// } +/// } +/// } +/// # Ok(()) +/// # } +/// # async fn process_message(message: &azure_messaging_servicebus::ReceivedMessage) -> Result<(), &'static str> { Ok(()) } +/// ``` +/// +/// ## Error Handling and Dead Lettering +/// +/// ```rust,no_run +/// use azure_messaging_servicebus::{ServiceBusClient, ReceiveMessageOptions, DeadLetterMessageOptions, CreateReceiverOptions, ServiceBusClientOptions}; +/// +/// use azure_identity::DeveloperToolsCredential; +/// # #[tokio::main] +/// # async fn main() -> Result<(), Box> { +/// let credential = DeveloperToolsCredential::new(None)?; +/// let client = ServiceBusClient::builder().open("myservicebus.servicebus.windows.net", credential.clone()).await?; +/// let receiver = client.create_receiver("my-queue", None).await?; +/// +/// if let Some(message) = receiver.receive_message(None).await? { +/// match validate_and_process(&message).await { +/// Ok(_) => { +/// receiver.complete_message(&message, None).await?; +/// } +/// Err(ValidationError::InvalidFormat) => { +/// // Dead letter messages that can't be processed +/// let dead_letter_options = DeadLetterMessageOptions { +/// reason: Some("ValidationFailed".to_string()), +/// error_description: Some("Invalid message format".to_string()), +/// properties_to_modify: None, +/// }; +/// receiver.dead_letter_message(&message, Some(dead_letter_options)).await?; +/// } +/// Err(ValidationError::TransientError) => { +/// // Abandon messages for transient errors to allow retry +/// receiver.abandon_message(&message, None).await?; +/// } +/// } +/// } +/// # Ok(()) +/// # } +/// # #[derive(Debug)] +/// # enum ValidationError { InvalidFormat, TransientError } +/// # async fn validate_and_process(message: &azure_messaging_servicebus::ReceivedMessage) -> Result<(), ValidationError> { Ok(()) } +/// ``` +pub struct Receiver { + connection: Arc, + entity_name: String, + subscription_name: Option, + receive_mode: ReceiveMode, + _options: ServiceBusClientOptions, + // Cached session and receiver to avoid creating new ones on each call + session: OnceCell>, + amqp_receiver: OnceCell>, + // Track deliveries by lock token for settlement operations + delivery_map: Arc>>, +} + +impl Receiver { + /// Creates a new receiver for a Service Bus entity. + /// + /// This method is used internally by the Service Bus client to create receiver instances. + /// Users should use [`crate::ServiceBusClient::create_receiver`] or + /// [`crate::ServiceBusClient::create_receiver_for_subscription`] instead. + /// + /// # Arguments + /// + /// * `connection` - The AMQP connection to use + /// * `entity_name` - The name of the queue or topic + /// * `subscription_name` - The subscription name (for topic subscriptions only) + /// * `receive_mode` - The receive mode to use + /// * `options` - Client configuration options + /// + /// # Returns + /// + /// Returns a `Result` that will be `Ok(receiver)` on success or an error on failure. + pub(crate) async fn new( + connection: Arc, + entity_name: String, + subscription_name: Option, + receive_mode: ReceiveMode, + options: ServiceBusClientOptions, + ) -> Result { + let entity_path = if let Some(ref subscription) = subscription_name { + format!("{}/subscriptions/{}", entity_name, subscription) + } else { + entity_name.clone() + }; + + debug!( + "Creating Receiver for entity: {} (mode: {:?})", + entity_path, receive_mode + ); + + trace!("Receiver created successfully for entity: {}", entity_path); + + Ok(Self { + connection, + entity_name, + subscription_name, + receive_mode, + _options: options, + session: OnceCell::new(), + amqp_receiver: OnceCell::new(), + delivery_map: Arc::new(Mutex::new(HashMap::new())), + }) + } + + /// Receives a single message from the Service Bus entity. + /// + /// This is a convenience method that calls [`receive_messages`](Receiver::receive_messages) + /// with `max_message_count` set to 1 and returns the first message if available. + /// + /// # Arguments + /// + /// * `options` - Configuration options for the receive operation + /// + /// # Returns + /// + /// Returns `Ok(Some(message))` if a message was received, `Ok(None)` if no message + /// was available within the timeout period, or an error if the operation failed. + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::{ServiceBusClient, receiver::ReceiveMessageOptions}; + /// use azure_identity::DeveloperToolsCredential; + /// use azure_core::time::Duration; + /// + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let credential = DeveloperToolsCredential::new(None)?; + /// let client = ServiceBusClient::builder().open("myservicebus.servicebus.windows.net", credential.clone()).await?; + /// let receiver = client.create_receiver("queue", None).await?; + /// let options = ReceiveMessageOptions { + /// max_message_count: 1, + /// max_wait_time: Some(Duration::seconds(30)), + /// }; + /// + /// if let Some(message) = receiver.receive_message(None).await? { + /// println!("Received: {:?}", String::from_utf8_lossy(message.body())); + /// receiver.complete_message(&message, None).await?; + /// } else { + /// println!("No message received within timeout"); + /// } + /// # Ok(()) + /// # } + /// ``` + pub async fn receive_message( + &self, + options: Option, + ) -> Result> { + let messages = self.receive_messages(1, options).await?; + Ok(messages.into_iter().next()) + } + + /// Receives multiple messages from the Service Bus entity. + /// + /// This method attempts to receive up to `max_message_count` messages from the entity. + /// The actual number of messages returned may be less than requested if: + /// - Fewer messages are available in the entity + /// - The timeout specified in options is reached + /// - An error occurs during the receive operation + /// + /// # Arguments + /// + /// * `max_message_count` - The maximum number of messages to receive + /// * `options` - Configuration options for the receive operation + /// + /// # Returns + /// + /// Returns `Ok(Vec)` containing the received messages, or an error + /// if the operation failed. An empty vector is returned if no messages were available. + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::{ServiceBusClient, receiver::ReceiveMessageOptions}; + /// use azure_identity::DeveloperToolsCredential; + /// use azure_core::time::Duration; + /// + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let credential = DeveloperToolsCredential::new(None)?; + /// let client = ServiceBusClient::builder().open("myservicebus.servicebus.windows.net", credential.clone()).await?; + /// let receiver = client.create_receiver("queue", None).await?; + /// let options = ReceiveMessageOptions { + /// max_message_count: 10, + /// max_wait_time: Some(Duration::seconds(30)), + /// }; + /// + /// let messages = receiver.receive_messages(10, Some(options)).await?; + /// println!("Received {} messages", messages.len()); + /// + /// for message in &messages { + /// // Process each message + /// println!("Message: {:?}", String::from_utf8_lossy(message.body())); + /// } + /// + /// // Complete all messages after successful processing + /// for message in &messages { + /// receiver.complete_message(message, None).await?; + /// } + /// # Ok(()) + /// # } + /// ``` + pub async fn receive_messages( + &self, + max_message_count: usize, + options: Option, + ) -> Result> { + debug!( + "receive_messages: max_message_count={}, max_wait_time={:?}", + max_message_count, + options.as_ref().and_then(|o| o.max_wait_time) + ); + + // Ensure session and receiver are available + let amqp_receiver = self.ensure_receiver().await?; + + let mut messages = Vec::new(); + + for i in 0..max_message_count { + debug!("receive_messages: iteration {}", i); + + let delivery_result = + if let Some(timeout_duration) = options.as_ref().and_then(|o| o.max_wait_time) { + debug!("receive_messages: using timeout {:?}", timeout_duration); + select! { + delivery = amqp_receiver.receive_delivery().fuse() => { + debug!("receive_messages: received delivery on iteration {}", i); + delivery + }, + _ = azure_core::sleep::sleep(timeout_duration).fuse() => { + debug!("receive_messages: timeout reached on iteration {}", i); + // Timeout reached - just return what we have so far + break; + }, + } + } else { + debug!( + "receive_messages: no timeout, blocking receive on iteration {}", + i + ); + amqp_receiver.receive_delivery().await + }; + + match delivery_result { + Ok(delivery) => { + debug!("receive_messages: processing delivery on iteration {}", i); + if let Some(message) = self.convert_delivery_to_message(delivery).await? { + messages.push(message); + } + } + Err(err) => { + debug!("receive_messages: error on iteration {}: {:?}", i, err); + if messages.is_empty() { + return Err(ServiceBusError::new( + ErrorKind::Amqp, + format!("Error receiving message: {:?}", err), + )); + } else { + break; + } + } + } + } + + debug!("receive_messages: returning {} messages", messages.len()); + Ok(messages) + } + + /// Ensures that a session and receiver are available, creating them if necessary. + async fn ensure_receiver(&self) -> Result> { + let receiver = self + .amqp_receiver + .get_or_try_init(|| async { + // First ensure we have a session + let session = self.ensure_session().await?; + + // Create AMQP receiver + let entity_path = self.get_entity_path(); + let amqp_source = AmqpSource::builder().with_address(entity_path).build(); + let amqp_receiver = AmqpReceiver::new(); + amqp_receiver + .attach(&session, amqp_source, None) + .await + .map_err(|e| { + ServiceBusError::new( + ErrorKind::Amqp, + format!("Failed to create receiver: {:?}", e), + ) + })?; + + Ok::, ServiceBusError>(Arc::new(amqp_receiver)) + }) + .await?; + + Ok(receiver.clone()) + } + + /// Ensures that a session is available, creating it if necessary. + async fn ensure_session(&self) -> Result> { + let session = self + .session + .get_or_try_init(|| async { + let session = AmqpSession::new(); + session + .begin( + self.connection.as_ref(), + Some(azure_core_amqp::AmqpSessionOptions { + incoming_window: Some(u32::MAX), + outgoing_window: Some(u32::MAX), + ..Default::default() + }), + ) + .await + .map_err(|e| { + ServiceBusError::new( + ErrorKind::Amqp, + format!("Failed to create session: {:?}", e), + ) + })?; + + Ok::, ServiceBusError>(Arc::new(session)) + }) + .await?; + + Ok(session.clone()) + } + + /// Ensures that a management client is available, creating it if necessary. + async fn ensure_management_client(&self) -> Result { + let session = self.ensure_session().await?; + + // For Service Bus management operations, we need to create a management client + // The client needs a unique node name and an access token for authorization + let client_node_name = format!("servicebus-receiver-management-{}", uuid::Uuid::new_v4()); + + // Create a default access token - in practice, this should come from the connection's + // authentication mechanism (CBS token, SAS token, etc.) + // For now, we'll use an empty token as a placeholder + let access_token = azure_core::credentials::AccessToken { + token: azure_core::credentials::Secret::new("placeholder-token".to_string()), + expires_on: time::OffsetDateTime::now_utc() + Duration::seconds(3600), + }; + + let management_client = azure_core_amqp::AmqpManagement::new( + session.as_ref().clone(), + client_node_name, + access_token, + ) + .map_err(|e| { + ServiceBusError::new( + ErrorKind::Amqp, + format!("Failed to create management client: {:?}", e), + ) + })?; + + // Attach the management client + management_client.attach().await.map_err(|e| { + ServiceBusError::new( + ErrorKind::Amqp, + format!("Failed to attach management client: {:?}", e), + ) + })?; + + Ok(management_client) + } + + /// Completes a message, removing it from the Service Bus entity. + /// + /// This operation marks the message as successfully processed and permanently removes + /// it from the queue or subscription. This operation is only available when using + /// [`ReceiveMode::PeekLock`]. + /// + /// Once a message is completed, it cannot be received again by any consumer. + /// + /// # Arguments + /// + /// * `message` - The message to complete (must have been received in PeekLock mode) + /// * `options` - Configuration options for the complete operation + /// + /// # Returns + /// + /// Returns `Ok(())` on success or an error if the operation failed. + /// + /// # Errors + /// + /// This method will return an error if: + /// - The receiver is not in `PeekLock` mode + /// - The message does not have a valid lock token + /// - The message lock has expired + /// - The message has already been settled + /// - A network or service error occurs + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::{ServiceBusClient, receiver::{ReceiveMessageOptions, CompleteMessageOptions}}; + /// use azure_identity::DeveloperToolsCredential; + /// + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let credential = DeveloperToolsCredential::new(None)?; + /// let client = ServiceBusClient::builder().open("myservicebus.servicebus.windows.net", credential.clone()).await?; + /// let receiver = client.create_receiver("queue", None).await?; + /// let options = ReceiveMessageOptions::default(); + /// if let Some(message) = receiver.receive_message(None).await? { + /// // Process the message + /// match process_message(&message).await { + /// Ok(_) => { + /// // Successfully processed - complete the message + /// receiver.complete_message(&message, None).await?; + /// println!("Message completed successfully"); + /// } + /// Err(e) => { + /// println!("Processing failed: {}", e); + /// // Handle the error (abandon, dead letter, etc.) + /// } + /// } + /// } + /// # Ok(()) + /// # } + /// # async fn process_message(message: &azure_messaging_servicebus::ReceivedMessage) -> Result<(), &'static str> { Ok(()) } + /// ``` + pub async fn complete_message( + &self, + message: &ReceivedMessage, + _options: Option, + ) -> Result<()> { + if self.receive_mode != ReceiveMode::PeekLock { + return Err(ServiceBusError::new( + ErrorKind::InvalidRequest, + "Complete message is only supported in PeekLock mode", + )); + } + + let lock_token = message.lock_token().ok_or_else(|| { + ServiceBusError::new( + ErrorKind::MessageLockLost, + "Message does not have a lock token", + ) + })?; + + debug!("Completing message with lock token: {}", lock_token); + + // Get the stored delivery for this lock token + let delivery = { + let mut delivery_map = self.delivery_map.lock().await; + delivery_map.remove(&lock_token).ok_or_else(|| { + ServiceBusError::new( + ErrorKind::MessageLockLost, + "Delivery not found for lock token - message may have already been settled or lock expired", + ) + })? + }; + + // Accept the delivery using AMQP + let amqp_receiver = self.ensure_receiver().await?; + amqp_receiver + .accept_delivery(&delivery) + .await + .map_err(|e| { + ServiceBusError::new( + ErrorKind::Amqp, + format!("Failed to accept delivery: {:?}", e), + ) + })?; + + trace!( + "Message completed successfully with lock token: {}", + lock_token + ); + Ok(()) + } + + /// Abandons a message, making it available for redelivery. + /// + /// This operation releases the lock on the message and makes it available for other + /// consumers to receive. The message's delivery count will be incremented. This operation + /// is only available when using [`ReceiveMode::PeekLock`]. + /// + /// If the message's delivery count exceeds the maximum delivery count configured on + /// the entity, it will be automatically moved to the dead letter queue. + /// + /// # Arguments + /// + /// * `message` - The message to abandon (must have been received in PeekLock mode) + /// * `options` - Configuration options for the abandon operation + /// + /// # Returns + /// + /// Returns `Ok(())` on success or an error if the operation failed. + /// + /// # Errors + /// + /// This method will return an error if: + /// - The receiver is not in `PeekLock` mode + /// - The message does not have a valid lock token + /// - The message lock has expired + /// - The message has already been settled + /// - A network or service error occurs + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::{ServiceBusClient, receiver::{ReceiveMessageOptions, AbandonMessageOptions}}; + /// use azure_identity::DeveloperToolsCredential; + /// use std::collections::HashMap; + /// + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let credential = DeveloperToolsCredential::new(None)?; + /// let client = ServiceBusClient::builder().open("myservicebus.servicebus.windows.net", credential.clone()).await?; + /// let receiver = client.create_receiver("queue", None).await?; + /// if let Some(message) = receiver.receive_message(None).await? { + /// match process_message(&message).await { + /// Ok(_) => { + /// receiver.complete_message(&message, None).await?; + /// } + /// Err(e) if is_retryable_error(&e) => { + /// // Abandon with tracking information + /// let mut properties = HashMap::new(); + /// properties.insert("abandon_reason".to_string(), "retryable_error".to_string()); + /// properties.insert("error_details".to_string(), e.to_string()); + /// + /// let abandon_options = AbandonMessageOptions { + /// properties_to_modify: Some(properties), + /// }; + /// + /// receiver.abandon_message(&message, Some(abandon_options)).await?; + /// println!("Message abandoned for retry"); + /// } + /// Err(_) => { + /// // Non-retryable error - dead letter the message + /// receiver.dead_letter_message(&message, None).await?; + /// } + /// } + /// } + /// # Ok(()) + /// # } + /// # async fn process_message(message: &azure_messaging_servicebus::ReceivedMessage) -> Result<(), Box> { Ok(()) } + /// # fn is_retryable_error(error: &Box) -> bool { true } + /// ``` + pub async fn abandon_message( + &self, + message: &ReceivedMessage, + _options: Option, + ) -> Result<()> { + if self.receive_mode != ReceiveMode::PeekLock { + return Err(ServiceBusError::new( + ErrorKind::InvalidRequest, + "Abandon message is only supported in PeekLock mode", + )); + } + + let lock_token = message.lock_token().ok_or_else(|| { + ServiceBusError::new( + ErrorKind::MessageLockLost, + "Message does not have a lock token", + ) + })?; + + debug!("Abandoning message with lock token: {}", lock_token); + + // Get the stored delivery for this lock token + let delivery = { + let mut delivery_map = self.delivery_map.lock().await; + delivery_map.remove(&lock_token).ok_or_else(|| { + ServiceBusError::new( + ErrorKind::MessageLockLost, + "Delivery not found for lock token - message may have already been settled or lock expired", + ) + })? + }; + + // Release the delivery using AMQP + let amqp_receiver = self.ensure_receiver().await?; + amqp_receiver + .release_delivery(&delivery) + .await + .map_err(|e| { + ServiceBusError::new( + ErrorKind::Amqp, + format!("Failed to release delivery: {:?}", e), + ) + })?; + + trace!( + "Message abandoned successfully with lock token: {}", + lock_token + ); + Ok(()) + } + + /// Dead letters a message, moving it to the dead letter queue. + /// + /// This operation moves the message to the dead letter sub-queue, where it can be + /// examined and potentially reprocessed. Dead lettered messages are not delivered + /// to regular consumers. This operation is only available when using + /// [`ReceiveMode::PeekLock`]. + /// + /// Dead lettered messages can be retrieved using a receiver configured to read from + /// the dead letter queue by setting the appropriate sub-queue option. + /// + /// # Arguments + /// + /// * `message` - The message to dead letter (must have been received in PeekLock mode) + /// * `options` - Configuration options including reason and error description + /// + /// # Returns + /// + /// Returns `Ok(())` on success or an error if the operation failed. + /// + /// # Errors + /// + /// This method will return an error if: + /// - The receiver is not in `PeekLock` mode + /// - The message does not have a valid lock token + /// - The message lock has expired + /// - The message has already been settled + /// - A network or service error occurs + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::receiver::{ReceiveMessageOptions, DeadLetterMessageOptions}; + /// use std::collections::HashMap; + /// + /// # async fn example(receiver: azure_messaging_servicebus::Receiver) -> Result<(), Box> { + /// if let Some(message) = receiver.receive_message(None).await? { + /// match validate_message_format(&message) { + /// Ok(_) => { + /// // Process valid message + /// receiver.complete_message(&message, None).await?; + /// } + /// Err(validation_error) => { + /// // Dead letter invalid messages with detailed information + /// let mut properties = HashMap::new(); + /// properties.insert("validation_failure".to_string(), validation_error.clone()); + /// properties.insert("processor_version".to_string(), "1.0.0".to_string()); + /// + /// let dead_letter_options = DeadLetterMessageOptions { + /// reason: Some("ValidationFailed".to_string()), + /// error_description: Some(format!("Message validation failed: {}", validation_error)), + /// properties_to_modify: Some(properties), + /// }; + /// + /// receiver.dead_letter_message(&message, Some(dead_letter_options)).await?; + /// println!("Message dead lettered due to validation failure"); + /// } + /// } + /// } + /// # Ok(()) + /// # } + /// # fn validate_message_format(message: &azure_messaging_servicebus::ReceivedMessage) -> Result<(), String> { Ok(()) } + /// ``` + pub async fn dead_letter_message( + &self, + message: &ReceivedMessage, + options: Option, + ) -> Result<()> { + if self.receive_mode != ReceiveMode::PeekLock { + return Err(ServiceBusError::new( + ErrorKind::InvalidRequest, + "Dead letter message is only supported in PeekLock mode", + )); + } + + let lock_token = message.lock_token().ok_or_else(|| { + ServiceBusError::new( + ErrorKind::MessageLockLost, + "Message does not have a lock token", + ) + })?; + + debug!( + "Dead lettering message with lock token: {}, reason: {:?}, description: {:?}", + lock_token, + options.as_ref().and_then(|o| o.reason.as_ref()), + options.as_ref().and_then(|o| o.error_description.as_ref()) + ); + + // Get the stored delivery for this lock token + let delivery = { + let mut delivery_map = self.delivery_map.lock().await; + delivery_map.remove(&lock_token).ok_or_else(|| { + ServiceBusError::new( + ErrorKind::MessageLockLost, + "Delivery not found for lock token - message may have already been settled or lock expired", + ) + })? + }; + + // Reject the delivery using AMQP + let amqp_receiver = self.ensure_receiver().await?; + amqp_receiver + .reject_delivery(&delivery) + .await + .map_err(|e| { + ServiceBusError::new( + ErrorKind::Amqp, + format!("Failed to reject delivery: {:?}", e), + ) + })?; + + trace!( + "Message dead lettered successfully with lock token: {}", + lock_token + ); + Ok(()) + } + + /// Defers a message, making it unavailable for normal message retrieval. + /// + /// This operation removes the message from the normal message flow and makes it only + /// retrievable by its sequence number using [`receive_deferred_message`](Receiver::receive_deferred_message) + /// or [`receive_deferred_messages`](Receiver::receive_deferred_messages). This operation + /// is only available when using [`ReceiveMode::PeekLock`]. + /// + /// Deferred messages are useful when you need to process messages out of order or when + /// message processing depends on some external condition that isn't currently met. + /// + /// **Note**: The current implementation is a placeholder. Full defer functionality + /// requires Service Bus management operations that are not yet implemented. + /// + /// # Arguments + /// + /// * `message` - The message to defer (must have been received in PeekLock mode) + /// * `options` - Configuration options for the defer operation + /// + /// # Returns + /// + /// Returns `Ok(())` on success or an error if the operation failed. + /// + /// # Errors + /// + /// This method will return an error if: + /// - The receiver is not in `PeekLock` mode + /// - The message does not have a valid lock token + /// - The message lock has expired + /// - The message has already been settled + /// - A network or service error occurs + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::receiver::{ReceiveMessageOptions, DeferMessageOptions}; + /// use std::collections::HashMap; + /// + /// # async fn example(receiver: azure_messaging_servicebus::Receiver) -> Result<(), Box> { + /// if let Some(message) = receiver.receive_message(None).await? { + /// if requires_deferred_processing(&message) { + /// // Defer the message with tracking information + /// let mut properties = HashMap::new(); + /// properties.insert("defer_reason".to_string(), "waiting_for_dependency".to_string()); + /// properties.insert("defer_until".to_string(), "2023-10-15T15:30:00Z".to_string()); + /// + /// let defer_options = DeferMessageOptions { + /// properties_to_modify: Some(properties), + /// }; + /// + /// receiver.defer_message(&message, Some(defer_options)).await?; + /// + /// // Store the sequence number for later retrieval + /// let sequence_number = message.system_properties().sequence_number; + /// println!("Message deferred with sequence number: {:?}", sequence_number); + /// } else { + /// // Process immediately + /// receiver.complete_message(&message, None).await?; + /// } + /// } + /// # Ok(()) + /// # } + /// # fn requires_deferred_processing(message: &azure_messaging_servicebus::ReceivedMessage) -> bool { false } + /// ``` + pub async fn defer_message( + &self, + message: &ReceivedMessage, + options: Option, + ) -> Result<()> { + if self.receive_mode != ReceiveMode::PeekLock { + return Err(ServiceBusError::new( + ErrorKind::InvalidRequest, + "Defer message is only supported in PeekLock mode", + )); + } + + let lock_token = message.lock_token().ok_or_else(|| { + ServiceBusError::new( + ErrorKind::MessageLockLost, + "Message does not have a lock token", + ) + })?; + + debug!("Deferring message with lock token: {}", lock_token); + + // Create management client and defer the message + let management_client = self.ensure_management_client().await?; + + let mut application_properties: azure_core_amqp::AmqpOrderedMap< + String, + azure_core_amqp::AmqpSimpleValue, + > = azure_core_amqp::AmqpOrderedMap::new(); + + // Add the lock token as a string representation + application_properties.insert("lock-token".to_string(), lock_token.to_string().into()); + + // Add any properties to modify if specified + if let Some(properties) = options + .as_ref() + .and_then(|o| o.properties_to_modify.as_ref()) + { + for (key, value) in properties { + application_properties.insert(key.clone(), value.clone().into()); + } + } + + let _response = management_client + .call( + "com.microsoft:defer-message".to_string(), + application_properties, + ) + .await + .map_err(|e| { + ServiceBusError::new(ErrorKind::Amqp, format!("Failed to defer message: {:?}", e)) + })?; + + // Remove the delivery from our tracking map since it's now deferred + let _delivery = { + let mut delivery_map = self.delivery_map.lock().await; + delivery_map.remove(&lock_token) + }; + + trace!( + "Message deferred successfully with lock token: {}", + lock_token + ); + Ok(()) + } + + /// Receives a deferred message by its sequence number. + /// + /// This method retrieves a message that was previously deferred using + /// [`defer_message`](Receiver::defer_message). Deferred messages can only be retrieved + /// by their sequence number and are not delivered through normal receive operations. + /// This operation is only available when using [`ReceiveMode::PeekLock`]. + /// + /// **Note**: The current implementation is a placeholder. Full deferred message retrieval + /// requires Service Bus management operations that are not yet implemented. + /// + /// # Arguments + /// + /// * `sequence_number` - The sequence number of the deferred message to retrieve + /// * `options` - Configuration options for the receive operation + /// + /// # Returns + /// + /// Returns `Ok(Some(message))` if the deferred message was found and retrieved, + /// `Ok(None)` if no message with the specified sequence number exists, or an error + /// if the operation failed. + /// + /// # Errors + /// + /// This method will return an error if: + /// - The receiver is not in `PeekLock` mode + /// - The sequence number is invalid + /// - A network or service error occurs + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::receiver::ReceiveDeferredMessagesOptions; + /// + /// # async fn example(receiver: azure_messaging_servicebus::Receiver) -> Result<(), Box> { + /// // Assume we previously stored sequence numbers of deferred messages + /// let sequence_number = 123456789i64; + /// + /// let options = Default::default(); + /// match receiver.receive_deferred_message(sequence_number, Some(options)).await? { + /// Some(message) => { + /// println!("Retrieved deferred message: {:?}", String::from_utf8_lossy(message.body())); + /// + /// // Process the message now that conditions are met + /// match process_deferred_message(&message).await { + /// Ok(_) => receiver.complete_message(&message, None).await?, + /// Err(_) => receiver.abandon_message(&message, None).await?, + /// } + /// } + /// None => { + /// println!("No deferred message found with sequence number: {}", sequence_number); + /// } + /// } + /// # Ok(()) + /// # } + /// # async fn process_deferred_message(message: &azure_messaging_servicebus::ReceivedMessage) -> Result<(), &'static str> { Ok(()) } + /// ``` + pub async fn receive_deferred_message( + &self, + sequence_number: i64, + _options: Option, + ) -> Result> { + if self.receive_mode != ReceiveMode::PeekLock { + return Err(ServiceBusError::new( + ErrorKind::InvalidRequest, + "Receive deferred message is only supported in PeekLock mode", + )); + } + + debug!( + "Receiving deferred message with sequence number: {}", + sequence_number + ); + + // TODO: Implement actual deferred message retrieval using Service Bus management operations + // Deferred messages require Service Bus management API calls to retrieve by sequence number. + // This is not a standard AMQP operation but a Service Bus-specific management operation. + // Implementation would involve: + // 1. Creating a management request with the sequence number + // 2. Sending the request via the AMQP management link + // 3. Processing the response to reconstruct the ReceivedMessage + // For now, this is a placeholder implementation. + + trace!( + "Attempted to receive deferred message with sequence number: {} (placeholder implementation)", + sequence_number + ); + + // Return None for now since this is not implemented + Ok(None) + } + + /// Receives multiple deferred messages by their sequence numbers. + /// + /// This method retrieves multiple messages that were previously deferred using + /// [`defer_message`](Receiver::defer_message). Each message is identified by its + /// sequence number. This operation is only available when using [`ReceiveMode::PeekLock`]. + /// + /// **Note**: The current implementation is a placeholder. Full deferred message retrieval + /// requires Service Bus management operations that are not yet implemented. + /// + /// # Arguments + /// + /// * `sequence_numbers` - The sequence numbers of the deferred messages to retrieve + /// * `options` - Configuration options for the receive operation + /// + /// # Returns + /// + /// Returns `Ok(Vec)` containing the retrieved deferred messages. + /// The vector may contain fewer messages than requested if some sequence numbers + /// don't correspond to existing deferred messages. + /// + /// # Errors + /// + /// This method will return an error if: + /// - The receiver is not in `PeekLock` mode + /// - One or more sequence numbers are invalid + /// - A network or service error occurs + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::receiver::ReceiveDeferredMessagesOptions; + /// + /// # async fn example(receiver: azure_messaging_servicebus::Receiver) -> Result<(), Box> { + /// // Assume we previously stored sequence numbers of deferred messages + /// let sequence_numbers = vec![123456789i64, 123456790i64, 123456791i64]; + /// + /// let options = Default::default(); + /// let messages = receiver.receive_deferred_messages(&sequence_numbers, Some(options)).await?; + /// + /// println!("Retrieved {} deferred messages", messages.len()); + /// + /// for message in &messages { + /// println!("Processing deferred message: {:?}", message.system_properties().sequence_number); + /// + /// match process_deferred_message(&message).await { + /// Ok(_) => { + /// receiver.complete_message(&message, None).await?; + /// println!("Deferred message processed successfully"); + /// } + /// Err(e) => { + /// println!("Failed to process deferred message: {}", e); + /// receiver.abandon_message(&message, None).await?; + /// } + /// } + /// } + /// # Ok(()) + /// # } + /// # async fn process_deferred_message(message: &azure_messaging_servicebus::ReceivedMessage) -> Result<(), &'static str> { Ok(()) } + /// ``` + pub async fn receive_deferred_messages( + &self, + sequence_numbers: &[i64], + _options: Option, + ) -> Result> { + if self.receive_mode != ReceiveMode::PeekLock { + return Err(ServiceBusError::new( + ErrorKind::InvalidRequest, + "Receive deferred messages is only supported in PeekLock mode", + )); + } + + if sequence_numbers.is_empty() { + return Ok(Vec::new()); + } + + debug!( + "Receiving {} deferred messages with sequence numbers: {:?}", + sequence_numbers.len(), + sequence_numbers + ); + + // Create management client and receive the deferred messages + let management_client = self.ensure_management_client().await?; + + let mut application_properties: azure_core_amqp::AmqpOrderedMap< + String, + azure_core_amqp::AmqpSimpleValue, + > = azure_core_amqp::AmqpOrderedMap::new(); + + // Add the sequence numbers as a comma-separated string + // Service Bus management operations don't support arrays in AmqpSimpleValue, + // so we'll pass the sequence numbers as a comma-separated string + let sequence_numbers_str = sequence_numbers + .iter() + .map(|n| n.to_string()) + .collect::>() + .join(","); + + application_properties.insert("sequence-numbers".to_string(), sequence_numbers_str.into()); + + // Set receiver settle mode based on receive mode + let settle_mode = match self.receive_mode { + ReceiveMode::PeekLock => 1u32, + ReceiveMode::ReceiveAndDelete => 0u32, + }; + application_properties.insert("receiver-settle-mode".to_string(), settle_mode.into()); + + let response = management_client + .call( + "com.microsoft:receive-by-sequence-number".to_string(), + application_properties, + ) + .await + .map_err(|e| { + ServiceBusError::new( + ErrorKind::Amqp, + format!("Failed to receive deferred messages: {:?}", e), + ) + })?; + + // Process the response to reconstruct ReceivedMessage instances + let messages = self.parse_deferred_messages_response(response).await?; + + trace!("Successfully received {} deferred messages", messages.len()); + + Ok(messages) + } + + /// Parses the response from a deferred messages retrieval operation. + /// + /// The Service Bus management response for deferred messages comes in a nested format: + /// - A map with key "messages" + /// - An array of message maps, each with key "message" + /// - Each "message" contains serialized AMQP binary data + async fn parse_deferred_messages_response( + &self, + response: azure_core_amqp::AmqpOrderedMap, + ) -> Result> { + use std::collections::HashMap; + + // Extract the "messages" field from the response + let messages_value = response.get("messages").ok_or_else(|| { + ServiceBusError::new( + ErrorKind::InvalidRequest, + "Management response missing 'messages' field", + ) + })?; + + // Parse the messages array + let messages_array = match messages_value { + AmqpValue::Array(array) => array, + _ => { + return Err(ServiceBusError::new( + ErrorKind::InvalidRequest, + "Expected 'messages' field to be an array", + )) + } + }; + + let mut received_messages = Vec::new(); + + for message_entry in messages_array { + // Each entry should be a map with a "message" key + let message_map = match message_entry { + AmqpValue::Map(map) => map, + _ => { + return Err(ServiceBusError::new( + ErrorKind::InvalidRequest, + "Expected message entry to be a map", + )) + } + }; + + // Extract the serialized message data + let message_data = message_map + .get(&AmqpValue::String("message".to_string())) + .ok_or_else(|| { + ServiceBusError::new( + ErrorKind::InvalidRequest, + "Message entry missing 'message' field", + ) + })?; + + // The message data should be binary AMQP data + let message_bytes = match message_data { + AmqpValue::Binary(bytes) => bytes, + _ => { + return Err(ServiceBusError::new( + ErrorKind::InvalidRequest, + "Expected message data to be binary", + )) + } + }; + + // Deserialize the AMQP message from binary data + // Note: This is a simplified reconstruction. In a full implementation, + // we would need to properly deserialize the AMQP message and extract + // all the Service Bus specific properties, system properties, etc. + + // For now, create a basic ReceivedMessage structure + // This would need to be enhanced to properly reconstruct from AMQP binary data + let system_properties = SystemProperties::default(); + + // Create a placeholder ReceivedMessage + // TODO: Implement proper AMQP binary deserialization + let received_message = ReceivedMessage::new( + message_bytes.clone(), + HashMap::new(), + system_properties, + None, // lock_token - deferred messages typically don't have lock tokens initially + ); + + received_messages.push(received_message); + } + + Ok(received_messages) + } + + /// Renews the lock on a message to extend the processing time. + /// + /// This operation extends the lock duration on a message, preventing it from being + /// automatically released and made available to other consumers. This is useful when + /// message processing takes longer than the default lock duration. This operation + /// is only available when using [`ReceiveMode::PeekLock`]. + /// + /// **Note**: The current implementation is a placeholder. Full lock renewal + /// requires Service Bus management operations that are not yet implemented. + /// + /// # Arguments + /// + /// * `message` - The message whose lock should be renewed + /// * `options` - Configuration options for the lock renewal operation + /// + /// # Returns + /// + /// Returns `Ok(locked_until)` with the new lock expiration time on success, + /// or an error if the operation failed. + /// + /// # Errors + /// + /// This method will return an error if: + /// - The receiver is not in `PeekLock` mode + /// - The message does not have a valid lock token + /// - The message lock has already expired + /// - The message has already been settled + /// - A network or service error occurs + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::{ReceiveMessageOptions, RenewMessageLockOptions, ServiceBusClientOptions}; + /// use azure_core::time::Duration; + /// use tokio::time::sleep; + /// + /// # async fn example(receiver: &azure_messaging_servicebus::Receiver) -> Result<(), Box> { + /// if let Some(message) = receiver.receive_message(None).await? { + /// // Renew lock for the received message + /// match receiver.renew_message_lock(&message, None).await { + /// Ok(locked_until) => { + /// println!("Lock renewed until: {:?}", locked_until); + /// } + /// Err(e) => { + /// println!("Failed to renew lock: {}", e); + /// } + /// } + /// } + /// # Ok(()) + /// # } + /// ``` + pub async fn renew_message_lock( + &self, + message: &ReceivedMessage, + _options: Option, + ) -> Result { + if self.receive_mode != ReceiveMode::PeekLock { + return Err(ServiceBusError::new( + ErrorKind::InvalidRequest, + "Renew message lock is only supported in PeekLock mode", + )); + } + + let lock_token = message.lock_token().ok_or_else(|| { + ServiceBusError::new( + ErrorKind::MessageLockLost, + "Message does not have a lock token", + ) + })?; + + debug!("Renewing message lock with lock token: {}", lock_token); + + // Create management client and renew the message lock + let management_client = self.ensure_management_client().await?; + + let mut application_properties: azure_core_amqp::AmqpOrderedMap< + String, + azure_core_amqp::AmqpSimpleValue, + > = azure_core_amqp::AmqpOrderedMap::new(); + + // Add the lock token as a string representation + application_properties.insert("lock-token".to_string(), lock_token.to_string().into()); + + let response = management_client + .call( + "com.microsoft:renew-lock".to_string(), + application_properties, + ) + .await + .map_err(|e| { + ServiceBusError::new( + ErrorKind::Amqp, + format!("Failed to renew message lock: {:?}", e), + ) + })?; + + // Extract the new expiration time from the response + let locked_until = response + .get("expiration") + .or_else(|| response.get("locked-until-utc")) + .ok_or_else(|| { + ServiceBusError::new( + ErrorKind::InvalidRequest, + "Management response did not contain expiration time", + ) + })?; + + // Convert the response value to an OffsetDateTime + let locked_until = match locked_until { + azure_core_amqp::AmqpValue::TimeStamp(timestamp) => { + let timestamp: azure_core_amqp::AmqpTimestamp = timestamp.clone(); + if let Some(system_time) = timestamp.0 { + OffsetDateTime::from(system_time) + } else { + OffsetDateTime::now_utc() + Duration::seconds(60) + } + } + _ => { + // Fallback to a default if we can't parse the timestamp + warn!( + "Could not parse expiration timestamp from management response, using default" + ); + OffsetDateTime::now_utc() + Duration::seconds(60) + } + }; + + trace!( + "Message lock renewed successfully with lock token: {}, new expiration: {}", + lock_token, + locked_until + ); + Ok(locked_until) + } + + /// Gets the name of the Service Bus entity (queue or topic) this receiver is connected to. + /// + /// # Returns + /// + /// Returns the entity name as a string slice. + /// + /// # Examples + /// + /// ```rust,no_run + /// # async fn example(receiver: azure_messaging_servicebus::Receiver) { + /// println!("Receiver is connected to entity: {}", receiver.entity_name()); + /// # } + /// ``` + pub fn entity_name(&self) -> &str { + &self.entity_name + } + + /// Gets the subscription name if this receiver is for a topic subscription. + /// + /// # Returns + /// + /// Returns `Some(subscription_name)` if this receiver is for a topic subscription, + /// or `None` if this receiver is for a queue. + /// + /// # Examples + /// + /// ```rust,no_run + /// # async fn example(receiver: azure_messaging_servicebus::Receiver) { + /// match receiver.subscription_name() { + /// Some(subscription) => { + /// println!("Receiver is for subscription: {} on topic: {}", + /// subscription, receiver.entity_name()); + /// } + /// None => { + /// println!("Receiver is for queue: {}", receiver.entity_name()); + /// } + /// } + /// # } + /// ``` + pub fn subscription_name(&self) -> Option<&str> { + self.subscription_name.as_deref() + } + + /// Gets the receive mode configured for this receiver. + /// + /// # Returns + /// + /// Returns the [`ReceiveMode`] that determines how messages are handled when received. + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::receiver::ReceiveMode; + /// + /// # async fn example(receiver: azure_messaging_servicebus::Receiver) { + /// match receiver.receive_mode() { + /// ReceiveMode::PeekLock => { + /// println!("Receiver uses PeekLock mode - messages must be explicitly settled"); + /// } + /// ReceiveMode::ReceiveAndDelete => { + /// println!("Receiver uses ReceiveAndDelete mode - messages are auto-deleted"); + /// } + /// } + /// # } + /// ``` + pub fn receive_mode(&self) -> ReceiveMode { + self.receive_mode.clone() + } + + /// Peeks at messages in the queue or subscription without removing them. + /// + /// This operation allows you to browse messages without affecting their state or + /// availability to other consumers. Unlike receiving messages, peeked messages: + /// - Are not locked or removed from the queue/subscription + /// - Cannot be completed, abandoned, or dead-lettered + /// - Do not affect delivery count + /// - Can include deferred messages (but not dead-lettered messages) + /// + /// This is useful for monitoring queue contents, debugging message flow, or + /// implementing custom message routing logic. + /// + /// # Arguments + /// + /// * `max_count` - Maximum number of messages to peek (must be > 0) + /// * `options` - Configuration options for the peek operation + /// + /// # Returns + /// + /// Returns `Ok(Vec)` containing the peeked messages. + /// The returned messages will not have lock tokens since they are not locked. + /// The vector may contain fewer messages than requested if fewer are available. + /// + /// # Errors + /// + /// This method will return an error if: + /// - `max_count` is 0 or negative + /// - A network or service error occurs + /// - The management operation fails + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::receiver::PeekMessagesOptions; + /// + /// # async fn example(receiver: azure_messaging_servicebus::Receiver) -> Result<(), Box> { + /// // Peek up to 10 messages from the beginning + /// let options = Default::default(); + /// let peeked_messages = receiver.peek_messages(10, Some(options)).await?; + /// + /// println!("Found {} messages in queue", peeked_messages.len()); + /// + /// for message in &peeked_messages { + /// if let Some(seq_num) = message.system_properties().sequence_number { + /// println!("Message sequence number: {}", seq_num); + /// } + /// println!("Message body: {:?}", message.body()); + /// } + /// + /// // Peek starting from a specific sequence number + /// let options_from_sequence = PeekMessagesOptions { + /// from_sequence_number: Some(123456789), + /// }; + /// let more_messages = receiver.peek_messages(5, Some(options_from_sequence)).await?; + /// println!("Found {} more messages starting from sequence 123456789", more_messages.len()); + /// # Ok(()) + /// # } + /// ``` + pub async fn peek_messages( + &self, + max_count: u32, + options: Option, + ) -> Result> { + if max_count == 0 { + return Err(ServiceBusError::new( + ErrorKind::InvalidRequest, + "Max count must be greater than 0", + )); + } + + debug!( + "Peeking up to {} messages with options: {:?}", + max_count, options + ); + + // Create management client for peek operation + let management_client = self.ensure_management_client().await?; + + let mut application_properties: azure_core_amqp::AmqpOrderedMap< + String, + azure_core_amqp::AmqpSimpleValue, + > = azure_core_amqp::AmqpOrderedMap::new(); + + // Set the maximum number of messages to peek + application_properties.insert("message-count".to_string(), max_count.into()); + + // Set the starting sequence number if provided + if let Some(from_sequence_number) = options.as_ref().and_then(|o| o.from_sequence_number) { + application_properties.insert( + "from-sequence-number".to_string(), + from_sequence_number.into(), + ); + } + + let response = management_client + .call( + "com.microsoft:peek-message".to_string(), + application_properties, + ) + .await + .map_err(|e| { + ServiceBusError::new(ErrorKind::Amqp, format!("Failed to peek messages: {:?}", e)) + })?; + + // Process the response to reconstruct ReceivedMessage instances + let messages = self.parse_peeked_messages_response(response).await?; + + trace!("Successfully peeked {} messages", messages.len()); + + Ok(messages) + } + + /// Parses the response from a peek messages operation. + /// + /// The Service Bus management response for peek messages comes in the same format + /// as deferred messages: + /// - A map with key "messages" + /// - An array of message maps, each with key "message" + /// - Each "message" contains serialized AMQP binary data + /// + /// Unlike deferred messages, peeked messages do not have lock tokens. + async fn parse_peeked_messages_response( + &self, + response: azure_core_amqp::AmqpOrderedMap, + ) -> Result> { + use std::collections::HashMap; + + // Extract the "messages" field from the response + let messages_value = response.get("messages").ok_or_else(|| { + ServiceBusError::new( + ErrorKind::InvalidRequest, + "Management response missing 'messages' field", + ) + })?; + + // Parse the messages array + let messages_array = match messages_value { + AmqpValue::Array(array) => array, + _ => { + return Err(ServiceBusError::new( + ErrorKind::InvalidRequest, + "Expected 'messages' field to be an array", + )) + } + }; + + let mut received_messages = Vec::new(); + + for message_entry in messages_array { + // Each entry should be a map with a "message" key + let message_map = match message_entry { + AmqpValue::Map(map) => map, + _ => { + return Err(ServiceBusError::new( + ErrorKind::InvalidRequest, + "Expected message entry to be a map", + )) + } + }; + + // Extract the serialized message data + let message_data = message_map + .get(&AmqpValue::String("message".to_string())) + .ok_or_else(|| { + ServiceBusError::new( + ErrorKind::InvalidRequest, + "Message entry missing 'message' field", + ) + })?; + + // The message data should be binary AMQP data + let message_bytes = match message_data { + AmqpValue::Binary(bytes) => bytes, + _ => { + return Err(ServiceBusError::new( + ErrorKind::InvalidRequest, + "Expected message data to be binary", + )) + } + }; + + // Deserialize the AMQP message from binary data + // Note: This is a simplified reconstruction. In a full implementation, + // we would need to properly deserialize the AMQP message and extract + // all the Service Bus specific properties, system properties, etc. + + // For now, create a basic ReceivedMessage structure + // This would need to be enhanced to properly reconstruct from AMQP binary data + let system_properties = SystemProperties::default(); + + // Create a ReceivedMessage for peeked message + // Peeked messages do not have lock tokens + let received_message = ReceivedMessage::new( + message_bytes.clone(), + HashMap::new(), + system_properties, + None, // lock_token - peeked messages never have lock tokens + ); + + received_messages.push(received_message); + } + + Ok(received_messages) + } + + /// Closes the receiver and releases associated resources. + /// + /// This method gracefully closes the receiver by detaching from the AMQP link and + /// ending the AMQP session. Once closed, the receiver cannot be used for further + /// operations and a new receiver must be created. + /// + /// It's recommended to call this method when you're finished with the receiver to + /// ensure proper cleanup of network resources. + /// + /// # Returns + /// + /// Returns `Ok(())` on successful closure or an error if cleanup operations fail. + /// Note that errors during cleanup are logged but generally don't indicate a + /// problem that requires user intervention. + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::receiver::ReceiveMessageOptions; + /// + /// # async fn example(receiver: azure_messaging_servicebus::Receiver) -> Result<(), Box> { + /// // Use the receiver for message operations + /// let options = Default::default(); + /// let messages = receiver.receive_messages(10, Some(options)).await?; + /// + /// // Process messages... + /// for message in &messages { + /// receiver.complete_message(message, None).await?; + /// } + /// + /// // Close the receiver when done + /// receiver.close().await?; + /// println!("Receiver closed successfully"); + /// # Ok(()) + /// # } + /// ``` + pub async fn close(&self) -> Result<()> { + let entity_path = self.get_entity_path(); + debug!("Closing Receiver for entity: {}", entity_path); + + // Detach the AMQP receiver if it exists + if let Some(receiver) = self.amqp_receiver.get() { + let receiver = receiver.clone(); + // Try to get exclusive access to the receiver for detachment + match Arc::try_unwrap(receiver) { + Ok(receiver) => { + match receiver.detach().await { + Ok(_) => { + trace!( + "AMQP receiver detached successfully for entity: {}", + entity_path + ); + } + Err(e) => { + // Log but don't fail - connection might already be closed + warn!( + "Failed to detach AMQP receiver for entity '{}': {}", + entity_path, e + ); + } + } + } + Err(_) => { + // Receiver is still being used elsewhere, skip detachment + trace!("AMQP receiver still in use for entity: {}", entity_path); + } + } + } + + // End the AMQP session if it exists + if let Some(session) = self.session.get() { + match session.end().await { + Ok(_) => { + trace!( + "AMQP session ended successfully for entity: {}", + entity_path + ); + } + Err(e) => { + // Log but don't fail - connection might already be closed + warn!( + "Failed to end AMQP session for entity '{}': {}", + entity_path, e + ); + } + } + } + + trace!("Receiver closed successfully for entity: {}", entity_path); + Ok(()) + } + + fn get_entity_path(&self) -> String { + if let Some(ref subscription) = self.subscription_name { + format!("{}/subscriptions/{}", self.entity_name, subscription) + } else { + self.entity_name.clone() + } + } + + /// Converts an AMQP delivery to a ReceivedMessage. + async fn convert_delivery_to_message( + &self, + delivery: AmqpDelivery, + ) -> Result> { + let message = delivery.message(); + + // Extract body + let body = match message.body() { + AmqpMessageBody::Binary(binary_data) => { + // Combine all binary chunks + binary_data + .iter() + .flat_map(|chunk| chunk.iter()) + .cloned() + .collect() + } + AmqpMessageBody::Value(value) => { + // Convert value to string and then to bytes + format!("{:?}", value).into_bytes() + } + AmqpMessageBody::Sequence(sequence) => { + // Convert sequence to string and then to bytes + format!("{:?}", sequence).into_bytes() + } + AmqpMessageBody::Empty => Vec::new(), + }; + + // Extract application properties + let mut properties = HashMap::new(); + if let Some(app_props) = message.application_properties() { + for (key, value) in app_props.0.iter() { + let value_str = match value { + AmqpSimpleValue::String(s) => s.clone(), + AmqpSimpleValue::Int(i) => i.to_string(), + AmqpSimpleValue::UInt(u) => u.to_string(), + AmqpSimpleValue::Long(l) => l.to_string(), + AmqpSimpleValue::ULong(ul) => ul.to_string(), + AmqpSimpleValue::Boolean(b) => b.to_string(), + _ => format!("{:?}", value), + }; + properties.insert(key.clone(), value_str); + } + } + + // Extract system properties from message properties + let mut system_properties = SystemProperties::default(); + + if let Some(msg_props) = message.properties() { + if let Some(message_id) = &msg_props.message_id { + system_properties.message_id = match message_id { + AmqpMessageId::String(s) => Some(s.clone()), + AmqpMessageId::Uuid(u) => Some(u.to_string()), + AmqpMessageId::Binary(b) => Some(format!("{:?}", b)), + AmqpMessageId::Ulong(u) => Some(u.to_string()), + }; + } + if let Some(correlation_id) = &msg_props.correlation_id { + system_properties.correlation_id = match correlation_id { + AmqpMessageId::String(s) => Some(s.clone()), + AmqpMessageId::Uuid(u) => Some(u.to_string()), + AmqpMessageId::Binary(b) => Some(format!("{:?}", b)), + AmqpMessageId::Ulong(u) => Some(u.to_string()), + }; + } + if let Some(content_type) = &msg_props.content_type { + system_properties.content_type = Some(content_type.into()); + } + if let Some(reply_to) = &msg_props.reply_to { + system_properties.reply_to = Some(reply_to.clone()); + } + if let Some(subject) = &msg_props.subject { + system_properties.subject = Some(subject.clone()); + } + } + + // Generate a lock token for PeekLock mode and store the delivery + let lock_token = if self.receive_mode == ReceiveMode::PeekLock { + let token = Uuid::new_v4(); + // Store the delivery for later settlement operations + let mut delivery_map = self.delivery_map.lock().await; + delivery_map.insert(token, delivery); + Some(token) + } else { + None + }; + + let received_message = + ReceivedMessage::new(body, properties, system_properties, lock_token); + + Ok(Some(received_message)) + } +} + +impl Drop for Receiver { + fn drop(&mut self) { + let entity_path = self.get_entity_path(); + trace!("Receiver for entity '{}' is being dropped", entity_path); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use azure_core_amqp::AmqpConnection; + use std::sync::Arc; + + /// Creates a mock connection for testing + fn create_test_connection() -> Arc { + Arc::new(AmqpConnection::new()) + } + + /// Creates a test ClientOptions + fn create_test_options() -> ServiceBusClientOptions { + Default::default() + } + + #[tokio::test] + async fn test_receiver_creation_queue() -> Result<()> { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name.clone(), + None, + ReceiveMode::PeekLock, + options, + ) + .await?; + + assert_eq!(receiver.entity_name(), &entity_name); + assert_eq!(receiver.subscription_name(), None); + assert_eq!(receiver.receive_mode(), ReceiveMode::PeekLock); + Ok(()) + } + + #[tokio::test] + async fn test_receiver_creation_subscription() -> Result<()> { + let connection = create_test_connection(); + let entity_name = "test-topic".to_string(); + let subscription_name = "test-subscription".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name.clone(), + Some(subscription_name.clone()), + ReceiveMode::ReceiveAndDelete, + options, + ) + .await?; + + assert_eq!(receiver.entity_name(), &entity_name); + assert_eq!( + receiver.subscription_name(), + Some(subscription_name.as_str()) + ); + assert_eq!(receiver.receive_mode(), ReceiveMode::ReceiveAndDelete); + Ok(()) + } + + #[test] + fn test_receive_mode_equality() { + assert_eq!(ReceiveMode::PeekLock, ReceiveMode::PeekLock); + assert_eq!(ReceiveMode::ReceiveAndDelete, ReceiveMode::ReceiveAndDelete); + assert_ne!(ReceiveMode::PeekLock, ReceiveMode::ReceiveAndDelete); + } + + #[test] + fn test_receive_message_options_default() { + let options = ReceiveMessageOptions::default(); + assert_eq!(options.max_message_count, 1); + assert_eq!(options.max_wait_time, Some(Duration::seconds(60))); + } + + #[test] + fn test_receive_message_options_custom() { + let options = ReceiveMessageOptions { + max_message_count: 10, + max_wait_time: Some(Duration::seconds(30)), + }; + + assert_eq!(options.max_message_count, 10); + assert_eq!(options.max_wait_time, Some(Duration::seconds(30))); + } + + #[test] + fn test_get_entity_path_queue() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver { + connection, + entity_name: entity_name.clone(), + subscription_name: None, + receive_mode: ReceiveMode::PeekLock, + _options: options, + session: OnceCell::new(), + amqp_receiver: OnceCell::new(), + delivery_map: Arc::new(Mutex::new(HashMap::new())), + }; + + assert_eq!(receiver.get_entity_path(), entity_name); + } + + #[test] + fn test_get_entity_path_subscription() { + let connection = create_test_connection(); + let entity_name = "test-topic".to_string(); + let subscription_name = "test-subscription".to_string(); + let options = create_test_options(); + + let receiver = Receiver { + connection, + entity_name: entity_name.clone(), + subscription_name: Some(subscription_name.clone()), + receive_mode: ReceiveMode::PeekLock, + _options: options, + session: OnceCell::new(), + amqp_receiver: OnceCell::new(), + delivery_map: Arc::new(Mutex::new(HashMap::new())), + }; + + let expected_path = format!("{}/subscriptions/{}", entity_name, subscription_name); + assert_eq!(receiver.get_entity_path(), expected_path); + } + + #[tokio::test] + async fn test_receiver_close() -> Result<()> { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await?; + + // Should not fail + receiver.close().await?; + Ok(()) + } + + #[test] + fn test_receiver_drop() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver { + connection, + entity_name, + subscription_name: None, + receive_mode: ReceiveMode::PeekLock, + _options: options, + session: OnceCell::new(), + amqp_receiver: OnceCell::new(), + delivery_map: Arc::new(Mutex::new(HashMap::new())), + }; + + // Should not panic when dropped + drop(receiver); + } + + #[tokio::test] + async fn test_complete_message_receive_and_delete_mode() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::ReceiveAndDelete, + options, + ) + .await + .unwrap(); + + let message = ReceivedMessage::new( + b"test".to_vec(), + HashMap::new(), + SystemProperties { + message_id: None, + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: None, + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + None, + ); + + let result = receiver.complete_message(&message, None).await; + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("PeekLock mode")); + } + + #[tokio::test] + async fn test_abandon_message_receive_and_delete_mode() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::ReceiveAndDelete, + options, + ) + .await + .unwrap(); + + let message = ReceivedMessage::new( + b"test".to_vec(), + HashMap::new(), + SystemProperties { + message_id: None, + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: None, + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + None, + ); + + let result = receiver.abandon_message(&message, None).await; + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("PeekLock mode")); + } + + #[tokio::test] + async fn test_dead_letter_message_receive_and_delete_mode() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::ReceiveAndDelete, + options, + ) + .await + .unwrap(); + + let message = ReceivedMessage::new( + b"test".to_vec(), + HashMap::new(), + SystemProperties { + message_id: None, + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: None, + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + None, + ); + + let result = receiver + .dead_letter_message( + &message, + Some(DeadLetterMessageOptions { + reason: Some("test reason".to_string()), + error_description: Some("test description".to_string()), + ..Default::default() + }), + ) + .await; + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("PeekLock mode")); + } + + #[tokio::test] + async fn test_complete_message_no_lock_token() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let message = ReceivedMessage::new( + b"test".to_vec(), + HashMap::new(), + SystemProperties { + message_id: None, + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: None, + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + None, // No lock token + ); + + let result = receiver.complete_message(&message, None).await; + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("lock token")); + } + + #[tokio::test] + async fn test_defer_message_receive_and_delete_mode() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::ReceiveAndDelete, + options, + ) + .await + .unwrap(); + + let message = ReceivedMessage::new( + b"test".to_vec(), + HashMap::new(), + SystemProperties { + message_id: None, + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: None, + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + None, + ); + + let result = receiver.defer_message(&message, None).await; + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("PeekLock mode")); + } + + #[tokio::test] + async fn test_defer_message_no_lock_token() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let message = ReceivedMessage::new( + b"test".to_vec(), + HashMap::new(), + SystemProperties { + message_id: None, + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: None, + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + None, // No lock token + ); + + let result = receiver.defer_message(&message, None).await; + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("lock token")); + } + + #[tokio::test] + async fn test_receive_deferred_message() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let result = receiver.receive_deferred_message(12345, None).await; + assert!(result.is_ok()); + // Should return None since it's not implemented yet + assert!(result.unwrap().is_none()); + } + + #[tokio::test] + async fn test_receive_deferred_messages() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let sequence_numbers = vec![12345, 67890]; + let result = receiver + .receive_deferred_messages(&sequence_numbers, None) + .await; + + // Should fail since we don't have a real connection, but validates the API + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!( + error_msg.contains("AMQP") + || error_msg.contains("Failed") + || error_msg.contains("management") + ); + } + + #[tokio::test] + async fn test_receive_deferred_message_receive_and_delete_mode() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::ReceiveAndDelete, + options, + ) + .await + .unwrap(); + + let result = receiver.receive_deferred_message(12345, None).await; + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("PeekLock mode")); + } + + #[tokio::test] + async fn test_defer_message_success() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let lock_token = Uuid::new_v4(); + let message = ReceivedMessage::new( + b"test".to_vec(), + HashMap::new(), + SystemProperties { + message_id: Some("test-msg-id".to_string()), + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: Some(12345), + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + Some(lock_token), + ); + + // Test that defer_message accepts a message with valid lock token + // Note: This will fail in practice without a real management client, + // but validates the input validation logic + let result = receiver.defer_message(&message, None).await; + + // The operation will fail because we're using a mock connection, + // but we're testing that it passes initial validation + assert!(result.is_err()); + // Should fail due to mock connection limitations + let error_msg = result.unwrap_err().to_string(); + assert!(error_msg.contains("AMQP") || error_msg.contains("Failed")); + } + + #[tokio::test] + async fn test_defer_message_with_properties() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let lock_token = Uuid::new_v4(); + let message = ReceivedMessage::new( + b"test".to_vec(), + HashMap::new(), + SystemProperties { + message_id: Some("test-msg-id".to_string()), + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: Some(12345), + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + Some(lock_token), + ); + + let mut properties_to_modify = HashMap::new(); + properties_to_modify.insert("CustomProperty".to_string(), "CustomValue".to_string()); + + let defer_options = DeferMessageOptions { + properties_to_modify: Some(properties_to_modify), + }; + + let result = receiver.defer_message(&message, Some(defer_options)).await; + + // Should fail on mock connection but pass validation + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!(error_msg.contains("AMQP") || error_msg.contains("Failed")); + } + + #[tokio::test] + async fn test_renew_message_lock_success() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let lock_token = Uuid::new_v4(); + let message = ReceivedMessage::new( + b"test".to_vec(), + HashMap::new(), + SystemProperties { + message_id: Some("test-msg-id".to_string()), + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: Some(12345), + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + Some(lock_token), + ); + + // Test that renew_message_lock accepts a message with valid lock token + // Note: This will fail in practice without a real management client, + // but validates the input validation logic + let result = receiver.renew_message_lock(&message, None).await; + + // The operation will fail because we're using a mock connection, + // but we're testing that it passes initial validation + assert!(result.is_err()); + // Should fail on mock connection but pass validation + let error_msg = result.unwrap_err().to_string(); + assert!(error_msg.contains("AMQP") || error_msg.contains("Failed")); + } + + #[tokio::test] + async fn test_renew_message_lock_receive_and_delete_mode() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::ReceiveAndDelete, + options, + ) + .await + .unwrap(); + + let message = ReceivedMessage::new( + b"test".to_vec(), + HashMap::new(), + SystemProperties { + message_id: None, + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: None, + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + None, + ); + + let result = receiver.renew_message_lock(&message, None).await; + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("PeekLock mode")); + } + + #[tokio::test] + async fn test_renew_message_lock_no_lock_token() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let message = ReceivedMessage::new( + b"test".to_vec(), + HashMap::new(), + SystemProperties { + message_id: None, + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: None, + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + None, // No lock token + ); + + let result = receiver.renew_message_lock(&message, None).await; + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("lock token")); + } + + #[tokio::test] + async fn test_message_lock_renewal_returns_valid_time() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let lock_token = Uuid::new_v4(); + let message = ReceivedMessage::new( + b"test".to_vec(), + HashMap::new(), + SystemProperties { + message_id: Some("test-msg-id".to_string()), + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: Some(12345), + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + Some(lock_token), + ); + + // Even though the operation will fail with a mock connection, + // we can verify that the method signature returns the expected type + let result = receiver.renew_message_lock(&message, None).await; + + // The operation should fail, but we're testing it doesn't panic + // and returns a proper Result + assert!(result.is_err()); + + // Verify error occurs but don't check specific message due to mock connection + let error_msg = result.unwrap_err().to_string(); + assert!(error_msg.contains("AMQP") || error_msg.contains("Failed")); + } + + #[tokio::test] + async fn test_defer_message_validates_lock_token_format() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + // Test with a valid UUID lock token format + let lock_token = Uuid::new_v4(); + let message = ReceivedMessage::new( + b"test".to_vec(), + HashMap::new(), + SystemProperties { + message_id: Some("test-msg-id".to_string()), + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: Some(12345), + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + Some(lock_token), + ); + + let result = receiver.defer_message(&message, None).await; + + // Should pass lock token validation and fail on mock connection + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!(error_msg.contains("AMQP") || error_msg.contains("Failed")); + } + + #[tokio::test] + async fn test_defer_and_receive_deferred_integration() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let lock_token = Uuid::new_v4(); + let sequence_number = 12345i64; + + let message = ReceivedMessage::new( + b"test deferred message".to_vec(), + HashMap::new(), + SystemProperties { + message_id: Some("test-msg-id".to_string()), + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: Some(sequence_number), + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + Some(lock_token), + ); + + // Test defer message + let defer_result = receiver.defer_message(&message, None).await; + + // Should fail on mock management client + assert!(defer_result.is_err()); + + // Test receive deferred message by sequence number + let receive_result = receiver + .receive_deferred_message(sequence_number, None) + .await; + + // Should succeed but return None since it's a placeholder implementation + assert!(receive_result.is_ok()); + assert!(receive_result.unwrap().is_none()); + } + + #[tokio::test] + async fn test_multiple_message_lock_renewals() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + // Create multiple messages with different lock tokens + let lock_token_1 = Uuid::new_v4(); + let lock_token_2 = Uuid::new_v4(); + + let message_1 = ReceivedMessage::new( + b"test message 1".to_vec(), + HashMap::new(), + SystemProperties { + message_id: Some("test-msg-id-1".to_string()), + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: Some(1), + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + Some(lock_token_1), + ); + + let message_2 = ReceivedMessage::new( + b"test message 2".to_vec(), + HashMap::new(), + SystemProperties { + message_id: Some("test-msg-id-2".to_string()), + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: Some(2), + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + Some(lock_token_2), + ); + + // Test renewing locks for multiple messages + let result_1 = receiver.renew_message_lock(&message_1, None).await; + let result_2 = receiver.renew_message_lock(&message_2, None).await; + + // Both should fail on mock management client but validate different lock tokens + assert!(result_1.is_err()); + assert!(result_2.is_err()); + + // Verify they're different lock tokens being processed + assert_ne!(lock_token_1, lock_token_2); + } + + #[tokio::test] + async fn test_defer_message_with_empty_properties() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let lock_token = Uuid::new_v4(); + let message = ReceivedMessage::new( + b"test".to_vec(), + HashMap::new(), + SystemProperties { + message_id: Some("test-msg-id".to_string()), + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: Some(12345), + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + Some(lock_token), + ); + + // Test with None properties_to_modify + let defer_options = DeferMessageOptions { + properties_to_modify: None, + }; + + let result = receiver.defer_message(&message, Some(defer_options)).await; + + // Should pass validation and fail on mock connection + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!(error_msg.contains("AMQP") || error_msg.contains("Failed")); + } + + #[tokio::test] + async fn test_renew_message_lock_with_different_options() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let lock_token = Uuid::new_v4(); + let message = ReceivedMessage::new( + b"test".to_vec(), + HashMap::new(), + SystemProperties { + message_id: Some("test-msg-id".to_string()), + correlation_id: None, + session_id: None, + content_type: None, + reply_to: None, + reply_to_session_id: None, + subject: None, + enqueued_time_utc: None, + sequence_number: Some(12345), + delivery_count: None, + time_to_live: None, + dead_letter_source: None, + dead_letter_reason: None, + dead_letter_error_description: None, + }, + Some(lock_token), + ); + + // Test with default options (currently not used but validates API) + let renew_options = RenewMessageLockOptions::default(); + + let result = receiver + .renew_message_lock(&message, Some(renew_options)) + .await; + + // Should pass validation and fail on management operation + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!(error_msg.contains("AMQP") || error_msg.contains("Failed")); + } + + #[tokio::test] + async fn test_receive_deferred_messages_basic() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let sequence_numbers = vec![12345, 67890]; + let options = ReceiveDeferredMessagesOptions::default(); + let result = receiver + .receive_deferred_messages(&sequence_numbers, Some(options)) + .await; + + // Should fail since we don't have a real connection, but validates the API + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!( + error_msg.contains("AMQP") + || error_msg.contains("Failed") + || error_msg.contains("management") + ); + } + + #[tokio::test] + async fn test_receive_deferred_messages_empty_sequence_numbers() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let sequence_numbers = vec![]; + let options = ReceiveDeferredMessagesOptions::default(); + let result = receiver + .receive_deferred_messages(&sequence_numbers, Some(options)) + .await; + + // Should return empty result for empty input + match result { + Ok(messages) => assert!(messages.is_empty()), + Err(_) => { + // May fail due to mock connection, but that's acceptable + } + } + } + + #[tokio::test] + async fn test_receive_deferred_messages_single_sequence_number() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let sequence_numbers = vec![42]; + let options = ReceiveDeferredMessagesOptions::default(); + let result = receiver + .receive_deferred_messages(&sequence_numbers, Some(options)) + .await; + + // Should fail since we don't have a real connection, but validates the API + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!( + error_msg.contains("AMQP") + || error_msg.contains("Failed") + || error_msg.contains("management") + ); + } + + #[tokio::test] + async fn test_parse_deferred_messages_response_empty() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + // Test with empty response + let mut response = azure_core_amqp::AmqpOrderedMap::new(); + response.insert("messages".to_string(), AmqpValue::Array(vec![])); + + let result = receiver.parse_deferred_messages_response(response).await; + assert!(result.is_ok()); + let messages = result.unwrap(); + assert!(messages.is_empty()); + } + + #[tokio::test] + async fn test_parse_deferred_messages_response_missing_messages_field() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + // Test with response missing "messages" field + let response = azure_core_amqp::AmqpOrderedMap::new(); + + let result = receiver.parse_deferred_messages_response(response).await; + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!(error_msg.contains("messages")); + } + + #[tokio::test] + async fn test_parse_deferred_messages_response_invalid_format() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + // Test with invalid "messages" field (not an array) + let mut response = azure_core_amqp::AmqpOrderedMap::new(); + response.insert( + "messages".to_string(), + AmqpValue::String("invalid".to_string()), + ); + + let result = receiver.parse_deferred_messages_response(response).await; + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!(error_msg.contains("array")); + } + + #[tokio::test] + async fn test_peek_messages_basic() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let peek_options = PeekMessagesOptions::default(); + + // This would fail in a real test because there's no actual management client + // But we're testing the parameter validation logic + let result = receiver.peek_messages(10, Some(peek_options)).await; + // In a mock environment this will fail at the AMQP level, which is expected + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_peek_messages_zero_count() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let peek_options = PeekMessagesOptions::default(); + + let result = receiver.peek_messages(0, Some(peek_options)).await; + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!(error_msg.contains("Max count must be greater than 0")); + } + + #[tokio::test] + async fn test_peek_messages_with_sequence_number() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + let peek_options = PeekMessagesOptions { + from_sequence_number: Some(12345), + }; + + // This would fail in a real test because there's no actual management client + // But we're testing that the sequence number parameter is correctly handled + let result = receiver.peek_messages(5, Some(peek_options)).await; + // In a mock environment this will fail at the AMQP level, which is expected + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_peek_messages_both_modes_allowed() { + // Test that peek works in both PeekLock and ReceiveAndDelete modes + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + // Test PeekLock mode + let receiver_peek_lock = Receiver::new( + connection.clone(), + entity_name.clone(), + None, + ReceiveMode::PeekLock, + options.clone(), + ) + .await + .unwrap(); + + let peek_options = PeekMessagesOptions::default(); + let result = receiver_peek_lock + .peek_messages(1, Some(peek_options.clone())) + .await; + // Should fail at AMQP level, not validation level + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!(!error_msg.contains("mode")); // Should not contain mode-related errors + + // Test ReceiveAndDelete mode + let receiver_receive_delete = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::ReceiveAndDelete, + options, + ) + .await + .unwrap(); + + let result = receiver_receive_delete + .peek_messages(1, Some(peek_options.clone())) + .await; + // Should fail at AMQP level, not validation level + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!(!error_msg.contains("mode")); // Should not contain mode-related errors + } + + #[tokio::test] + async fn test_parse_peeked_messages_response_missing_messages() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + // Test with response missing "messages" field + let response = azure_core_amqp::AmqpOrderedMap::new(); + + let result = receiver.parse_peeked_messages_response(response).await; + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!(error_msg.contains("messages")); + } + + #[tokio::test] + async fn test_parse_peeked_messages_response_invalid_format() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + // Test with invalid "messages" field (not an array) + let mut response = azure_core_amqp::AmqpOrderedMap::new(); + response.insert( + "messages".to_string(), + AmqpValue::String("invalid".to_string()), + ); + + let result = receiver.parse_peeked_messages_response(response).await; + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!(error_msg.contains("array")); + } + + #[tokio::test] + async fn test_parse_peeked_messages_response_empty_array() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + // Test with empty messages array + let mut response = azure_core_amqp::AmqpOrderedMap::new(); + response.insert("messages".to_string(), AmqpValue::Array(vec![])); + + let result = receiver.parse_peeked_messages_response(response).await; + assert!(result.is_ok()); + let messages = result.unwrap(); + assert_eq!(messages.len(), 0); + } + + #[tokio::test] + async fn test_parse_peeked_messages_response_invalid_message_entry() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let receiver = Receiver::new( + connection, + entity_name, + None, + ReceiveMode::PeekLock, + options, + ) + .await + .unwrap(); + + // Test with invalid message entry (not a map) + let mut response = azure_core_amqp::AmqpOrderedMap::new(); + let messages_array = vec![AmqpValue::String("invalid".to_string())]; + response.insert("messages".to_string(), AmqpValue::Array(messages_array)); + + let result = receiver.parse_peeked_messages_response(response).await; + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!(error_msg.contains("map")); + } + + #[tokio::test] + async fn test_peek_messages_options_default() { + let options = PeekMessagesOptions::default(); + assert_eq!(options.from_sequence_number, None); + } + + #[tokio::test] + async fn test_peek_messages_options_with_sequence_number() { + let options = PeekMessagesOptions { + from_sequence_number: Some(12345), + }; + assert_eq!(options.from_sequence_number, Some(12345)); + } +} diff --git a/sdk/servicebus/azure_messaging_servicebus/src/sender.rs b/sdk/servicebus/azure_messaging_servicebus/src/sender.rs new file mode 100644 index 0000000000..ec5a5f40fe --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/src/sender.rs @@ -0,0 +1,771 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +use crate::{client::ServiceBusClientOptions, ErrorKind, Message, Result, ServiceBusError}; +use azure_core::fmt::SafeDebug; +use azure_core_amqp::{ + AmqpConnection, AmqpMessage, AmqpSender, AmqpSenderApis, AmqpSession, AmqpSessionApis, + AmqpTarget, +}; +use std::sync::Arc; +use tracing::{debug, trace}; + +/// Options for sending a single message. +/// +/// This struct contains optional parameters that can be specified when sending +/// a message using [`Sender::send_message`]. +/// +/// Currently no options are available, but this provides +/// extensibility for future parameters +#[derive(SafeDebug, Clone, Default)] +pub struct SendMessageOptions {} + +/// Options for sending multiple messages. +/// +/// This struct contains optional parameters that can be specified when sending +/// multiple messages using [`Sender::send_messages`]. +/// +/// Currently no options are available, but this provides +/// extensibility for future parameters +#[derive(SafeDebug, Clone, Default)] +pub struct SendMessagesOptions {} + +/// Options for creating a message batch. +/// +/// This struct contains optional parameters that can be specified when creating +/// a message batch using [`Sender::create_message_batch`]. +/// +/// # Examples +/// +/// ```rust,no_run +/// use azure_messaging_servicebus::CreateMessageBatchOptions; +/// +/// // Create with custom maximum size +/// let options = CreateMessageBatchOptions { +/// maximum_size_in_bytes: Some(512 * 1024), // 512 KB +/// }; +/// ``` +#[derive(SafeDebug, Clone, Default)] +pub struct CreateMessageBatchOptions { + /// Maximum size in bytes for the message batch. + /// + /// If `None`, Service Bus will use the maximum message size allowed by the namespace. + pub maximum_size_in_bytes: Option, +} + +/// Options for sending a message batch. +/// +/// This struct contains optional parameters that can be specified when sending +/// a message batch using [`Sender::send_message_batch`]. +/// +/// Currently no options are available, but this provides +/// extensibility for future parameters +#[derive(SafeDebug, Clone, Default)] +pub struct SendMessageBatchOptions {} + +/// Options for scheduling a message. +/// +/// This struct contains optional parameters that can be specified when scheduling +/// a message using [`Sender::schedule_message`]. +/// +/// Currently no options are available, but this provides +/// extensibility for future parameters +#[derive(SafeDebug, Clone, Default)] +pub struct ScheduleMessageOptions {} + +/// Options for scheduling multiple messages. +/// +/// This struct contains optional parameters that can be specified when scheduling +/// multiple messages using [`Sender::schedule_message`]. +/// +/// Currently no options are available, but this provides +/// extensibility for future parameters +#[derive(SafeDebug, Clone, Default)] +pub struct ScheduleMessagesOptions {} + +/// Options for canceling scheduled messages. +/// +/// This struct contains optional parameters that can be specified when canceling +/// scheduled messages using [`Sender::cancel_scheduled_message`]. +/// +/// Currently no options are available, but this provides +/// extensibility for future parameters +#[derive(SafeDebug, Clone, Default)] +pub struct CancelScheduledMessagesOptions {} + +/// A sender for sending messages to a Service Bus queue or topic. +pub struct Sender { + connection: Arc, + entity_name: String, + //options: Option, +} + +impl Sender { + /// Creates a new sender. + pub(crate) async fn new( + connection: Arc, + entity_name: String, + _options: ServiceBusClientOptions, + ) -> Result { + debug!("Creating Sender for entity: {}", entity_name); + + trace!("Sender created successfully for entity: {}", entity_name); + + Ok(Self { + connection, + entity_name, + //options: Some(options), + }) + } + + /// Sends a single message to the Service Bus entity. + /// + /// # Arguments + /// + /// * `message` - The message to send + /// * `options` - Optional parameters for the send operation + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::{Message, SendMessageOptions, ServiceBusClient}; + /// use azure_identity::DeveloperToolsCredential; + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let credential = DeveloperToolsCredential::new(None)?; + /// let client = ServiceBusClient::builder().open("myservicebus.servicebus.windows.net", credential.clone()).await?; + /// let sender = client.create_sender("queue", None).await?; + /// + /// let message = Message::new("Hello, World!".as_bytes()); + /// sender.send_message(message, None).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn send_message( + &self, + message: Message, + _options: Option, + ) -> Result<()> { + debug!("Sending message to entity: {}", self.entity_name); + + // Create AMQP session + let session = AmqpSession::new(); + session.begin(&self.connection, None).await?; + + // Create AMQP sender + let amqp_sender = AmqpSender::new(); + + // Create target with the entity name + let target = AmqpTarget::from(self.entity_name.clone()); + + // Attach the sender to the session + amqp_sender + .attach(&session, "sender-link".to_string(), target, None) + .await?; + + // Convert Message to AmqpMessage + let amqp_message: AmqpMessage = message.into(); + + // Send the message + let _outcome = amqp_sender.send(amqp_message, None).await?; + + // Detach the sender + amqp_sender.detach().await?; + + // End the session + session.end().await?; + + trace!("Message sent successfully to entity: {}", self.entity_name); + Ok(()) + } + + /// Sends multiple messages to the Service Bus entity. + /// + /// # Arguments + /// + /// * `messages` - Vector of messages to send + /// * `options` - Optional parameters for the send operation + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::{Message, SendMessagesOptions, ServiceBusClient}; + /// use azure_identity::DeveloperToolsCredential; + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let credential = DeveloperToolsCredential::new(None)?; + /// let client = ServiceBusClient::builder().open("myservicebus.servicebus.windows.net", credential.clone()).await?; + /// let sender = client.create_sender("queue", None).await?; + /// + /// let messages = vec![ + /// Message::new("Message 1".as_bytes()), + /// Message::new("Message 2".as_bytes()), + /// ]; + /// let options = SendMessagesOptions::default(); + /// sender.send_messages(messages, Some(options)).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn send_messages( + &self, + messages: Vec, + _options: Option, + ) -> Result<()> { + debug!( + "Sending {} messages to entity: {}", + messages.len(), + self.entity_name + ); + + // Create AMQP session + let session = AmqpSession::new(); + session.begin(&self.connection, None).await?; + + // Create AMQP sender + let amqp_sender = AmqpSender::new(); + + // Create target with the entity name + let target = AmqpTarget::from(self.entity_name.clone()); + + // Attach the sender to the session + amqp_sender + .attach(&session, "sender-link".to_string(), target, None) + .await?; + + // Send messages one by one + for message in messages { + let amqp_message: AmqpMessage = message.into(); + let _outcome = amqp_sender.send(amqp_message, None).await?; + } + + // Detach the sender + amqp_sender.detach().await?; + + // End the session + session.end().await?; + + trace!("Messages sent successfully to entity: {}", self.entity_name); + Ok(()) + } + + /// Creates a new message batch with default size limits. + /// + /// The batch allows for efficient sending of multiple messages in a single operation. + /// Messages should be added using `try_add_message()` and then sent using `send_message_batch()`. + /// + /// # Arguments + /// + /// * `options` - Optional parameters for batch creation + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::{ServiceBusClient, Message, CreateMessageBatchOptions}; + /// use azure_identity::DeveloperToolsCredential; + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let credential = DeveloperToolsCredential::new(None)?; + /// let client = ServiceBusClient::builder().open("myservicebus.servicebus.windows.net", credential.clone()).await?; + /// let sender = client.create_sender("queue_name", None).await?; + /// let options = CreateMessageBatchOptions::default(); + /// let mut batch = sender.create_message_batch(Some(options)).await?; + /// + /// let message1 = Message::from_string("Hello"); + /// let message2 = Message::from_string("World"); + /// + /// if batch.try_add_message(message1) && batch.try_add_message(message2) { + /// sender.send_message_batch(batch, None).await?; + /// } + /// # Ok(()) + /// # } + /// ``` + pub async fn create_message_batch( + &self, + options: Option, + ) -> crate::Result { + debug!( + "Creating message batch for entity: {} with max size: {:?}", + self.entity_name, + options.as_ref().and_then(|o| o.maximum_size_in_bytes) + ); + + let maximum_size_in_bytes = options.as_ref().and_then(|o| o.maximum_size_in_bytes); + Ok(crate::MessageBatch::new(maximum_size_in_bytes)) + } + + /// Sends a message batch to the Service Bus entity. + /// + /// This method provides better performance than sending messages individually + /// by reducing network round trips and AMQP overhead. + /// + /// # Arguments + /// + /// * `batch` - The message batch to send + /// * `options` - Optional parameters for the send operation + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::{ServiceBusClient, Message, CreateMessageBatchOptions, SendMessageBatchOptions}; + /// use azure_identity::DeveloperToolsCredential; + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let credential = DeveloperToolsCredential::new(None)?; + /// let client = ServiceBusClient::builder().open("myservicebus.servicebus.windows.net", credential.clone()).await?; + /// let sender = client.create_sender("queue_name", None).await?; + /// let mut batch = sender.create_message_batch(None).await?; + /// + /// // Add multiple messages to the batch + /// let messages = vec![ + /// Message::from_string("Message 1"), + /// Message::from_string("Message 2"), + /// Message::new("Message 3"), + /// ]; + /// + /// let mut current_batch = batch; + /// for message in messages { + /// if !current_batch.try_add_message(message) { + /// // Batch is full, send it and create a new one + /// sender.send_message_batch(current_batch, None).await?; + /// current_batch = sender.create_message_batch(None).await?; + /// // Note: In a real scenario, you'd want to retry adding the message + /// // that didn't fit to the new batch + /// } + /// } + /// + /// // Send the final batch if it has messages + /// if !current_batch.is_empty() { + /// sender.send_message_batch(current_batch, None).await?; + /// } + /// # Ok(()) + /// # } + /// ``` + pub async fn send_message_batch( + &self, + batch: crate::MessageBatch, + _options: Option, + ) -> crate::Result<()> { + let messages = batch.into_messages(); + + if messages.is_empty() { + debug!("Empty batch provided, nothing to send"); + return Ok(()); + } + + debug!( + "Sending message batch with {} messages to entity: {}", + messages.len(), + self.entity_name + ); + + // Create AMQP session + let session = AmqpSession::new(); + session.begin(&self.connection, None).await?; + + // Create AMQP sender + let amqp_sender = AmqpSender::new(); + + // Create target with the entity name + let target = AmqpTarget::from(self.entity_name.clone()); + + // Attach the sender to the session + amqp_sender + .attach(&session, "sender-link".to_string(), target, None) + .await?; + + // Send all messages in the batch + // Note: In a full implementation, we would use AMQP transfer batching, + // but for now we send them sequentially in a single session + for message in messages { + let amqp_message: AmqpMessage = message.into(); + let _outcome = amqp_sender.send(amqp_message, None).await?; + } + + // Detach the sender + amqp_sender.detach().await?; + + // End the session + session.end().await?; + + trace!( + "Message batch sent successfully to entity: {}", + self.entity_name + ); + Ok(()) + } + + /// Schedules a message to be sent at a specific time. + /// + /// # Arguments + /// + /// * `message` - The message to schedule + /// * `scheduled_enqueue_time` - The time when the message should be enqueued + /// * `options` - Optional parameters for the schedule operation + /// + /// # Returns + /// + /// The sequence number assigned to the scheduled message + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::{ServiceBusClient, Message, ScheduleMessageOptions}; + /// use azure_identity::DeveloperToolsCredential; + /// use time::OffsetDateTime; + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let credential = DeveloperToolsCredential::new(None)?; + /// let client = ServiceBusClient::builder().open("myservicebus.servicebus.windows.net", credential.clone()).await?; + /// let sender = client.create_sender("queue_name", None).await?; + /// let message = Message::from_string("Hello, World!"); + /// let schedule_time = OffsetDateTime::now_utc() + time::Duration::minutes(30); + /// + /// let sequence_number = sender.schedule_message(message, schedule_time, None).await?; + /// println!("Message scheduled with sequence number: {}", sequence_number); + /// # Ok(()) + /// # } + /// ``` + pub async fn schedule_message( + &self, + mut message: Message, + scheduled_enqueue_time: time::OffsetDateTime, + _options: Option, + ) -> Result { + debug!( + "Scheduling message for entity: {} at time: {}", + self.entity_name, scheduled_enqueue_time + ); + + // Set the scheduled enqueue time on the message + message.set_scheduled_enqueue_time(scheduled_enqueue_time); + + // Create AMQP session + let session = AmqpSession::new(); + session.begin(&self.connection, None).await?; + + // Create AMQP sender + let amqp_sender = AmqpSender::new(); + + // Create target with the entity name + let target = AmqpTarget::from(self.entity_name.clone()); + + // Attach the sender to the session + amqp_sender + .attach(&session, "sender-link".to_string(), target, None) + .await?; + + // Convert Message to AmqpMessage + let amqp_message: AmqpMessage = message.into(); + + // Send the message (the scheduling is handled by the message properties) + let _outcome = amqp_sender.send(amqp_message, None).await?; + + // Detach the sender + amqp_sender.detach().await?; + + // End the session + session.end().await?; + + // TODO: In a real implementation, we would need to use AMQP management operations + // to get the actual sequence number from the broker. For now, return a placeholder. + let sequence_number = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as i64; // Use timestamp as dummy sequence number + + trace!( + "Message scheduled successfully for entity: {} with sequence number: {}", + self.entity_name, + sequence_number + ); + + Ok(sequence_number) + } + + /// Cancels a scheduled message using its sequence number. + /// + /// # Arguments + /// + /// * `sequence_number` - The sequence number of the scheduled message to cancel + /// * `options` - Optional parameters for the cancel operation + /// + /// # Examples + /// + /// ```rust,no_run + /// use azure_messaging_servicebus::{ServiceBusClient, Message, ScheduleMessageOptions, CancelScheduledMessagesOptions}; + /// use azure_identity::DeveloperToolsCredential; + /// use time::OffsetDateTime; + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let credential = DeveloperToolsCredential::new(None)?; + /// let client = ServiceBusClient::builder().open("myservicebus.servicebus.windows.net", credential.clone()).await?; + /// let sender = client.create_sender("queue_name", None).await?; + /// let message = Message::from_string("Hello, World!"); + /// let schedule_time = OffsetDateTime::now_utc() + time::Duration::minutes(30); + /// + /// // Schedule a message + /// let sequence_number = sender.schedule_message(message, schedule_time, None).await?; + /// + /// // Cancel the scheduled message + /// sender.cancel_scheduled_message(sequence_number, None).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn cancel_scheduled_message( + &self, + sequence_number: i64, + _options: Option, + ) -> Result<()> { + debug!( + "Canceling scheduled message for entity: {} with sequence number: {}", + self.entity_name, sequence_number + ); + + // TODO: Implement actual message cancellation using AMQP management operations + // This would typically involve sending a management request to the broker + // to cancel the scheduled message by its sequence number. + // For now, this is a no-op since we don't have the management client implemented. + + trace!( + "Scheduled message canceled successfully for entity: {} with sequence number: {}", + self.entity_name, + sequence_number + ); + + Err(ServiceBusError::new( + ErrorKind::Unknown, + "NOT_IMPLEMENTED - CancelScheduledMessage", + )) + } + + /// Gets the name of the Service Bus entity (queue or topic). + pub fn entity_name(&self) -> &str { + &self.entity_name + } + + /// Closes the sender. + pub async fn close(&self) -> Result<()> { + debug!("Closing Sender for entity: {}", self.entity_name); + + // Sender creates sessions and senders on-demand for each send operation, + // so there are no persistent AMQP resources to clean up here. + // The connection will handle closing any outstanding sessions. + + trace!( + "Sender closed successfully for entity: {}", + self.entity_name + ); + Ok(()) + } +} + +impl Drop for Sender { + fn drop(&mut self) { + trace!("Sender for entity '{}' is being dropped", self.entity_name); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Message; + use azure_core_amqp::AmqpConnection; + use std::sync::Arc; + + /// Creates a mock connection for testing + fn create_test_connection() -> Arc { + Arc::new(AmqpConnection::new()) + } + + /// Creates a test ClientOptions + fn create_test_options() -> ServiceBusClientOptions { + Default::default() + } + + #[tokio::test] + async fn test_sender_creation() -> Result<()> { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + let options = create_test_options(); + + let sender = Sender::new(connection, entity_name.clone(), options).await?; + + assert_eq!(sender.entity_name(), &entity_name); + Ok(()) + } + + #[test] + fn test_sender_entity_name() { + let connection = create_test_connection(); + let entity_name = "test-topic".to_string(); + + let sender = Sender { + connection, + entity_name: entity_name.clone(), + }; + + assert_eq!(sender.entity_name(), &entity_name); + } + + #[tokio::test] + async fn test_sender_close() -> Result<()> { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + + let options = create_test_options(); + + let sender = Sender::new(connection, entity_name, options).await?; + + // Should not fail + sender.close().await?; + Ok(()) + } + + #[test] + fn test_sender_drop() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + + let sender = Sender { + connection, + entity_name, + }; + + // Should not panic when dropped + drop(sender); + } + + #[test] + fn test_message_creation() { + let message = Message::new("Hello, World!".as_bytes()); + assert_eq!(message.body(), "Hello, World!".as_bytes()); + + let string_message = Message::from_string("Hello, String!"); + assert_eq!(string_message.body_as_string().unwrap(), "Hello, String!"); + } + + #[test] + fn test_message_properties() { + let mut message = Message::new("test".as_bytes()); + + message.set_message_id("msg-123"); + message.set_correlation_id("corr-456"); + message.set_subject("Test Subject"); + message.set_content_type("text/plain"); + + assert_eq!(message.message_id(), Some(&"msg-123".to_string())); + assert_eq!(message.correlation_id(), Some(&"corr-456".to_string())); + assert_eq!(message.subject(), Some(&"Test Subject".to_string())); + assert_eq!(message.content_type(), Some(&"text/plain".to_string())); + } + + #[test] + fn test_message_custom_properties() { + let mut message = Message::new("test".as_bytes()); + + message.set_property("custom_key", "custom_value"); + message.set_property("number", "42"); + + assert_eq!( + message.property("custom_key"), + Some(&"custom_value".to_string()) + ); + assert_eq!(message.property("number"), Some(&"42".to_string())); + assert_eq!(message.property("non_existent"), None); + + assert_eq!(message.properties().len(), 2); + } + + #[tokio::test] + async fn test_create_message_batch() { + let connection = create_test_connection(); + let entity_name = "test-queue".to_string(); + + let sender = Sender { + connection, + entity_name, + }; + + let batch_options = CreateMessageBatchOptions { + maximum_size_in_bytes: Some(1024), + }; + let batch = sender + .create_message_batch(Some(batch_options)) + .await + .unwrap(); + + assert!(batch.is_empty()); + assert_eq!(batch.maximum_size_in_bytes(), 1024); + } + + #[test] + fn test_batch_try_add_message() { + let mut batch = crate::MessageBatch::new(Some(1024)); + let message = Message::new("Hello, World!"); + + let result = batch.try_add_message(message); + assert!(result); + assert_eq!(batch.count(), 1); + assert!(!batch.is_empty()); + } + + #[test] + fn test_batch_try_add_message_exceeds_limit() { + let mut batch = crate::MessageBatch::new(Some(50)); // Very small limit + let large_message = Message::new("x".repeat(1000)); // Large message + + let result = batch.try_add_message(large_message); + assert!(!result); + assert_eq!(batch.count(), 0); + assert!(batch.is_empty()); + } + + #[test] + fn test_batch_multiple_messages() { + let mut batch = crate::MessageBatch::new(None); + + for i in 0..5 { + let message = Message::new(format!("Message {}", i)); + assert!(batch.try_add_message(message)); + } + + assert_eq!(batch.count(), 5); + assert!(!batch.is_empty()); + + let messages = batch.into_messages(); + assert_eq!(messages.len(), 5); + + for (i, message) in messages.iter().enumerate() { + assert_eq!(message.body_as_string().unwrap(), format!("Message {}", i)); + } + } + + #[test] + fn test_batch_with_message_properties() { + let mut batch = crate::MessageBatch::new(None); + + let mut message = Message::new("Test message with properties"); + message.set_message_id("test-id-123"); + message.set_correlation_id("correlation-456"); + message.set_content_type("text/plain"); + message.set_property("custom-prop", "custom-value"); + + assert!(batch.try_add_message(message)); + assert_eq!(batch.count(), 1); + + // Verify size estimation includes property overhead + assert!(batch.size_in_bytes() > "Test message with properties".len()); + } + + #[test] + fn test_create_batch_options() { + let options = CreateMessageBatchOptions::default(); + assert!(options.maximum_size_in_bytes.is_none()); + + let options_with_size = CreateMessageBatchOptions { + maximum_size_in_bytes: Some(2048), + }; + + assert_eq!(options_with_size.maximum_size_in_bytes, Some(2048)); + } +} diff --git a/sdk/servicebus/azure_messaging_servicebus/test-resources-post.ps1 b/sdk/servicebus/azure_messaging_servicebus/test-resources-post.ps1 new file mode 100644 index 0000000000..9ac5ebdbe6 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/test-resources-post.ps1 @@ -0,0 +1,93 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# IMPORTANT: Do not invoke this file directly. Please instead run eng/common/TestResources/New-TestResources.ps1 from the repository root. + +param ( + [hashtable] $AdditionalParameters = @{}, + [hashtable] $DeploymentOutputs, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $SubscriptionId, + + [Parameter(ParameterSetName = 'Provisioner', Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $TenantId, + + [Parameter()] + [ValidatePattern('^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$')] + [string] $TestApplicationId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $Environment, + + # Captures any arguments from eng/New-TestResources.ps1 not declared here (no parameter errors). + [Parameter(ValueFromRemainingArguments = $true)] + $RemainingArguments +) + +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +if ($CI) { + if (!$AdditionalParameters['deployResources']) { + Write-Host "Skipping post-provisioning script because resources weren't deployed" + return + } + az cloud set -n $Environment + az login --federated-token $env:ARM_OIDC_TOKEN --service-principal -t $TenantId -u $TestApplicationId + az account set --subscription $SubscriptionId +} + +Write-Host "##[group]Service Bus Post-Deployment Setup" + +# Extract deployment outputs +$namespaceName = $DeploymentOutputs['SERVICEBUS_NAMESPACE_NAME'] +$resourceGroup = $DeploymentOutputs['RESOURCE_GROUP'] + +Write-Host "Service Bus Namespace: $namespaceName" +Write-Host "Resource Group: $resourceGroup" + +# Retrieve connection strings (these contain secrets so aren't in Bicep outputs) +Write-Host "Retrieving Service Bus connection strings..." + +try { + $connectionString = az servicebus namespace authorization-rule keys list ` + --resource-group $resourceGroup ` + --namespace-name $namespaceName ` + --name RootManageSharedAccessKey ` + --query primaryConnectionString ` + --output tsv + + $listenOnlyConnectionString = az servicebus namespace authorization-rule keys list ` + --resource-group $resourceGroup ` + --namespace-name $namespaceName ` + --name ListenOnly ` + --query primaryConnectionString ` + --output tsv + + $sendOnlyConnectionString = az servicebus namespace authorization-rule keys list ` + --resource-group $resourceGroup ` + --namespace-name $namespaceName ` + --name SendOnly ` + --query primaryConnectionString ` + --output tsv + + Write-Host "āœ… Connection strings retrieved successfully" + + # Set additional outputs for the test pipeline + if ($CI) { + Write-Host "##vso[task.setvariable variable=SERVICEBUS_CONNECTION_STRING;issecret=true]$connectionString" + Write-Host "##vso[task.setvariable variable=SERVICEBUS_LISTEN_ONLY_CONNECTION_STRING;issecret=true]$listenOnlyConnectionString" + Write-Host "##vso[task.setvariable variable=SERVICEBUS_SEND_ONLY_CONNECTION_STRING;issecret=true]$sendOnlyConnectionString" + } +} +catch { + Write-Warning "Failed to retrieve connection strings: $($_.Exception.Message)" +} + +Write-Host "##[endgroup]" + +Write-Host "Service Bus post-deployment setup completed successfully." diff --git a/sdk/servicebus/azure_messaging_servicebus/test-resources-pre.ps1 b/sdk/servicebus/azure_messaging_servicebus/test-resources-pre.ps1 new file mode 100644 index 0000000000..de350247bf --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/test-resources-pre.ps1 @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# IMPORTANT: Do not invoke this file directly. Please instead run eng/common/TestResources/New-TestResources.ps1 from the repository root. + +[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] +param ( + [hashtable] $AdditionalParameters = @{}, + + # Captures any arguments from eng/New-TestResources.ps1 not declared here (no parameter errors). + [Parameter(ValueFromRemainingArguments = $true)] + $RemainingArguments +) + +# This script currently performs no pre-deployment steps. +# Future pre-deployment logic for Service Bus resources can be added here. + +Write-Host "Service Bus pre-deployment: No additional setup required." diff --git a/sdk/servicebus/azure_messaging_servicebus/test-resources.bicep b/sdk/servicebus/azure_messaging_servicebus/test-resources.bicep new file mode 100644 index 0000000000..bd721f68c9 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/test-resources.bicep @@ -0,0 +1,200 @@ +@description('Base name of the resource to be created.') +@minLength(16) +param baseName string = resourceGroup().name + +@description('The location where the resources will be created.') +param location string = resourceGroup().location + +@description('Indicates if the tenant is a TME tenant. If true, local (SAS) authentication is enabled.') +param tenantIsTME bool = false + +@description('The client OID to grant access to test resources.') +param testApplicationOid string + +var serviceBusNamespaceName = 'sb-${baseName}' +var queueName = 'testqueue' +var topicName = 'testtopic' +var subscriptionName = 'testsubscription' + +var serviceBusDataOwnerRoleId = '090c5cfd-751d-490a-894a-3ce6f1109419' +var serviceBusDataReceiverRoleId = '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0' +var serviceBusDataSenderRoleId = '69a216fc-b8fb-44d8-bc22-1f3c2cd27a39' +var azureContributorRoleId = 'b24988ac-6180-42a0-ab88-20f7382dd24c' + +resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2024-01-01' = { + name: serviceBusNamespaceName + location: location + sku: { + name: 'Standard' + tier: 'Standard' + } + properties: { + minimumTlsVersion: '1.2' + publicNetworkAccess: 'Enabled' + disableLocalAuth: !tenantIsTME // Disable local auth for non-TME tenants + zoneRedundant: false + } + + resource rootSharedAccessKey 'AuthorizationRules@2022-10-01-preview' = { + name: 'RootManageSharedAccessKey' + properties: { + rights: [ + 'Listen' + 'Manage' + 'Send' + ] + } + } + + resource listenOnlyKey 'AuthorizationRules@2022-10-01-preview' = { + name: 'ListenOnly' + properties: { + rights: [ + 'Listen' + ] + } + } + + resource sendOnlyKey 'AuthorizationRules@2022-10-01-preview' = { + name: 'SendOnly' + properties: { + rights: [ + 'Send' + ] + } + } + + resource queue 'queues@2022-10-01-preview' = { + name: queueName + properties: { + lockDuration: 'PT30S' + maxSizeInMegabytes: 1024 + requiresDuplicateDetection: false + requiresSession: false + defaultMessageTimeToLive: 'P14D' + deadLetteringOnMessageExpiration: false + duplicateDetectionHistoryTimeWindow: 'PT10M' + maxDeliveryCount: 10 + autoDeleteOnIdle: 'P10675199DT2H48M5.4775807S' + enablePartitioning: false + enableExpress: false + status: 'Active' + } + } + + resource topic 'topics@2022-10-01-preview' = { + name: topicName + properties: { + maxSizeInMegabytes: 1024 + requiresDuplicateDetection: false + defaultMessageTimeToLive: 'P14D' + duplicateDetectionHistoryTimeWindow: 'PT10M' + enableBatchedOperations: true + enablePartitioning: false + enableExpress: false + status: 'Active' + autoDeleteOnIdle: 'P10675199DT2H48M5.4775807S' + supportOrdering: true + } + + resource subscription 'subscriptions@2022-10-01-preview' = { + name: subscriptionName + properties: { + lockDuration: 'PT30S' + requiresSession: false + defaultMessageTimeToLive: 'P14D' + deadLetteringOnMessageExpiration: false + deadLetteringOnFilterEvaluationExceptions: true + maxDeliveryCount: 10 + enableBatchedOperations: true + autoDeleteOnIdle: 'P10675199DT2H48M5.4775807S' + status: 'Active' + } + } + } + + resource networkRuleSet 'networkrulesets@2022-10-01-preview' = { + name: 'default' + properties: { + publicNetworkAccess: 'Enabled' + defaultAction: 'Allow' + virtualNetworkRules: [] + ipRules: [] + trustedServiceAccessEnabled: false + } + } +} + +// Role assignment for Service Bus Data Owner +resource roleAssignments_sbDataOwner 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(serviceBusNamespace.id, 'Azure Service Bus Data Owner') + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', serviceBusDataOwnerRoleId) // Azure Service Bus Data Owner + principalId: testApplicationOid + } + dependsOn: [ + serviceBusNamespace::queue + serviceBusNamespace::topic + ] +} + +// Role assignment for Contributor (for management operations) +resource roleAssignments_contributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(serviceBusNamespace.id, 'Azure Contributor') + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', azureContributorRoleId) // Azure Contributor + principalId: testApplicationOid + } + dependsOn: [ + serviceBusNamespace::queue + serviceBusNamespace::topic + ] +} + +// Role assignment for Service Bus Data Receiver +resource roleAssignments_sbDataReceiver 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(serviceBusNamespace.id, 'Azure Service Bus Data Receiver') + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', serviceBusDataReceiverRoleId) // Azure Service Bus Data Receiver + principalId: testApplicationOid + } + dependsOn: [ + serviceBusNamespace::queue + serviceBusNamespace::topic + ] +} + +// Role assignment for Service Bus Data Sender +resource roleAssignments_sbDataSender 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(serviceBusNamespace.id, 'Azure Service Bus Data Sender') + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', serviceBusDataSenderRoleId) // Azure Service Bus Data Sender + principalId: testApplicationOid + } + dependsOn: [ + serviceBusNamespace::queue + serviceBusNamespace::topic + ] +} + +// Outputs for test environment variables +output SERVICEBUS_NAMESPACE string = replace( + replace(serviceBusNamespace.properties.serviceBusEndpoint, ':443/', ''), + 'https://', + '' +) + +output SERVICEBUS_NAMESPACE_NAME string = serviceBusNamespace.name + +output SERVICEBUS_QUEUE_NAME string = serviceBusNamespace::queue.name + +output SERVICEBUS_TOPIC_NAME string = serviceBusNamespace::topic.name + +output SERVICEBUS_SUBSCRIPTION_NAME string = serviceBusNamespace::topic::subscription.name + +// Connection strings contain secrets and should be retrieved via Azure CLI or portal +// output SERVICEBUS_CONNECTION_STRING string = serviceBusNamespace::rootSharedAccessKey.listKeys().primaryConnectionString +// output SERVICEBUS_LISTEN_ONLY_CONNECTION_STRING string = serviceBusNamespace::listenOnlyKey.listKeys().primaryConnectionString +// output SERVICEBUS_SEND_ONLY_CONNECTION_STRING string = serviceBusNamespace::sendOnlyKey.listKeys().primaryConnectionString + +output RESOURCE_GROUP string = resourceGroup().name diff --git a/sdk/servicebus/azure_messaging_servicebus/tests/README.md b/sdk/servicebus/azure_messaging_servicebus/tests/README.md new file mode 100644 index 0000000000..915e795c7b --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/tests/README.md @@ -0,0 +1,229 @@ +# Azure Service Bus Live Tests + +This directory contains live integration tests for the Azure Service Bus SDK for Rust. These tests validate the SDK functionality against a real Azure Service Bus namespace. + +## Prerequisites + +1. **Azure Service Bus Namespace**: You need an active Azure Service Bus namespace with at least one queue configured. + +2. **Authentication**: You can use either connection string authentication or TokenCredential-based authentication. + +3. **Test Resources**: The tests require specific Azure Service Bus entities to be created beforehand. + +## Required Azure Resources + +### For Connection String Tests (`servicebus_live_tests.rs`) + +Create the following resources in your Service Bus namespace: + +- **Queue**: A standard queue for basic send/receive operations +- **Topic**: A topic for publish/subscribe scenarios (optional) +- **Subscription**: A subscription on the topic (optional, only needed for topic tests) + +### For TokenCredential Tests (`servicebus_token_credential_tests.rs`) + +- **Queue**: Same queue as above +- **Azure AD App Registration** (for ClientSecretCredential tests): Optional, but recommended for comprehensive testing + +## Environment Variables + +### Connection String Authentication + +Set these environment variables for connection string-based tests: + +```bash +# Required for all connection string tests +export SERVICEBUS_CONNECTION_STRING="Endpoint=sb://your-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=your-key" +export SERVICEBUS_QUEUE_NAME="test-queue" + +# Optional for topic/subscription tests +export SERVICEBUS_TOPIC_NAME="test-topic" +export SERVICEBUS_SUBSCRIPTION_NAME="test-subscription" +``` + +### TokenCredential Authentication + +Set these environment variables for TokenCredential-based tests: + +```bash +# Required for all TokenCredential tests +export SERVICEBUS_NAMESPACE="your-namespace.servicebus.windows.net" +export SERVICEBUS_QUEUE_NAME="test-queue" + +# For DeveloperToolsCredential (one of these approaches): + +# Option 1: Service Principal with Client Secret +export AZURE_TENANT_ID="your-tenant-id" +export AZURE_CLIENT_ID="your-client-id" +export AZURE_CLIENT_SECRET="your-client-secret" + +# Option 2: Use Azure CLI (run 'az login' first) +# No additional environment variables needed + +# Option 3: Use Managed Identity (when running on Azure) +# No additional environment variables needed +``` + +## Required Permissions + +For TokenCredential authentication, ensure your identity has the following RBAC roles: + +- **Azure Service Bus Data Owner** or **Azure Service Bus Data Sender** and **Azure Service Bus Data Receiver** +- Scope: Service Bus namespace or specific queue/topic + +## Running the Tests + +### All Tests (Unit + Live) + +```bash +cargo test +``` + +### Only Live Tests + +```bash +# Connection string tests +cargo test servicebus_live_tests + +# TokenCredential tests +cargo test servicebus_token_credential_tests + +# All live tests +cargo test --test '*' +``` + +### Individual Test Cases + +```bash +# Test basic send/receive +cargo test test_send_receive_queue_message + +# Test batch operations +cargo test test_send_multiple_messages + +# Test message properties +cargo test test_message_properties + +# Test PeekLock operations +cargo test test_peek_lock_operations + +# Test TokenCredential authentication +cargo test test_default_azure_credential +``` + +### Running with Logging + +```bash +RUST_LOG=debug cargo test test_send_receive_queue_message +``` + +## Test Coverage + +### Connection String Tests (`servicebus_live_tests.rs`) + +- **Basic Send/Receive**: Send a message to a queue and receive it back +- **Batch Operations**: Send multiple messages and receive them +- **Message Properties**: Test standard and custom message properties +- **PeekLock Mode**: Test complete and abandon operations +- **ReceiveAndDelete Mode**: Test automatic message deletion +- **Topic/Subscription**: Test publish/subscribe messaging (optional) +- **Scheduled Messages**: Test delayed message delivery + +### TokenCredential Tests (`servicebus_token_credential_tests.rs`) + +- **DeveloperToolsCredential**: Test with default credential chain +- **ClientSecretCredential**: Test with service principal authentication +- **AzureCliCredential**: Test with Azure CLI credentials +- **Batch Operations**: Test multiple messages with TokenCredential +- **Message Properties**: Test comprehensive property handling + +## Test Environment Setup + +### Azure Portal Setup + +1. **Create Service Bus Namespace**: + - Go to Azure Portal → Create a resource → Service Bus + - Choose pricing tier (Standard or Premium for topics) + - Create the namespace + +2. **Create Queue**: + - In your Service Bus namespace → Queues → Add + - Name: Use the value you set in `SERVICEBUS_QUEUE_NAME` + - Configure as needed (defaults are fine for testing) + +3. **Create Topic and Subscription** (Optional): + - In your Service Bus namespace → Topics → Add + - Name: Use the value you set in `SERVICEBUS_TOPIC_NAME` + - Create a subscription under the topic + - Name: Use the value you set in `SERVICEBUS_SUBSCRIPTION_NAME` + +4. **Get Connection String**: + - In your Service Bus namespace → Shared access policies + - Select "RootManageSharedAccessKey" + - Copy the "Primary Connection String" + +### Azure AD Setup (for TokenCredential) + +1. **Create App Registration**: + - Go to Azure AD → App registrations → New registration + - Note down Application (client) ID and Directory (tenant) ID + +2. **Create Client Secret**: + - In your app registration → Certificates & secrets + - Create a new client secret and copy the value + +3. **Assign Permissions**: + - Go to your Service Bus namespace → Access control (IAM) + - Add role assignment → Azure Service Bus Data Owner + - Assign to your app registration + +## Common Issues + +### Authentication Errors + +- **Connection String**: Verify the connection string is correct and has proper permissions +- **TokenCredential**: Ensure proper RBAC roles are assigned +- **Azure CLI**: Run `az login` and verify you're logged into the correct tenant + +### Missing Resources + +- **Queue Not Found**: Verify the queue exists and the name matches `SERVICEBUS_QUEUE_NAME` +- **Topic/Subscription**: These tests are optional and will be skipped if not configured + +### Network Issues + +- **Firewall**: Ensure your Service Bus namespace allows connections from your IP +- **Corporate Networks**: Some corporate networks may block AMQP connections + +## Debugging + +Enable verbose logging to troubleshoot issues: + +```bash +RUST_LOG=azure_messaging_servicebus=debug,azure_core_amqp=debug cargo test +``` + +For even more detailed AMQP protocol debugging: + +```bash +RUST_LOG=trace cargo test test_send_receive_queue_message +``` + +## Performance Considerations + +Live tests create real network connections and send actual messages, so: + +- Tests may take 10-30 seconds to complete +- Consider running against a dedicated test namespace +- Clean up test messages if needed (tests attempt to complete/delete sent messages) +- Some tests include delays for scheduled message testing + +## Contributing + +When adding new live tests: + +1. Follow the existing naming convention: `test_` +2. Use the `#[recorded::test(live)]` attribute for live tests +3. Clean up resources (complete/delete messages) in tests +4. Add appropriate assertions to validate functionality +5. Document any new environment variables or setup requirements diff --git a/sdk/servicebus/azure_messaging_servicebus/tests/common/mod.rs b/sdk/servicebus/azure_messaging_servicebus/tests/common/mod.rs new file mode 100644 index 0000000000..4820ba9c87 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/tests/common/mod.rs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +use std::{env, error::Error}; + +#[allow(dead_code)] +static INIT_LOGGING: std::sync::Once = std::sync::Once::new(); + +#[allow(dead_code)] +pub fn setup() { + INIT_LOGGING.call_once(|| { + println!("Setting up test logger..."); + + use tracing_subscriber::{fmt::format::FmtSpan, EnvFilter}; + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE) + .with_ansi(std::env::var("NO_COLOR").map_or(true, |v| v.is_empty())) + .with_writer(std::io::stderr) + .init(); + }); +} + +#[allow(dead_code)] +pub fn get_connection_string() -> Result> { + env::var("SERVICEBUS_CONNECTION_STRING") + .map_err(|_| "SERVICEBUS_CONNECTION_STRING environment variable not set".into()) +} + +#[allow(dead_code)] +pub fn get_queue_name() -> Result> { + env::var("SERVICEBUS_QUEUE_NAME") + .map_err(|_| "SERVICEBUS_QUEUE_NAME environment variable not set".into()) +} + +#[allow(dead_code)] +pub fn get_servicebus_namespace() -> Result> { + env::var("SERVICEBUS_NAMESPACE") + .map_err(|_| "SERVICEBUS_NAMESPACE environment variable not set".into()) +} + +#[allow(dead_code)] +pub fn get_topic_name() -> Result> { + env::var("SERVICEBUS_TOPIC_NAME") + .map_err(|_| "SERVICEBUS_TOPIC_NAME environment variable not set".into()) +} + +#[allow(dead_code)] +pub fn get_subscription_name() -> Result> { + env::var("SERVICEBUS_SUBSCRIPTION_NAME") + .map_err(|_| "SERVICEBUS_SUBSCRIPTION_NAME environment variable not set".into()) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_authentication.rs b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_authentication.rs new file mode 100644 index 0000000000..29ed282abe --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_authentication.rs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! Tests for Service Bus authentication methods. + +mod common; +use azure_core::Uuid; +use azure_core_test::{recorded, TestContext}; +use azure_messaging_servicebus::{ + CompleteMessageOptions, CreateReceiverOptions, CreateSenderOptions, Message, + ReceiveMessageOptions, ReceiveMode, ServiceBusClient, +}; +use common::{get_queue_name, get_servicebus_namespace}; +use std::{env, error::Error}; + +#[recorded::test(live)] +async fn test_token_credential_message_properties(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing message properties with TokenCredential"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Create message with comprehensive properties + let message_id = Uuid::new_v4().to_string(); + let correlation_id = Uuid::new_v4().to_string(); + + let mut message = Message::from_string("TokenCredential properties test"); + message.set_message_id(&message_id); + message.set_correlation_id(&correlation_id); + message.set_content_type("application/json"); + message.set_subject("TokenCredential Test"); + message.set_reply_to("reply-queue"); + + // Add custom properties + message.set_property("credential_type", "DeveloperToolsCredential"); + message.set_property("test_name", "test_token_credential_message_properties"); + message.set_property("environment", "live_test"); + message.set_property("number_value", "123"); + message.set_property("boolean_value", "true"); + + // Send message + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + sender.send_message(message, None).await?; + + // Receive and validate message + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let messages = receiver + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + assert!(!messages.is_empty(), "Should receive the message"); + + let received_message = &messages[0]; + + // Validate standard properties + assert_eq!(received_message.message_id(), Some(message_id).as_ref()); + assert_eq!( + received_message.correlation_id(), + Some(correlation_id).as_ref() + ); + assert_eq!( + received_message.system_properties().content_type.as_ref(), + Some("application/json".to_string()).as_ref() + ); + assert_eq!( + received_message.system_properties().subject.as_ref(), + Some("TokenCredential Test".to_string()).as_ref() + ); + assert_eq!( + received_message.system_properties().reply_to.as_ref(), + Some("reply-queue".to_string()).as_ref() + ); + + // Validate custom properties + assert_eq!( + received_message.property("credential_type"), + Some("DeveloperToolsCredential".to_string()).as_ref() + ); + assert_eq!( + received_message.property("test_name"), + Some("test_token_credential_message_properties".to_string()).as_ref() + ); + assert_eq!( + received_message.property("environment"), + Some("live_test".to_string()).as_ref() + ); + assert_eq!( + received_message.property("number_value"), + Some("123".to_string()).as_ref() + ); + assert_eq!( + received_message.property("boolean_value"), + Some("true".to_string()).as_ref() + ); + + println!("All message properties validated successfully"); + + // Complete message + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + + // Clean up + receiver.close().await?; + sender.close().await?; + client.close().await?; + + println!("TokenCredential message properties test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_client_with_token_credential(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing client creation with DeveloperToolsCredential for namespace: {}", + namespace + ); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Test basic client operations + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let receiver = client + .create_receiver(&queue_name, Some(CreateReceiverOptions::default())) + .await?; + + // Clean up + receiver.close().await?; + sender.close().await?; + client.close().await?; + + println!("Client TokenCredential test completed successfully"); + Ok(()) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_batching.rs b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_batching.rs new file mode 100644 index 0000000000..569328d3b4 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_batching.rs @@ -0,0 +1,1023 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! Tests for Service Bus authentication methods. + +mod common; + +use azure_core::time::Duration; +use azure_core_test::{recorded, TestContext}; +use azure_messaging_servicebus::{ + CompleteMessageOptions, CreateMessageBatchOptions, CreateReceiverOptions, CreateSenderOptions, + Message, ReceiveMessageOptions, ReceiveMode, SendMessagesOptions, ServiceBusClient, +}; +use common::{get_queue_name, get_servicebus_namespace}; +use std::{env, error::Error}; +use uuid::Uuid; + +#[recorded::test(live)] +async fn test_token_credential_batch_operations(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing batch operations with TokenCredential"); + + // Use recording credential for this test + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Send multiple messages + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let message_count = 3; + let mut messages = Vec::new(); + + for i in 0..message_count { + let message_id = format!("token-batch-{}-{}", Uuid::new_v4(), i); + let mut message = Message::from_string(format!("TokenCredential batch message {}", i)); + message.set_message_id(&message_id); + message.set_property("credential_type", "DeveloperToolsCredential"); + message.set_property("test_name", "test_token_credential_batch_operations"); + message.set_property("batch_index", i.to_string()); + messages.push(message); + } + + let send_options = SendMessagesOptions::default(); + sender.send_messages(messages, Some(send_options)).await?; + println!( + "Sent {} messages successfully with TokenCredential", + message_count + ); + + // Receive all messages + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let received_messages = receiver + .receive_messages(message_count, Some(ReceiveMessageOptions::default())) + .await?; + + assert_eq!( + received_messages.len(), + message_count, + "Should receive all sent messages" + ); + + // Verify and complete all messages + for message in received_messages.iter() { + if let Some(cred_type) = message.property("credential_type") { + assert_eq!(cred_type, "DeveloperToolsCredential"); + } + + receiver + .complete_message(message, Some(CompleteMessageOptions::default())) + .await?; + } + + println!( + "Received and completed {} messages successfully", + received_messages.len() + ); + + // Clean up + receiver.close().await?; + sender.close().await?; + client.close().await?; + + println!("TokenCredential batch operations test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_message_batch_send_receive(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing message batch send and receive for queue: {}", + queue_name + ); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client.create_sender(&queue_name, None).await?; + + // Create a batch and add multiple messages + let mut batch = sender.create_message_batch(None).await?; + let batch_id = Uuid::new_v4().to_string(); + let mut expected_messages = Vec::new(); + + println!("Creating batch with batch ID: {}", batch_id); + + // Add 5 messages to the batch + for i in 0..5 { + let message_id = format!("batch-{}-msg-{}", batch_id, i); + let message_body = format!("Batch message {} content", i); + + let mut message = Message::from_string(&message_body); + message.set_message_id(&message_id); + message.set_correlation_id(&batch_id); + message.set_property("batch_id", &batch_id); + message.set_property("sequence", i.to_string()); + message.set_property("test_type", "batch_send_receive"); + + if batch.try_add_message(message) { + expected_messages.push((message_id, message_body)); + println!("Added message {} to batch", i); + } else { + panic!("Failed to add message {} to batch", i); + } + } + + assert_eq!(batch.count(), 5); + assert!(!batch.is_empty()); + println!( + "Batch contains {} messages, size: {} bytes", + batch.count(), + batch.size_in_bytes() + ); + + // Send the entire batch + sender.send_message_batch(batch, None).await?; + println!("Batch sent successfully"); + + // Create receiver and receive the messages + let receiver = client + .create_receiver(&queue_name, Some(CreateReceiverOptions::default())) + .await?; + + // Receive messages - may come back in any order + let mut received_messages = Vec::new(); + let mut attempts = 0; + while received_messages.len() < 5 && attempts < 10 { + let messages = receiver + .receive_messages(5, Some(ReceiveMessageOptions::default())) + .await?; + for msg in messages { + if msg.correlation_id() == Some(&batch_id) { + received_messages.push(msg); + } + } + attempts += 1; + if received_messages.len() < 5 { + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + } + } + + assert_eq!( + received_messages.len(), + 5, + "Should receive all 5 batch messages" + ); + println!("Received {} messages from batch", received_messages.len()); + + // Verify all expected messages were received + for (expected_id, expected_body) in expected_messages { + let found = received_messages + .iter() + .find(|msg| msg.message_id() == Some(&expected_id)); + assert!(found.is_some(), "Message with ID {} not found", expected_id); + + let msg = found.unwrap(); + assert_eq!(msg.body_as_string().unwrap(), expected_body); + assert_eq!(msg.correlation_id(), Some(&batch_id)); + assert_eq!(msg.property("batch_id"), Some(&batch_id)); + + // Complete the message + receiver + .complete_message(msg, Some(CompleteMessageOptions::default())) + .await?; + println!("Verified and completed message: {}", expected_id); + } + + // Clean up + receiver.close().await?; + sender.close().await?; + client.close().await?; + + println!("Message batch test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_batch_size_limits(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing batch size limits"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client.create_sender(&queue_name, None).await?; + + // Create a small batch to test size limits + let options = CreateMessageBatchOptions { + maximum_size_in_bytes: Some(2048), + }; + + let mut batch = sender.create_message_batch(Some(options)).await?; + + assert_eq!(batch.maximum_size_in_bytes(), 1024); + println!( + "Created batch with size limit: {} bytes", + batch.maximum_size_in_bytes() + ); + + // Add messages until the batch is full + let mut added_count = 0; + let batch_id = Uuid::new_v4().to_string(); + + for i in 0..100 { + let message_body = format!( + "Size limit test message {} with some content to use space", + i + ); + let mut message = Message::from_string(&message_body); + message.set_message_id(format!("size-test-{}-{}", batch_id, i)); + message.set_correlation_id(&batch_id); + message.set_property("test_type", "size_limits"); + + if batch.try_add_message(message) { + added_count += 1; + println!( + "Added message {} (batch size: {} bytes)", + i, + batch.size_in_bytes() + ); + } else { + println!( + "Batch full after {} messages, current size: {} bytes", + added_count, + batch.size_in_bytes() + ); + break; + } + } + + // Should have added some messages but not all 100 + assert!(added_count > 0, "Should have added at least one message"); + assert!( + added_count < 100, + "Should not have added all 100 messages due to size limit" + ); + + // Send the batch if it's not empty + if !batch.is_empty() { + sender.send_message_batch(batch, None).await?; + println!( + "Size-limited batch sent successfully with {} messages", + added_count + ); + + // Clean up the messages + let receiver = client + .create_receiver(&queue_name, Some(CreateReceiverOptions::default())) + .await?; + let messages = receiver + .receive_messages(added_count as usize, Some(ReceiveMessageOptions::default())) + .await?; + + for msg in messages { + if msg.correlation_id() == Some(&batch_id) { + receiver + .complete_message(&msg, Some(CompleteMessageOptions::default())) + .await?; + } + } + receiver.close().await?; + } + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Batch size limit test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_empty_batch_handling(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing empty batch handling"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + // Create an empty batch + let batch = sender.create_message_batch(None).await?; + assert!(batch.is_empty()); + assert_eq!(batch.count(), 0); + + // Sending an empty batch should succeed but do nothing + sender.send_message_batch(batch, None).await?; + println!("Empty batch handled successfully"); + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Empty batch test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_batch_with_different_options(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing batch creation with different options"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client.create_sender(&queue_name, None).await?; + + // Test default batch options + let mut default_batch = sender.create_message_batch(None).await?; + assert!(default_batch.maximum_size_in_bytes() > 0); + + // Test batch with custom size + let custom_options = CreateMessageBatchOptions { + maximum_size_in_bytes: Some(2048), + }; + + let mut custom_batch = sender.create_message_batch(Some(custom_options)).await?; + assert_eq!(custom_batch.maximum_size_in_bytes(), 2048); + + // Test that we can add messages to both batches + let mut message = Message::from_string("Test message for batch options"); + message.set_message_id(Uuid::new_v4().to_string()); + message.set_property("test_type", "batch_options"); + + // Clone message for both batches + let message_copy = Message::from_string("Test message for batch options"); + let mut message_copy = message_copy; + message_copy.set_message_id(Uuid::new_v4().to_string()); + message_copy.set_property("test_type", "batch_options"); + + assert!(default_batch.try_add_message(message)); + assert!(custom_batch.try_add_message(message_copy)); + + // Send both batches + sender.send_message_batch(default_batch, None).await?; + sender.send_message_batch(custom_batch, None).await?; + + // Clean up the messages + let receiver = client + .create_receiver(&queue_name, Some(CreateReceiverOptions::default())) + .await?; + let messages = receiver + .receive_messages(5, Some(ReceiveMessageOptions::default())) + .await?; + + for msg in messages { + if msg.property("test_type") == Some(&"batch_options".to_string()) { + receiver + .complete_message(&msg, Some(CompleteMessageOptions::default())) + .await?; + } + } + + // Clean up + receiver.close().await?; + sender.close().await?; + client.close().await?; + + println!("Batch options test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_batch_overflow_handling(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing batch overflow handling"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + // Create a very small batch to force overflow + let options = CreateMessageBatchOptions { + maximum_size_in_bytes: Some(512), + }; + + let mut batch = sender.create_message_batch(Some(options)).await?; + + let batch_id = Uuid::new_v4().to_string(); + let mut batches_sent = 0; + let mut total_messages_sent = 0; + + // Try to add many messages, creating new batches when overflow occurs + for i in 0..20 { + let message_body = format!( + "Overflow test message {} with substantial content to trigger overflow", + i + ); + let mut message = Message::from_string(&message_body); + message.set_message_id(format!("overflow-{}-{}", batch_id, i)); + message.set_correlation_id(&batch_id); + message.set_property("test_type", "overflow_handling"); + + if !batch.try_add_message(message) { + // Current batch is full, send it and create a new one + if !batch.is_empty() { + let message_count = batch.count(); + sender.send_message_batch(batch, None).await?; + batches_sent += 1; + total_messages_sent += message_count; + println!("Sent batch {} with messages", batches_sent); + } + + let options = azure_messaging_servicebus::CreateMessageBatchOptions { + maximum_size_in_bytes: Some(2048), + }; + + // Create new batch and add the message + batch = sender.create_message_batch(Some(options)).await?; + + let mut retry_message = Message::from_string(&message_body); + retry_message.set_message_id(format!("overflow-{}-{}", batch_id, i)); + retry_message.set_correlation_id(&batch_id); + retry_message.set_property("test_type", "overflow_handling"); + + if !batch.try_add_message(retry_message) { + println!("Message too large even for new batch, skipping"); + } + } + } + + // Send the final batch if it has messages + if !batch.is_empty() { + total_messages_sent += batch.count(); + sender.send_message_batch(batch, None).await?; + batches_sent += 1; + } + + println!( + "Sent {} batches with total {} messages", + batches_sent, total_messages_sent + ); + + // Clean up the messages + let receiver = client + .create_receiver(&queue_name, Some(CreateReceiverOptions::default())) + .await?; + let mut cleanup_attempts = 0; + while cleanup_attempts < 5 { + let cleanup_options = ReceiveMessageOptions { + max_message_count: 10, + max_wait_time: Some(Duration::seconds(5)), // Short timeout for cleanup + }; + let messages = receiver.receive_messages(10, Some(cleanup_options)).await?; + let mut found_test_messages = false; + + for msg in messages { + if msg.correlation_id() == Some(&batch_id) { + receiver + .complete_message(&msg, Some(CompleteMessageOptions::default())) + .await?; + found_test_messages = true; + } + } + + if !found_test_messages { + break; + } + cleanup_attempts += 1; + } + + // Clean up + receiver.close().await?; + sender.close().await?; + client.close().await?; + + println!("Batch overflow handling test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_multiple_senders_receivers(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing multiple senders and receivers from same client"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Create multiple senders and receivers + let sender1 = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let sender2 = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let receiver1 = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let receiver2 = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + + // Send messages from different senders + let message_id_1 = Uuid::new_v4().to_string(); + let message_id_2 = Uuid::new_v4().to_string(); + + let mut message1 = Message::from_string("Message from sender 1"); + message1.set_message_id(&message_id_1); + + let mut message2 = Message::from_string("Message from sender 2"); + message2.set_message_id(&message_id_2); + + sender1.send_message(message1, None).await?; + sender2.send_message(message2, None).await?; + + // Receive messages with different receivers + let messages1 = receiver1 + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + let messages2 = receiver2 + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + // Complete messages + if let Some(msg) = messages1.first() { + receiver1 + .complete_message(msg, Some(CompleteMessageOptions::default())) + .await?; + } + if let Some(msg) = messages2.first() { + receiver2 + .complete_message(msg, Some(CompleteMessageOptions::default())) + .await?; + } + + // Clean up + receiver2.close().await?; + receiver1.close().await?; + sender2.close().await?; + sender1.close().await?; + client.close().await?; + + println!("Multiple senders/receivers test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_receiver_lifecycle(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing receiver lifecycle operations"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + // Create and close receiver multiple times + for i in 0..3 { + // Send a message for this iteration + let message_id = format!("receiver-lifecycle-{}-{}", Uuid::new_v4(), i); + let mut message = Message::from_string(format!("Receiver lifecycle test {}", i)); + message.set_message_id(&message_id); + message.set_property("test_type", "receiver_lifecycle"); + + sender.send_message(message, None).await?; + + // Create receiver, receive message, and close + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let messages = receiver + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + if let Some(received_message) = messages.first() { + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + } + + receiver.close().await?; + + println!("Receiver lifecycle iteration {} completed", i); + } + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Receiver lifecycle test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_basic_send_receive_round_trip(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing basic send/receive round trip for queue: {}", + queue_name + ); + + // Create client with recording credential (consistent with EventHubs pattern) + let credential = recording.credential(); + let client = ServiceBusClient::builder() + .open(&namespace, credential.clone()) + .await?; + + // Purge any existing messages from the queue to ensure clean test + let purge_receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::ReceiveAndDelete, + sub_queue: None, + }), + ) + .await?; + + loop { + let purge_options = ReceiveMessageOptions { + max_message_count: 10, + max_wait_time: Some(Duration::seconds(2)), // Short timeout for cleanup + }; + let purge_messages = purge_receiver + .receive_messages(10, Some(purge_options)) + .await?; + if purge_messages.is_empty() { + break; + } + println!("Purged {} existing messages", purge_messages.len()); + } + purge_receiver.close().await?; + + // Send a test message + let message_id = Uuid::new_v4().to_string(); + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let mut message = Message::from_string("Hello, Service Bus!"); + message.set_message_id(&message_id); + message.set_property("test_property", "test_value"); + message.set_property("test_type", "round_trip"); + + sender.send_message(message, None).await?; + println!("Message sent successfully"); + + // Receive the message + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let messages = receiver + .receive_messages(10, Some(ReceiveMessageOptions::default())) + .await?; // Try to get up to 10 messages + + println!("Received {} messages", messages.len()); + for (i, msg) in messages.iter().enumerate() { + println!( + "Message {}: ID = {:?}, Body = {:?}", + i, + msg.message_id(), + msg.body_as_string() + ); + } + + assert!( + !messages.is_empty(), + "Should receive the message we just sent" + ); + + // Find our message by content since IDs might not match due to recording framework + let our_message = messages + .iter() + .find(|msg| msg.body_as_string().unwrap_or_default() == "Hello, Service Bus!"); + + assert!( + our_message.is_some(), + "Should find our message by body content" + ); + let received_message = our_message.unwrap(); + assert_eq!( + received_message.body_as_string()?, + "Hello, Service Bus!", + "Message body should match" + ); + assert_eq!( + received_message.property("test_property"), + Some("test_value".to_string()).as_ref(), + "Custom property should match" + ); + + // Complete the message + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + println!("Message completed successfully"); + + // Clean up + receiver.close().await?; + sender.close().await?; + client.close().await?; + + println!("Basic round trip test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_batch_send(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing batch send for queue: {}", queue_name); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let mut batch = sender.create_message_batch(None).await?; + let batch_id = Uuid::new_v4().to_string(); + + for i in 0..3 { + let message_id = format!("batch-{}-{}", batch_id, i); + let mut message = Message::from_string(format!("Batch message {}", i)); + message.set_message_id(&message_id); + message.set_correlation_id(&batch_id); + message.set_property("test_type", "batch_test"); + message.set_property("sequence", i.to_string()); + + let added = batch.try_add_message(message); + assert!(added, "Should be able to add message {} to batch", i); + } + + assert_eq!(batch.count(), 3); + println!("Batch contains {} messages", batch.count()); + + sender.send_message_batch(batch, None).await?; + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Batch send test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_batch_partitioned_queue(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing partitioned queue batch send for queue: {}", + queue_name + ); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let mut batch = sender.create_message_batch(None).await?; + let session_id = format!("partition-session-{}", Uuid::new_v4()); + + // Create messages with the same session ID to ensure they go to the same partition + for i in 0..5 { + let message_id = format!("partitioned-{}-{}", session_id, i); + let mut message = Message::from_string(format!("Partitioned message {}", i)); + message.set_message_id(&message_id); + message.set_session_id(&session_id); + message.set_property("test_type", "partitioned_batch"); + message.set_property("sequence", i.to_string()); + + let added = batch.try_add_message(message); + assert!( + added, + "Should be able to add partitioned message {} to batch", + i + ); + } + + assert_eq!(batch.count(), 5); + sender.send_message_batch(batch, None).await?; + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Partitioned queue batch test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_batch_size_limits_alt(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing batch size limits for queue: {}", queue_name); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + // Create a batch with a small size limit for testing + let options = azure_messaging_servicebus::CreateMessageBatchOptions { + maximum_size_in_bytes: Some(2048), + }; + + let mut batch = sender.create_message_batch(Some(options)).await?; + + // Add messages until batch is full + let mut added_count = 0; + for i in 0..50 { + let message_text = format!("Size test message {} with some content to use space", i); + let mut message = Message::from_string(&message_text); + message.set_message_id(format!("size-test-{}", i)); + message.set_property("test_type", "size_limits"); + message.set_property("sequence", i.to_string()); + + if batch.try_add_message(message) { + added_count += 1; + } else { + println!("Batch became full after {} messages", added_count); + break; + } + } + + assert!( + added_count > 0, + "Should have been able to add at least one message" + ); + assert!( + added_count < 50, + "Batch should have size limits preventing all messages" + ); + + println!( + "Successfully added {} messages to size-limited batch", + added_count + ); + sender.send_message_batch(batch, None).await?; + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Batch size limits test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_batch_empty_batch(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing empty batch send for queue: {}", queue_name); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let batch = sender.create_message_batch(None).await?; + + assert!(batch.is_empty()); + assert_eq!(batch.count(), 0); + + // Sending empty batch should succeed (no-op) + sender.send_message_batch(batch, None).await?; + println!("Empty batch sent successfully (no-op)"); + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Empty batch test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_batch_single_message_batch(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing single message batch send for queue: {}", + queue_name + ); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let mut batch = sender.create_message_batch(None).await?; + + let mut message = Message::from_string("Single message in batch test"); + message.set_message_id("single-batch-message-id"); + message.set_property("test_type", "single_message_batch"); + + let added = batch.try_add_message(message); + assert!(added, "Should be able to add single message to batch"); + assert_eq!(batch.count(), 1); + assert!(!batch.is_empty()); + + sender.send_message_batch(batch, None).await?; + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Single message batch test completed successfully"); + Ok(()) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_client.rs b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_client.rs new file mode 100644 index 0000000000..c69335709b --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_client.rs @@ -0,0 +1,441 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! Tests for Service Bus authentication methods. + +mod common; + +use azure_core::time::Duration; +use azure_core_test::{recorded, TestContext}; +use azure_messaging_servicebus::{ + AbandonMessageOptions, CompleteMessageOptions, CreateReceiverOptions, CreateSenderOptions, + Message, ReceiveMessageOptions, ReceiveMode, SendMessageOptions, ServiceBusClient, +}; +use common::{get_queue_name, get_servicebus_namespace}; +use std::{env, error::Error}; +use uuid::Uuid; + +#[recorded::test(live)] +async fn test_client_lifecycle(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing client lifecycle operations"); + + // Create and close client multiple times + for i in 0..3 { + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let receiver = client + .create_receiver(&queue_name, Some(CreateReceiverOptions::default())) + .await?; + + // Send a simple message to verify connectivity + let message_id = format!("lifecycle-test-{}-{}", Uuid::new_v4(), i); + let mut message = Message::from_string(format!("Lifecycle test message {}", i)); + message.set_message_id(&message_id); + + sender.send_message(message, None).await?; + + // Try to receive the message + let messages = receiver + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + if let Some(received_message) = messages.first() { + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + } + + // Clean up + receiver.close().await?; + sender.close().await?; + client.close().await?; + + println!("Lifecycle iteration {} completed", i); + } + + println!("Client lifecycle test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_multiple_senders_receivers(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing multiple senders and receivers from same client"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Create multiple senders and receivers + let sender1 = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let sender2 = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let receiver1 = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let receiver2 = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + + // Send messages from different senders + let message_id_1 = Uuid::new_v4().to_string(); + let message_id_2 = Uuid::new_v4().to_string(); + + let mut message1 = Message::from_string("Message from sender 1"); + message1.set_message_id(&message_id_1); + + let mut message2 = Message::from_string("Message from sender 2"); + message2.set_message_id(&message_id_2); + + sender1.send_message(message1, None).await?; + sender2.send_message(message2, None).await?; + + // Receive messages with different receivers + let messages1 = receiver1 + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + let messages2 = receiver2 + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + // Complete messages + if let Some(msg) = messages1.first() { + receiver1 + .complete_message(msg, Some(CompleteMessageOptions::default())) + .await?; + } + if let Some(msg) = messages2.first() { + receiver2 + .complete_message(msg, Some(CompleteMessageOptions::default())) + .await?; + } + + // Clean up + receiver2.close().await?; + receiver1.close().await?; + sender2.close().await?; + sender1.close().await?; + client.close().await?; + + println!("Multiple senders/receivers test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_peek_lock_operations(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing PeekLock operations for queue: {}", queue_name); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Send a test message for abandon testing + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let message_id = Uuid::new_v4().to_string(); + let mut message = Message::from_string("PeekLock test message"); + message.set_message_id(&message_id); + message.set_property("test_type", "peek_lock"); + + sender.send_message(message, None).await?; + sender.close().await?; + + // Receive and abandon the message + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let messages = receiver + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + assert!( + !messages.is_empty(), + "Should receive the abandon test message" + ); + let received_message = &messages[0]; + assert_eq!( + received_message.message_id(), + Some(message_id.clone()).as_ref() + ); + + // Abandon the message (should make it available again) + receiver + .abandon_message(received_message, Some(AbandonMessageOptions::default())) + .await?; + + // Try to receive it again (it should be available since we abandoned it) + let messages_after_abandon = receiver + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + if !messages_after_abandon.is_empty() { + let re_received = &messages_after_abandon[0]; + assert_eq!(re_received.message_id(), Some(message_id).as_ref()); + + // Complete it this time to clean up + receiver + .complete_message(re_received, Some(CompleteMessageOptions::default())) + .await?; + } + + // Clean up + receiver.close().await?; + client.close().await?; + + println!("PeekLock operations test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_receiver_lifecycle(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing receiver lifecycle operations"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + // Create and close receiver multiple times + for i in 0..3 { + // Send a message for this iteration + let message_id = format!("receiver-lifecycle-{}-{}", Uuid::new_v4(), i); + let mut message = Message::from_string(format!("Receiver lifecycle test {}", i)); + message.set_message_id(&message_id); + message.set_property("test_type", "receiver_lifecycle"); + + sender.send_message(message, None).await?; + + // Create receiver, receive message, and close + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let messages = receiver + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + if let Some(received_message) = messages.first() { + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + } + + receiver.close().await?; + + println!("Receiver lifecycle iteration {} completed", i); + } + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Receiver lifecycle test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_basic_send_receive_round_trip(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing basic send/receive round trip for queue: {}", + queue_name + ); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Purge any existing messages from the queue to ensure clean test + let purge_receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::ReceiveAndDelete, + sub_queue: None, + }), + ) + .await?; + + loop { + let purge_options = ReceiveMessageOptions { + max_message_count: 10, + max_wait_time: Some(Duration::seconds(2)), // Short timeout for cleanup + }; + let purge_messages = purge_receiver + .receive_messages(10, Some(purge_options)) + .await?; + if purge_messages.is_empty() { + break; + } + println!("Purged {} existing messages", purge_messages.len()); + } + purge_receiver.close().await?; + + // Send a test message + let message_id = Uuid::new_v4().to_string(); + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let mut message = Message::from_string("Hello, Service Bus!"); + message.set_message_id(&message_id); + message.set_property("test_property", "test_value"); + message.set_property("test_type", "round_trip"); + + sender.send_message(message, None).await?; + println!("Message sent successfully"); + + // Receive the message + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let messages = receiver + .receive_messages(10, Some(ReceiveMessageOptions::default())) + .await?; // Try to get up to 10 messages + + println!("Received {} messages", messages.len()); + for (i, msg) in messages.iter().enumerate() { + println!( + "Message {}: ID = {:?}, Body = {:?}", + i, + msg.message_id(), + msg.body_as_string() + ); + } + + assert!( + !messages.is_empty(), + "Should receive the message we just sent" + ); + + // Find our message by content since IDs might not match due to recording framework + let our_message = messages + .iter() + .find(|msg| msg.body_as_string().unwrap_or_default() == "Hello, Service Bus!"); + + assert!( + our_message.is_some(), + "Should find our message by body content" + ); + let received_message = our_message.unwrap(); + assert_eq!( + received_message.body_as_string()?, + "Hello, Service Bus!", + "Message body should match" + ); + assert_eq!( + received_message.property("test_property"), + Some("test_value".to_string()).as_ref(), + "Custom property should match" + ); + + // Complete the message + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + println!("Message completed successfully"); + + // Clean up + receiver.close().await?; + sender.close().await?; + client.close().await?; + + println!("Basic round trip test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_resend(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing message resend for queue: {}", queue_name); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let message_id = "resend-test-id-12345"; + let mut message = Message::from_string("Resend test message"); + message.set_message_id(message_id); + message.set_property("test_type", "resend_test"); + + // Send the message first time + let send_options = SendMessageOptions::default(); + sender + .send_message(message.clone(), Some(send_options)) + .await?; + println!("Message sent first time"); + + // Resend the same message (should succeed) + sender.send_message(message, None).await?; + println!("Message resent successfully"); + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Resend test completed successfully"); + Ok(()) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_message.rs b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_message.rs new file mode 100644 index 0000000000..e89eb30964 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_message.rs @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! Tests for Service Bus message properties and handling. + +mod common; + +use azure_core_test::{recorded, TestContext}; +use azure_messaging_servicebus::{ + CompleteMessageOptions, CreateReceiverOptions, CreateSenderOptions, Message, + ReceiveMessageOptions, ReceiveMode, ServiceBusClient, +}; +use common::{get_queue_name, get_servicebus_namespace}; +use std::error::Error; +use time::OffsetDateTime; +use uuid::Uuid; + +#[recorded::test(live)] +async fn test_message_properties_preservation(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing message properties preservation for queue: {}", + queue_name + ); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Send message with comprehensive properties + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let message_id = Uuid::new_v4().to_string(); + let correlation_id = Uuid::new_v4().to_string(); + + let mut message = Message::from_string("Properties preservation test"); + message.set_message_id(&message_id); + message.set_correlation_id(&correlation_id); + message.set_content_type("application/json"); + message.set_subject("Test Subject"); + message.set_reply_to("reply-queue"); + message.set_property("custom_prop_1", "value1"); + message.set_property("custom_prop_2", "value2"); + message.set_property("number_prop", "42"); + + sender.send_message(message, None).await?; + sender.close().await?; + + // Receive and verify properties + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let messages = receiver + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + assert!(!messages.is_empty(), "Should receive the message"); + + let received_message = &messages[0]; + + // Verify standard properties + assert_eq!(received_message.message_id(), Some(message_id).as_ref()); + assert_eq!( + received_message.correlation_id(), + Some(correlation_id).as_ref() + ); + assert_eq!( + received_message.system_properties().content_type.as_ref(), + Some("application/json".to_string()).as_ref() + ); + assert_eq!( + received_message.system_properties().subject.as_ref(), + Some("Test Subject".to_string()).as_ref() + ); + assert_eq!( + received_message.system_properties().reply_to.as_ref(), + Some("reply-queue".to_string()).as_ref() + ); + + // Verify custom properties + assert_eq!( + received_message.property("custom_prop_1"), + Some("value1".to_string()).as_ref() + ); + assert_eq!( + received_message.property("custom_prop_2"), + Some("value2".to_string()).as_ref() + ); + assert_eq!( + received_message.property("number_prop"), + Some("42".to_string()).as_ref() + ); + + // Complete message + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + + // Clean up + receiver.close().await?; + client.close().await?; + + println!("Message properties preservation test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_message_properties_comprehensive(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing comprehensive message properties for queue: {}", + queue_name + ); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let mut message = Message::from_string("Comprehensive properties test message"); + + // Set all possible standard AMQP properties + message.set_message_id("comprehensive-prop-test-id"); + message.set_correlation_id("correlation-comprehensive-123"); + message.set_session_id("session-comprehensive-456"); + message.set_reply_to("comprehensive-reply-queue"); + message.set_reply_to_session_id("reply-session-comprehensive-789"); + message.set_content_type("application/json"); + message.set_subject("Comprehensive Properties Test Message"); + + // Set comprehensive custom application properties + message.set_property("test_type", "comprehensive_properties"); + message.set_property("string_prop", "comprehensive_string_value"); + message.set_property("int_prop", "12345"); + message.set_property("bool_prop", "true"); + message.set_property("float_prop", "99.99"); + message.set_property("timestamp_prop", OffsetDateTime::now_utc().to_string()); + message.set_property("version", "2.0.1"); + message.set_property("environment", "test"); + message.set_property("region", "us-east-1"); + message.set_property("category", "integration-test"); + + sender.send_message(message, None).await?; + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Comprehensive properties test completed successfully"); + Ok(()) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_receiver.rs b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_receiver.rs new file mode 100644 index 0000000000..25af38f59f --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_receiver.rs @@ -0,0 +1,473 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! Tests for Service Bus authentication methods. + +mod common; + +use azure_core::time::Duration; +use azure_core_test::{recorded, TestContext}; +use azure_messaging_servicebus::{ + CompleteMessageOptions, CreateReceiverOptions, CreateSenderOptions, Message, + ReceiveMessageOptions, ReceiveMode, ServiceBusClient, +}; +use common::{get_queue_name, get_servicebus_namespace}; +use std::{env, error::Error}; +use uuid::Uuid; + +#[recorded::test(live)] +async fn test_multiple_senders_receivers(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing multiple senders and receivers from same client"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Create multiple senders and receivers + let sender1 = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let sender2 = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let receiver1 = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let receiver2 = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + + // Send messages from different senders + let message_id_1 = Uuid::new_v4().to_string(); + let message_id_2 = Uuid::new_v4().to_string(); + + let mut message1 = Message::from_string("Message from sender 1"); + message1.set_message_id(&message_id_1); + + let mut message2 = Message::from_string("Message from sender 2"); + message2.set_message_id(&message_id_2); + + sender1.send_message(message1, None).await?; + sender2.send_message(message2, None).await?; + + // Receive messages with different receivers + let messages1 = receiver1 + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + let messages2 = receiver2 + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + // Complete messages + if let Some(msg) = messages1.first() { + receiver1 + .complete_message(msg, Some(CompleteMessageOptions::default())) + .await?; + } + if let Some(msg) = messages2.first() { + receiver2 + .complete_message(msg, Some(CompleteMessageOptions::default())) + .await?; + } + + // Clean up + receiver2.close().await?; + receiver1.close().await?; + sender2.close().await?; + sender1.close().await?; + client.close().await?; + + println!("Multiple senders/receivers test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_receive_single_message(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing single message receive for queue: {}", queue_name); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Send a test message first + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let message_id = Uuid::new_v4().to_string(); + let mut message = Message::from_string("Single receive test message"); + message.set_message_id(&message_id); + message.set_property("test_type", "single_receive"); + + sender.send_message(message, None).await?; + sender.close().await?; + + // Receive the message + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let messages = receiver + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + assert!(!messages.is_empty(), "Should receive the message we sent"); + + let received_message = &messages[0]; + assert_eq!(received_message.message_id(), Some(message_id).as_ref()); + assert_eq!( + received_message.body_as_string()?, + "Single receive test message" + ); + + // Complete the message + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + + // Clean up + receiver.close().await?; + client.close().await?; + + println!("Single message receive test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_receive_multiple_messages(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing multiple message receive for queue: {}", queue_name); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Send multiple test messages + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let message_count = 5; + let mut sent_message_ids = Vec::new(); + + for i in 0..message_count { + let message_id = format!("multi-receive-{}-{}", Uuid::new_v4(), i); + sent_message_ids.push(message_id.clone()); + + let mut message = Message::from_string(format!("Multiple receive test message {}", i)); + message.set_message_id(&message_id); + message.set_property("test_type", "multiple_receive"); + message.set_property("sequence", i.to_string()); + + sender.send_message(message, None).await?; + } + + sender.close().await?; + + // Receive the messages + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let messages = receiver + .receive_messages( + message_count as usize, + Some(ReceiveMessageOptions::default()), + ) + .await?; + + assert_eq!( + messages.len(), + message_count as usize, + "Should receive all sent messages" + ); + + // Verify and complete all messages + for received_message in &messages { + let received_id = received_message.message_id().unwrap(); + assert!( + sent_message_ids.contains(received_id), + "Received message ID should be in sent list" + ); + + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + } + + // Clean up + receiver.close().await?; + client.close().await?; + + println!("Multiple message receive test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_receive_and_delete_mode(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing ReceiveAndDelete mode for queue: {}", queue_name); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Send a test message + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let message_id = Uuid::new_v4().to_string(); + let mut message = Message::from_string("ReceiveAndDelete test message"); + message.set_message_id(&message_id); + message.set_property("test_type", "receive_and_delete"); + + sender.send_message(message, None).await?; + sender.close().await?; + + // Receive with ReceiveAndDelete mode + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::ReceiveAndDelete, + sub_queue: None, + }), + ) + .await?; + let messages = receiver + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + assert!(!messages.is_empty(), "Should receive the message"); + let received_message = &messages[0]; + assert_eq!(received_message.message_id(), Some(message_id).as_ref()); + + // In ReceiveAndDelete mode, messages are automatically deleted + // No need to call complete_message() + + // Clean up + receiver.close().await?; + client.close().await?; + + println!("ReceiveAndDelete mode test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_receiver_lifecycle(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing receiver lifecycle operations"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + // Create and close receiver multiple times + for i in 0..3 { + // Send a message for this iteration + let message_id = format!("receiver-lifecycle-{}-{}", Uuid::new_v4(), i); + let mut message = Message::from_string(format!("Receiver lifecycle test {}", i)); + message.set_message_id(&message_id); + message.set_property("test_type", "receiver_lifecycle"); + + sender.send_message(message, None).await?; + + // Create receiver, receive message, and close + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let messages = receiver + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + if let Some(received_message) = messages.first() { + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + } + + receiver.close().await?; + + println!("Receiver lifecycle iteration {} completed", i); + } + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Receiver lifecycle test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_basic_send_receive_round_trip(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing basic send/receive round trip for queue: {}", + queue_name + ); + + // Create client with recording credential (consistent with EventHubs pattern) + let credential = recording.credential(); + let client = ServiceBusClient::builder() + .open(&namespace, credential.clone()) + .await?; + + // Purge any existing messages from the queue to ensure clean test + let purge_receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::ReceiveAndDelete, + sub_queue: None, + }), + ) + .await?; + + loop { + let purge_options = ReceiveMessageOptions { + max_message_count: 10, + max_wait_time: Some(Duration::seconds(2)), // Short timeout for cleanup + }; + let purge_messages = purge_receiver + .receive_messages(10, Some(purge_options)) + .await?; + if purge_messages.is_empty() { + break; + } + println!("Purged {} existing messages", purge_messages.len()); + } + purge_receiver.close().await?; + + // Send a test message + let message_id = Uuid::new_v4().to_string(); + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let mut message = Message::from_string("Hello, Service Bus!"); + message.set_message_id(&message_id); + message.set_property("test_property", "test_value"); + message.set_property("test_type", "round_trip"); + + sender.send_message(message, None).await?; + println!("Message sent successfully"); + + // Receive the message + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let messages = receiver + .receive_messages(10, Some(ReceiveMessageOptions::default())) + .await?; // Try to get up to 10 messages + + println!("Received {} messages", messages.len()); + for (i, msg) in messages.iter().enumerate() { + println!( + "Message {}: ID = {:?}, Body = {:?}", + i, + msg.message_id(), + msg.body_as_string() + ); + } + + assert!( + !messages.is_empty(), + "Should receive the message we just sent" + ); + + // Find our message by content since IDs might not match due to recording framework + let our_message = messages + .iter() + .find(|msg| msg.body_as_string().unwrap_or_default() == "Hello, Service Bus!"); + + assert!( + our_message.is_some(), + "Should find our message by body content" + ); + let received_message = our_message.unwrap(); + assert_eq!( + received_message.body_as_string()?, + "Hello, Service Bus!", + "Message body should match" + ); + assert_eq!( + received_message.property("test_property"), + Some("test_value".to_string()).as_ref(), + "Custom property should match" + ); + + // Complete the message + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + println!("Message completed successfully"); + + // Clean up + receiver.close().await?; + sender.close().await?; + client.close().await?; + + println!("Basic round trip test completed successfully"); + Ok(()) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_round_trip.rs b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_round_trip.rs new file mode 100644 index 0000000000..c45d4473a7 --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_round_trip.rs @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! Tests for Service Bus authentication methods. + +mod common; + +use azure_core::time::Duration; +use azure_core_test::{recorded, TestContext}; +use azure_messaging_servicebus::{ + CompleteMessageOptions, CreateReceiverOptions, CreateSenderOptions, Message, + ReceiveMessageOptions, ReceiveMode, ServiceBusClient, +}; +use common::{get_queue_name, get_servicebus_namespace}; +use std::{env, error::Error}; +use uuid::Uuid; + +#[recorded::test(live)] +async fn test_multiple_senders_receivers(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing multiple senders and receivers from same client"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Create multiple senders and receivers + let sender1 = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let sender2 = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let receiver1 = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let receiver2 = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + + // Send messages from different senders + let message_id_1 = Uuid::new_v4().to_string(); + let message_id_2 = Uuid::new_v4().to_string(); + + let mut message1 = Message::from_string("Message from sender 1"); + message1.set_message_id(&message_id_1); + + let mut message2 = Message::from_string("Message from sender 2"); + message2.set_message_id(&message_id_2); + + sender1.send_message(message1, None).await?; + sender2.send_message(message2, None).await?; + + // Receive messages with different receivers + let messages1 = receiver1 + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + let messages2 = receiver2 + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + // Complete messages + if let Some(msg) = messages1.first() { + receiver1 + .complete_message(msg, Some(CompleteMessageOptions::default())) + .await?; + } + if let Some(msg) = messages2.first() { + receiver2 + .complete_message(msg, Some(CompleteMessageOptions::default())) + .await?; + } + + // Clean up + receiver2.close().await?; + receiver1.close().await?; + sender2.close().await?; + sender1.close().await?; + client.close().await?; + + println!("Multiple senders/receivers test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_basic_send_receive_round_trip(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing basic send/receive round trip for queue: {}", + queue_name + ); + + // Create client with recording credential (consistent with EventHubs pattern) + let credential = recording.credential(); + let client = ServiceBusClient::builder() + .open(&namespace, credential.clone()) + .await?; + + // Purge any existing messages from the queue to ensure clean test + let purge_receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::ReceiveAndDelete, + sub_queue: None, + }), + ) + .await?; + + loop { + let purge_options = ReceiveMessageOptions { + max_message_count: 10, + max_wait_time: Some(Duration::seconds(2)), // Short timeout for cleanup + }; + let purge_messages = purge_receiver + .receive_messages(10, Some(purge_options)) + .await?; + if purge_messages.is_empty() { + break; + } + println!("Purged {} existing messages", purge_messages.len()); + } + purge_receiver.close().await?; + + // Send a test message + let message_id = Uuid::new_v4().to_string(); + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let mut message = Message::from_string("Hello, Service Bus!"); + message.set_message_id(&message_id); + message.set_property("test_property", "test_value"); + message.set_property("test_type", "round_trip"); + + sender.send_message(message, None).await?; + println!("Message sent successfully"); + + // Receive the message + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let messages = receiver + .receive_messages(10, Some(ReceiveMessageOptions::default())) + .await?; // Try to get up to 10 messages + + println!("Received {} messages", messages.len()); + for (i, msg) in messages.iter().enumerate() { + println!( + "Message {}: ID = {:?}, Body = {:?}", + i, + msg.message_id(), + msg.body_as_string() + ); + } + + assert!( + !messages.is_empty(), + "Should receive the message we just sent" + ); + + // Find our message by content since IDs might not match due to recording framework + let our_message = messages + .iter() + .find(|msg| msg.body_as_string().unwrap_or_default() == "Hello, Service Bus!"); + + assert!( + our_message.is_some(), + "Should find our message by body content" + ); + let received_message = our_message.unwrap(); + assert_eq!( + received_message.body_as_string()?, + "Hello, Service Bus!", + "Message body should match" + ); + assert_eq!( + received_message.property("test_property"), + Some("test_value".to_string()).as_ref(), + "Custom property should match" + ); + + // Complete the message + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + println!("Message completed successfully"); + + // Clean up + receiver.close().await?; + sender.close().await?; + client.close().await?; + + println!("Basic round trip test completed successfully"); + Ok(()) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_schedule.rs b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_schedule.rs new file mode 100644 index 0000000000..478a94bb7d --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_schedule.rs @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! Tests for Service Bus scheduled message functionality. + +mod common; + +use azure_core::time::Duration; +use azure_core_test::{recorded, TestContext}; +use azure_messaging_servicebus::{ + CancelScheduledMessagesOptions, CreateSenderOptions, Message, ScheduleMessageOptions, + ServiceBusClient, +}; +use common::{get_queue_name, get_servicebus_namespace}; +use std::error::Error; +use time::OffsetDateTime; +use uuid::Uuid; + +#[recorded::test(live)] +async fn test_scheduled_message_send(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing scheduled message send for queue: {}", queue_name); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + // Schedule message for 2 seconds in the future + let scheduled_time = OffsetDateTime::now_utc() + Duration::seconds(2); + let message_id = Uuid::new_v4().to_string(); + + let mut message = Message::from_string("Scheduled message test"); + message.set_message_id(&message_id); + message.set_property("test_type", "scheduled_send"); + + let schedule_options = ScheduleMessageOptions::default(); + let sequence_number = sender + .schedule_message(message, scheduled_time, Some(schedule_options)) + .await?; + + println!( + "Scheduled message with sequence number: {}", + sequence_number + ); + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Scheduled message send test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_schedule_message(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing schedule message for queue: {}", queue_name); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let message_id = "scheduled-test-id-12345"; + let mut message = Message::from_string("Scheduled message test"); + message.set_message_id(message_id); + message.set_property("test_type", "schedule_test"); + + let scheduled_time = OffsetDateTime::now_utc() + Duration::seconds(300); // 5 minutes + let schedule_options = ScheduleMessageOptions::default(); + let sequence_number = sender + .schedule_message(message, scheduled_time, Some(schedule_options)) + .await?; + + assert!(sequence_number > 0); + println!( + "Scheduled message with sequence number: {}", + sequence_number + ); + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Schedule message test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_schedule_message_then_cancel(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing schedule then cancel message for queue: {}", + queue_name + ); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let message_id = "cancel-test-id-12345"; + let mut message = Message::from_string("Cancel scheduled message test"); + message.set_message_id(message_id); + message.set_property("test_type", "schedule_cancel_test"); + + let scheduled_time = OffsetDateTime::now_utc() + Duration::seconds(3600); // 1 hour + let schedule_options = ScheduleMessageOptions::default(); + let sequence_number = sender + .schedule_message(message, scheduled_time, Some(schedule_options)) + .await?; + + assert!(sequence_number > 0); + println!( + "Scheduled message with sequence number: {}", + sequence_number + ); + + // Cancel the scheduled message + let cancel_options = CancelScheduledMessagesOptions::default(); + sender + .cancel_scheduled_message(sequence_number, Some(cancel_options)) + .await?; + println!("Cancelled scheduled message: {}", sequence_number); + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Schedule then cancel test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_schedule_multiple_messages(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing multiple scheduled messages for queue: {}", + queue_name + ); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let base_time = OffsetDateTime::now_utc() + Duration::seconds(600); // 10 minutes + let mut sequence_numbers = Vec::new(); + + for i in 0..3 { + let message_id = format!("scheduled-multi-{}", i); + let mut message = Message::from_string(format!("Scheduled message {}", i)); + message.set_message_id(&message_id); + message.set_property("test_type", "schedule_multiple"); + message.set_property("sequence", i.to_string()); + + let scheduled_time = base_time + Duration::minutes(i); // 1 minute apart + let sequence_number = sender + .schedule_message(message, scheduled_time, None) + .await?; + + assert!(sequence_number > 0); + sequence_numbers.push(sequence_number); + println!( + "Scheduled message {} with sequence number: {}", + i, sequence_number + ); + } + + // Verify all sequence numbers are unique + let mut sorted_numbers = sequence_numbers.clone(); + sorted_numbers.sort(); + sorted_numbers.dedup(); + assert_eq!( + sorted_numbers.len(), + 3, + "All sequence numbers should be unique" + ); + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Multiple scheduled messages test completed successfully"); + Ok(()) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_sender.rs b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_sender.rs new file mode 100644 index 0000000000..8c90b7292b --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_sender.rs @@ -0,0 +1,877 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! Tests for Service Bus authentication methods. + +mod common; + +use azure_core::time::Duration; +use azure_core_test::{recorded, TestContext}; +use azure_messaging_servicebus::{ + AbandonMessageOptions, CompleteMessageOptions, CreateReceiverOptions, CreateSenderOptions, + Message, ReceiveMessageOptions, ReceiveMode, SendMessagesOptions, ServiceBusClient, +}; +use common::{get_queue_name, get_servicebus_namespace}; +use std::{env, error::Error}; +use time::OffsetDateTime; +use uuid::Uuid; + +#[recorded::test(live)] +async fn test_multiple_senders_receivers(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing multiple senders and receivers from same client"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Create multiple senders and receivers + let sender1 = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let sender2 = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let receiver1 = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let receiver2 = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + + // Send messages from different senders + let message_id_1 = Uuid::new_v4().to_string(); + let message_id_2 = Uuid::new_v4().to_string(); + + let mut message1 = Message::from_string("Message from sender 1"); + message1.set_message_id(&message_id_1); + + let mut message2 = Message::from_string("Message from sender 2"); + message2.set_message_id(&message_id_2); + + sender1.send_message(message1, None).await?; + sender2.send_message(message2, None).await?; + + // Receive messages with different receivers + let messages1 = receiver1 + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + let messages2 = receiver2 + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + // Complete messages + if let Some(msg) = messages1.first() { + receiver1 + .complete_message(msg, Some(CompleteMessageOptions::default())) + .await?; + } + if let Some(msg) = messages2.first() { + receiver2 + .complete_message(msg, Some(CompleteMessageOptions::default())) + .await?; + } + + // Clean up + receiver2.close().await?; + receiver1.close().await?; + sender2.close().await?; + sender1.close().await?; + client.close().await?; + + println!("Multiple senders/receivers test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_receiver_lifecycle(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing receiver lifecycle operations"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + // Create and close receiver multiple times + for i in 0..3 { + // Send a message for this iteration + let message_id = format!("receiver-lifecycle-{}-{}", Uuid::new_v4(), i); + let mut message = Message::from_string(format!("Receiver lifecycle test {}", i)); + message.set_message_id(&message_id); + message.set_property("test_type", "receiver_lifecycle"); + + sender.send_message(message, None).await?; + + // Create receiver, receive message, and close + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let messages = receiver + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + if let Some(received_message) = messages.first() { + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + } + + receiver.close().await?; + + println!("Receiver lifecycle iteration {} completed", i); + } + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Receiver lifecycle test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_basic_send_receive_round_trip(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing basic send/receive round trip for queue: {}", + queue_name + ); + + // Create client with recording credential (consistent with EventHubs pattern) + let credential = recording.credential(); + let client = ServiceBusClient::builder() + .open(&namespace, credential.clone()) + .await?; + + // Purge any existing messages from the queue to ensure clean test + let purge_receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::ReceiveAndDelete, + sub_queue: None, + }), + ) + .await?; + + loop { + let purge_options = ReceiveMessageOptions { + max_message_count: 10, + max_wait_time: Some(Duration::seconds(2)), // Short timeout for cleanup + }; + let purge_messages = purge_receiver + .receive_messages(10, Some(purge_options)) + .await?; + if purge_messages.is_empty() { + break; + } + println!("Purged {} existing messages", purge_messages.len()); + } + purge_receiver.close().await?; + + // Send a test message + let message_id = Uuid::new_v4().to_string(); + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let mut message = Message::from_string("Hello, Service Bus!"); + message.set_message_id(&message_id); + message.set_property("test_property", "test_value"); + message.set_property("test_type", "round_trip"); + + sender.send_message(message, None).await?; + println!("Message sent successfully"); + + // Receive the message + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let messages = receiver + .receive_messages(10, Some(ReceiveMessageOptions::default())) + .await?; // Try to get up to 10 messages + + println!("Received {} messages", messages.len()); + for (i, msg) in messages.iter().enumerate() { + println!( + "Message {}: ID = {:?}, Body = {:?}", + i, + msg.message_id(), + msg.body_as_string() + ); + } + + assert!( + !messages.is_empty(), + "Should receive the message we just sent" + ); + + // Find our message by content since IDs might not match due to recording framework + let our_message = messages + .iter() + .find(|msg| msg.body_as_string().unwrap_or_default() == "Hello, Service Bus!"); + + assert!( + our_message.is_some(), + "Should find our message by body content" + ); + let received_message = our_message.unwrap(); + assert_eq!( + received_message.body_as_string()?, + "Hello, Service Bus!", + "Message body should match" + ); + assert_eq!( + received_message.property("test_property"), + Some("test_value".to_string()).as_ref(), + "Custom property should match" + ); + + // Complete the message + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + println!("Message completed successfully"); + + // Clean up + receiver.close().await?; + sender.close().await?; + client.close().await?; + + println!("Basic round trip test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_send_verify_receive_verify_empty(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing send -> verify queue has message -> receive -> verify queue is empty for queue: {}", + queue_name + ); + + // Create client with recording credential + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Step 1: Purge any existing messages from the queue to ensure clean test + println!("Purging any existing messages from queue"); + let purge_receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::ReceiveAndDelete, + sub_queue: None, + }), + ) + .await?; + + loop { + let purge_options = ReceiveMessageOptions { + max_message_count: 10, + max_wait_time: Some(Duration::seconds(2)), // Short timeout for cleanup + }; + let purge_messages = purge_receiver + .receive_messages(10, Some(purge_options)) + .await?; + if purge_messages.is_empty() { + break; + } + println!("Purged {} existing messages", purge_messages.len()); + } + purge_receiver.close().await?; + + // Step 2: Verify queue is initially empty + println!("Verifying queue is initially empty"); + let verify_receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + + let empty_check_options = ReceiveMessageOptions { + max_message_count: 1, + max_wait_time: Some(Duration::seconds(2)), // Short timeout to check if empty + }; + let initial_messages = verify_receiver + .receive_messages(1, Some(empty_check_options)) + .await?; + assert!( + initial_messages.is_empty(), + "Queue should be empty at start, but found {} messages", + initial_messages.len() + ); + println!("āœ“ Confirmed queue is empty"); + + // Step 3: Send a test message + let message_id = Uuid::new_v4().to_string(); + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let mut message = Message::from_string("Test message for send-verify-receive-verify cycle"); + message.set_message_id(&message_id); + message.set_property("test_type", "send_verify_receive_verify"); + message.set_property("timestamp", OffsetDateTime::now_utc().to_string()); + + sender.send_message(message, None).await?; + println!("āœ“ Message sent successfully with ID: {}", message_id); + + // Step 4: Verify our message is in the queue + println!("Verifying our message is in the queue"); + + // Wait a moment for message to be available + tokio::time::sleep(std::time::Duration::from_millis(3000)).await; + + // Create a peek receiver to verify without consuming the message + let peek_receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + + let verify_options = ReceiveMessageOptions { + max_message_count: 20, // Get multiple messages to search through + max_wait_time: Some(Duration::seconds(10)), // Reasonable timeout + }; + let verification_messages = peek_receiver + .receive_messages(20, Some(verify_options)) + .await?; + + println!( + "Found {} messages in queue during verification", + verification_messages.len() + ); + + // Find our specific message by content + let our_message = verification_messages.iter().find(|msg| { + msg.body_as_string().unwrap_or_default() + == "Test message for send-verify-receive-verify cycle" + }); + + assert!( + our_message.is_some(), + "Should find our message in the queue" + ); + let found_message = our_message.unwrap(); + + assert_eq!( + found_message.body_as_string()?, + "Test message for send-verify-receive-verify cycle", + "Message body should match what we sent" + ); + + // Abandon all messages so they go back to the queue for the next step + for msg in &verification_messages { + peek_receiver + .abandon_message(msg, Some(AbandonMessageOptions::default())) + .await?; + } + println!("āœ“ Confirmed our message is in queue with correct content"); + + // Step 5: Actually receive and complete our specific message + println!("Receiving and completing our specific message"); + + // Create a new receiver for actually processing the message + let completion_receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + + let completion_options = ReceiveMessageOptions { + max_message_count: 20, // Get multiple messages to search through + max_wait_time: Some(Duration::seconds(10)), + }; + let completion_messages = completion_receiver + .receive_messages(20, Some(completion_options)) + .await?; + + // Find our specific message by content + let our_message = completion_messages.iter().find(|msg| { + msg.body_as_string().unwrap_or_default() + == "Test message for send-verify-receive-verify cycle" + }); + + assert!(our_message.is_some(), "Should find our message to complete"); + let message_to_complete = our_message.unwrap(); + + // Complete our specific message to remove it from the queue + let complete_options = CompleteMessageOptions::default(); + completion_receiver + .complete_message(message_to_complete, Some(complete_options)) + .await?; + + // Abandon all other messages so they stay in the queue + for msg in &completion_messages { + if msg.body_as_string().unwrap_or_default() + != "Test message for send-verify-receive-verify cycle" + { + completion_receiver + .abandon_message(msg, Some(AbandonMessageOptions::default())) + .await?; + } + } + + println!("āœ“ Our specific message completed successfully"); + + // Step 6: Verify our specific message is no longer in the queue + println!("Verifying our specific message is no longer in the queue"); + let final_verify_options = ReceiveMessageOptions { + max_message_count: 20, // Get multiple messages to search through + max_wait_time: Some(Duration::seconds(5)), // Reasonable timeout + }; + let final_messages = completion_receiver + .receive_messages(20, Some(final_verify_options)) + .await?; + + // Check that our specific message is not in the queue anymore + let our_message_found = final_messages.iter().any(|msg| { + msg.body_as_string().unwrap_or_default() + == "Test message for send-verify-receive-verify cycle" + }); + + assert!( + !our_message_found, + "Our specific message should no longer be in the queue after completion" + ); + + // Abandon all other messages so they stay in the queue for other tests + for msg in &final_messages { + completion_receiver + .abandon_message(msg, Some(AbandonMessageOptions::default())) + .await?; + } + + println!("āœ“ Confirmed our specific message is no longer in the queue"); + + // Clean up + peek_receiver.close().await?; + completion_receiver.close().await?; + sender.close().await?; + client.close().await?; + + println!("Send-verify-receive-verify test completed successfully!"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_send_single_message(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing single message send for queue: {}", queue_name); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let message_id = Uuid::new_v4().to_string(); + let mut message = Message::from_string("Single message test"); + message.set_message_id(&message_id); + message.set_property("test_type", "single_send"); + + sender.send_message(message, None).await?; + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Single message send test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_send_multiple_messages(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing multiple message send for queue: {}", queue_name); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let message_count = 5; + let mut messages = Vec::new(); + + for i in 0..message_count { + let message_id = format!("multi-send-{}-{}", Uuid::new_v4(), i); + let mut message = Message::from_string(format!("Multiple send test message {}", i)); + message.set_message_id(&message_id); + message.set_property("test_type", "multiple_send"); + message.set_property("sequence", i.to_string()); + messages.push(message); + } + + let send_options = SendMessagesOptions::default(); + sender.send_messages(messages, Some(send_options)).await?; + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Multiple message send test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_send_message_with_properties(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing message send with properties for queue: {}", + queue_name + ); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let message_id = Uuid::new_v4().to_string(); + let correlation_id = Uuid::new_v4().to_string(); + + let mut message = Message::from_string("Message with comprehensive properties"); + message.set_message_id(&message_id); + message.set_correlation_id(&correlation_id); + message.set_content_type("text/plain"); + message.set_subject("Test Subject"); + message.set_reply_to("reply-queue"); + + // Add custom properties + message.set_property("test_type", "properties_test"); + message.set_property("priority", "high"); + message.set_property("region", "us-west"); + message.set_property("version", "1.0"); + message.set_property("timestamp", OffsetDateTime::now_utc().to_string()); + + sender.send_message(message, None).await?; + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Message properties send test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_sender_lifecycle(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing sender lifecycle operations"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Create and close sender multiple times + for i in 0..3 { + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let message_id = format!("sender-lifecycle-{}-{}", Uuid::new_v4(), i); + let mut message = Message::from_string(format!("Sender lifecycle test {}", i)); + message.set_message_id(&message_id); + message.set_property("test_type", "sender_lifecycle"); + + sender.send_message(message, None).await?; + sender.close().await?; + + println!("Sender lifecycle iteration {} completed", i); + } + + // Clean up + client.close().await?; + + println!("Sender lifecycle test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_send_message_id(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing message ID handling for queue: {}", queue_name); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let custom_message_id = "test-message-id-12345"; + let mut message = Message::from_string("Hello world with custom ID"); + message.set_message_id(custom_message_id); + message.set_property("test_type", "message_id_test"); + + sender.send_message(message, None).await?; + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Message ID test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_send_amqp_annotated_message(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing AMQP annotated message send for queue: {}", + queue_name + ); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + // Create message with comprehensive AMQP properties + let message_id = "amqp-annotated-id-12345"; + let correlation_id = "correlation-98765"; + let mut message = Message::from_string("AMQP annotated message with all properties"); + + // Set standard AMQP properties + message.set_message_id(message_id); + message.set_correlation_id(correlation_id); + message.set_content_type("application/json"); + message.set_subject("AMQP Test Subject"); + message.set_reply_to("reply-queue"); + message.set_reply_to_session_id("reply-session-123"); + + // Set custom application properties of different types + message.set_property("test_type", "amqp_annotated"); + message.set_property("string-prop", "string-value"); + message.set_property("number-prop", "42"); + message.set_property("bool-prop", "true"); + message.set_property("float-prop", "3.14159"); + message.set_property("timestamp", OffsetDateTime::now_utc().to_string()); + + sender.send_message(message, None).await?; + + // Clean up + sender.close().await?; + client.close().await?; + + println!("AMQP annotated message test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_send_amqp_value_body(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing AMQP value body message send for queue: {}", + queue_name + ); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + // Create a message with structured JSON data (simulating AMQP Value body) + let structured_data = serde_json::json!({ + "id": 123, + "name": "test-entity", + "properties": { + "key1": "value1", + "key2": 42, + "nested": { + "level": 2, + "data": [1, 2, 3, 4, 5] + } + }, + "timestamp": OffsetDateTime::now_utc().to_string() + }); + + let mut message = Message::from_string(structured_data.to_string()); + message.set_message_id("amqp-value-body-id"); + message.set_content_type("application/json"); + message.set_property("test_type", "amqp_value_body"); + + sender.send_message(message, None).await?; + + // Clean up + sender.close().await?; + client.close().await?; + + println!("AMQP value body test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_send_amqp_sequence_body(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing AMQP sequence body message send for queue: {}", + queue_name + ); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + // Create a message with sequence-like data + let sequence_data = vec![ + serde_json::json!({"item": "first", "value": 1}), + serde_json::json!({"item": "second", "value": 2}), + serde_json::json!({"item": "third", "value": 3}), + serde_json::json!({"item": "fourth", "value": 4}), + ]; + + let mut message = Message::from_string(&serde_json::to_string(&sequence_data)?); + message.set_message_id("amqp-sequence-body-id"); + message.set_content_type("application/json"); + message.set_property("test_type", "amqp_sequence_body"); + message.set_property("sequence_length", sequence_data.len().to_string()); + + sender.send_message(message, None).await?; + + // Clean up + sender.close().await?; + client.close().await?; + + println!("AMQP sequence body test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_send_multiple_byte_slices(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing multiple byte slices message send for queue: {}", + queue_name + ); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + // Simulate sending binary data in multiple chunks + let chunk1 = b"First chunk of binary data containing important information"; + let chunk2 = b"Second chunk with more binary content and metadata"; + let chunk3 = b"Third and final chunk completing the binary message"; + + let combined_data = [chunk1.as_slice(), chunk2.as_slice(), chunk3.as_slice()].concat(); + let mut message = Message::new(combined_data); + message.set_message_id("multi-byte-slices-id"); + message.set_content_type("application/octet-stream"); + message.set_property("test_type", "multiple_byte_slices"); + message.set_property("chunk_count", "3"); + + sender.send_message(message, None).await?; + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Multiple byte slices test completed successfully"); + Ok(()) +} diff --git a/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_topic_subscription.rs b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_topic_subscription.rs new file mode 100644 index 0000000000..db89c8b19c --- /dev/null +++ b/sdk/servicebus/azure_messaging_servicebus/tests/servicebus_topic_subscription.rs @@ -0,0 +1,664 @@ +// Copyright (c) Microsoft Corporation. All Rights reserved +// Licensed under the MIT license. + +//! Tests for Service Bus authentication methods. + +mod common; + +use azure_core::time::Duration; +use azure_core_test::{recorded, TestContext}; +use azure_messaging_servicebus::{ + AbandonMessageOptions, CompleteMessageOptions, CreateReceiverOptions, CreateSenderOptions, + Message, ReceiveMessageOptions, ReceiveMode, ServiceBusClient, +}; +use common::{get_queue_name, get_servicebus_namespace, get_subscription_name, get_topic_name}; +use std::{env, error::Error}; +use time::OffsetDateTime; +use uuid::Uuid; + +#[recorded::test(live)] +async fn test_multiple_senders_receivers(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing multiple senders and receivers from same client"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Create multiple senders and receivers + let sender1 = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let sender2 = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + let receiver1 = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let receiver2 = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + + // Send messages from different senders + let message_id_1 = Uuid::new_v4().to_string(); + let message_id_2 = Uuid::new_v4().to_string(); + + let mut message1 = Message::from_string("Message from sender 1"); + message1.set_message_id(&message_id_1); + + let mut message2 = Message::from_string("Message from sender 2"); + message2.set_message_id(&message_id_2); + + sender1.send_message(message1, None).await?; + sender2.send_message(message2, None).await?; + + // Receive messages with different receivers + let messages1 = receiver1 + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + let messages2 = receiver2 + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + // Complete messages + if let Some(msg) = messages1.first() { + receiver1 + .complete_message(msg, Some(CompleteMessageOptions::default())) + .await?; + } + if let Some(msg) = messages2.first() { + receiver2 + .complete_message(msg, Some(CompleteMessageOptions::default())) + .await?; + } + + // Clean up + receiver2.close().await?; + receiver1.close().await?; + sender2.close().await?; + sender1.close().await?; + client.close().await?; + + println!("Multiple senders/receivers test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_receiver_lifecycle(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!("Testing receiver lifecycle operations"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + // Create and close receiver multiple times + for i in 0..3 { + // Send a message for this iteration + let message_id = format!("receiver-lifecycle-{}-{}", Uuid::new_v4(), i); + let mut message = Message::from_string(format!("Receiver lifecycle test {}", i)); + message.set_message_id(&message_id); + message.set_property("test_type", "receiver_lifecycle"); + + sender.send_message(message, None).await?; + + // Create receiver, receive message, and close + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let messages = receiver + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + if let Some(received_message) = messages.first() { + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + } + + receiver.close().await?; + + println!("Receiver lifecycle iteration {} completed", i); + } + + // Clean up + sender.close().await?; + client.close().await?; + + println!("Receiver lifecycle test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_basic_send_receive_round_trip(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + let namespace = get_servicebus_namespace()?; + let queue_name = get_queue_name()?; + + println!( + "Testing basic send/receive round trip for queue: {}", + queue_name + ); + + // Create client with recording credential (consistent with EventHubs pattern) + let credential = recording.credential(); + let client = ServiceBusClient::builder() + .open(&namespace, credential.clone()) + .await?; + + // Purge any existing messages from the queue to ensure clean test + let purge_receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::ReceiveAndDelete, + sub_queue: None, + }), + ) + .await?; + + loop { + let purge_options = ReceiveMessageOptions { + max_message_count: 10, + max_wait_time: Some(Duration::seconds(2)), // Short timeout for cleanup + }; + let purge_messages = purge_receiver + .receive_messages(10, Some(purge_options)) + .await?; + if purge_messages.is_empty() { + break; + } + println!("Purged {} existing messages", purge_messages.len()); + } + purge_receiver.close().await?; + + // Send a test message + let message_id = Uuid::new_v4().to_string(); + let sender = client + .create_sender(&queue_name, Some(CreateSenderOptions::default())) + .await?; + + let mut message = Message::from_string("Hello, Service Bus!"); + message.set_message_id(&message_id); + message.set_property("test_property", "test_value"); + message.set_property("test_type", "round_trip"); + + sender.send_message(message, None).await?; + println!("Message sent successfully"); + + // Receive the message + let receiver = client + .create_receiver( + &queue_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + let messages = receiver + .receive_messages(10, Some(ReceiveMessageOptions::default())) + .await?; // Try to get up to 10 messages + + println!("Received {} messages", messages.len()); + for (i, msg) in messages.iter().enumerate() { + println!( + "Message {}: ID = {:?}, Body = {:?}", + i, + msg.message_id(), + msg.body_as_string() + ); + } + + assert!( + !messages.is_empty(), + "Should receive the message we just sent" + ); + + // Find our message by content since IDs might not match due to recording framework + let our_message = messages + .iter() + .find(|msg| msg.body_as_string().unwrap_or_default() == "Hello, Service Bus!"); + + assert!( + our_message.is_some(), + "Should find our message by body content" + ); + let received_message = our_message.unwrap(); + assert_eq!( + received_message.body_as_string()?, + "Hello, Service Bus!", + "Message body should match" + ); + assert_eq!( + received_message.property("test_property"), + Some("test_value".to_string()).as_ref(), + "Custom property should match" + ); + + // Complete the message + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + println!("Message completed successfully"); + + // Clean up + receiver.close().await?; + sender.close().await?; + client.close().await?; + + println!("Basic round trip test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_topic_subscription_messaging(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + // Check if topic and subscription environment variables are set + if env::var("SERVICEBUS_TOPIC_NAME").is_err() + || env::var("SERVICEBUS_SUBSCRIPTION_NAME").is_err() + { + println!("Skipping topic/subscription test - SERVICEBUS_TOPIC_NAME or SERVICEBUS_SUBSCRIPTION_NAME not set"); + return Ok(()); + } + + let namespace = get_servicebus_namespace()?; + let topic_name = get_topic_name()?; + let subscription_name = get_subscription_name()?; + + println!( + "Testing topic subscription messaging for topic: {} subscription: {}", + topic_name, subscription_name + ); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Send message to topic + let message_id = Uuid::new_v4().to_string(); + let sender = client + .create_sender(&topic_name, Some(CreateSenderOptions::default())) + .await?; + + let mut message = Message::from_string("Topic subscription test message"); + message.set_message_id(&message_id); + message.set_property("test_name", "test_topic_subscription_messaging"); + message.set_property("timestamp", OffsetDateTime::now_utc().to_string()); + + sender.send_message(message, None).await?; + sender.close().await?; + println!("Message sent to topic successfully"); + + // Receive from subscription + let receiver = client + .create_receiver_for_subscription( + &topic_name, + &subscription_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + + let messages = receiver + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + assert!( + !messages.is_empty(), + "Should receive message from subscription" + ); + let received_message = &messages[0]; + assert_eq!(received_message.message_id(), Some(message_id).as_ref()); + + // Verify message content and properties + assert_eq!( + received_message.body_as_string()?, + "Topic subscription test message" + ); + if let Some(test_name) = received_message.property("test_name") { + assert_eq!(test_name, "test_topic_subscription_messaging"); + } + + // Complete the message + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + receiver.close().await?; + client.close().await?; + + println!("Topic subscription messaging test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_topic_multiple_messages(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + // Check if topic and subscription environment variables are set + if env::var("SERVICEBUS_TOPIC_NAME").is_err() + || env::var("SERVICEBUS_SUBSCRIPTION_NAME").is_err() + { + println!("Skipping topic multiple messages test - SERVICEBUS_TOPIC_NAME or SERVICEBUS_SUBSCRIPTION_NAME not set"); + return Ok(()); + } + + let namespace = get_servicebus_namespace()?; + let topic_name = get_topic_name()?; + let subscription_name = get_subscription_name()?; + + println!( + "Testing multiple messages to topic: {} subscription: {}", + topic_name, subscription_name + ); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Send multiple messages to topic + let sender = client + .create_sender(&topic_name, Some(CreateSenderOptions::default())) + .await?; + let message_count = 3; + let mut sent_message_ids = Vec::new(); + + for i in 0..message_count { + let message_id = format!("topic-multi-{}-{}", Uuid::new_v4(), i); + sent_message_ids.push(message_id.clone()); + + let mut message = Message::from_string(format!("Topic multiple message {}", i)); + message.set_message_id(&message_id); + message.set_property("test_name", "test_topic_multiple_messages"); + message.set_property("sequence", i.to_string()); + + sender.send_message(message, None).await?; + } + + sender.close().await?; + println!("Sent {} messages to topic successfully", message_count); + + // Receive from subscription + let receiver = client + .create_receiver_for_subscription( + &topic_name, + &subscription_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + + let messages = receiver + .receive_messages( + message_count as usize, + Some(ReceiveMessageOptions::default()), + ) + .await?; + + assert_eq!( + messages.len(), + message_count as usize, + "Should receive all sent messages from subscription" + ); + + // Verify and complete all messages + for received_message in &messages { + let received_id = received_message.message_id().unwrap(); + assert!( + sent_message_ids.contains(received_id), + "Received message ID should be in sent list" + ); + + if let Some(test_name) = received_message.property("test_name") { + assert_eq!(test_name, "test_topic_multiple_messages"); + } + + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + } + + receiver.close().await?; + client.close().await?; + + println!("Topic multiple messages test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_topic_subscription_with_properties(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + // Check if topic and subscription environment variables are set + if env::var("SERVICEBUS_TOPIC_NAME").is_err() + || env::var("SERVICEBUS_SUBSCRIPTION_NAME").is_err() + { + println!("Skipping topic properties test - SERVICEBUS_TOPIC_NAME or SERVICEBUS_SUBSCRIPTION_NAME not set"); + return Ok(()); + } + + let namespace = get_servicebus_namespace()?; + let topic_name = get_topic_name()?; + let subscription_name = get_subscription_name()?; + + println!("Testing topic subscription with message properties"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Send message with comprehensive properties to topic + let sender = client + .create_sender(&topic_name, Some(CreateSenderOptions::default())) + .await?; + let message_id = Uuid::new_v4().to_string(); + let correlation_id = Uuid::new_v4().to_string(); + + let mut message = Message::from_string("Topic message with properties"); + message.set_message_id(&message_id); + message.set_correlation_id(&correlation_id); + message.set_content_type("application/json"); + message.set_subject("Topic Test Subject"); + message.set_reply_to("topic-reply"); + + // Add custom properties + message.set_property("test_name", "test_topic_subscription_with_properties"); + message.set_property("category", "important"); + message.set_property("region", "global"); + message.set_property("priority", "high"); + + sender.send_message(message, None).await?; + sender.close().await?; + + // Receive from subscription and verify properties + let receiver = client + .create_receiver_for_subscription( + &topic_name, + &subscription_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + + let messages = receiver + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + assert!( + !messages.is_empty(), + "Should receive the message from subscription" + ); + + let received_message = &messages[0]; + + // Verify standard properties + assert_eq!(received_message.message_id(), Some(message_id).as_ref()); + assert_eq!( + received_message.correlation_id(), + Some(correlation_id).as_ref() + ); + assert_eq!( + received_message.system_properties().content_type.as_ref(), + Some("application/json".to_string()).as_ref() + ); + assert_eq!( + received_message.system_properties().subject.as_ref(), + Some("Topic Test Subject".to_string()).as_ref() + ); + assert_eq!( + received_message.system_properties().reply_to.as_ref(), + Some("topic-reply".to_string()).as_ref() + ); + + // Verify custom properties + assert_eq!( + received_message.property("test_name"), + Some("test_topic_subscription_with_properties".to_string()).as_ref() + ); + assert_eq!( + received_message.property("category"), + Some("important".to_string()).as_ref() + ); + assert_eq!( + received_message.property("region"), + Some("global".to_string()).as_ref() + ); + assert_eq!( + received_message.property("priority"), + Some("high".to_string()).as_ref() + ); + + // Complete the message + receiver + .complete_message(received_message, Some(CompleteMessageOptions::default())) + .await?; + receiver.close().await?; + client.close().await?; + + println!("Topic subscription properties test completed successfully"); + Ok(()) +} + +#[recorded::test(live)] +async fn test_topic_subscription_peek_lock(ctx: TestContext) -> Result<(), Box> { + let recording = ctx.recording(); + + // Check if topic and subscription environment variables are set + if env::var("SERVICEBUS_TOPIC_NAME").is_err() + || env::var("SERVICEBUS_SUBSCRIPTION_NAME").is_err() + { + println!("Skipping topic peek lock test - SERVICEBUS_TOPIC_NAME or SERVICEBUS_SUBSCRIPTION_NAME not set"); + return Ok(()); + } + + let namespace = get_servicebus_namespace()?; + let topic_name = get_topic_name()?; + let subscription_name = get_subscription_name()?; + + println!("Testing topic subscription PeekLock operations"); + + let client = ServiceBusClient::builder() + .open(&namespace, recording.credential()) + .await?; + + // Send a message to topic for abandon testing + let sender = client + .create_sender(&topic_name, Some(CreateSenderOptions::default())) + .await?; + let message_id = Uuid::new_v4().to_string(); + + let mut message = Message::from_string("Topic abandon test message"); + message.set_message_id(&message_id); + message.set_property("test_name", "test_topic_subscription_peek_lock"); + + sender.send_message(message, None).await?; + sender.close().await?; + + // Receive and abandon the message from subscription + let receiver = client + .create_receiver_for_subscription( + &topic_name, + &subscription_name, + Some(CreateReceiverOptions { + receive_mode: ReceiveMode::PeekLock, + sub_queue: None, + }), + ) + .await?; + + let messages = receiver + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + assert!( + !messages.is_empty(), + "Should receive the message from subscription" + ); + let received_message = &messages[0]; + assert_eq!( + received_message.message_id(), + Some(message_id.clone()).as_ref() + ); + + // Abandon the message (should make it available again) + receiver + .abandon_message(received_message, Some(AbandonMessageOptions::default())) + .await?; + + // Try to receive it again (it should be available since we abandoned it) + let messages_after_abandon = receiver + .receive_messages(1, Some(ReceiveMessageOptions::default())) + .await?; + + if !messages_after_abandon.is_empty() { + let re_received = &messages_after_abandon[0]; + assert_eq!(re_received.message_id(), Some(message_id).as_ref()); + + // Complete it this time to clean up + receiver + .complete_message(re_received, Some(CompleteMessageOptions::default())) + .await?; + } + + receiver.close().await?; + client.close().await?; + + println!("Topic subscription PeekLock test completed successfully"); + Ok(()) +} diff --git a/sdk/servicebus/ci.yml b/sdk/servicebus/ci.yml new file mode 100644 index 0000000000..eaa4685eb9 --- /dev/null +++ b/sdk/servicebus/ci.yml @@ -0,0 +1,31 @@ +# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. + +trigger: + branches: + include: + - main + - feature/* + - release/* + - hotfix/* + paths: + include: + - sdk/servicebus/ + +pr: + branches: + include: + - main + - feature/* + - release/* + - hotfix/* + paths: + include: + - sdk/servicebus/ + +extends: + template: /eng/pipelines/templates/jobs/archetype-sdk-client.yml + parameters: + ServiceDirectory: servicebus + Artifacts: + - name: azure_messaging_servicebus + safeName: AzureMessagingServiceBus