mod common;
use common::{
create_test_client, mock_list_scheduled_success, mock_schedule_success, setup_mock_server,
};
use common::{mock_cancel_scheduled_success, mock_get_scheduled_success};
use sendly::{Error, ListScheduledMessagesOptions, ScheduleMessageRequest, ScheduledMessageStatus};
use serde_json::json;
use wiremock::matchers::{method, path, path_regex, query_param};
use wiremock::{Mock, ResponseTemplate};
#[tokio::test]
async fn test_schedule_success() {
let mock_server = setup_mock_server().await;
mock_schedule_success().mount(&mock_server).await;
let client = create_test_client(&mock_server.uri());
let result = client
.messages()
.schedule(ScheduleMessageRequest {
to: "+15551234567".to_string(),
text: "Scheduled message".to_string(),
scheduled_at: "2025-01-20T10:00:00Z".to_string(),
from: None,
message_type: None,
metadata: None,
})
.await;
assert!(result.is_ok());
let scheduled = result.unwrap();
assert_eq!(scheduled.id, "sched_abc123");
assert_eq!(scheduled.to, "+15551234567");
assert_eq!(scheduled.text, "Scheduled message");
assert_eq!(scheduled.status, ScheduledMessageStatus::Scheduled);
assert_eq!(scheduled.credits_reserved, 1);
}
#[tokio::test]
async fn test_schedule_invalid_phone() {
let mock_server = setup_mock_server().await;
let client = create_test_client(&mock_server.uri());
let result = client
.messages()
.schedule(ScheduleMessageRequest {
to: "invalid-phone".to_string(),
text: "Test".to_string(),
scheduled_at: "2025-01-20T10:00:00Z".to_string(),
from: None,
message_type: None,
metadata: None,
})
.await;
assert!(result.is_err());
match result.unwrap_err() {
Error::Validation { message } => {
assert!(message.contains("Invalid phone number format"));
}
_ => panic!("Expected Validation error"),
}
}
#[tokio::test]
async fn test_schedule_empty_text() {
let mock_server = setup_mock_server().await;
let client = create_test_client(&mock_server.uri());
let result = client
.messages()
.schedule(ScheduleMessageRequest {
to: "+15551234567".to_string(),
text: "".to_string(),
scheduled_at: "2025-01-20T10:00:00Z".to_string(),
from: None,
message_type: None,
metadata: None,
})
.await;
assert!(result.is_err());
match result.unwrap_err() {
Error::Validation { message } => {
assert!(message.contains("Message text is required"));
}
_ => panic!("Expected Validation error"),
}
}
#[tokio::test]
async fn test_schedule_text_too_long() {
let mock_server = setup_mock_server().await;
let client = create_test_client(&mock_server.uri());
let long_text = "a".repeat(1601);
let result = client
.messages()
.schedule(ScheduleMessageRequest {
to: "+15551234567".to_string(),
text: long_text,
scheduled_at: "2025-01-20T10:00:00Z".to_string(),
from: None,
message_type: None,
metadata: None,
})
.await;
assert!(result.is_err());
match result.unwrap_err() {
Error::Validation { message } => {
assert!(message.contains("exceeds maximum length"));
}
_ => panic!("Expected Validation error"),
}
}
#[tokio::test]
async fn test_schedule_empty_scheduled_at() {
let mock_server = setup_mock_server().await;
let client = create_test_client(&mock_server.uri());
let result = client
.messages()
.schedule(ScheduleMessageRequest {
to: "+15551234567".to_string(),
text: "Test".to_string(),
scheduled_at: "".to_string(),
from: None,
message_type: None,
metadata: None,
})
.await;
assert!(result.is_err());
match result.unwrap_err() {
Error::Validation { message } => {
assert!(message.contains("scheduled_at is required"));
}
_ => panic!("Expected Validation error"),
}
}
#[tokio::test]
async fn test_schedule_authentication_error() {
let mock_server = setup_mock_server().await;
Mock::given(method("POST"))
.and(path("/messages/schedule"))
.respond_with(ResponseTemplate::new(401).set_body_json(json!({
"error": "Invalid API key"
})))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let result = client
.messages()
.schedule(ScheduleMessageRequest {
to: "+15551234567".to_string(),
text: "Test".to_string(),
scheduled_at: "2025-01-20T10:00:00Z".to_string(),
from: None,
message_type: None,
metadata: None,
})
.await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::Authentication { .. }));
}
#[tokio::test]
async fn test_schedule_insufficient_credits() {
let mock_server = setup_mock_server().await;
Mock::given(method("POST"))
.and(path("/messages/schedule"))
.respond_with(ResponseTemplate::new(402).set_body_json(json!({
"error": "Insufficient credits"
})))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let result = client
.messages()
.schedule(ScheduleMessageRequest {
to: "+15551234567".to_string(),
text: "Test".to_string(),
scheduled_at: "2025-01-20T10:00:00Z".to_string(),
from: None,
message_type: None,
metadata: None,
})
.await;
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
Error::InsufficientCredits { .. }
));
}
#[tokio::test]
async fn test_schedule_rate_limit() {
let mock_server = setup_mock_server().await;
Mock::given(method("POST"))
.and(path("/messages/schedule"))
.respond_with(
ResponseTemplate::new(429)
.set_body_json(json!({"error": "Rate limit exceeded"}))
.insert_header("Retry-After", "60"),
)
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let result = client
.messages()
.schedule(ScheduleMessageRequest {
to: "+15551234567".to_string(),
text: "Test".to_string(),
scheduled_at: "2025-01-20T10:00:00Z".to_string(),
from: None,
message_type: None,
metadata: None,
})
.await;
assert!(result.is_err());
match result.unwrap_err() {
Error::RateLimit { retry_after, .. } => {
assert_eq!(retry_after, Some(60));
}
_ => panic!("Expected RateLimit error"),
}
}
#[tokio::test]
async fn test_schedule_server_error() {
let mock_server = setup_mock_server().await;
Mock::given(method("POST"))
.and(path("/messages/schedule"))
.respond_with(ResponseTemplate::new(500).set_body_json(json!({
"error": "Internal server error"
})))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let result = client
.messages()
.schedule(ScheduleMessageRequest {
to: "+15551234567".to_string(),
text: "Test".to_string(),
scheduled_at: "2025-01-20T10:00:00Z".to_string(),
from: None,
message_type: None,
metadata: None,
})
.await;
assert!(result.is_err());
match result.unwrap_err() {
Error::Api { status_code, .. } => {
assert_eq!(status_code, 500);
}
_ => panic!("Expected Api error"),
}
}
#[tokio::test]
async fn test_list_scheduled_success() {
let mock_server = setup_mock_server().await;
mock_list_scheduled_success().mount(&mock_server).await;
let client = create_test_client(&mock_server.uri());
let result = client.messages().list_scheduled(None).await;
assert!(result.is_ok());
let list = result.unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list.data[0].id, "sched_1");
assert_eq!(list.data[0].status, ScheduledMessageStatus::Scheduled);
}
#[tokio::test]
async fn test_list_scheduled_with_options() {
let mock_server = setup_mock_server().await;
Mock::given(method("GET"))
.and(path("/messages/scheduled"))
.and(query_param("limit", "50"))
.and(query_param("offset", "10"))
.and(query_param("status", "scheduled"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [],
"count": 0
})))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let options = ListScheduledMessagesOptions::new()
.limit(50)
.offset(10)
.status(ScheduledMessageStatus::Scheduled);
let result = client.messages().list_scheduled(Some(options)).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_list_scheduled_authentication_error() {
let mock_server = setup_mock_server().await;
Mock::given(method("GET"))
.and(path("/messages/scheduled"))
.respond_with(ResponseTemplate::new(401).set_body_json(json!({
"error": "Invalid API key"
})))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let result = client.messages().list_scheduled(None).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::Authentication { .. }));
}
#[tokio::test]
async fn test_list_scheduled_not_found() {
let mock_server = setup_mock_server().await;
Mock::given(method("GET"))
.and(path("/messages/scheduled"))
.respond_with(ResponseTemplate::new(404).set_body_json(json!({
"error": "Resource not found"
})))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let result = client.messages().list_scheduled(None).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::NotFound { .. }));
}
#[tokio::test]
async fn test_list_scheduled_rate_limit() {
let mock_server = setup_mock_server().await;
Mock::given(method("GET"))
.and(path("/messages/scheduled"))
.respond_with(
ResponseTemplate::new(429)
.set_body_json(json!({"error": "Rate limit exceeded"}))
.insert_header("Retry-After", "30"),
)
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let result = client.messages().list_scheduled(None).await;
assert!(result.is_err());
match result.unwrap_err() {
Error::RateLimit { retry_after, .. } => {
assert_eq!(retry_after, Some(30));
}
_ => panic!("Expected RateLimit error"),
}
}
#[tokio::test]
async fn test_list_scheduled_server_error() {
let mock_server = setup_mock_server().await;
Mock::given(method("GET"))
.and(path("/messages/scheduled"))
.respond_with(ResponseTemplate::new(500).set_body_json(json!({
"error": "Internal server error"
})))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let result = client.messages().list_scheduled(None).await;
assert!(result.is_err());
match result.unwrap_err() {
Error::Api { status_code, .. } => {
assert_eq!(status_code, 500);
}
_ => panic!("Expected Api error"),
}
}
#[tokio::test]
async fn test_get_scheduled_success() {
let mock_server = setup_mock_server().await;
mock_get_scheduled_success().mount(&mock_server).await;
let client = create_test_client(&mock_server.uri());
let result = client.messages().get_scheduled("sched_abc123").await;
assert!(result.is_ok());
let scheduled = result.unwrap();
assert_eq!(scheduled.id, "sched_abc123");
assert_eq!(scheduled.status, ScheduledMessageStatus::Scheduled);
}
#[tokio::test]
async fn test_get_scheduled_empty_id() {
let mock_server = setup_mock_server().await;
let client = create_test_client(&mock_server.uri());
let result = client.messages().get_scheduled("").await;
assert!(result.is_err());
match result.unwrap_err() {
Error::Validation { message } => {
assert!(message.contains("Scheduled message ID is required"));
}
_ => panic!("Expected Validation error"),
}
}
#[tokio::test]
async fn test_get_scheduled_not_found() {
let mock_server = setup_mock_server().await;
Mock::given(method("GET"))
.and(path_regex(r"^/messages/scheduled/.*$"))
.respond_with(ResponseTemplate::new(404).set_body_json(json!({
"error": "Scheduled message not found"
})))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let result = client.messages().get_scheduled("sched_nonexistent").await;
assert!(result.is_err());
match result.unwrap_err() {
Error::NotFound { message } => {
assert!(message.contains("not found"));
}
_ => panic!("Expected NotFound error"),
}
}
#[tokio::test]
async fn test_get_scheduled_authentication_error() {
let mock_server = setup_mock_server().await;
Mock::given(method("GET"))
.and(path("/messages/scheduled/sched_test"))
.respond_with(ResponseTemplate::new(401).set_body_json(json!({
"error": "Invalid API key"
})))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let result = client.messages().get_scheduled("sched_test").await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::Authentication { .. }));
}
#[tokio::test]
async fn test_get_scheduled_rate_limit() {
let mock_server = setup_mock_server().await;
Mock::given(method("GET"))
.and(path("/messages/scheduled/sched_test"))
.respond_with(
ResponseTemplate::new(429)
.set_body_json(json!({"error": "Rate limit exceeded"}))
.insert_header("Retry-After", "45"),
)
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let result = client.messages().get_scheduled("sched_test").await;
assert!(result.is_err());
match result.unwrap_err() {
Error::RateLimit { retry_after, .. } => {
assert_eq!(retry_after, Some(45));
}
_ => panic!("Expected RateLimit error"),
}
}
#[tokio::test]
async fn test_get_scheduled_server_error() {
let mock_server = setup_mock_server().await;
Mock::given(method("GET"))
.and(path("/messages/scheduled/sched_test"))
.respond_with(ResponseTemplate::new(500).set_body_json(json!({
"error": "Internal server error"
})))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let result = client.messages().get_scheduled("sched_test").await;
assert!(result.is_err());
match result.unwrap_err() {
Error::Api { status_code, .. } => {
assert_eq!(status_code, 500);
}
_ => panic!("Expected Api error"),
}
}
#[tokio::test]
async fn test_cancel_scheduled_success() {
let mock_server = setup_mock_server().await;
mock_cancel_scheduled_success().mount(&mock_server).await;
let client = create_test_client(&mock_server.uri());
let result = client.messages().cancel_scheduled("sched_abc123").await;
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.id, "sched_abc123");
assert_eq!(response.status, ScheduledMessageStatus::Cancelled);
assert_eq!(response.credits_refunded, 1);
}
#[tokio::test]
async fn test_cancel_scheduled_empty_id() {
let mock_server = setup_mock_server().await;
let client = create_test_client(&mock_server.uri());
let result = client.messages().cancel_scheduled("").await;
assert!(result.is_err());
match result.unwrap_err() {
Error::Validation { message } => {
assert!(message.contains("Scheduled message ID is required"));
}
_ => panic!("Expected Validation error"),
}
}
#[tokio::test]
async fn test_cancel_scheduled_not_found() {
let mock_server = setup_mock_server().await;
Mock::given(method("DELETE"))
.and(path_regex(r"^/messages/scheduled/.*$"))
.respond_with(ResponseTemplate::new(404).set_body_json(json!({
"error": "Scheduled message not found"
})))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let result = client
.messages()
.cancel_scheduled("sched_nonexistent")
.await;
assert!(result.is_err());
match result.unwrap_err() {
Error::NotFound { message } => {
assert!(message.contains("not found"));
}
_ => panic!("Expected NotFound error"),
}
}
#[tokio::test]
async fn test_cancel_scheduled_authentication_error() {
let mock_server = setup_mock_server().await;
Mock::given(method("DELETE"))
.and(path("/messages/scheduled/sched_test"))
.respond_with(ResponseTemplate::new(401).set_body_json(json!({
"error": "Invalid API key"
})))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let result = client.messages().cancel_scheduled("sched_test").await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::Authentication { .. }));
}
#[tokio::test]
async fn test_cancel_scheduled_rate_limit() {
let mock_server = setup_mock_server().await;
Mock::given(method("DELETE"))
.and(path("/messages/scheduled/sched_test"))
.respond_with(
ResponseTemplate::new(429)
.set_body_json(json!({"error": "Rate limit exceeded"}))
.insert_header("Retry-After", "60"),
)
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let result = client.messages().cancel_scheduled("sched_test").await;
assert!(result.is_err());
match result.unwrap_err() {
Error::RateLimit { retry_after, .. } => {
assert_eq!(retry_after, Some(60));
}
_ => panic!("Expected RateLimit error"),
}
}
#[tokio::test]
async fn test_cancel_scheduled_server_error() {
let mock_server = setup_mock_server().await;
Mock::given(method("DELETE"))
.and(path("/messages/scheduled/sched_test"))
.respond_with(ResponseTemplate::new(500).set_body_json(json!({
"error": "Internal server error"
})))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let result = client.messages().cancel_scheduled("sched_test").await;
assert!(result.is_err());
match result.unwrap_err() {
Error::Api { status_code, .. } => {
assert_eq!(status_code, 500);
}
_ => panic!("Expected Api error"),
}
}