Skip to content

Commit

Permalink
add unit test for mention sanitization
Browse files Browse the repository at this point in the history
  • Loading branch information
IAmTomahawkx committed Nov 24, 2024
1 parent 0fda34c commit 5377294
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 8 deletions.
4 changes: 2 additions & 2 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ services:
redis:
image: eqalpha/keydb
ports:
- "14079:6379"
- "6379:6379"

# MongoDB
database:
image: mongo
ports:
- "14017:27017"
- "27017:27017"
volumes:
- ./.data/db:/data/db

Expand Down
6 changes: 6 additions & 0 deletions crates/core/config/Revolt.test.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
[database]
mongodb = "mongodb://localhost"
redis = "redis://localhost/"

[rabbit]
host = "127.0.0.1"
port = 5672
username = "rabbituser"
password = "rabbitpass"
10 changes: 8 additions & 2 deletions crates/core/database/src/util/bulk_permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ async fn calculate_members_permissions<'a>(
) -> HashMap<String, PermissionValue> {
let mut resp = HashMap::new();

let (_, channel_role_permissions) = match query
let (_, channel_role_permissions, channel_default_permissions) = match query
.channel
.as_ref()
.expect("A channel must be assigned to calculate channel permissions")
Expand All @@ -183,13 +183,15 @@ async fn calculate_members_permissions<'a>(
Channel::TextChannel {
id,
role_permissions,
default_permissions,
..
}
| Channel::VoiceChannel {
id,
role_permissions,
default_permissions,
..
} => (id, role_permissions),
} => (id, role_permissions, default_permissions),
_ => panic!("Calculation of member permissions must be done on a server channel"),
};

Expand Down Expand Up @@ -273,6 +275,10 @@ async fn calculate_members_permissions<'a>(
// Get the user's server permissions
let mut permission = calculate_server_permissions(&query.server, user, member);

if let Some(defaults) = channel_default_permissions {
permission.apply(defaults.into());
}

// Get the applicable role overrides
let mut roles = channel_role_permissions
.iter()
Expand Down
4 changes: 2 additions & 2 deletions crates/delta/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub mod util;

use revolt_config::config;
use revolt_database::events::client::EventV1;
use revolt_database::{Database, MongoDb, AMQP};
use revolt_database::AMQP;
use rocket::{Build, Rocket};
use rocket_cors::{AllowedOrigins, CorsOptions};
use rocket_prometheus::PrometheusMetrics;
Expand All @@ -34,7 +34,7 @@ pub async fn web() -> Rocket<Build> {
db.migrate_database().await.unwrap();

// Setup Authifier event channel
let (sender, receiver) = unbounded();
let (_, receiver) = unbounded();

// Setup Authifier
let authifier = db.clone().to_authifier().await;
Expand Down
215 changes: 215 additions & 0 deletions crates/delta/src/routes/channels/message_send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,218 @@ pub async fn message_send(
.into_model(Some(model_user), model_member),
))
}

#[cfg(test)]
mod test {
use std::collections::HashMap;

use crate::{rocket, util::test::TestHarness};
use revolt_database::{
util::{idempotency::IdempotencyKey, reference::Reference},
Channel, Member, Message, PartialChannel, PartialMember, Role, Server,
};
use revolt_models::v0::{self, DataCreateServerChannel};
use revolt_permissions::{ChannelPermission, OverrideField};

#[rocket::async_test]
async fn message_mention_constraints() {
let harness = TestHarness::new().await;
let (_, _, user) = harness.new_user().await;
let (_, _, second_user) = harness.new_user().await;

let (server, channels) = Server::create(
&harness.db,
v0::DataCreateServer {
name: "Test Server".to_string(),
..Default::default()
},
&user,
true,
)
.await
.expect("Failed to create test server");

let server_mut: &mut Server = &mut server.clone();
let mut locked_channel = Channel::create_server_channel(
&harness.db,
server_mut,
DataCreateServerChannel {
channel_type: v0::LegacyServerChannelType::Text,
name: "Hidden Channel".to_string(),
description: None,
nsfw: Some(false),
},
true,
)
.await
.expect("Failed to make new channel");

let role = Role {
name: "Show Hidden Channel".to_string(),
permissions: OverrideField { a: 0, d: 0 },
colour: None,
hoist: false,
rank: 5,
};

let role_id = role
.create(&harness.db, &server.id)
.await
.expect("Failed to create the role");

let mut overrides = HashMap::new();
overrides.insert(
role_id.clone(),
OverrideField {
a: (ChannelPermission::ViewChannel) as i64,
d: 0,
},
);

let partial = PartialChannel {
name: None,
owner: None,
description: None,
icon: None,
nsfw: None,
active: None,
permissions: None,
role_permissions: Some(overrides),
default_permissions: Some(OverrideField {
a: 0,
d: ChannelPermission::ViewChannel as i64,
}),
last_message_id: None,
};
locked_channel
.update(&harness.db, partial, vec![])
.await
.expect("Failed to update the channel permissions for special role");

Member::create(&harness.db, &server, &user, Some(channels.clone()))
.await
.expect("Failed to create member");
let member = Reference::from_unchecked(user.id.clone())
.as_member(&harness.db, &server.id)
.await
.expect("Failed to get member");

// Second user is not part of the server
let message = Message::create_from_api(
&harness.db,
Some(&harness.amqp),
locked_channel.clone(),
v0::DataMessageSend {
content: Some(format!("<@{}>", second_user.id)),
nonce: None,
attachments: None,
replies: None,
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
Some(user.clone().into(&harness.db, Some(&user)).await),
Some(member.clone().into()),
user.limits().await,
IdempotencyKey::unchecked_from_string("0".to_string()),
false,
true,
)
.await
.expect("Failed to create message");

// The mention should not go through here
assert!(
message.mentions.is_none() || message.mentions.unwrap().is_empty(),
"Mention failed to be scrubbed when the user is not part of the server"
);

Member::create(&harness.db, &server, &second_user, Some(channels.clone()))
.await
.expect("Failed to create second member");
let mut second_member = Reference::from_unchecked(second_user.id.clone())
.as_member(&harness.db, &server.id)
.await
.expect("Failed to get second member");

// Second user cannot see the channel
let message = Message::create_from_api(
&harness.db,
Some(&harness.amqp),
locked_channel.clone(),
v0::DataMessageSend {
content: Some(format!("<@{}>", second_user.id)),
nonce: None,
attachments: None,
replies: None,
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
Some(user.clone().into(&harness.db, Some(&user)).await),
Some(member.clone().into()),
user.limits().await,
IdempotencyKey::unchecked_from_string("1".to_string()),
false,
true,
)
.await
.expect("Failed to create message");

// The mention should not go through here
assert!(
message.mentions.is_none() || message.mentions.unwrap().is_empty(),
"Mention failed to be scrubbed when the user cannot see the channel"
);

let second_member_roles = vec![role_id.clone()];
let partial = PartialMember {
id: None,
joined_at: None,
nickname: None,
avatar: None,
timeout: None,
roles: Some(second_member_roles),
};
second_member
.update(&harness.db, partial, vec![])
.await
.expect("Failed to update the second user's roles");

// This time the mention SHOULD go through
let message = Message::create_from_api(
&harness.db,
Some(&harness.amqp),
locked_channel.clone(),
v0::DataMessageSend {
content: Some(format!("<@{}>", second_user.id)),
nonce: None,
attachments: None,
replies: None,
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
Some(user.clone().into(&harness.db, Some(&user)).await),
Some(member.clone().into()),
user.limits().await,
IdempotencyKey::unchecked_from_string("2".to_string()),
false,
true,
)
.await
.expect("Failed to create message");

// The mention SHOULD go through here
assert!(
message.mentions.is_some() && !message.mentions.unwrap().is_empty(),
"Mention was scrubbed when the user can see the channel"
);
}
}
20 changes: 18 additions & 2 deletions crates/delta/src/util/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@ use authifier::{
use futures::StreamExt;
use rand::Rng;
use redis_kiss::redis::aio::PubSub;
use revolt_database::{events::client::EventV1, Database, User};
use revolt_database::{events::client::EventV1, Database, User, AMQP};
use revolt_models::v0;
use rocket::local::asynchronous::Client;

pub struct TestHarness {
pub client: Client,
authifier: Authifier,
pub db: Database,
pub amqp: AMQP,
sub: PubSub,
event_buffer: Vec<(String, EventV1)>,
}

impl TestHarness {
pub async fn new() -> TestHarness {
dotenv::dotenv().ok();

let config = revolt_config::config().await;
let client = Client::tracked(crate::web().await)
.await
.expect("valid rocket instance");
Expand All @@ -43,10 +44,25 @@ impl TestHarness {
.expect("`Authifier`")
.clone();

let connection = amqprs::connection::Connection::open(
&amqprs::connection::OpenConnectionArguments::new(
&config.rabbit.host,
config.rabbit.port,
&config.rabbit.username,
&config.rabbit.password,
),
)
.await
.unwrap();
let channel = connection.open_channel(None).await.unwrap();

let amqp = AMQP::new(connection, channel);

TestHarness {
client,
authifier,
db,
amqp,
sub,
event_buffer: vec![],
}
Expand Down

0 comments on commit 5377294

Please sign in to comment.