use crate::security::*;
use ed25519_dalek::PublicKey;
#[cfg(feature = "handler")]
use crate::handler::InteractionHandler;
#[cfg(feature = "handler")]
use crate::types;
#[cfg(feature = "handler")]
use crate::types::interaction::{
Context, InteractionResponse, InteractionResponseBuilder, InteractionResponseType,
};
#[cfg(feature = "handler")]
use crate::*;
#[cfg(feature = "handler")]
use actix_web::{http, test, web, App, HttpRequest};
#[cfg(feature = "handler")]
use std::sync::Mutex;
#[cfg(feature = "handler")]
use log::error;
const TEST_PUB_KEY: &str = "82d8d97fe0641e68a1b0b11220f05e9ea0539a0cdc002119d4a9e9e025aba1e9";
/*------------------------------
SECURITY TESTS
*/
#[test]
// Discord interaction verification test OK 1
fn crypto_verify_test_ok() {
let bytes = hex::decode(TEST_PUB_KEY).unwrap();
let pbk = PublicKey::from_bytes(&bytes).expect("Failed to convert public key.");
let res = verify_discord_message(pbk,
"c41278a0cf22bf8f3061756063cd7ef548a3df23d0ffc5496209aa0ad4d9593343801bf11e099f41bca1afcac2c70734eebafede3dec7aac1caa5d8fade5af0c",
"1616343571",
&String::from("{\"type\" : 1}"));
match res {
Err(ValidationError::KeyConversionError { name }) => panic!(
"One of the keys failed to convert to proper types! Key: {}",
name
),
Err(ValidationError::InvalidSignatureError) => {
panic!("Unexpected invalidation of signature")
}
Ok(_) => {
// Good!
}
}
}
#[test]
#[should_panic]
// Discord interacton verification test invalid 1
fn crypto_verify_test_fail() {
let bytes = hex::decode(TEST_PUB_KEY).unwrap();
let pbk = PublicKey::from_bytes(&bytes).expect("Failed to convert public key.");
let res = verify_discord_message(pbk,
"69696969696969696696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696696969",
"1616343571",
&String::from("{\"type\" : 1}"));
match res {
Err(ValidationError::KeyConversionError { name }) => panic!(
"One of the keys failed to convert to proper types! Key: {}",
name
),
Err(ValidationError::InvalidSignatureError) => {
panic!("Unexpected invalidation of signature")
} // This is what it should be!
Ok(_) => {
// Good!
}
}
}
/*-------------------------------
Discord Interactions API tests (endpoint: /api/discord/interactions)
*/
#[cfg(feature = "handler")]
macro_rules! interaction_app_init {
($ih: ident) => {
test::init_service(App::new().app_data($ih.clone()).route(
"/api/discord/interactions",
web::post().to(
|data: web::Data<Mutex<InteractionHandler>>, req: HttpRequest, body: String| async move {
data.lock().unwrap().interaction(req, body).await
},
),
))
.await;
};
}
#[cfg(all(feature = "handler", not(feature = "extended-handler")))]
macro_rules! init_handler {
() => {
InteractionHandler::new(0, TEST_PUB_KEY, None)
};
}
#[cfg(feature = "extended-handler")]
macro_rules! init_handler {
() => {
InteractionHandler::new(0, TEST_PUB_KEY, Some(&String::new()))
};
}
#[cfg(feature = "handler")]
#[actix_rt::test]
// Request with bad content with no Content-Type header present
// Expected result: Return 400 without panicking
async fn interactions_no_content_type_header_test() {
let ih = init_handler!();
let data = web::Data::new(Mutex::new(ih));
let mut app = interaction_app_init!(data);
let req = test::TestRequest::post()
.uri("/api/discord/interactions")
.set_payload("This is some malformed text { the system : can't really handle }")
.to_request();
let res: types::MessageError = test::read_response_json(&mut app, req).await;
assert_eq!(res.message, "Bad Content-Type");
}
#[cfg(feature = "handler")]
#[actix_rt::test]
// Request with bad content with no Content-Type header present
// Expected result: Return 400 without panicking
async fn interactions_bad_content_type_header_test() {
let ih = init_handler!();
let data = web::Data::new(Mutex::new(ih));
let mut app = interaction_app_init!(data);
let req = test::TestRequest::post()
.uri("/api/discord/interactions")
.header("Content-Type", "plain/text")
.set_payload("This is some malformed text { the system : can't really handle }")
.to_request();
let res: types::MessageError = test::read_response_json(&mut app, req).await;
assert_eq!(res.message, "Bad Content-Type");
}
#[cfg(feature = "handler")]
#[actix_rt::test]
// Request with missing X-Signature-Ed25519 Header
// Expected result: Return 400 without panicking
async fn interactions_no_signature_header_test() {
let ih = init_handler!();
let data = web::Data::new(Mutex::new(ih));
let mut app = interaction_app_init!(data);
let req = test::TestRequest::post()
.uri("/api/discord/interactions")
.header("Content-Type", "application/json")
.header("X-Signature-Timestamp", "1229349")
.set_payload("This is some malformed text { the system : can't really handle }")
.to_request();
let res: types::MessageError = test::read_response_json(&mut app, req).await;
assert_eq!(res.message, "Bad signature data");
}
#[cfg(feature = "handler")]
#[actix_rt::test]
// Request with missing X-Signature-Timestamp Header
// Expected result: Return 400 without panicking
async fn interactions_no_timestamp_header_test() {
let ih = init_handler!();
let data = web::Data::new(Mutex::new(ih));
let mut app = interaction_app_init!(data);
let req = test::TestRequest::post()
.uri("/api/discord/interactions")
.header("Content-Type", "application/json")
.header("X-Signature-Ed25519", "69696969696969696696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696696969")
.set_payload("This is some malformed text { the system : can't really handle }")
.to_request();
let res: types::MessageError = test::read_response_json(&mut app, req).await;
assert_eq!(res.message, "Bad signature data");
}
#[cfg(feature = "handler")]
#[actix_rt::test]
// Request with missing a signature that is too short (< 512 bits)
// Expected result: Return 400 without panicking
async fn interactions_bad_signature_length_short_test() {
let ih = init_handler!();
let data = web::Data::new(Mutex::new(ih));
let mut app = interaction_app_init!(data);
let req = test::TestRequest::post()
.uri("/api/discord/interactions")
.header("Content-Type", "application/json")
.header(
"X-Signature-Ed25519",
"69696969696969696696969696969696969696969696969696969696969696969",
)
.set_payload("This is some malformed text { the system : can't really handle }")
.to_request();
let res: types::MessageError = test::read_response_json(&mut app, req).await;
assert_eq!(res.message, "Bad signature data");
}
#[cfg(feature = "handler")]
#[actix_rt::test]
// Request with missing a signature that is too long (> 512 bits)
// Expected result: Return 400 without panicking
async fn interactions_bad_signature_length_too_long_test() {
let ih = init_handler!();
let data = web::Data::new(Mutex::new(ih));
let mut app = interaction_app_init!(data);
let req = test::TestRequest::post()
.uri("/api/discord/interactions")
.header("Content-Type", "application/json")
.header("X-Signature-Ed25519", "6969696969696969669696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969669696969696969696969696696969696969696969696969696969696969696969")
.set_payload("This is some malformed text { the system : can't really handle }")
.to_request();
let res: types::MessageError = test::read_response_json(&mut app, req).await;
assert_eq!(res.message, "Bad signature data");
}
#[cfg(feature = "handler")]
#[actix_rt::test]
// Normal ping request
// Expected result: Return 200 with payload
async fn interactions_ping_test() {
let ih = init_handler!();
let data = web::Data::new(Mutex::new(ih));
let mut app = interaction_app_init!(data);
let req = test::TestRequest::post()
.uri("/api/discord/interactions")
.header("Content-Type", "application/json")
.header("X-Signature-Ed25519", "c41278a0cf22bf8f3061756063cd7ef548a3df23d0ffc5496209aa0ad4d9593343801bf11e099f41bca1afcac2c70734eebafede3dec7aac1caa5d8fade5af0c")
.header("X-Signature-Timestamp", "1616343571")
.set_payload("{\"type\" : 1}")
.to_request();
let res: types::interaction::InteractionResponse =
test::read_response_json(&mut app, req).await;
assert_eq!(
res.r#type,
types::interaction::InteractionResponseType::Pong
);
}
#[cfg(feature = "handler")]
#[actix_rt::test]
// Bad content but OK signature test
// Expected result: Return 400 with error, don't panic
async fn interactions_bad_body_test() {
let ih = init_handler!();
let data = web::Data::new(Mutex::new(ih));
let mut app = interaction_app_init!(data);
let req = test::TestRequest::post()
.uri("/api/discord/interactions")
.header("Content-Type", "application/json")
.header("X-Signature-Ed25519", "51c5defa19cc2471a361c00c87a7f380d9e9d6cd21f05b65d3c223aac0b7d258277a09d0a016108e0be1338d985ed4ce0dae55e5ac93db5957a37ce31d007505")
.header("X-Signature-Timestamp", "1616343571")
.set_payload("this is some malformed {\"data\" : cant handle}")
.to_request();
let res = test::call_service(&mut app, req).await;
assert_eq!(res.status(), http::StatusCode::BAD_REQUEST);
}
#[cfg(feature = "handler")]
#[allow(unused_must_use)]
#[slash_command]
async fn normal_handle_test(ctx: Context) -> InteractionResponse {
return ctx.respond().content("TEST").finish();
}
#[cfg(feature = "handler")]
#[allow(unused_must_use)]
#[slash_command]
async fn normal_handle_value_test(ctx: Context) -> InteractionResponse {
let response = ctx.respond().content("TEST").finish();
return response;
}
#[cfg(feature = "handler")]
#[allow(unused_must_use)]
#[slash_command]
async fn normal_handle_direct_test(_ctx: Context) -> InteractionResponse {
return InteractionResponseBuilder::default()
.content("TEST")
.finish();
}
#[cfg(feature = "handler")]
#[actix_rt::test]
async fn interactions_normal_handle_test() {
let mut ih = init_handler!();
ih.add_global_command("test", normal_handle_test);
let data = web::Data::new(Mutex::new(ih));
let mut app = interaction_app_init!(data);
let req = test::TestRequest::post()
.uri("/api/discord/interactions")
.header("Content-Type", "application/json")
.header("X-Signature-Ed25519", "a27ed2fd0e91da58667bec63d14406e5274a0427edad9530b7d95e9d2b0fc4ee17f74e8a6bd3acd6623a05f1bde9e598fa37f3eedfe479da0a00da7827595e0b")
.header("X-Signature-Timestamp", "1616343571")
.set_payload("{\"type\":2,\"token\":\"awQabcabc\",\"member\":{\"user\":{\"id\":\"317209107000066050\",\"username\":\"C0der\",\"avatar\":\"a_d5efa99b3eeaa7dd43acca82f5692432\",\"discriminator\":\"1337\",\"public_flags\":131141},\"roles\":[],\"premium_since\":null,\"permissions\":\"2147483647\",\"pending\":false,\"nick\":null,\"mute\":false,\"joined_at\":\"2017-03-13T19:19:14.040000+00:00\",\"is_pending\":false,\"deaf\":false},\"id\":\"786008729715212338\",\"guild_id\":\"290926798626357999\",\"data\":{\"name\":\"test\",\"id\":\"771825006014889984\"},\"channel_id\":\"645027906669510667\"}")
.to_request();
let res: types::interaction::InteractionResponse =
test::read_response_json(&mut app, req).await;
let expected_data = InteractionResponseBuilder::default()
.content("TEST")
.finish();
//let expected_res = HttpResponse::build(StatusCode::OK).json(expected_data);
assert_eq!(res, expected_data);
}
#[cfg(feature = "handler")]
#[actix_rt::test]
async fn interactions_normal_from_value_handle_test() {
let mut ih = init_handler!();
ih.add_global_command("test", normal_handle_direct_test);
let data = web::Data::new(Mutex::new(ih));
let mut app = interaction_app_init!(data);
let req = test::TestRequest::post()
.uri("/api/discord/interactions")
.header("Content-Type", "application/json")
.header("X-Signature-Ed25519", "a27ed2fd0e91da58667bec63d14406e5274a0427edad9530b7d95e9d2b0fc4ee17f74e8a6bd3acd6623a05f1bde9e598fa37f3eedfe479da0a00da7827595e0b")
.header("X-Signature-Timestamp", "1616343571")
.set_payload("{\"type\":2,\"token\":\"awQabcabc\",\"member\":{\"user\":{\"id\":\"317209107000066050\",\"username\":\"C0der\",\"avatar\":\"a_d5efa99b3eeaa7dd43acca82f5692432\",\"discriminator\":\"1337\",\"public_flags\":131141},\"roles\":[],\"premium_since\":null,\"permissions\":\"2147483647\",\"pending\":false,\"nick\":null,\"mute\":false,\"joined_at\":\"2017-03-13T19:19:14.040000+00:00\",\"is_pending\":false,\"deaf\":false},\"id\":\"786008729715212338\",\"guild_id\":\"290926798626357999\",\"data\":{\"name\":\"test\",\"id\":\"771825006014889984\"},\"channel_id\":\"645027906669510667\"}")
.to_request();
let res: types::interaction::InteractionResponse =
test::read_response_json(&mut app, req).await;
let expected_data = InteractionResponseBuilder::default()
.content("TEST")
.finish();
//let expected_res = HttpResponse::build(StatusCode::OK).json(expected_data);
assert_eq!(res, expected_data);
}
#[cfg(feature = "handler")]
#[actix_rt::test]
async fn interactions_normal_from_direct_call_handle_test() {
let mut ih = init_handler!();
ih.add_global_command("test", normal_handle_value_test);
let data = web::Data::new(Mutex::new(ih));
let mut app = interaction_app_init!(data);
let req = test::TestRequest::post()
.uri("/api/discord/interactions")
.header("Content-Type", "application/json")
.header("X-Signature-Ed25519", "a27ed2fd0e91da58667bec63d14406e5274a0427edad9530b7d95e9d2b0fc4ee17f74e8a6bd3acd6623a05f1bde9e598fa37f3eedfe479da0a00da7827595e0b")
.header("X-Signature-Timestamp", "1616343571")
.set_payload("{\"type\":2,\"token\":\"awQabcabc\",\"member\":{\"user\":{\"id\":\"317209107000066050\",\"username\":\"C0der\",\"avatar\":\"a_d5efa99b3eeaa7dd43acca82f5692432\",\"discriminator\":\"1337\",\"public_flags\":131141},\"roles\":[],\"premium_since\":null,\"permissions\":\"2147483647\",\"pending\":false,\"nick\":null,\"mute\":false,\"joined_at\":\"2017-03-13T19:19:14.040000+00:00\",\"is_pending\":false,\"deaf\":false},\"id\":\"786008729715212338\",\"guild_id\":\"290926798626357999\",\"data\":{\"name\":\"test\",\"id\":\"771825006014889984\"},\"channel_id\":\"645027906669510667\"}")
.to_request();
let res: types::interaction::InteractionResponse =
test::read_response_json(&mut app, req).await;
let expected_data = InteractionResponseBuilder::default()
.content("TEST")
.finish();
//let expected_res = HttpResponse::build(StatusCode::OK).json(expected_data);
assert_eq!(res, expected_data);
}
#[cfg(feature = "handler")]
use crate::types::interaction::WebhookMessage;
#[cfg(feature = "handler")]
#[slash_command_test]
#[defer]
async fn deffered_handle_test(ctx: Context) -> InteractionResponse {
return ctx.respond().content("TEST").finish();
}
#[cfg(feature = "handler")]
#[slash_command_test]
#[defer]
async fn deffered_handle_value_test(ctx: Context) -> InteractionResponse {
let response = ctx.respond().content("TEST").finish();
return response;
}
#[cfg(feature = "handler")]
#[slash_command_test]
#[defer]
async fn deffered_handle_direct_test(_ctx: Context) -> InteractionResponse {
return InteractionResponseBuilder::default()
.content("TEST")
.finish();
}
#[cfg(feature = "handler")]
#[actix_rt::test]
async fn interactions_deffered_handle_test() {
let mut ih = init_handler!();
ih.add_global_command("test", deffered_handle_test);
let data = web::Data::new(Mutex::new(ih));
let mut app = interaction_app_init!(data);
let req = test::TestRequest::post()
.uri("/api/discord/interactions")
.header("Content-Type", "application/json")
.header("X-Signature-Ed25519", "a27ed2fd0e91da58667bec63d14406e5274a0427edad9530b7d95e9d2b0fc4ee17f74e8a6bd3acd6623a05f1bde9e598fa37f3eedfe479da0a00da7827595e0b")
.header("X-Signature-Timestamp", "1616343571")
.set_payload("{\"type\":2,\"token\":\"awQabcabc\",\"member\":{\"user\":{\"id\":\"317209107000066050\",\"username\":\"C0der\",\"avatar\":\"a_d5efa99b3eeaa7dd43acca82f5692432\",\"discriminator\":\"1337\",\"public_flags\":131141},\"roles\":[],\"premium_since\":null,\"permissions\":\"2147483647\",\"pending\":false,\"nick\":null,\"mute\":false,\"joined_at\":\"2017-03-13T19:19:14.040000+00:00\",\"is_pending\":false,\"deaf\":false},\"id\":\"786008729715212338\",\"guild_id\":\"290926798626357999\",\"data\":{\"name\":\"test\",\"id\":\"771825006014889984\"},\"channel_id\":\"645027906669510667\"}")
.to_request();
let res: types::interaction::InteractionResponse =
test::read_response_json(&mut app, req).await;
let expected_data = InteractionResponse {
r#type: InteractionResponseType::DefferedChannelMessageWithSource,
data: None,
};
//let expected_res = HttpResponse::build(StatusCode::OK).json(expected_data);
assert_eq!(res, expected_data);
}
#[cfg(feature = "handler")]
#[actix_rt::test]
async fn interactions_deffered_from_value_handle_test() {
let mut ih = init_handler!();
ih.add_global_command("test", deffered_handle_value_test);
let data = web::Data::new(Mutex::new(ih));
let mut app = interaction_app_init!(data);
let req = test::TestRequest::post()
.uri("/api/discord/interactions")
.header("Content-Type", "application/json")
.header("X-Signature-Ed25519", "a27ed2fd0e91da58667bec63d14406e5274a0427edad9530b7d95e9d2b0fc4ee17f74e8a6bd3acd6623a05f1bde9e598fa37f3eedfe479da0a00da7827595e0b")
.header("X-Signature-Timestamp", "1616343571")
.set_payload("{\"type\":2,\"token\":\"awQabcabc\",\"member\":{\"user\":{\"id\":\"317209107000066050\",\"username\":\"C0der\",\"avatar\":\"a_d5efa99b3eeaa7dd43acca82f5692432\",\"discriminator\":\"1337\",\"public_flags\":131141},\"roles\":[],\"premium_since\":null,\"permissions\":\"2147483647\",\"pending\":false,\"nick\":null,\"mute\":false,\"joined_at\":\"2017-03-13T19:19:14.040000+00:00\",\"is_pending\":false,\"deaf\":false},\"id\":\"786008729715212338\",\"guild_id\":\"290926798626357999\",\"data\":{\"name\":\"test\",\"id\":\"771825006014889984\"},\"channel_id\":\"645027906669510667\"}")
.to_request();
let res: types::interaction::InteractionResponse =
test::read_response_json(&mut app, req).await;
let expected_data = InteractionResponse {
r#type: InteractionResponseType::DefferedChannelMessageWithSource,
data: None,
};
//let expected_res = HttpResponse::build(StatusCode::OK).json(expected_data);
assert_eq!(res, expected_data);
}
#[cfg(feature = "handler")]
#[actix_rt::test]
async fn interactions_deffered_from_direct_value_handle_test() {
let mut ih = init_handler!();
ih.add_global_command("test", deffered_handle_direct_test);
let data = web::Data::new(Mutex::new(ih));
let mut app = interaction_app_init!(data);
let req = test::TestRequest::post()
.uri("/api/discord/interactions")
.header("Content-Type", "application/json")
.header("X-Signature-Ed25519", "a27ed2fd0e91da58667bec63d14406e5274a0427edad9530b7d95e9d2b0fc4ee17f74e8a6bd3acd6623a05f1bde9e598fa37f3eedfe479da0a00da7827595e0b")
.header("X-Signature-Timestamp", "1616343571")
.set_payload("{\"type\":2,\"token\":\"awQabcabc\",\"member\":{\"user\":{\"id\":\"317209107000066050\",\"username\":\"C0der\",\"avatar\":\"a_d5efa99b3eeaa7dd43acca82f5692432\",\"discriminator\":\"1337\",\"public_flags\":131141},\"roles\":[],\"premium_since\":null,\"permissions\":\"2147483647\",\"pending\":false,\"nick\":null,\"mute\":false,\"joined_at\":\"2017-03-13T19:19:14.040000+00:00\",\"is_pending\":false,\"deaf\":false},\"id\":\"786008729715212338\",\"guild_id\":\"290926798626357999\",\"data\":{\"name\":\"test\",\"id\":\"771825006014889984\"},\"channel_id\":\"645027906669510667\"}")
.to_request();
let res: types::interaction::InteractionResponse =
test::read_response_json(&mut app, req).await;
let expected_data = InteractionResponse {
r#type: InteractionResponseType::DefferedChannelMessageWithSource,
data: None,
};
//let expected_res = HttpResponse::build(StatusCode::OK).json(expected_data);
assert_eq!(res, expected_data);
}