add test and permission check
This commit is contained in:
@@ -20,3 +20,327 @@ pub fn router() -> Router<AppState> {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user