use serde_derive::{Deserialize, Serialize};
use serde_json::{Map, Value};
#[derive(Serialize, Deserialize)]
pub struct ListDirectoriesOptions {
pub domain: Option<String>,
pub search: Option<String>,
pub after: Option<String>,
pub before: Option<String>,
pub limit: Option<u8>,
}
#[derive(Serialize, Deserialize)]
pub struct ListUsersOptions {
pub directory: Option<String>,
pub group: Option<String>,
pub after: Option<String>,
pub before: Option<String>,
pub limit: Option<u8>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct User {
pub id: String,
#[serde(rename = "idp_id")]
pub idp_id: String,
pub emails: Vec<Email>,
#[serde(rename = "first_name")]
pub first_name: String,
#[serde(rename = "last_name")]
pub last_name: String,
pub username: String,
pub groups: Vec<Group>,
pub state: String,
#[serde(rename = "raw_attributes")]
pub raw_attributes: Option<Map<String, Value>>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Email {
pub primary: bool,
#[serde(rename = "type")]
pub type_field: String,
pub value: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Group {
pub id: String,
pub name: String,
#[serde(rename = "raw_attributes")]
pub raw_attributes: Option<Map<String, Value>>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Directory {
pub id: String,
pub domain: String,
pub name: String,
#[serde(rename = "organization_id")]
pub organization_id: String,
pub state: String,
#[serde(rename = "type")]
pub type_field: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListDirectoriesResponse {
pub list_metadata: Option<ListMetadata>,
pub data: Vec<Directory>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListUsersResponse {
pub list_metadata: ListMetadata,
pub data: Vec<User>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListMetadata {
pub before: Option<String>,
pub after: Option<String>,
}
#[cfg(test)]
mod tests {
use crate::client::Client;
use crate::directory_sync::{ListDirectoriesOptions, ListUsersOptions};
use expect_test::expect;
use httpmock::Method::DELETE;
use httpmock::Method::GET;
use httpmock::MockServer;
const TEST_API_KEY: &str = "TEST_API_KEY";
#[async_std::test]
async fn get_user() -> Result<(), Box<dyn std::error::Error>> {
const USER_ID: &str = "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ";
let server = MockServer::start();
let client = Client::new(Some(server.base_url()), TEST_API_KEY.to_string());
let mock = server.mock(|when, then| {
when.method(GET)
.path(format!("/directory_users/{}", USER_ID))
.header("Authorization", &format!("Bearer {}", TEST_API_KEY));
then.status(200)
.header("Content-Type", mime::JSON.as_str())
.body(r#"{"id":"directory_user_01E1JG7J09H96KYP8HM9B0G5SJ","idp_id":"2836","first_name":"Marcelina","last_name":"Davis","emails":[{"primary":true,"type":"work","value":"marcelina@foo-corp.com"}],"username":"marcelina@foo-corp.com","groups":[{"id":"directory_group_01E64QTDNS0EGJ0FMCVY9BWGZT","name":"Engineering"}],"state":"active"}"#);
});
let user = client.get_user(USER_ID).await?;
mock.assert();
expect![[r#"
User {
id: "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ",
idp_id: "2836",
emails: [
Email {
primary: true,
type_field: "work",
value: "marcelina@foo-corp.com",
},
],
first_name: "Marcelina",
last_name: "Davis",
username: "marcelina@foo-corp.com",
groups: [
Group {
id: "directory_group_01E64QTDNS0EGJ0FMCVY9BWGZT",
name: "Engineering",
raw_attributes: None,
},
],
state: "active",
raw_attributes: None,
}
"#]]
.assert_debug_eq(&user);
Ok(())
}
#[async_std::test]
async fn get_user_not_found() -> Result<(), Box<dyn std::error::Error>> {
const USER_ID: &str = "workos_user_xxxx";
let server = MockServer::start();
let client = Client::new(Some(server.base_url()), TEST_API_KEY.to_string());
let mock = server.mock(|when, then| {
when.method(GET)
.path(format!("/directory_users/{}", USER_ID))
.header("Authorization", &format!("Bearer {}", TEST_API_KEY));
then.status(404);
});
let res = client.get_user(USER_ID).await;
mock.assert();
expect![[r#"resource was not found, id: `workos_user_xxxx`"#]]
.assert_eq(&res.unwrap_err().to_string());
Ok(())
}
#[async_std::test]
async fn get_user_unknown_error() -> Result<(), Box<dyn std::error::Error>> {
const USER_ID: &str = "workos_user_xxxx";
let server = MockServer::start();
let client = Client::new(Some(server.base_url()), TEST_API_KEY.to_string());
let mock = server.mock(|when, then| {
when.method(GET)
.path(format!("/directory_users/{}", USER_ID))
.header("Authorization", &format!("Bearer {}", TEST_API_KEY));
then.status(500);
});
let res = client.get_user(USER_ID).await;
mock.assert();
expect![[r#"unknown error"#]].assert_eq(&res.unwrap_err().to_string());
Ok(())
}
#[async_std::test]
async fn get_group() -> Result<(), Box<dyn std::error::Error>> {
const GROUP_ID: &str = "directory_group_01E1JJS84MFPPQ3G655FHTKX6Z";
let server = MockServer::start();
let client = Client::new(Some(server.base_url()), TEST_API_KEY.to_string());
let mock = server.mock(|when, then| {
when.method(GET)
.path(format!("/directory_groups/{}", GROUP_ID))
.header("Authorization", &format!("Bearer {}", TEST_API_KEY));
then.status(200)
.header("Content-Type", mime::JSON.as_str())
.body(r#"{"id":"directory_group_01E1JJS84MFPPQ3G655FHTKX6Z","name":"Developers"}"#);
});
let group = client.get_group(GROUP_ID).await?;
mock.assert();
expect![[r#"
Group {
id: "directory_group_01E1JJS84MFPPQ3G655FHTKX6Z",
name: "Developers",
raw_attributes: None,
}
"#]]
.assert_debug_eq(&group);
Ok(())
}
#[async_std::test]
async fn list_directories() -> Result<(), Box<dyn std::error::Error>> {
const DOMAIN: &str = "domain";
const SEARCH: &str = "search";
const AFTER: &str = "after";
const BEFORE: &str = "before";
const LIMIT: u8 = 10;
let server = MockServer::start();
let client = Client::new(Some(server.base_url()), TEST_API_KEY.to_string());
let mock = server.mock(|when, then| {
when.method(GET)
.path("/directories")
.query_param("domain", DOMAIN)
.query_param("search", SEARCH)
.query_param("after", AFTER)
.query_param("before", BEFORE)
.query_param("limit", &LIMIT.to_string())
.header("Authorization", &format!("Bearer {}", TEST_API_KEY));
then.status(200)
.header("Content-Type", mime::JSON.as_str())
.body(r#"{ "data": [{ "id": "directory_01ECAZ4NV9QMV47GW873HDCX74", "domain": "foo-corp.com", "name": "Foo Corp", "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", "object": "directory", "state": "unlinked", "type": "gsuite directory" }, { "id": "directory_01E8CS3GSBEBZ1F1CZAEE3KHDG", "domain": "foo-corp.com", "external_key": "r3NDlInUnAe6i4wG", "name": "Foo Corp", "organization_id": "org_01EHZNVPK3SFK441A1RGBFPANT", "object": "directory", "state": "linked", "type": "okta scim v2.0" }] }"#);
});
let directories = client
.list_directories(ListDirectoriesOptions {
domain: Some(DOMAIN.to_string()),
search: Some(SEARCH.to_string()),
after: Some(AFTER.to_string()),
before: Some(BEFORE.to_string()),
limit: Some(LIMIT),
})
.await?;
mock.assert();
expect![[r#"
ListDirectoriesResponse {
list_metadata: None,
data: [
Directory {
id: "directory_01ECAZ4NV9QMV47GW873HDCX74",
domain: "foo-corp.com",
name: "Foo Corp",
organization_id: "org_01EHZNVPK3SFK441A1RGBFSHRT",
state: "unlinked",
type_field: "gsuite directory",
},
Directory {
id: "directory_01E8CS3GSBEBZ1F1CZAEE3KHDG",
domain: "foo-corp.com",
name: "Foo Corp",
organization_id: "org_01EHZNVPK3SFK441A1RGBFPANT",
state: "linked",
type_field: "okta scim v2.0",
},
],
}
"#]]
.assert_debug_eq(&directories);
Ok(())
}
#[async_std::test]
async fn list_users() -> Result<(), Box<dyn std::error::Error>> {
const DIRECTORY: &str = "directory";
const GROUP: &str = "group";
const AFTER: &str = "after";
const BEFORE: &str = "before";
const LIMIT: u8 = 10;
let server = MockServer::start();
let client = Client::new(Some(server.base_url()), TEST_API_KEY.to_string());
let mock = server.mock(|when, then| {
when.method(GET)
.path("/directory_users")
.query_param("directory", DIRECTORY)
.query_param("group", GROUP)
.query_param("after", AFTER)
.query_param("before", BEFORE)
.query_param("limit", &LIMIT.to_string())
.header("Authorization", &format!("Bearer {}", TEST_API_KEY));
then.status(200)
.header("Content-Type", mime::JSON.as_str())
.body(r#"{"data":[{"id":"directory_user_01E1JJHG3BFJ3FNRRHSFWEBNCS","idp_id":"1902","emails":[{"primary":true,"type":"work","value":"jan@foo-corp.com"}],"first_name":"Jan","last_name":"Brown","username":"jan@foo-corp.com","groups":[{"id":"directory_group_01E64QTDNS0EGJ0FMCVY9BWGZT","name":"Engineering"}],"state":"active"},{"id":"directory_user_01E1JJHG10ANRA2V6PAX3GD7TE","idp_id":"8953","emails":[{"primary":true,"type":"work","value":"rosalinda@foo-corp.com"}],"first_name":"Rosalinda","last_name":"Swift","username":"rosalinda@foo-corp.com","groups":[{"id":"directory_group_01E64QTDNS0EGJ0FMCVY9BWGZT","name":"Engineering"}],"state":"active"}],"object":"list","listMetadata":{"after":"directory_user_01E4RH82CC8QAP8JTRCTNDSS4C","before":"directory_user_01E4RH828021B9ZZB8KH8E2Z1W"}}"#);
});
let users = client
.list_users(ListUsersOptions {
directory: Some(DIRECTORY.to_string()),
group: Some(GROUP.to_string()),
after: Some(AFTER.to_string()),
before: Some(BEFORE.to_string()),
limit: Some(LIMIT),
})
.await?;
mock.assert();
expect![[r#"
ListUsersResponse {
list_metadata: ListMetadata {
before: Some(
"directory_user_01E4RH828021B9ZZB8KH8E2Z1W",
),
after: Some(
"directory_user_01E4RH82CC8QAP8JTRCTNDSS4C",
),
},
data: [
User {
id: "directory_user_01E1JJHG3BFJ3FNRRHSFWEBNCS",
idp_id: "1902",
emails: [
Email {
primary: true,
type_field: "work",
value: "jan@foo-corp.com",
},
],
first_name: "Jan",
last_name: "Brown",
username: "jan@foo-corp.com",
groups: [
Group {
id: "directory_group_01E64QTDNS0EGJ0FMCVY9BWGZT",
name: "Engineering",
raw_attributes: None,
},
],
state: "active",
raw_attributes: None,
},
User {
id: "directory_user_01E1JJHG10ANRA2V6PAX3GD7TE",
idp_id: "8953",
emails: [
Email {
primary: true,
type_field: "work",
value: "rosalinda@foo-corp.com",
},
],
first_name: "Rosalinda",
last_name: "Swift",
username: "rosalinda@foo-corp.com",
groups: [
Group {
id: "directory_group_01E64QTDNS0EGJ0FMCVY9BWGZT",
name: "Engineering",
raw_attributes: None,
},
],
state: "active",
raw_attributes: None,
},
],
}
"#]]
.assert_debug_eq(&users);
Ok(())
}
#[async_std::test]
async fn delete_directory() -> Result<(), Box<dyn std::error::Error>> {
const DIRECTORY_ID: &str = "directory_01ECAZ4NV9QMV47GW873HDCX74";
let server = MockServer::start();
let client = Client::new(Some(server.base_url()), TEST_API_KEY.to_string());
let mock = server.mock(|when, then| {
when.method(DELETE)
.path(format!("/directories/{}", DIRECTORY_ID))
.header("Authorization", &format!("Bearer {}", TEST_API_KEY));
then.status(200);
});
client.delete_directory(DIRECTORY_ID).await?;
mock.assert();
Ok(())
}
}