Files
oxspeak_server/src/routes/message/routes.rs
T
2026-09-24 21:10:11 +02:00

347 lines
11 KiB
Rust

use super::handlers;
use crate::core::state::AppState;
use axum::{Router, routing::get};
pub fn router() -> Router<AppState> {
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<AppState>,
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
);
}
}