use serde::{Deserialize, Serialize};
use crate::client::Client;
use crate::control::audit::AuditLogsClient;
use crate::control::members::{InvitationsClient, MembersClient};
use crate::control::teams::TeamsClient;
use crate::control::vaults::VaultsClient;
use crate::control::{Page, SortOrder};
use crate::Error;
#[derive(Clone)]
pub struct OrganizationControlClient {
client: Client,
organization_id: String,
}
impl OrganizationControlClient {
pub(crate) fn new(client: Client, organization_id: impl Into<String>) -> Self {
Self {
client,
organization_id: organization_id.into(),
}
}
pub fn organization_id(&self) -> &str {
&self.organization_id
}
pub fn vaults(&self) -> VaultsClient {
VaultsClient::new(self.client.clone(), self.organization_id.clone())
}
pub fn members(&self) -> MembersClient {
MembersClient::new(self.client.clone(), self.organization_id.clone())
}
pub fn teams(&self) -> TeamsClient {
TeamsClient::new(self.client.clone(), self.organization_id.clone())
}
pub fn invitations(&self) -> InvitationsClient {
InvitationsClient::new(self.client.clone(), self.organization_id.clone())
}
pub fn audit(&self) -> AuditLogsClient {
AuditLogsClient::new(self.client.clone(), self.organization_id.clone())
}
#[cfg(feature = "rest")]
pub async fn get(&self) -> Result<OrganizationInfo, Error> {
let path = format!("/control/v1/organizations/{}", self.organization_id);
self.client.inner().control_get(&path).await
}
#[cfg(not(feature = "rest"))]
pub async fn get(&self) -> Result<OrganizationInfo, Error> {
Err(Error::configuration(
"REST feature is required for control API",
))
}
#[cfg(feature = "rest")]
pub async fn update(
&self,
request: UpdateOrganizationRequest,
) -> Result<OrganizationInfo, Error> {
let path = format!("/control/v1/organizations/{}", self.organization_id);
self.client.inner().control_patch(&path, &request).await
}
#[cfg(not(feature = "rest"))]
pub async fn update(
&self,
_request: UpdateOrganizationRequest,
) -> Result<OrganizationInfo, Error> {
Err(Error::configuration(
"REST feature is required for control API",
))
}
pub fn delete(&self) -> DeleteOrganizationRequest {
DeleteOrganizationRequest {
client: self.clone(),
confirmation: None,
}
}
}
impl std::fmt::Debug for OrganizationControlClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OrganizationControlClient")
.field("organization_id", &self.organization_id)
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct OrganizationsClient {
client: Client,
}
impl OrganizationsClient {
pub(crate) fn new(client: Client) -> Self {
Self { client }
}
pub fn list(&self) -> ListOrganizationsRequest {
ListOrganizationsRequest {
client: self.client.clone(),
limit: None,
cursor: None,
sort: None,
}
}
#[cfg(feature = "rest")]
pub async fn create(
&self,
request: CreateOrganizationRequest,
) -> Result<OrganizationInfo, Error> {
self.client
.inner()
.control_post("/control/v1/organizations", &request)
.await
}
#[cfg(not(feature = "rest"))]
pub async fn create(
&self,
_request: CreateOrganizationRequest,
) -> Result<OrganizationInfo, Error> {
Err(Error::configuration(
"REST feature is required for control API",
))
}
}
impl std::fmt::Debug for OrganizationsClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OrganizationsClient")
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrganizationInfo {
pub id: String,
pub name: String,
pub display_name: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CreateOrganizationRequest {
pub name: String,
pub display_name: Option<String>,
}
impl CreateOrganizationRequest {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
display_name: None,
}
}
#[must_use]
pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
self.display_name = Some(display_name.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UpdateOrganizationRequest {
pub display_name: Option<String>,
}
impl UpdateOrganizationRequest {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
self.display_name = Some(display_name.into());
self
}
}
pub struct ListOrganizationsRequest {
client: Client,
limit: Option<usize>,
cursor: Option<String>,
sort: Option<SortOrder>,
}
impl ListOrganizationsRequest {
#[must_use]
pub fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
#[must_use]
pub fn cursor(mut self, cursor: impl Into<String>) -> Self {
self.cursor = Some(cursor.into());
self
}
#[must_use]
pub fn sort(mut self, order: SortOrder) -> Self {
self.sort = Some(order);
self
}
#[cfg(feature = "rest")]
async fn execute(self) -> Result<Page<OrganizationInfo>, Error> {
let mut path = "/control/v1/organizations".to_string();
let mut query_parts = Vec::new();
if let Some(limit) = self.limit {
query_parts.push(format!("limit={}", limit));
}
if let Some(cursor) = &self.cursor {
query_parts.push(format!("cursor={}", urlencoding::encode(cursor)));
}
if let Some(sort) = &self.sort {
query_parts.push(format!("sort={}", sort.as_str()));
}
if !query_parts.is_empty() {
path.push('?');
path.push_str(&query_parts.join("&"));
}
self.client.inner().control_get(&path).await
}
#[cfg(not(feature = "rest"))]
async fn execute(self) -> Result<Page<OrganizationInfo>, Error> {
Err(Error::configuration(
"REST feature is required for control API",
))
}
}
impl std::future::IntoFuture for ListOrganizationsRequest {
type Output = Result<Page<OrganizationInfo>, Error>;
type IntoFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
pub struct DeleteOrganizationRequest {
client: OrganizationControlClient,
confirmation: Option<String>,
}
impl DeleteOrganizationRequest {
#[must_use]
pub fn confirm(mut self, confirmation: impl Into<String>) -> Self {
self.confirmation = Some(confirmation.into());
self
}
#[cfg(feature = "rest")]
async fn execute(self) -> Result<(), Error> {
let expected = format!("DELETE {}", self.client.organization_id);
match &self.confirmation {
Some(c) if c == &expected => {
let path = format!("/control/v1/organizations/{}", self.client.organization_id);
self.client.client.inner().control_delete(&path).await
}
Some(c) => Err(Error::invalid_argument(format!(
"Invalid confirmation. Expected '{}', got '{}'",
expected, c
))),
None => Err(Error::invalid_argument(
"Deletion requires confirmation. Call .confirm(\"DELETE org_id\") first",
)),
}
}
#[cfg(not(feature = "rest"))]
async fn execute(self) -> Result<(), Error> {
let _ = self.confirmation;
Err(Error::configuration(
"REST feature is required for control API",
))
}
}
impl std::future::IntoFuture for DeleteOrganizationRequest {
type Output = Result<(), Error>;
type IntoFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::BearerCredentialsConfig;
use crate::transport::mock::MockTransport;
use std::sync::Arc;
async fn create_test_client() -> Client {
let mock_transport = Arc::new(MockTransport::new());
Client::builder()
.url("https://api.example.com")
.credentials(BearerCredentialsConfig::new("test"))
.build_with_transport(mock_transport)
.await
.unwrap()
}
#[test]
fn test_create_organization_request() {
let req = CreateOrganizationRequest::new("my-org").with_display_name("My Organization");
assert_eq!(req.name, "my-org");
assert_eq!(req.display_name, Some("My Organization".to_string()));
}
#[test]
fn test_update_organization_request() {
let req = UpdateOrganizationRequest::new().with_display_name("New Name");
assert_eq!(req.display_name, Some("New Name".to_string()));
}
#[test]
fn test_delete_organization_confirmation_validation() {
let org_id = "org_test";
let expected = format!("DELETE {}", org_id);
assert_eq!(expected, "DELETE org_test");
assert_ne!("DELETE wrong_org", expected);
}
#[tokio::test]
async fn test_organization_control_client_accessors() {
let client = create_test_client().await;
let org = OrganizationControlClient::new(client, "org_test");
assert_eq!(org.organization_id(), "org_test");
}
#[tokio::test]
async fn test_organization_control_client_debug() {
let client = create_test_client().await;
let org = OrganizationControlClient::new(client, "org_test");
let debug = format!("{:?}", org);
assert!(debug.contains("OrganizationControlClient"));
assert!(debug.contains("org_test"));
}
#[tokio::test]
async fn test_organizations_client_debug() {
let client = create_test_client().await;
let orgs = OrganizationsClient::new(client);
let debug = format!("{:?}", orgs);
assert!(debug.contains("OrganizationsClient"));
}
#[tokio::test]
async fn test_organization_sub_clients() {
let client = create_test_client().await;
let org = OrganizationControlClient::new(client, "org_test");
let _vaults = org.vaults();
let _members = org.members();
let _teams = org.teams();
let _invitations = org.invitations();
let _audit_logs = org.audit();
}
#[tokio::test]
async fn test_list_organizations_request_builders() {
let client = create_test_client().await;
let orgs = OrganizationsClient::new(client);
let _request = orgs
.list()
.limit(50)
.cursor("cursor_xyz")
.sort(SortOrder::Descending);
}
#[tokio::test]
async fn test_delete_organization_request_builder() {
let client = create_test_client().await;
let org = OrganizationControlClient::new(client, "org_test");
let _request = org.delete().confirm("DELETE org_test");
}
}
#[cfg(all(test, feature = "rest"))]
mod wiremock_tests {
use super::*;
use crate::auth::BearerCredentialsConfig;
use crate::Client;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
async fn create_mock_client(server: &MockServer) -> Client {
Client::builder()
.url(server.uri())
.insecure()
.credentials(BearerCredentialsConfig::new("test_token"))
.build()
.await
.unwrap()
}
#[tokio::test]
async fn test_list_organizations() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/control/v1/organizations"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"items": [
{
"id": "org_123",
"name": "my-org",
"display_name": "My Organization",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-02T00:00:00Z"
}
],
"page_info": {
"has_next": false,
"next_cursor": null,
"total_count": 1
}
})))
.mount(&server)
.await;
let client = create_mock_client(&server).await;
let orgs = OrganizationsClient::new(client);
let result = orgs.list().await;
assert!(result.is_ok());
let page = result.unwrap();
assert_eq!(page.items.len(), 1);
assert_eq!(page.items[0].name, "my-org");
}
#[tokio::test]
async fn test_list_organizations_with_filters() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/control/v1/organizations"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"items": [],
"page_info": {
"has_next": false,
"next_cursor": null,
"total_count": 0
}
})))
.mount(&server)
.await;
let client = create_mock_client(&server).await;
let orgs = OrganizationsClient::new(client);
let result = orgs
.list()
.limit(10)
.cursor("cursor_abc")
.sort(SortOrder::Descending)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_create_organization() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/control/v1/organizations"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "org_new",
"name": "new-org",
"display_name": "New Organization",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
})))
.mount(&server)
.await;
let client = create_mock_client(&server).await;
let orgs = OrganizationsClient::new(client);
let request =
CreateOrganizationRequest::new("new-org").with_display_name("New Organization");
let result = orgs.create(request).await;
assert!(result.is_ok());
let org = result.unwrap();
assert_eq!(org.name, "new-org");
}
#[tokio::test]
async fn test_get_organization() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/control/v1/organizations/org_abc"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "org_abc",
"name": "test-org",
"display_name": "Test Organization",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-02T00:00:00Z"
})))
.mount(&server)
.await;
let client = create_mock_client(&server).await;
let org = OrganizationControlClient::new(client, "org_abc");
let result = org.get().await;
assert!(result.is_ok());
let info = result.unwrap();
assert_eq!(info.id, "org_abc");
}
#[tokio::test]
async fn test_update_organization() {
let server = MockServer::start().await;
Mock::given(method("PATCH"))
.and(path("/control/v1/organizations/org_abc"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "org_abc",
"name": "test-org",
"display_name": "Updated Organization",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-03T00:00:00Z"
})))
.mount(&server)
.await;
let client = create_mock_client(&server).await;
let org = OrganizationControlClient::new(client, "org_abc");
let request =
UpdateOrganizationRequest::default().with_display_name("Updated Organization");
let result = org.update(request).await;
assert!(result.is_ok());
let info = result.unwrap();
assert_eq!(info.display_name, Some("Updated Organization".to_string()));
}
#[tokio::test]
async fn test_delete_organization() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/control/v1/organizations/org_abc"))
.respond_with(ResponseTemplate::new(204))
.mount(&server)
.await;
let client = create_mock_client(&server).await;
let org = OrganizationControlClient::new(client, "org_abc");
let result = org.delete().confirm("DELETE org_abc").await;
assert!(result.is_ok());
}
}