Skip to content
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
132 changes: 128 additions & 4 deletions src/sms_verification/prelude_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,23 +140,77 @@ struct PreludeCreateVerificationRequest {
dispatch_id: Option<String>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PreludeBlockedReason {
/// The signature of the SDK signals is expired. They should be sent within the hour following their collection.
ExpiredSignature,
/// The phone number is part of the configured block list.
InBlockList,
/// The phone number is not a valid line number (e.g. landline).
InvalidPhoneLine,
/// The phone number is not a valid phone number (e.g. unallocated range).
InvalidPhoneNumber,
/// The signature of the SDK signals is invalid.
InvalidSignature,
/// The phone number has made too many verification attempts.
RepeatedAttempts,
/// The verification attempt was deemed suspicious by the anti-fraud system.
Suspicious,
/// Prelude API returned Blocked status without a reason.
Unknown,
/// Prelude API returned a blocked reason we don't recognise.
Unknown(String),
}

impl<'de> serde::Deserialize<'de> for PreludeBlockedReason {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = Option::<String>::deserialize(deserializer)?;
match s.as_deref() {
None => Ok(PreludeBlockedReason::Unknown("absent".to_string())),
Some("expired_signature") => Ok(PreludeBlockedReason::ExpiredSignature),
Some("in_block_list") => Ok(PreludeBlockedReason::InBlockList),
Some("invalid_phone_line") => Ok(PreludeBlockedReason::InvalidPhoneLine),
Some("invalid_phone_number") => Ok(PreludeBlockedReason::InvalidPhoneNumber),
Some("invalid_signature") => Ok(PreludeBlockedReason::InvalidSignature),
Some("repeated_attempts") => Ok(PreludeBlockedReason::RepeatedAttempts),
Some("suspicious") => Ok(PreludeBlockedReason::Suspicious),
Some(other) => Ok(PreludeBlockedReason::Unknown(other.to_string())),
}
}
}

impl serde::Serialize for PreludeBlockedReason {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
PreludeBlockedReason::Unknown(raw) => serializer.serialize_str(raw),
other => serializer.serialize_str(&other.to_string()),
}
}
}

impl Default for PreludeBlockedReason {
fn default() -> Self {
PreludeBlockedReason::Unknown("absent".to_string())
}
}

impl std::fmt::Display for PreludeBlockedReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PreludeBlockedReason::ExpiredSignature => write!(f, "expired_signature"),
PreludeBlockedReason::InBlockList => write!(f, "in_block_list"),
PreludeBlockedReason::InvalidPhoneLine => write!(f, "invalid_phone_line"),
PreludeBlockedReason::InvalidPhoneNumber => write!(f, "invalid_phone_number"),
PreludeBlockedReason::InvalidSignature => write!(f, "invalid_signature"),
PreludeBlockedReason::RepeatedAttempts => write!(f, "repeated_attempts"),
PreludeBlockedReason::Suspicious => write!(f, "suspicious"),
PreludeBlockedReason::Unknown(s) => write!(f, "unknown({})", s),
}
}
}

#[derive(Serialize, Deserialize, Debug)]
Expand All @@ -170,6 +224,7 @@ pub enum PreludeCreateVerificationResponse {
},
Blocked {
id: String,
#[serde(default)]
reason: PreludeBlockedReason,
},
}
Expand Down Expand Up @@ -252,6 +307,18 @@ impl PreludeAPI {

let verification_response = response.json::<PreludeCreateVerificationResponse>().await?;

if let PreludeCreateVerificationResponse::Blocked {
reason: PreludeBlockedReason::Unknown(ref raw),
ref id,
} = verification_response
{
tracing::warn!(
prelude_id = %id,
raw_reason = %raw,
"Prelude returned unrecognised blocked reason"
);
}

Ok(verification_response)
}

Expand Down Expand Up @@ -288,3 +355,60 @@ impl PreludeAPI {
Ok(check_response)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn deserialize_blocked_with_known_reason() {
let json = r#"{"status":"blocked","id":"x","reason":"repeated_attempts"}"#;
let resp: PreludeCreateVerificationResponse = serde_json::from_str(json).unwrap();
assert!(matches!(
resp,
PreludeCreateVerificationResponse::Blocked {
reason: PreludeBlockedReason::RepeatedAttempts,
..
}
));
}

#[test]
fn deserialize_blocked_with_unknown_reason() {
let json = r#"{"status":"blocked","id":"x","reason":"brand_new_reason"}"#;
let resp: PreludeCreateVerificationResponse = serde_json::from_str(json).unwrap();
match resp {
PreludeCreateVerificationResponse::Blocked { reason, .. } => {
assert_eq!(
reason,
PreludeBlockedReason::Unknown("brand_new_reason".to_string())
);
}
other => panic!("Expected Blocked variant, got {:?}", other),
}
}

#[test]
fn deserialize_blocked_without_reason_field() {
let json = r#"{"status":"blocked","id":"x"}"#;
let resp: PreludeCreateVerificationResponse = serde_json::from_str(json).unwrap();
match resp {
PreludeCreateVerificationResponse::Blocked { reason, .. } => {
assert_eq!(reason, PreludeBlockedReason::Unknown("absent".to_string()));
}
other => panic!("Expected Blocked variant, got {:?}", other),
}
}

#[test]
fn deserialize_blocked_with_null_reason() {
let json = r#"{"status":"blocked","id":"x","reason":null}"#;
let resp: PreludeCreateVerificationResponse = serde_json::from_str(json).unwrap();
match resp {
PreludeCreateVerificationResponse::Blocked { reason, .. } => {
assert_eq!(reason, PreludeBlockedReason::Unknown("absent".to_string()));
}
other => panic!("Expected Blocked variant, got {:?}", other),
}
}
}
9 changes: 4 additions & 5 deletions src/sms_verification/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,11 @@ impl SmsVerificationService {

if let PreludeCreateVerificationResponse::Blocked { id, reason } = &prelude_response {
tracing::info!(
"Phone number blocked for reason: {:?}. prelude id: {}",
reason,
id
prelude_id = %id,
reason = %reason,
"Phone number blocked by Prelude"
);
SmsVerificationRepository::mark_failed(&mut executor, id, &format!("{:?}", reason))
.await?;
SmsVerificationRepository::mark_failed(&mut executor, id, &reason.to_string()).await?;

return Err(SmsVerificationError::Blocked);
}
Expand Down
2 changes: 1 addition & 1 deletion src/sms_verification/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1628,7 +1628,7 @@ async fn test_service_blocked_phone_number(pool: PgPool) {
);
let failure_reason = failure_reason.unwrap();
assert!(
failure_reason.contains("RepeatedAttempts"),
failure_reason.contains("repeated_attempts"),
"failure_reason should contain the blocked reason, got: {}",
failure_reason
);
Expand Down
Loading