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

chore: add tests #3

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
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
7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ serde-aux = "4.2"
serde_repr = "0.1"
chrono = { version = "0.4", default-features = false, features = ["time"] }
cached = "0.44"
ulid = { version = "1.0", features = ["serde"] }
ulid = "1.0"


[dev-dependencies]
dotenvy = "0.15"
httpmock = "0.6"

[dev-dependencies.tokio]
version = "1.31"
default_features = false
features = ["macros", "rt", "rt-multi-thread"]
default-features = false
features = ["macros", "rt"]
12 changes: 0 additions & 12 deletions bacon.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,6 @@ command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"]
need_stdout = false
on_success = "back" # so that we don't open the browser at each change

# You can run your application and have the result displayed in bacon,
# *if* it makes sense for this crate. You can run an example the same
# way. Don't forget the `--color always` part or the errors won't be
# properly parsed.
# If you want to pass options to your program, a `--` separator
# will be needed.
[jobs.run]
command = ["cargo", "run", "--color", "always"]
need_stdout = true
allow_warnings = true

# You may define here keybindings that would be specific to
# a project, for example a shortcut to launch a specific job.
# Shortcuts to internal functions (scrolling, toggling, etc.)
Expand All @@ -53,5 +42,4 @@ shift-d = "job:doc-open"
c = "job:check"
shift-c = "job:check-all"
t = "job:test"
r = "job:run"
p = "job:clippy"
7 changes: 6 additions & 1 deletion src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,20 @@ pub enum Environment {
/// Sandbox environment which can be used for testing
#[default]
Sandbox,
/// Custom Environment
///
/// Mostly used for Mock testing Environment
Custom(String),
}

impl Environment {
/// Base URL for the two kinds of Environment
#[must_use]
pub const fn base_url(&self) -> &str {
pub fn base_url(&self) -> &str {
match self {
Self::Production => "https://pay.pesapal.com/v3",
Self::Sandbox => "https://cybqa.pesapal.com/pesapalv3",
Self::Custom(url) => url.as_str(),
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use serde::Deserialize;
use serde::{Deserialize, Serialize};

#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
Expand Down Expand Up @@ -26,7 +26,7 @@ pub enum PesaPalError {
}

/// Error response for the Pesapal API error
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[non_exhaustive]
pub struct PesaPalErrorResponse {
pub code: String,
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ mod pesapal;
pub use environment::Environment;
pub use error::{PesaPalError, PesaPalErrorResponse, PesaPalResult, TransactionStatusError};

pub use crate::pesapal::auth::{AuthenticationRequest, AuthenticationResponse};
pub use crate::pesapal::list_ipn::{IPNList, IPNListResponse, ListIPN};
pub use crate::pesapal::refund::{Refund, RefundRequest, RefundResponse};
pub use crate::pesapal::register_ipn::{NotificationType, RegisterIPN, RegisterIPNResponse};
Expand Down
4 changes: 2 additions & 2 deletions src/pesapal.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
mod auth;
pub mod auth;
pub mod list_ipn;
pub mod refund;
pub mod register_ipn;
Expand All @@ -23,7 +23,7 @@ static PESAPAL_PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION");

/// [`PesaPal`] This is the client struct which allows communication with
/// the `PesaPal` services
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct PesaPal {
/// Consumer Key - This is provided by the PesaPal
consumer_key: String,
Expand Down
25 changes: 17 additions & 8 deletions src/pesapal/auth.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
use cached::proc_macro::cached;
use cached::TimedSizedCache;
use chrono::{DateTime, NaiveDateTime, Utc};
use serde::{Deserialize, Deserializer};
use serde::{Deserialize, Deserializer, Serialize};
use serde_aux::prelude::{deserialize_default_from_null, deserialize_number_from_string};
use serde_json::json;

use crate::{PesaPal, PesaPalError, PesaPalErrorResponse};

pub static AUTHENTICATION_URL: &str = "/api/Auth/RequestToken";

#[derive(Serialize)]
pub struct AuthenticationRequest<'auth> {
consumer_key: &'auth str,
consumer_secret: &'auth str,
}

/// Response returned from the authentication function
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -56,12 +63,13 @@ pub type AccessToken = String;
convert = r#"{ format!("{}", client.consumer_key) }"#,
result = true
)]
pub async fn auth(client: &PesaPal) -> Result<AccessToken, PesaPalError> {
let url = format!("{}/api/Auth/RequestToken", client.env.base_url());
let payload = json!({
"consumer_key": client.consumer_key,
"consumer_secret": client.consumer_secret
});
pub(crate) async fn auth(client: &PesaPal) -> Result<AccessToken, PesaPalError> {
let url = format!("{}/{AUTHENTICATION_URL}", client.env.base_url());

let payload = AuthenticationRequest {
consumer_key: &client.consumer_key,
consumer_secret: &client.consumer_secret,
};

let response = client.http_client.post(url).json(&payload).send().await?;

Expand Down Expand Up @@ -109,6 +117,7 @@ mod tests {
}

#[tokio::test]
#[ignore = ""]
async fn test_cached_access_token() {
dotenv().ok();

Expand Down
46 changes: 33 additions & 13 deletions src/pesapal/submit_order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@

use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use serde_aux::prelude::deserialize_default_from_null;
use serde_aux::prelude::deserialize_number_from_string;

use super::PesaPal;
use crate::error::{PesaPalError, PesaPalErrorResponse, PesaPalResult};
use crate::error::{PesaPalError, PesaPalResult};

const SUBMIT_ORDER_REQUEST_URL: &str = "api/Transactions/SubmitOrderRequest";

Expand Down Expand Up @@ -153,7 +153,7 @@ impl From<SubmitOrder<'_>> for SubmitOrderRequest {
}

/// The Submit Order response after a payment has been created successfully
#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)]
#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct SubmitOrderResponse {
/// Unique order id generated by PesaPal
pub order_tracking_id: String,
Expand All @@ -163,11 +163,10 @@ pub struct SubmitOrderResponse {
///
/// Redirect to this URl or load it within an iframe
pub redirect_url: String,
/// Error message
#[serde(deserialize_with = "deserialize_default_from_null")]
pub error: Option<PesaPalErrorResponse>,

/// Response Message
pub status: String,
#[serde(deserialize_with = "deserialize_number_from_string")]
pub status: u16,
}

/// This is the submit order builder
Expand Down Expand Up @@ -207,7 +206,6 @@ pub struct SubmitOrder<'pesa> {
#[builder(setter(into, strip_option), default)]
#[doc = r"If your business has multiple stores / branches, you can define the name of the store / branch to which this particular payment will be accredited to."]
branch: Option<String>,

#[doc = r"The billing address of the customer"]
billing_address: BillingAddress,
}
Expand Down Expand Up @@ -254,13 +252,35 @@ impl SubmitOrder<'_> {
.json::<SubmitOrderRequest>(&self.into())
.send()
.await?;
print!("{}", response.status());
match response.status().is_success() {
true => Ok(response.json().await?),
false => Err(PesaPalError::SubmitOrderError(response.json().await?)),
}
}
}

let res: SubmitOrderResponse = response.json().await?;
#[cfg(test)]
mod test {
use super::*;

if let Some(error) = res.error {
return Err(PesaPalError::SubmitOrderError(error));
}
#[test]
fn test_submit_order_with_no_email_and_phone() {
let client = PesaPal::default();
let order = SubmitOrder::builder(&client)
.currency("KES")
.amount(2500)
.description("Shopping")
.callback_url("https://example.com")
.cancellation_url("https://example.com")
.notification_id("AABBCCDDEEFFGG")
.redirect_mode(RedirectMode::ParentWindow)
.branch("EA")
.billing_address(BillingAddress::default())
.build();

Ok(res)
assert!(order.is_err_and(|x| x
.to_string()
.contains("either email or phone number must be provided")))
}
}
32 changes: 32 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use httpmock::prelude::*;
use pesapal::{Environment, PesaPal};
use serde_json::json;

pub(crate) async fn pesapal_client() -> (PesaPal, MockServer) {
dotenvy::dotenv().ok();

let server = MockServer::start_async().await;

let client = PesaPal::new(
dotenvy::var("CONSUMER_KEY").expect("consumer_key not present"),
dotenvy::var("CONSUMER_SECRET").expect("consumer_secret not present"),
Environment::Custom(server.base_url()),
);

let auth_response = json!({
"token": "token",
"expiryDate": "2021-08-26T12:29:30.5177702Z",
"error": null,
"status": "200",
"message": "Request processed successfully"
});

server
.mock_async(|when, then| {
when.path_contains("/api/Auth/RequestToken").method(POST);
then.json_body(auth_response).status(200);
})
.await;

(client, server)
}
2 changes: 2 additions & 0 deletions tests/it/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

mod submit_order;
42 changes: 42 additions & 0 deletions tests/it/submit_order.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use dotenvy::dotenv;
use pesapal::{BillingAddress, Environment, NotificationType, PesaPal, RedirectMode};

#[tokio::test]
async fn test_submit_order() {
dotenv().ok();
let client = PesaPal::new(
dotenvy::var("CONSUMER_KEY").unwrap(),
dotenvy::var("CONSUMER_SECRET").unwrap(),
Environment::Sandbox,
);

// let response = client
// .register_ipn_url()
// .url("http://example.com")
// .ipn_notification_type(NotificationType::Get)
// .build()
// .unwrap()
// .send()
// .await
// .unwrap();

client
.submit_order()
.currency("KES")
.amount(2500)
.description("Shopping")
.callback_url("https://example.com")
.cancellation_url("https://example.com")
.notification_id("response.ipn_id")
.redirect_mode(RedirectMode::ParentWindow)
.branch("Branch")
.billing_address(BillingAddress {
email_address: Some("yasir@gmail.com".to_string()),
..Default::default()
})
.build()
.unwrap()
.send()
.await
.unwrap();
}
Loading
Loading