matrixclient-testsuite

A TestSuite for Matrix Clients like sytest for Servers (Mainly written for Daydream)
git clone git://archive.git.mtrnord.blog/MTRNord/matrixclient-testsuite.git
Log | Files | Refs

commit 1e5b5812de10eff07fcfd1961b84398c3a267224
Author: Marcel <mtrnord1@gmail.com>
Date:   Sun,  7 Jun 2020 19:07:01 +0200

Initial Commit

Took 1 hour 42 minutes

Diffstat:
A.idea/.gitignore | 8++++++++
A.idea/misc.xml | 7+++++++
A.idea/modules.xml | 9+++++++++
A.idea/vcs.xml | 7+++++++
ARocket.toml | 7+++++++
Amxctest.iml | 13+++++++++++++
Asrc/data/mod.rs | 31+++++++++++++++++++++++++++++++
Asrc/error.rs | 36++++++++++++++++++++++++++++++++++++
Asrc/ruma_wrapper.rs | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/tests.rs | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/utils.rs | 6++++++
11 files changed, 382 insertions(+), 0 deletions(-)

diff --git a/.idea/.gitignore b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/misc.xml b/.idea/misc.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectRootManager"> + <output url="file://$PROJECT_DIR$/out" /> + </component> +</project> +\ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/mxctest.iml" filepath="$PROJECT_DIR$/mxctest.iml" /> + </modules> + </component> +</project> +\ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="" vcs="Git" /> + </component> +</project> +\ No newline at end of file diff --git a/Rocket.toml b/Rocket.toml @@ -0,0 +1,7 @@ +[production] +address = "localhost" +port = 8448 + +[development] +address = "localhost" +port = 8448 diff --git a/mxctest.iml b/mxctest.iml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="RUST_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + <excludeFolder url="file://$MODULE_DIR$/target" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module> +\ No newline at end of file diff --git a/src/data/mod.rs b/src/data/mod.rs @@ -0,0 +1,31 @@ +use crate::error::Result; +use crate::tests::Tests; +use ruma::identifiers::UserId; +use std::convert::TryFrom; +use std::sync::Mutex; + +#[derive(Debug, Default)] +pub struct Data { + pub users: Users, + pub current_test: Mutex<Tests>, +} + +impl Data { + pub fn set_current_test(&self, next_test: Tests) { + let mut state = self.current_test.lock().expect("Could not lock mutex"); + *state = next_test; + } +} + +#[derive(Debug, Default)] +pub struct Users; + +impl Users { + pub fn find_from_token(&self, _token: &str) -> Result<Option<(UserId, String)>> { + // TODO run tests + Ok(Some(( + UserId::try_from("@carl:example.com").unwrap(), + "KCZFUCGSLZ".to_string(), + ))) + } +} diff --git a/src/error.rs b/src/error.rs @@ -0,0 +1,36 @@ +use std::io; +use thiserror::Error; + +pub type Result<T> = std::result::Result<T, Error>; + +#[derive(Error, Debug)] +pub enum Error { + #[error("tried to parse invalid string")] + StringFromBytesError { + #[from] + source: std::string::FromUtf8Error, + }, + #[error("tried to parse invalid identifier")] + SerdeJsonError { + #[from] + source: serde_json::Error, + }, + #[error("tried to parse invalid identifier")] + RumaIdentifierError { + #[from] + source: ruma::identifiers::Error, + }, + #[error("tried to parse invalid event")] + RumaEventError { + #[from] + source: ruma::events::InvalidEvent, + }, + #[error("bad request")] + BadRequest(&'static str), +} + +#[derive(Error, Debug)] +pub enum TestErrors { + #[error(transparent)] + Io(#[from] io::Error), +} diff --git a/src/ruma_wrapper.rs b/src/ruma_wrapper.rs @@ -0,0 +1,179 @@ +/// From https://git.koesters.xyz/timo/conduit/src/commit/88d091fca1c1db9f238c7a42467f63a619201f8c/src/ruma_wrapper.rs +use crate::utils; +use log::warn; +use rocket::{ + data::{Data, FromData, FromDataFuture, Transform, TransformFuture, Transformed}, + http::Status, + response::{self, Responder}, + Outcome::*, + Request, State, +}; +use ruma::{api::Endpoint, identifiers::UserId}; +use std::{convert::TryInto, io::Cursor, ops::Deref}; +use tokio::io::AsyncReadExt; + +const MESSAGE_LIMIT: u64 = 20 * 1024 * 1024; // 20 MB + +/// This struct converts rocket requests into ruma structs by converting them into http requests +/// first. +pub struct Ruma<T> { + pub body: T, + pub user_id: Option<UserId>, + pub device_id: Option<String>, + pub json_body: Option<Box<serde_json::value::RawValue>>, // This is None when body is not a valid string +} + +impl<'a, T: Endpoint> FromData<'a> for Ruma<T> { + type Error = (); // TODO: Better error handling + type Owned = Data; + type Borrowed = Self::Owned; + + fn transform<'r>( + _req: &'r Request<'_>, + data: Data, + ) -> TransformFuture<'r, Self::Owned, Self::Error> { + Box::pin(async move { Transform::Owned(Success(data)) }) + } + + fn from_data( + request: &'a Request<'_>, + outcome: Transformed<'a, Self>, + ) -> FromDataFuture<'a, Self, Self::Error> { + Box::pin(async move { + let data = rocket::try_outcome!(outcome.owned()); + + let (user_id, device_id) = if T::METADATA.requires_authentication { + let db = request + .guard::<State<'_, crate::data::Data>>() + .await + .unwrap(); + + // Get token from header or query value + let token = match request + .headers() + .get_one("Authorization") + .map(|s| s[7..].to_owned()) // Split off "Bearer " + .or_else(|| request.get_query_value("access_token").and_then(|r| r.ok())) + { + // TODO: M_MISSING_TOKEN + None => return Failure((Status::Unauthorized, ())), + Some(token) => token, + }; + + // Check if token is valid + match db.users.find_from_token(&token).unwrap() { + // TODO: M_UNKNOWN_TOKEN + None => return Failure((Status::Unauthorized, ())), + Some((user_id, device_id)) => (Some(user_id), Some(device_id)), + } + } else { + (None, None) + }; + + let mut http_request = http::Request::builder() + .uri(request.uri().to_string()) + .method(&*request.method().to_string()); + for header in request.headers().iter() { + http_request = http_request.header(header.name.as_str(), &*header.value); + } + + let mut handle = data.open().take(MESSAGE_LIMIT); + let mut body = Vec::new(); + handle.read_to_end(&mut body).await.unwrap(); + + let http_request = http_request.body(body.clone()).unwrap(); + log::info!("{:?}", http_request); + + match T::try_from(http_request) { + Ok(t) => Success(Ruma { + body: t, + user_id, + device_id, + // TODO: Can we avoid parsing it again? (We only need this for append_pdu) + json_body: utils::string_from_bytes(&body) + .ok() + .and_then(|s| serde_json::value::RawValue::from_string(s).ok()), + }), + Err(e) => { + warn!("{:?}", e); + Failure((Status::BadRequest, ())) + } + } + }) + } +} + +impl<T> Deref for Ruma<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.body + } +} + +/// This struct converts ruma responses into rocket http responses. +pub struct MatrixResult<T, E = ruma::api::client::Error>(pub std::result::Result<T, E>); + +impl<T, E> TryInto<http::Response<Vec<u8>>> for MatrixResult<T, E> +where + T: TryInto<http::Response<Vec<u8>>>, + E: Into<http::Response<Vec<u8>>>, +{ + type Error = T::Error; + + fn try_into(self) -> Result<http::Response<Vec<u8>>, T::Error> { + match self.0 { + Ok(t) => t.try_into(), + Err(e) => Ok(e.into()), + } + } +} + +#[rocket::async_trait] +impl<'r, T, E> Responder<'r> for MatrixResult<T, E> +where + T: Send + TryInto<http::Response<Vec<u8>>>, + T::Error: Send, + E: Into<http::Response<Vec<u8>>> + Send, +{ + async fn respond_to(self, _: &'r Request<'_>) -> response::Result<'r> { + let http_response: Result<http::Response<_>, _> = self.try_into(); + match http_response { + Ok(http_response) => { + let mut response = rocket::response::Response::build(); + + let status = http_response.status(); + response.raw_status(status.into(), ""); + + for header in http_response.headers() { + response + .raw_header(header.0.to_string(), header.1.to_str().unwrap().to_owned()); + } + + response + .sized_body(Cursor::new(http_response.into_body())) + .await; + + response.raw_header("Access-Control-Allow-Origin", "*"); + response.raw_header( + "Access-Control-Allow-Methods", + "GET, POST, PUT, DELETE, OPTIONS", + ); + response.raw_header( + "Access-Control-Allow-Headers", + "Origin, X-Requested-With, Content-Type, Accept, Authorization", + ); + response.ok() + } + Err(_) => Err(Status::InternalServerError), + } + } +} + +impl<T, E> Deref for MatrixResult<T, E> { + type Target = Result<T, E>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/src/tests.rs b/src/tests.rs @@ -0,0 +1,79 @@ +use crate::error::TestErrors; +use rocket::data::{FromData, FromDataFuture, Transform, TransformFuture, Transformed}; +use rocket::http::ContentType; +use rocket::http::Status; +use rocket::response::Responder; +use rocket::response::{self, Response}; +use rocket::{Data, Outcome::*, Request}; +use serde::{Deserialize, Serialize}; +use std::io::Cursor; +use tokio::io::AsyncReadExt; + +#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)] +#[serde(tag = "test")] +#[serde(rename_all = "snake_case")] +pub enum Tests { + Start, + SyncTimeout, +} + +impl Default for Tests { + fn default() -> Self { + Tests::Start + } +} + +impl<'a> FromData<'a> for Tests { + type Error = TestErrors; + type Owned = String; + type Borrowed = str; + + fn transform<'r>( + request: &'r Request<'_>, + data: Data, + ) -> TransformFuture<'r, Self::Owned, Self::Error> { + Box::pin(async move { + let mut stream = data.open(); + let mut string = String::new(); + let outcome = match stream.read_to_string(&mut string).await { + Ok(_) => Success(string), + Err(e) => Failure((Status::InternalServerError, TestErrors::Io(e))), + }; + + // Returning `Borrowed` here means we get `Borrowed` in `from_data`. + Transform::Borrowed(outcome) + }) + } + + fn from_data( + request: &'a Request<'_>, + outcome: Transformed<'a, Self>, + ) -> FromDataFuture<'a, Self, Self::Error> { + Box::pin(async move { + // Retrieve a borrow to the now transformed `String` (an &str). This + // is only correct because we know we _always_ return a `Borrowed` from + // `transform` above. + let data = try_outcome!(outcome.borrowed()); + + let test_enum: Tests = serde_json::from_str(data).unwrap(); + Success(test_enum) + }) + } +} + +/// This struct converts test responses into rocket http responses. +pub struct TestsResult(pub std::result::Result<Tests, TestErrors>); + +#[rocket::async_trait] +impl<'r> Responder<'r> for TestsResult { + async fn respond_to(self, _: &'r Request<'_>) -> response::Result<'r> { + match self.0 { + Ok(v) => Response::build() + .header(ContentType::JSON) + .sized_body(Cursor::new(serde_json::to_string(&v).unwrap())) + .await + .ok(), + Err(v) => Err(Status::InternalServerError), + } + } +} diff --git a/src/utils.rs b/src/utils.rs @@ -0,0 +1,6 @@ +use crate::error::Result; + +/// Parses the bytes into a string. +pub fn string_from_bytes(bytes: &[u8]) -> Result<String> { + Ok(String::from_utf8(bytes.to_vec())?) +}