use super::handlers; use crate::core::state::AppState; use axum::{Router, routing::get}; pub fn router() -> Router { Router::new() .route("/messages", get(handlers::get_all).post(handlers::create)) .route( "/messages/{id}", get(handlers::get_by_id) .put(handlers::update) .delete(handlers::delete), ) .route( "/messages/{message_id}/reactions", axum::routing::post(handlers::add_reaction), ) .route( "/messages/{message_id}/reactions/{emoji_id}", axum::routing::delete(handlers::remove_reaction), ) } #[cfg(test)] pub(crate) mod permission_tests { use super::*; use crate::config::{AppConfig, DEFAULT_CONFIG_TOML}; use crate::core::App; use crate::http::context::{CurrentUser, RequestContext}; use crate::models::{channel, channel_user_permission, emoji, server_user, user}; use crate::permissions::ChannelPermission; use axum::{ body::Body, http::{Method, Request, StatusCode}, }; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; use std::time::Instant; use tower::ServiceExt; use uuid::Uuid; pub(crate) struct Fixture { pub state: AppState, pub user: user::Model, pub channel_id: Uuid, } impl Fixture { pub async fn new() -> Self { let mut config: AppConfig = toml::from_str(DEFAULT_CONFIG_TOML).unwrap(); config.database.url = "sqlite::memory:".into(); config.network.stun_servers.clear(); config.media.root = format!("src/routes/attachment/.permission-test-{}", Uuid::new_v4()); let state = App::build(config).await.unwrap().state; let user = user::ActiveModel { id: Set(Uuid::new_v4()), username: Set(format!("test-{}", Uuid::new_v4())), password: Set(String::new()), pub_key: Set(None), created_at: Set(chrono::Utc::now()), updated_at: Set(chrono::Utc::now()), is_superuser: Set(false), } .insert(&state.db) .await .unwrap(); server_user::ActiveModel { id: Set(Uuid::new_v4()), server_id: Set(state.default_server.id), user_id: Set(user.id), username: Set(None), joined_at: Set(chrono::Utc::now()), updated_at: Set(chrono::Utc::now()), } .insert(&state.db) .await .unwrap(); let channel = channel::ActiveModel { id: Set(Uuid::new_v4()), server_id: Set(Some(state.default_server.id)), category_id: Set(None), channel_type: Set(channel::ChannelType::Text), name: Set(Some("test".into())), created_at: Set(chrono::Utc::now()), updated_at: Set(chrono::Utc::now()), } .insert(&state.db) .await .unwrap(); Self { state, user, channel_id: channel.id, } } pub async fn grant(&self, permissions: ChannelPermission) { channel_user_permission::ActiveModel { id: Set(Uuid::new_v4()), channel_id: Set(self.channel_id), user_id: Set(self.user.id), permissions: Set(permissions.bits() as i64), } .insert(&self.state.db) .await .unwrap(); } pub async fn request( &self, router: Router, method: Method, uri: &str, body: Body, content_type: Option<&str>, ) -> StatusCode { let mut builder = Request::builder().method(method.clone()).uri(uri); if let Some(content_type) = content_type { builder = builder.header("content-type", content_type); } let mut request = builder.body(body).unwrap(); request.extensions_mut().insert(RequestContext { request_id: Uuid::new_v4(), started_at: Instant::now(), method, uri: uri.parse().unwrap(), user: Some(CurrentUser(self.user.clone())), }); router .with_state(self.state.clone()) .oneshot(request) .await .unwrap() .status() } } #[tokio::test] async fn message_list_requires_read_channel() { let fixture = Fixture::new().await; let uri = format!("/messages?channel_id={}", fixture.channel_id); // L'accès aux messages du canal demande READ_CHANNEL. assert_eq!( fixture .request(router(), Method::GET, &uri, Body::empty(), None) .await, StatusCode::FORBIDDEN ); fixture.grant(ChannelPermission::READ_CHANNEL).await; assert_eq!( fixture .request(router(), Method::GET, &uri, Body::empty(), None) .await, StatusCode::OK ); } #[tokio::test] async fn message_create_requires_send_message() { let fixture = Fixture::new().await; let payload = format!( r#"{{"channel_id":"{}","content":"hello","file_ids":[]}}"#, fixture.channel_id ); let send = || Body::from(payload.clone()); // SEND_MESSAGE est distinct du droit de lire le canal. assert_eq!( fixture .request( router(), Method::POST, "/messages", send(), Some("application/json") ) .await, StatusCode::FORBIDDEN ); fixture.grant(ChannelPermission::SEND_MESSAGE).await; assert_eq!( fixture .request( router(), Method::POST, "/messages", send(), Some("application/json") ) .await, StatusCode::CREATED ); } #[tokio::test] async fn message_edit_and_delete_require_own_permissions() { // L'édition et la suppression d'un message propre ont des permissions distinctes. for (method, permission, success) in [ ( Method::PUT, ChannelPermission::EDIT_OWN_MESSAGE, StatusCode::OK, ), ( Method::DELETE, ChannelPermission::DELETE_OWN_MESSAGE, StatusCode::NO_CONTENT, ), ] { let fixture = Fixture::new().await; let message = fixture .state .services .message .create_message_with_attachments( fixture.channel_id, fixture.user.id, "hello".into(), Vec::new(), None, ) .await .unwrap(); let uri = format!("/messages/{}", message.id); let body = || Body::from(r#"{"content":"edited"}"#); assert_eq!( fixture .request( router(), method.clone(), &uri, body(), Some("application/json") ) .await, StatusCode::FORBIDDEN ); fixture.grant(permission).await; assert_eq!( fixture .request(router(), method, &uri, body(), Some("application/json")) .await, success ); } } #[tokio::test] async fn reactions_require_read_and_add_permissions() { let fixture = Fixture::new().await; // Prépare un message et un emoji afin de tester les deux opérations de réaction. let message = fixture .state .services .message .create_message_with_attachments( fixture.channel_id, fixture.user.id, "hello".into(), Vec::new(), None, ) .await .unwrap(); let emoji = emoji::ActiveModel { id: Set(Uuid::new_v4()), server_id: Set(Some(fixture.state.default_server.id)), name: Set("wave".into()), emoji_type: Set("unicode".into()), unicode_sequence: Set(Some("👋".into())), supports_skin_tone: Set(false), file_path: Set(None), mime_type: Set(None), file_size: Set(None), is_animated: Set(false), sha256: Set(None), created_at: Set(chrono::Utc::now()), updated_at: Set(chrono::Utc::now()), } .insert(&fixture.state.db) .await .unwrap(); let add_uri = format!("/messages/{}/reactions", message.id); let remove_uri = format!("{add_uri}/{}", emoji.id); let payload = || Body::from(format!(r#"{{"emoji_id":"{}"}}"#, emoji.id)); // Sans permission, l'ajout et le retrait sont refusés. assert_eq!( fixture .request( router(), Method::POST, &add_uri, payload(), Some("application/json") ) .await, StatusCode::FORBIDDEN ); assert_eq!( fixture .request(router(), Method::DELETE, &remove_uri, Body::empty(), None) .await, StatusCode::FORBIDDEN ); fixture.grant(ChannelPermission::READ_CHANNEL).await; // Lire le canal seul ne suffit pas pour ajouter une réaction. assert_eq!( fixture .request( router(), Method::POST, &add_uri, payload(), Some("application/json") ) .await, StatusCode::FORBIDDEN ); channel_user_permission::Entity::delete_many() .filter(channel_user_permission::Column::UserId.eq(fixture.user.id)) .exec(&fixture.state.db) .await .unwrap(); fixture .grant(ChannelPermission::READ_CHANNEL | ChannelPermission::ADD_REACTIONS) .await; // L'ajout requiert les deux droits; le retrait est ensuite autorisé aussi. assert_eq!( fixture .request( router(), Method::POST, &add_uri, payload(), Some("application/json") ) .await, StatusCode::CREATED ); assert_eq!( fixture .request(router(), Method::DELETE, &remove_uri, Body::empty(), None) .await, StatusCode::NO_CONTENT ); } }