Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stripe integration #101

Merged
merged 5 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:

- name: Run Integration Tests
env:
TEST_CREDENTIAL: ${{ secrets.TEST_CREDENTIAL }}
PASSWORD: ${{ secrets.PASSWORD }}
CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }}
run: ./test/expect

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

This file was deleted.

2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions examples/config/rpc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ topic="events"

[auth]
url="https://txpipe.us.auth0.com"

[stripe]
url = "https://api.stripe.com/v1"
api_key = ""
8 changes: 8 additions & 0 deletions src/bin/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,17 @@ struct Auth {
url: String,
}
#[derive(Debug, Clone, Deserialize)]
struct Stripe {
url: String,
api_key: String,
}
#[derive(Debug, Clone, Deserialize)]
struct Config {
addr: String,
db_path: String,
crds_path: PathBuf,
auth: Auth,
stripe: Stripe,
secret: String,
topic: String,
kafka_producer: HashMap<String, String>,
Expand Down Expand Up @@ -69,6 +75,8 @@ impl From<Config> for GrpcConfig {
db_path: value.db_path,
crds_path: value.crds_path,
auth_url: value.auth.url,
stripe_url: value.stripe.url,
stripe_api_key: value.stripe.api_key,
secret: value.secret,
kafka: value.kafka_producer,
topic: value.topic,
Expand Down
11 changes: 9 additions & 2 deletions src/domain/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@ use super::{error::Error, project::cache::ProjectDrivenCache};

use crate::domain::Result;

#[async_trait::async_trait]
pub trait Auth0Driven: Send + Sync {
fn verify(&self, token: &str) -> Result<String>;
async fn find_info(&self, token: &str) -> Result<(String, String)>;
}

pub type UserId = String;
pub type Token = String;
pub type SecretId = String;

#[derive(Debug, Clone)]
pub enum Credential {
Auth0(UserId),
Auth0(UserId, Token),
ApiKey(SecretId),
}

Expand All @@ -19,7 +26,7 @@ pub async fn assert_project_permission(
project_id: &str,
) -> Result<()> {
match credential {
Credential::Auth0(user_id) => {
Credential::Auth0(user_id, _) => {
let result = project_cache
.find_user_permission(user_id, project_id)
.await?;
Expand Down
9 changes: 9 additions & 0 deletions src/domain/event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ pub struct ProjectCreated {
pub namespace: String,
pub owner: String,
pub status: String,
pub billing_provider: String,
pub billing_provider_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub billing_subscription_id: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
Expand All @@ -30,7 +34,9 @@ into_event!(ProjectCreated);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectUpdated {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
pub updated_at: DateTime<Utc>,
}
Expand Down Expand Up @@ -176,6 +182,9 @@ mod tests {
namespace: "sonic-vegas".into(),
owner: "user id".into(),
status: ProjectStatus::Active.to_string(),
billing_provider: "stripe".into(),
billing_provider_id: "stripe id".into(),
billing_subscription_id: None,
created_at: Utc::now(),
updated_at: Utc::now(),
}
Expand Down
135 changes: 100 additions & 35 deletions src/domain/project/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use tracing::{error, info};
use uuid::Uuid;

use crate::domain::{
auth::{Credential, UserId},
auth::{Auth0Driven, Credential, Token, UserId},
error::Error,
event::{
EventDrivenBridge, ProjectCreated, ProjectDeleted, ProjectSecretCreated, ProjectUpdated,
Expand All @@ -20,31 +20,39 @@ use crate::domain::{
utils, Result, MAX_SECRET, PAGE_SIZE_DEFAULT, PAGE_SIZE_MAX,
};

use super::{cache::ProjectDrivenCache, Project, ProjectSecret};
use super::{cache::ProjectDrivenCache, Project, ProjectSecret, StripeDriven};

pub async fn fetch(cache: Arc<dyn ProjectDrivenCache>, cmd: FetchCmd) -> Result<Vec<Project>> {
let user_id = assert_credential(&cmd.credential)?;
let (user_id, _) = assert_credential(&cmd.credential)?;

cache.find(&user_id, &cmd.page, &cmd.page_size).await
}

pub async fn create(
cache: Arc<dyn ProjectDrivenCache>,
event: Arc<dyn EventDrivenBridge>,
auth0: Arc<dyn Auth0Driven>,
stripe: Arc<dyn StripeDriven>,
cmd: CreateCmd,
) -> Result<()> {
let user_id = assert_credential(&cmd.credential)?;
let (user_id, token) = assert_credential(&cmd.credential)?;

if cache.find_by_namespace(&cmd.namespace).await?.is_some() {
return Err(Error::CommandMalformed("invalid project namespace".into()));
}

let (name, email) = auth0.find_info(&token).await?;
let billing_provider_id = stripe.create_customer(&name, &email).await?;

let evt = ProjectCreated {
id: cmd.id,
namespace: cmd.namespace.clone(),
name: cmd.name,
owner: user_id,
status: ProjectStatus::Active.to_string(),
billing_provider: "stripe".into(),
billing_provider_id,
billing_subscription_id: None,
created_at: Utc::now(),
updated_at: Utc::now(),
};
Expand Down Expand Up @@ -207,9 +215,9 @@ pub async fn verify_secret(
Ok(secret)
}

fn assert_credential(credential: &Credential) -> Result<UserId> {
fn assert_credential(credential: &Credential) -> Result<(UserId, Token)> {
match credential {
Credential::Auth0(user_id) => Ok(user_id.into()),
Credential::Auth0(user_id, token) => Ok((user_id.into(), token.into())),
Credential::ApiKey(_) => Err(Error::Unauthorized(
"project rpc doesnt support secret".into(),
)),
Expand All @@ -221,7 +229,7 @@ async fn assert_permission(
project_id: &str,
) -> Result<()> {
match credential {
Credential::Auth0(user_id) => {
Credential::Auth0(user_id, _) => {
let result = cache.find_user_permission(user_id, project_id).await?;
if result.is_none() {
return Err(Error::Unauthorized("user doesnt have permission".into()));
Expand Down Expand Up @@ -371,10 +379,29 @@ mod tests {
}
}

mock! {
pub FakeAuth0Driven { }

#[async_trait::async_trait]
impl Auth0Driven for FakeAuth0Driven {
fn verify(&self, token: &str) -> Result<String>;
async fn find_info(&self, token: &str) -> Result<(String, String)>;
}
}

mock! {
pub FakeStripeDriven { }

#[async_trait::async_trait]
impl StripeDriven for FakeStripeDriven {
async fn create_customer(&self, name: &str, email: &str) -> Result<String>;
}
}

impl Default for FetchCmd {
fn default() -> Self {
Self {
credential: Credential::Auth0("user id".into()),
credential: Credential::Auth0("user id".into(), "token".into()),
page: 1,
page_size: 12,
}
Expand All @@ -383,7 +410,7 @@ mod tests {
impl Default for CreateCmd {
fn default() -> Self {
Self {
credential: Credential::Auth0("user id".into()),
credential: Credential::Auth0("user id".into(), "token".into()),
id: Uuid::new_v4().to_string(),
name: "New Project".into(),
namespace: "sonic-vegas".into(),
Expand All @@ -393,7 +420,7 @@ mod tests {
impl Default for UpdateCmd {
fn default() -> Self {
Self {
credential: Credential::Auth0("user id".into()),
credential: Credential::Auth0("user id".into(), "token".into()),
id: Uuid::new_v4().to_string(),
name: "Other name".into(),
}
Expand All @@ -402,7 +429,7 @@ mod tests {
impl Default for CreateSecretCmd {
fn default() -> Self {
Self {
credential: Credential::Auth0("user id".into()),
credential: Credential::Auth0("user id".into(), "token".into()),
id: Uuid::new_v4().to_string(),
project_id: Uuid::new_v4().to_string(),
name: "Key 1".into(),
Expand Down Expand Up @@ -437,34 +464,30 @@ mod tests {
let mut cache = MockFakeProjectDrivenCache::new();
cache.expect_find_by_namespace().return_once(|_| Ok(None));

let mut event = MockFakeEventDrivenBridge::new();
event.expect_dispatch().return_once(|_| Ok(()));

let cmd = CreateCmd::default();

let result = create(Arc::new(cache), Arc::new(event), cmd).await;
assert!(result.is_ok());
}
let mut auth0 = MockFakeAuth0Driven::new();
auth0
.expect_find_info()
.return_once(|_| Ok(("user name".into(), "user email".into())));

#[tokio::test]
async fn it_should_update_project() {
let mut cache = MockFakeProjectDrivenCache::new();
cache
.expect_find_user_permission()
.return_once(|_, _| Ok(Some(ProjectUser::default())));
cache
.expect_find_by_id()
.return_once(|_| Ok(Some(Project::default())));
cache
.expect_find_secret_by_project_id()
.return_once(|_| Ok(Vec::new()));
let mut stripe = MockFakeStripeDriven::new();
stripe
.expect_create_customer()
.return_once(|_, _| Ok("stripe id".into()));

let mut event = MockFakeEventDrivenBridge::new();
event.expect_dispatch().return_once(|_| Ok(()));

let cmd = UpdateCmd::default();
let cmd = CreateCmd::default();

let result = create(
Arc::new(cache),
Arc::new(event),
Arc::new(auth0),
Arc::new(stripe),
cmd,
)
.await;

let result = update(Arc::new(cache), Arc::new(event), cmd).await;
assert!(result.is_ok());
}

Expand All @@ -475,27 +498,69 @@ mod tests {
.expect_find_by_namespace()
.return_once(|_| Ok(Some(Project::default())));

let auth0 = MockFakeAuth0Driven::new();
let stripe = MockFakeStripeDriven::new();
let event = MockFakeEventDrivenBridge::new();

let cmd = CreateCmd::default();

let result = create(Arc::new(cache), Arc::new(event), cmd).await;
let result = create(
Arc::new(cache),
Arc::new(event),
Arc::new(auth0),
Arc::new(stripe),
cmd,
)
.await;

assert!(result.is_err());
}
#[tokio::test]
async fn it_should_fail_create_project_when_invalid_permission() {
let cache = MockFakeProjectDrivenCache::new();
let auth0 = MockFakeAuth0Driven::new();
let stripe = MockFakeStripeDriven::new();
let event = MockFakeEventDrivenBridge::new();

let cmd = CreateCmd {
credential: Credential::ApiKey("xxxx".into()),
..Default::default()
};

let result = create(Arc::new(cache), Arc::new(event), cmd).await;
let result = create(
Arc::new(cache),
Arc::new(event),
Arc::new(auth0),
Arc::new(stripe),
cmd,
)
.await;

assert!(result.is_err());
}

#[tokio::test]
async fn it_should_update_project() {
let mut cache = MockFakeProjectDrivenCache::new();
cache
.expect_find_user_permission()
.return_once(|_, _| Ok(Some(ProjectUser::default())));
cache
.expect_find_by_id()
.return_once(|_| Ok(Some(Project::default())));
cache
.expect_find_secret_by_project_id()
.return_once(|_| Ok(Vec::new()));

let mut event = MockFakeEventDrivenBridge::new();
event.expect_dispatch().return_once(|_| Ok(()));

let cmd = UpdateCmd::default();

let result = update(Arc::new(cache), Arc::new(event), cmd).await;
assert!(result.is_ok());
}

#[tokio::test]
async fn it_should_create_project_secret() {
let mut cache = MockFakeProjectDrivenCache::new();
Expand Down
Loading
Loading