use crate::client::HttpClient;
use crate::error::Result;
use crate::types::{
Contact, ContactsResponse, CreateContactParams, ListContactsParams, SuppressParams,
SuppressReason, UpdateContactParams,
};
#[derive(Debug, Clone)]
pub struct Contacts {
client: HttpClient,
list_id: String,
}
impl Contacts {
pub fn new(client: HttpClient, list_id: impl Into<String>) -> Self {
Self {
client,
list_id: list_id.into(),
}
}
fn path(&self, suffix: &str) -> String {
format!("/contact-lists/{}/contacts{}", self.list_id, suffix)
}
pub async fn create(&self, params: &CreateContactParams) -> Result<Contact> {
self.client.post(&self.path(""), params).await
}
pub async fn get(&self, id: &str) -> Result<Contact> {
self.client.get(&self.path(&format!("/{}", id))).await
}
pub async fn update(&self, id: &str, params: &UpdateContactParams) -> Result<Contact> {
self.client
.put(&self.path(&format!("/{}", id)), params)
.await
}
pub async fn delete(&self, id: &str) -> Result<()> {
self.client.delete(&self.path(&format!("/{}", id))).await
}
pub async fn list(&self, params: &ListContactsParams) -> Result<ContactsResponse> {
self.client.get_with_params(&self.path(""), params).await
}
pub async fn suppress(&self, id: &str, reason: SuppressReason) -> Result<()> {
let params = SuppressParams { reason };
self.client
.post_no_response(&self.path(&format!("/{}/suppress", id)), ¶ms)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::ClientConfig;
use crate::types::ContactStatus;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
async fn setup() -> (MockServer, Contacts) {
let mock_server = MockServer::start().await;
let config = ClientConfig::new("test_key").base_url(mock_server.uri());
let client = HttpClient::new(config).unwrap();
let contacts = Contacts::new(client, "list_123");
(mock_server, contacts)
}
#[tokio::test]
async fn test_create_contact() {
let (mock_server, contacts) = setup().await;
Mock::given(method("POST"))
.and(path("/api/v1/contact-lists/list_123/contacts"))
.respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({
"success": true,
"data": {
"id": "contact_123",
"email": "john@example.com",
"firstName": "John",
"lastName": "Doe",
"status": "active",
"source": "api",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
}
})))
.mount(&mock_server)
.await;
let params = CreateContactParams {
email: "john@example.com".to_string(),
first_name: Some("John".to_string()),
last_name: Some("Doe".to_string()),
..Default::default()
};
let contact = contacts.create(¶ms).await.unwrap();
assert_eq!(contact.id, "contact_123");
assert_eq!(contact.status, ContactStatus::Active);
}
#[tokio::test]
async fn test_get_contact() {
let (mock_server, contacts) = setup().await;
Mock::given(method("GET"))
.and(path("/api/v1/contact-lists/list_123/contacts/contact_123"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"success": true,
"data": {
"id": "contact_123",
"email": "john@example.com",
"status": "active",
"source": "api",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
}
})))
.mount(&mock_server)
.await;
let contact = contacts.get("contact_123").await.unwrap();
assert_eq!(contact.id, "contact_123");
}
#[tokio::test]
async fn test_update_contact() {
let (mock_server, contacts) = setup().await;
Mock::given(method("PUT"))
.and(path("/api/v1/contact-lists/list_123/contacts/contact_123"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"success": true,
"data": {
"id": "contact_123",
"email": "john@example.com",
"firstName": "Johnny",
"status": "active",
"source": "api",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-02T00:00:00Z"
}
})))
.mount(&mock_server)
.await;
let params = UpdateContactParams {
first_name: Some("Johnny".to_string()),
..Default::default()
};
let contact = contacts.update("contact_123", ¶ms).await.unwrap();
assert_eq!(contact.first_name, Some("Johnny".to_string()));
}
#[tokio::test]
async fn test_delete_contact() {
let (mock_server, contacts) = setup().await;
Mock::given(method("DELETE"))
.and(path("/api/v1/contact-lists/list_123/contacts/contact_123"))
.respond_with(ResponseTemplate::new(204))
.mount(&mock_server)
.await;
contacts.delete("contact_123").await.unwrap();
}
#[tokio::test]
async fn test_list_contacts() {
let (mock_server, contacts) = setup().await;
Mock::given(method("GET"))
.and(path("/api/v1/contact-lists/list_123/contacts"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"success": true,
"data": {
"contacts": [
{"id": "contact_1", "email": "a@example.com", "status": "active", "source": "api", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z"},
{"id": "contact_2", "email": "b@example.com", "status": "active", "source": "api", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z"}
],
"pagination": {"page": 1, "limit": 10, "total": 2, "totalPages": 1, "hasNext": false, "hasPrev": false}
}
})))
.mount(&mock_server)
.await;
let result = contacts.list(&ListContactsParams::default()).await.unwrap();
assert_eq!(result.contacts.len(), 2);
}
#[tokio::test]
async fn test_suppress_contact() {
let (mock_server, contacts) = setup().await;
Mock::given(method("POST"))
.and(path(
"/api/v1/contact-lists/list_123/contacts/contact_123/suppress",
))
.respond_with(ResponseTemplate::new(204))
.mount(&mock_server)
.await;
contacts
.suppress("contact_123", SuppressReason::Manual)
.await
.unwrap();
}
}