diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index afc2a04..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Build - -on: - push: - pull_request: - branches: [ "main" ] - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - submodules: true - - name: Install Protoc - uses: arduino/setup-protoc@v2 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - uses: Swatinem/rust-cache@v2 - with: - cache-on-failure: true - - name: Build - run: cargo build --verbose - diff --git a/.github/workflows/build_artifacts.yml b/.github/workflows/build_artifacts.yml new file mode 100644 index 0000000..e33dd72 --- /dev/null +++ b/.github/workflows/build_artifacts.yml @@ -0,0 +1,36 @@ +name: Build Artifacts + +on: + workflow_dispatch: + push: + +jobs: + build: + strategy: + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] + name: Build ${{ matrix.os }} + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v3 + with: + submodules: 'true' + - uses: arduino/setup-protoc@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + - name: cargo build --release + uses: clechasseur/rs-cargo@v1 + with: + command: build + args: --release + - uses: actions/upload-artifact@v3 + with: + name: ecdar_api-${{ matrix.os }} + path: ${{ runner.os == 'Windows' && 'target/release/ecdar_api.exe' || 'target/release/ecdar_api' }} + if-no-files-found: error + retention-days: 7 diff --git a/.github/workflows/check_format.yml b/.github/workflows/check_format.yml index 83e5ef9..1ace49e 100644 --- a/.github/workflows/check_format.yml +++ b/.github/workflows/check_format.yml @@ -6,13 +6,10 @@ on: jobs: fmt: - name: cargo fmt + name: cargo fmt & Clippy lint and check runs-on: ubuntu-latest steps: - - name: Install Protoc - uses: arduino/setup-protoc@v2 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + - run: sudo apt-get install llvm protobuf-compiler - uses: actions/checkout@v3 with: submodules: 'true' @@ -27,23 +24,7 @@ jobs: with: command: fmt args: --all -- --check - - clippy: - name: Clippy lint and check - runs-on: ubuntu-latest - steps: - - name: Install Protoc - uses: arduino/setup-protoc@v2 - - uses: actions/checkout@v3 - with: - submodules: 'true' - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - uses: Swatinem/rust-cache@v2 - with: - cache-on-failure: true - name: clippy --all-targets --all-features uses: clechasseur/rs-clippy-check@v3 with: - args: --all-targets --all-features \ No newline at end of file + args: --all-targets --all-features -- -D warnings diff --git a/.gitmodules b/.gitmodules index 6293847..6974b96 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "Ecdar-ProtoBuf"] - path = Ecdar-ProtoBuf - url = https://github.com/ECDAR-AAU-SW-P5/Ecdar-ProtoBuf.git + path = Ecdar-ProtoBuf + url = https://github.com/Ecdar/Ecdar-ProtoBuf + branch = SW5 # main diff --git a/Cargo.toml b/Cargo.toml index f202e55..501b233 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "api_server" +name = "ecdar_api" version = "0.0.1" build = "src/build.rs" edition = "2021" @@ -18,18 +18,21 @@ authors = [ 'William Woldum' ] +[lib] +name = "ecdar_api" +proc-macro = true + [[bin]] name = "server" path = "src/main.rs" [dependencies] -migration = { path = "migration" } tokio = { version = "1.33.0", features = ["full"] } dotenv = "0.15.0" sea-orm = { version = "^0.12.0", features = ["sqlx-postgres", "runtime-async-std-native-tls", "macros", "tests-cfg", "sqlx-sqlite"] } async-trait = { version = "0.1.73", features = [] } futures = "0.3.28" -tonic = "0.10.2" +tonic = "0.11.0" prost = "0.12.1" log = "0.4.20" jsonwebtoken = "9.1.0" @@ -37,14 +40,17 @@ serde = "1.0.189" chrono = "0.4.31" uuid = { version = "1.5.0", features = ["v4"] } regex = "1.10.2" -mockall = "0.11.4" +mockall = "0.12.1" bcrypt = "0.15.0" serde_json = "1.0.108" -ecdar_api_macros = { version = "0.1.0", path = "ecdar_api_macros" } +syn = { version = "2.0", features = ["full"] } +quote = "1.0" +convert_case = "0.6.0" +migration = { path = "migration" } thiserror = "1.0.50" [build-dependencies] -tonic-build = "0.10.2" +tonic-build = "0.11.0" [lints.clippy] complexity = "deny" diff --git a/Ecdar-ProtoBuf b/Ecdar-ProtoBuf index 79d3c1d..f5ae959 160000 --- a/Ecdar-ProtoBuf +++ b/Ecdar-ProtoBuf @@ -1 +1 @@ -Subproject commit 79d3c1d880b9dac488ccb5eb79352409cdc14033 +Subproject commit f5ae9598ebd6de74e17a7a03ae1b0896ae322ef7 diff --git a/ecdar_api_macros/Cargo.toml b/ecdar_api_macros/Cargo.toml deleted file mode 100644 index c9bdfde..0000000 --- a/ecdar_api_macros/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "ecdar_api_macros" -version = "0.1.0" -edition = "2021" - -[lib] -proc-macro = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -syn = { version = "2.0", features = ["full"] } -quote = "1.0" -convert_case = "0.6.0" diff --git a/src/api/auth.rs b/src/api/auth.rs index 19de77d..9182ac3 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -3,6 +3,7 @@ use jsonwebtoken::{ decode, encode, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation, }; +use jsonwebtoken::errors::{Error, ErrorKind}; use serde::{Deserialize, Serialize}; use std::{env, fmt::Display, str::FromStr}; use tonic::{ @@ -12,13 +13,24 @@ use tonic::{ /// This method is used to validate the access token (not refresh). pub fn validation_interceptor(mut req: Request<()>) -> Result, Status> { - let token = match req.token_string().map_err(|err| { + /* + let token = match req.token_string().map_err(|err| Status::internal(format!( "could not stringify user id in request metadata, internal error {}", err )) - })? { - Some(token) => Token::from_str(TokenType::AccessToken, &token), + */ + let token = match req + .token_string() + .map_err(|e| { + format!( + "could not stringify user id in request metadata, internal error {}", + e + ) + }) + .map_err(Status::internal)? + { + Some(token) => Token::from_str(TokenType::AccessToken, token), None => return Err(Status::unauthenticated("Token not found")), }; @@ -60,7 +72,6 @@ impl TokenType { /// /// # Panics /// This method will panic if the token secret environment variable is not set. - #[allow(clippy::expect_used)] fn secret(&self) -> String { match self { TokenType::AccessToken => env::var("ACCESS_TOKEN_HS512_SECRET") @@ -105,7 +116,7 @@ impl Token { /// /// let token = Token::new(TokenType::AccessToken, "1").unwrap(); /// ``` - pub fn new(token_type: TokenType, uid: &str) -> Result { + pub fn new>(token_type: TokenType, uid: T) -> Result { let now = Utc::now(); let expiration = now .checked_add_signed(token_type.duration()) @@ -113,7 +124,7 @@ impl Token { .timestamp(); let claims = Claims { - sub: uid.to_owned(), + sub: uid.into(), exp: expiration as usize, }; @@ -141,7 +152,7 @@ impl Token { /// /// assert_eq!(refresh_token.token_type(), TokenType::RefreshToken); /// ``` - pub fn refresh(uid: &str) -> Result { + pub fn refresh>(uid: T) -> Result { Token::new(TokenType::RefreshToken, uid) } @@ -158,7 +169,7 @@ impl Token { /// /// assert_eq!(access_token.token_type(), TokenType::AccessToken); /// ``` - pub fn access(uid: &str) -> Result { + pub fn access>(uid: T) -> Result { Token::new(TokenType::AccessToken, uid) } @@ -174,10 +185,10 @@ impl Token { /// /// let token = Token::from_str(TokenType::AccessToken, "token") /// ``` - pub fn from_str(token_type: TokenType, token: &str) -> Token { + pub fn from_str>(token_type: TokenType, token: T) -> Token { Token { token_type, - token: token.to_string(), + token: token.into(), } } /// Validate the token. Returns the token data if the token is valid. @@ -205,34 +216,6 @@ impl Token { Err(err) => Err(err.into()), } } - /// # Examples - /// - /// ``` - /// use ecdar_api::controllers::auth::{Token, TokenType}; - /// - /// let token = Token::from_str(TokenType::AccessToken, "token"); - /// - /// assert_eq!(token.as_str(), "token"); - /// ``` - #[allow(dead_code)] - pub fn as_str(&self) -> &str { - &self.token - } - /// Returns the token type. - /// - /// # Examples - /// - /// ``` - /// use ecdar_api::controllers::auth::{Token, TokenType}; - /// - /// let token = Token::new(TokenType::AccessToken, "1").unwrap(); - /// - /// assert_eq!(token.token_type(), TokenType::AccessToken); - /// ``` - #[allow(dead_code)] - pub fn token_type(&self) -> TokenType { - self.token_type.clone() - } } impl Display for Token { @@ -254,21 +237,21 @@ pub enum TokenError { Unknown(String), } -/// This is used to convert a [jsonwebtoken::errors::ErrorKind] to a [TokenError]. -impl From for TokenError { - fn from(error_kind: jsonwebtoken::errors::ErrorKind) -> Self { +/// This is used to convert a [ErrorKind] to a [TokenError]. +impl From for TokenError { + fn from(error_kind: ErrorKind) -> Self { match error_kind { - jsonwebtoken::errors::ErrorKind::InvalidToken => TokenError::InvalidToken, - jsonwebtoken::errors::ErrorKind::InvalidSignature => TokenError::InvalidSignature, - jsonwebtoken::errors::ErrorKind::ExpiredSignature => TokenError::ExpiredSignature, + ErrorKind::InvalidToken => TokenError::InvalidToken, + ErrorKind::InvalidSignature => TokenError::InvalidSignature, + ErrorKind::ExpiredSignature => TokenError::ExpiredSignature, _ => TokenError::Unknown("Unknown token error".to_string()), } } } -/// This is used to convert a [jsonwebtoken::errors::Error] to a [TokenError]. -impl From for TokenError { - fn from(error: jsonwebtoken::errors::Error) -> Self { +/// This is used to convert a [Error] to a [TokenError]. +impl From for TokenError { + fn from(error: Error) -> Self { TokenError::from(error.kind().clone()) } } @@ -320,5 +303,133 @@ impl RequestExt for Request { } #[cfg(test)] -#[path = "../tests/api/auth.rs"] -mod tests; +mod tests { + use crate::api::auth::{RequestExt, Token, TokenError, TokenType}; + use std::{env, str::FromStr}; + use tonic::{metadata::MetadataValue, Request}; + + #[tokio::test] + async fn request_token_trims_bearer() { + let token = "Bearer 1234567890"; + let mut request = Request::new(()); + request + .metadata_mut() + .insert("authorization", MetadataValue::from_str(token).unwrap()); + + let result = request.token_str().unwrap().unwrap(); + + assert_eq!(result, token.trim_start_matches("Bearer ")); + } + + #[tokio::test] + async fn request_token_no_token_returns_none() { + let request = Request::new(()); + let result = request.token_str().unwrap(); + + assert!(result.is_none()); + } + + #[tokio::test] + async fn token_new_access_returns_token() { + env::set_var("ACCESS_TOKEN_HS512_SECRET", "access_secret"); + + let uid = "1"; + let result = Token::new(TokenType::AccessToken, uid); + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn token_new_refresh_returns_token() { + env::set_var("REFRESH_TOKEN_HS512_SECRET", "refresh_secret"); + + let uid = "1"; + let result = Token::new(TokenType::RefreshToken, uid); + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn validate_token_valid_access_returns_tokendata() { + env::set_var("ACCESS_TOKEN_HS512_SECRET", "access_secret"); + + let token = Token::new(TokenType::AccessToken, "1").unwrap(); + let result = token.validate(); + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn validate_token_valid_refresh_returns_tokendata() { + env::set_var("REFRESH_TOKEN_HS512_SECRET", "refresh_secret"); + + let token = Token::new(TokenType::RefreshToken, "1").unwrap(); + let result = token.validate(); + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn validate_token_invalid_returns_err() { + env::set_var("ACCESS_TOKEN_HS512_SECRET", "access_secret"); + env::set_var("REFRESH_TOKEN_HS512_SECRET", "refresh_secret"); + + let result_access = Token::from_str(TokenType::AccessToken, "invalid_token").validate(); + let result_refresh = Token::from_str(TokenType::RefreshToken, "invalid_token").validate(); + + assert_eq!(result_access.unwrap_err(), TokenError::InvalidToken); + assert_eq!(result_refresh.unwrap_err(), TokenError::InvalidToken); + } + + #[tokio::test] + async fn token_type_access_returns_access() { + env::set_var("ACCESS_TOKEN_HS512_SECRET", "access_secret"); + + let token = Token::new(TokenType::AccessToken, "1").unwrap(); + let result = token.token_type; + + assert_eq!(result, TokenType::AccessToken); + } + + #[tokio::test] + async fn token_type_refresh_returns_refresh() { + env::set_var("REFRESH_TOKEN_HS512_SECRET", "refresh_secret"); + + let token = Token::new(TokenType::RefreshToken, "1").unwrap(); + let result = token.token_type; + + assert_eq!(result, TokenType::RefreshToken); + } + + #[tokio::test] + async fn token_to_string_returns_string() { + env::set_var("ACCESS_TOKEN_HS512_SECRET", "access_secret"); + + let token = Token::new(TokenType::AccessToken, "1").unwrap(); + let result = token.to_string(); + + assert_eq!(result, token.token); + } + + #[tokio::test] + async fn token_from_str_returns_token() { + env::set_var("ACCESS_TOKEN_HS512_SECRET", "access_secret"); + + let token = Token::new(TokenType::AccessToken, "1").unwrap(); + let token_from_str = Token::from_str(TokenType::AccessToken, token.token); + + let result = token_from_str.validate(); + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn token_from_str_invalid_returns_err() { + env::set_var("ACCESS_TOKEN_HS512_SECRET", "access_secret"); + + let token = Token::from_str(TokenType::AccessToken, "invalid_token"); + let result = token.validate(); + + assert!(result.is_err()); + } +} diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index 6b1066e..cdd03da 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -1,4 +1,4 @@ -use crate::controllers::controller_collection::ControllerCollection; +use crate::controllers::ControllerCollection; /// The collection of all controllers that Ecdar API offers. #[derive(Clone)] @@ -17,8 +17,9 @@ impl ConcreteEcdarApi { /// The module uses the attribute macro `endpoints` to automatically implement the `endpoints` function as specified by the protobuffers. /// Therefore, if new endpoints or services are added and implemented by the api server, then the macro will automatically add it to the list. /// The macro can be found in the `ecdar_api_macros` crate. -#[ecdar_api_macros::endpoints] +#[ecdar_api::endpoints] mod routes { + // TODO: Sometime maybe update it such that some endpoints are combined (queries, users, access, etc) to one endpoint (update project perhaps), but it takes an object with the updates use super::super::server::protobuf::{ ecdar_api_auth_server::EcdarApiAuth, ecdar_api_server::EcdarApi, ecdar_backend_server::EcdarBackend, CreateAccessRequest, CreateProjectRequest, diff --git a/src/api/server.rs b/src/api/server.rs index 316a834..b4cc94e 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -7,12 +7,12 @@ use crate::api::ecdar_api::ConcreteEcdarApi; use crate::api::server::protobuf::ecdar_api_auth_server::EcdarApiAuthServer; use crate::api::server::protobuf::ecdar_api_server::EcdarApiServer; use crate::api::server::protobuf::ecdar_backend_server::EcdarBackendServer; -use crate::controllers::controller_collection::ControllerCollection; +use crate::controllers::ControllerCollection; pub mod protobuf { tonic::include_proto!("ecdar_proto_buf"); } -#[allow(clippy::expect_used)] + pub async fn start_grpc_server( controllers: ControllerCollection, ) -> Result<(), Box> { @@ -33,7 +33,7 @@ pub async fn start_grpc_server( svc.clone(), auth::validation_interceptor, )) - .add_service(EcdarBackendServer::new(svc.clone())) + .add_service(EcdarBackendServer::new(svc)) .serve(addr) .await?; Ok(()) diff --git a/src/build.rs b/src/build.rs index 02170ec..7efe543 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1,4 +1,3 @@ -#[allow(clippy::expect_used)] fn main() { tonic_build::configure() .type_attribute( @@ -109,6 +108,10 @@ fn main() { "SystemClock", "#[derive(serde::Serialize, serde::Deserialize)]", ) + .type_attribute( + "SyntaxFailure", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) .enum_attribute("clock", "#[derive(serde::Serialize, serde::Deserialize)]") .enum_attribute( "node_type", diff --git a/src/contexts/access_context.rs b/src/contexts/access_context.rs new file mode 100644 index 0000000..ca224fc --- /dev/null +++ b/src/contexts/access_context.rs @@ -0,0 +1,543 @@ +use crate::api::server::protobuf::AccessInfo; +use crate::contexts::{DatabaseContextTrait, EntityContextTrait}; +use crate::entities::access; +use sea_orm::prelude::async_trait::async_trait; +use sea_orm::ActiveValue::{Set, Unchanged}; +use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, DbErr, EntityTrait, QueryFilter}; +use std::sync::Arc; + +#[async_trait] +pub trait AccessContextTrait: EntityContextTrait { + /// Searches for an access entity by `User` and `Project` id, + /// returning [`Some`] if any entity was found, [`None`] otherwise + /// # Errors + /// Errors on failed connection, execution error or constraint violations. + async fn get_access_by_uid_and_project_id( + &self, + uid: i32, + project_id: i32, + ) -> Result, DbErr>; + /// Returns all [`access::Model`] that are associated with a given `Project`` + async fn get_access_by_project_id(&self, project_id: i32) -> Result, DbErr>; +} + +pub struct AccessContext { + db_context: Arc, +} + +#[async_trait] +impl AccessContextTrait for AccessContext { + async fn get_access_by_uid_and_project_id( + &self, + uid: i32, + project_id: i32, + ) -> Result, DbErr> { + access::Entity::find() + .filter( + Condition::all() + .add(access::Column::UserId.eq(uid)) + .add(access::Column::ProjectId.eq(project_id)), + ) + .one(&self.db_context.get_connection()) + .await + } + + async fn get_access_by_project_id(&self, project_id: i32) -> Result, DbErr> { + access::Entity::find() + .filter(access::Column::ProjectId.eq(project_id)) + .into_model::() + .all(&self.db_context.get_connection()) + .await + } +} + +impl AccessContext { + pub fn new(db_context: Arc) -> AccessContext { + AccessContext { db_context } + } +} + +#[async_trait] +impl EntityContextTrait for AccessContext { + /// Used for creating an [`access::Model`] entity + /// # Example + /// ``` + /// let access = access::Model { + /// id: Default::default(), + /// role: Role::Editor, + /// user_id: 1, + /// project_id: 1 + /// }; + /// let context : AccessContext = AccessContext::new(...); + /// context.create(model); + /// ``` + async fn create(&self, entity: access::Model) -> Result { + let access = access::ActiveModel { + id: Default::default(), + role: Set(entity.role), + project_id: Set(entity.project_id), + user_id: Set(entity.user_id), + }; + access.insert(&self.db_context.get_connection()).await + //Ok(access.insert(&self.db_context.get_connection()).await?) + } + + /// Returns a single access entity (uses primary key) + /// # Example + /// ``` + /// let context : AccessContext = AccessContext::new(...); + /// let model : Model = context.get_by_id(1).unwrap(); + /// ``` + async fn get_by_id(&self, entity_id: i32) -> Result, DbErr> { + access::Entity::find_by_id(entity_id) + .one(&self.db_context.get_connection()) + .await + } + + /// Returns all the access entities + /// # Example + /// ``` + /// let context : AccessContext = AccessContext::new(...); + /// let model : vec = context.get_all().unwrap(); + /// ``` + async fn get_all(&self) -> Result, DbErr> { + access::Entity::find() + .all(&self.db_context.get_connection()) + .await + } + + /// Updates and returns the given access entity + /// # Example + /// ``` + /// let context : AccessContext = AccessContext::new(...); + /// let access = context.get_by_id(1).unwrap(); + /// let updated_access = Model { + /// id: access.id, + /// role: Role::Reader, + /// user_id: access.user_id, + /// project_id: access.project_id + /// } + /// ``` + /// # Note + /// The access entity's ids will never be changed. If this behavior is wanted, delete the old access and create a new one. + async fn update(&self, entity: access::Model) -> Result { + access::ActiveModel { + id: Unchanged(entity.id), + role: Set(entity.role), + project_id: Unchanged(entity.project_id), + user_id: Unchanged(entity.user_id), + } + .update(&self.db_context.get_connection()) + .await + } + + /// Deletes an access entity by id + async fn delete(&self, entity_id: i32) -> Result { + let access = self.get_by_id(entity_id).await?; + match access { + None => Err(DbErr::RecordNotFound("No record was deleted".into())), + Some(access) => access::Entity::delete_by_id(entity_id) + .exec(&self.db_context.get_connection()) + .await + .map(|_| access), + } + } +} +#[cfg(test)] +mod tests { + use super::super::helpers::{ + create_accesses, create_projects, create_users, get_reset_database_context, + }; + use crate::api::server::protobuf::AccessInfo; + use crate::contexts::AccessContextTrait; + use crate::contexts::EntityContextTrait; + use crate::{ + contexts::AccessContext, + entities::{access, project, user}, + to_active_models, + }; + use sea_orm::{entity::prelude::*, IntoActiveModel}; + //use crate::contexts::helpers::get_reset_database_context; + + async fn seed_db() -> (AccessContext, access::Model, user::Model, project::Model) { + let db_context = get_reset_database_context().await; + + let access_context = AccessContext::new(db_context); + + let user = create_users(1)[0].clone(); + let project = create_projects(1, user.id)[0].clone(); + let access = create_accesses(1, user.id, project.id)[0].clone(); + + user::Entity::insert(user.clone().into_active_model()) + .exec(&access_context.db_context.get_connection()) + .await + .unwrap(); + project::Entity::insert(project.clone().into_active_model()) + .exec(&access_context.db_context.get_connection()) + .await + .unwrap(); + + (access_context, access, user, project) + } + + // Test the functionality of the 'create' function, which creates a access in the contexts + #[tokio::test] + async fn create_test() { + let (access_context, access, _, _) = seed_db().await; + + let created_access = access_context.create(access.clone()).await.unwrap(); + + let fetched_access = access::Entity::find_by_id(created_access.id) + .one(&access_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + // Assert if the fetched access is the same as the created access + assert_eq!(access, created_access); + assert_eq!(fetched_access, created_access); + } + + #[tokio::test] + async fn create_check_unique_pair_project_id_user_id_test() { + let (access_context, access, _, _) = seed_db().await; + + let _created_access_1 = access_context.create(access.clone()).await.unwrap(); + let _created_access_2 = access_context.create(access.clone()).await; + + assert!(matches!( + _created_access_2.unwrap_err().sql_err(), + Some(SqlErr::UniqueConstraintViolation(_)) + )); + } + + #[tokio::test] + async fn create_invalid_role_test() { + let (access_context, mut access, _, _) = seed_db().await; + + access.role = "abc".into(); + + let created_access = access_context.create(access.clone()).await; + + assert!(matches!( + created_access.unwrap_err().sql_err(), + Some(SqlErr::ForeignKeyConstraintViolation(_)) + )); + } + + #[tokio::test] + async fn create_auto_increment_test() { + let (access_context, _, user, project_1) = seed_db().await; + + let mut project_2 = create_projects(1, user.id)[0].clone(); + project_2.id = project_1.id + 1; + project_2.name = "project_2".to_string(); + + project::Entity::insert(project_2.into_active_model()) + .exec(&access_context.db_context.get_connection()) + .await + .unwrap(); + + let access_1 = access::Model { + id: 0, + role: "Editor".to_string(), + project_id: 1, + user_id: user.id, + }; + + let access_2 = access::Model { + id: 0, + role: "Editor".to_string(), + project_id: 2, + user_id: user.id, + }; + + let created_access1 = access_context.create(access_1.clone()).await.unwrap(); + let created_access2 = access_context.create(access_2.clone()).await.unwrap(); + + let fetched_access1 = access::Entity::find_by_id(created_access1.id) + .one(&access_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + let fetched_access2 = access::Entity::find_by_id(created_access2.id) + .one(&access_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + assert_ne!(fetched_access1.id, fetched_access2.id); + assert_ne!(created_access1.id, created_access2.id); + assert_eq!(created_access1.id, fetched_access1.id); + assert_eq!(created_access2.id, fetched_access2.id); + } + + #[tokio::test] + async fn get_by_id_test() { + let (access_context, access, _, _) = seed_db().await; + + access::Entity::insert(access.clone().into_active_model()) + .exec(&access_context.db_context.get_connection()) + .await + .unwrap(); + + // Fetches the access created using the 'get_by_id' function + let fetched_access = access_context.get_by_id(access.id).await.unwrap().unwrap(); + + // Assert if the fetched access is the same as the created access + assert_eq!(access, fetched_access); + } + + #[tokio::test] + async fn get_by_non_existing_id_test() { + let (access_context, _, _, _) = seed_db().await; + + let fetched_access = access_context.get_by_id(1).await.unwrap(); + + assert!(fetched_access.is_none()); + } + + #[tokio::test] + async fn get_all_test() { + let (access_context, _, user, project) = seed_db().await; + + // Creates a model of the access which will be created + let new_accesses = create_accesses(1, user.id, project.id); + + // Creates the access in the contexts using the 'create' function + access::Entity::insert_many(to_active_models!(new_accesses.clone())) + .exec(&access_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(access_context.get_all().await.unwrap().len(), 1); + + let mut sorted: Vec = new_accesses.clone(); + sorted.sort_by_key(|k| k.id); + + for (i, access) in sorted.into_iter().enumerate() { + assert_eq!(access, new_accesses[i]); + } + } + + #[tokio::test] + async fn get_all_empty_test() { + let (access_context, _, _, _) = seed_db().await; + + let result = access_context.get_all().await.unwrap(); + let empty_accesses: Vec = vec![]; + + assert_eq!(empty_accesses, result); + } + + #[tokio::test] + async fn update_test() { + let (access_context, access, _, _) = seed_db().await; + + access::Entity::insert(access.clone().into_active_model()) + .exec(&access_context.db_context.get_connection()) + .await + .unwrap(); + + let new_access = access::Model { ..access }; + + let updated_access = access_context.update(new_access.clone()).await.unwrap(); + + let fetched_access = access::Entity::find_by_id(updated_access.id) + .one(&access_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + assert_eq!(new_access, updated_access); + assert_eq!(updated_access, fetched_access); + } + + #[tokio::test] + async fn update_modifies_role_test() { + let (access_context, access, _, _) = seed_db().await; + + let access = access::Model { + role: "Editor".into(), + ..access + }; + + access::Entity::insert(access.clone().into_active_model()) + .exec(&access_context.db_context.get_connection()) + .await + .unwrap(); + + let new_access = access::Model { + role: "Commenter".into(), + ..access + }; + + let updated_access = access_context.update(new_access.clone()).await.unwrap(); + + assert_ne!(access, updated_access); + assert_ne!(access, new_access); + } + + #[tokio::test] + async fn update_does_not_modify_id_test() { + let (access_context, access, _, _) = seed_db().await; + access::Entity::insert(access.clone().into_active_model()) + .exec(&access_context.db_context.get_connection()) + .await + .unwrap(); + + let updated_access = access::Model { + id: &access.id + 1, + ..access.clone() + }; + let res = access_context.update(updated_access.clone()).await; + + assert!(matches!(res.unwrap_err(), DbErr::RecordNotUpdated)); + } + + #[tokio::test] + async fn update_does_not_modify_project_id_test() { + let (access_context, access, _, _) = seed_db().await; + + access::Entity::insert(access.clone().into_active_model()) + .exec(&access_context.db_context.get_connection()) + .await + .unwrap(); + + let updated_access = access::Model { + project_id: &access.project_id + 1, + ..access.clone() + }; + let res = access_context.update(updated_access.clone()).await.unwrap(); + + assert_eq!(access, res); + } + + #[tokio::test] + async fn update_does_not_modify_user_id_test() { + let (access_context, access, _, _) = seed_db().await; + + access::Entity::insert(access.clone().into_active_model()) + .exec(&access_context.db_context.get_connection()) + .await + .unwrap(); + + let updated_access = access::Model { + user_id: &access.user_id + 1, + ..access.clone() + }; + let res = access_context.update(updated_access.clone()).await.unwrap(); + + assert_eq!(access, res); + } + + #[tokio::test] + async fn update_invalid_role_test() { + let (access_context, mut access, _, _) = seed_db().await; + + access::Entity::insert(access.clone().into_active_model()) + .exec(&access_context.db_context.get_connection()) + .await + .unwrap(); + + access.role = "abc".into(); + + let updated_access = access_context.update(access.clone()).await; + + assert!(matches!( + updated_access.unwrap_err().sql_err(), + Some(SqlErr::ForeignKeyConstraintViolation(_)) + )); + } + + #[tokio::test] + async fn update_non_existing_id_test() { + let (access_context, access, _, _) = seed_db().await; + + let updated_access = access_context.update(access.clone()).await; + + assert!(matches!( + updated_access.unwrap_err(), + DbErr::RecordNotUpdated + )); + } + + #[tokio::test] + async fn delete_test() { + let (access_context, access, _, _) = seed_db().await; + + access::Entity::insert(access.clone().into_active_model()) + .exec(&access_context.db_context.get_connection()) + .await + .unwrap(); + + let deleted_access = access_context.delete(access.id).await.unwrap(); + + let all_accesses = access::Entity::find() + .all(&access_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(access, deleted_access); + assert!(all_accesses.is_empty()); + } + + #[tokio::test] + async fn delete_non_existing_id_test() { + let (access_context, _, _, _) = seed_db().await; + + let deleted_access = access_context.delete(1).await; + + assert!(matches!( + deleted_access.unwrap_err(), + DbErr::RecordNotFound(_) + )); + } + + #[tokio::test] + async fn get_by_uid_and_project_id_test() { + let (access_context, expected_access, user, project) = seed_db().await; + + access::Entity::insert(expected_access.clone().into_active_model()) + .exec(&access_context.db_context.get_connection()) + .await + .unwrap(); + + let access = access_context + .get_access_by_uid_and_project_id(user.id, project.id) + .await; + + assert_eq!(access.unwrap().unwrap(), expected_access); + } + + #[tokio::test] + async fn get_access_by_project_id_test_returns_ok() { + let (access_context, expected_access, _, model) = seed_db().await; + + let expected_access_access_info_vector = vec![AccessInfo { + id: expected_access.id, + project_id: expected_access.project_id, + user_id: expected_access.user_id, + role: expected_access.role.clone(), + }]; + + access::Entity::insert(expected_access.clone().into_active_model()) + .exec(&access_context.db_context.get_connection()) + .await + .unwrap(); + + let access = access_context.get_access_by_project_id(model.id).await; + + assert_eq!(access.unwrap(), expected_access_access_info_vector); + } + + #[tokio::test] + async fn get_access_by_project_id_test_returns_empty() { + let (access_context, _, _, model) = seed_db().await; + + let access = access_context.get_access_by_project_id(model.id).await; + + assert!(access.unwrap().is_empty()); + } +} diff --git a/src/contexts/context_collection.rs b/src/contexts/context_collection.rs index 615ea8f..37c8f3f 100644 --- a/src/contexts/context_collection.rs +++ b/src/contexts/context_collection.rs @@ -1,4 +1,4 @@ -use crate::contexts::context_traits::*; +use crate::contexts::*; use std::sync::Arc; #[derive(Clone)] diff --git a/src/contexts/context_impls/access_context.rs b/src/contexts/context_impls/access_context.rs deleted file mode 100644 index dae2817..0000000 --- a/src/contexts/context_impls/access_context.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::api::server::protobuf::AccessInfo; -use crate::contexts::context_traits::{ - AccessContextTrait, DatabaseContextTrait, EntityContextTrait, -}; -use crate::entities::access; -use sea_orm::prelude::async_trait::async_trait; -use sea_orm::ActiveValue::{Set, Unchanged}; -use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, DbErr, EntityTrait, QueryFilter}; -use std::sync::Arc; - -pub struct AccessContext { - db_context: Arc, -} - -#[async_trait] -impl AccessContextTrait for AccessContext { - async fn get_access_by_uid_and_project_id( - &self, - uid: i32, - project_id: i32, - ) -> Result, DbErr> { - access::Entity::find() - .filter( - Condition::all() - .add(access::Column::UserId.eq(uid)) - .add(access::Column::ProjectId.eq(project_id)), - ) - .one(&self.db_context.get_connection()) - .await - } - - async fn get_access_by_project_id(&self, project_id: i32) -> Result, DbErr> { - access::Entity::find() - .filter(access::Column::ProjectId.eq(project_id)) - .into_model::() - .all(&self.db_context.get_connection()) - .await - } -} - -impl AccessContext { - pub fn new(db_context: Arc) -> AccessContext { - AccessContext { db_context } - } -} - -#[async_trait] -impl EntityContextTrait for AccessContext { - /// Used for creating an [`access::Model`] entity - /// # Example - /// ``` - /// let access = access::Model { - /// id: Default::default(), - /// role: Role::Editor, - /// user_id: 1, - /// project_id: 1 - /// }; - /// let context : AccessContext = AccessContext::new(...); - /// context.create(model); - /// ``` - async fn create(&self, entity: access::Model) -> Result { - let access = access::ActiveModel { - id: Default::default(), - role: Set(entity.role), - project_id: Set(entity.project_id), - user_id: Set(entity.user_id), - }; - let access: access::Model = access.insert(&self.db_context.get_connection()).await?; - Ok(access) - } - - /// Returns a single access entity (uses primary key) - /// # Example - /// ``` - /// let context : AccessContext = AccessContext::new(...); - /// let model : Model = context.get_by_id(1).unwrap(); - /// ``` - async fn get_by_id(&self, entity_id: i32) -> Result, DbErr> { - access::Entity::find_by_id(entity_id) - .one(&self.db_context.get_connection()) - .await - } - - /// Returns all the access entities - /// # Example - /// ``` - /// let context : AccessContext = AccessContext::new(...); - /// let model : vec = context.get_all().unwrap(); - /// ``` - async fn get_all(&self) -> Result, DbErr> { - access::Entity::find() - .all(&self.db_context.get_connection()) - .await - } - - /// Updates and returns the given access entity - /// # Example - /// ``` - /// let context : AccessContext = AccessContext::new(...); - /// let access = context.get_by_id(1).unwrap(); - /// let updated_access = Model { - /// id: access.id, - /// role: Role::Reader, - /// user_id: access.user_id, - /// project_id: access.project_id - /// } - /// ``` - /// # Note - /// The access entity's ids will never be changed. If this behavior is wanted, delete the old access and create a new one. - async fn update(&self, entity: access::Model) -> Result { - access::ActiveModel { - id: Unchanged(entity.id), - role: Set(entity.role), - project_id: Unchanged(entity.project_id), - user_id: Unchanged(entity.user_id), - } - .update(&self.db_context.get_connection()) - .await - } - - /// Deletes a access entity by id - async fn delete(&self, entity_id: i32) -> Result { - let access = self.get_by_id(entity_id).await?; - match access { - None => Err(DbErr::RecordNotFound("No record was deleted".into())), - Some(access) => { - access::Entity::delete_by_id(entity_id) - .exec(&self.db_context.get_connection()) - .await?; - Ok(access) - } - } - } -} -#[cfg(test)] -#[path = "../../tests/contexts/access_context.rs"] -mod access_context_tests; diff --git a/src/contexts/context_impls/in_use_context.rs b/src/contexts/context_impls/in_use_context.rs deleted file mode 100644 index 46745da..0000000 --- a/src/contexts/context_impls/in_use_context.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::contexts::context_traits::{ - DatabaseContextTrait, EntityContextTrait, InUseContextTrait, -}; -use crate::entities::in_use; -use async_trait::async_trait; -use chrono::Utc; -use sea_orm::{ActiveModelTrait, DbErr, EntityTrait, Set, Unchanged}; -use std::sync::Arc; - -pub struct InUseContext { - db_context: Arc, -} - -impl InUseContextTrait for InUseContext {} - -impl InUseContext { - pub fn new(db_context: Arc) -> InUseContext { - InUseContext { db_context } - } -} -#[async_trait] -impl EntityContextTrait for InUseContext { - /// Used for creating a Model entity - async fn create(&self, entity: in_use::Model) -> Result { - let in_use = in_use::ActiveModel { - project_id: Set(entity.project_id), - session_id: Set(entity.session_id), - latest_activity: Set(Utc::now().naive_local()), - }; - let in_use: in_use::Model = in_use.insert(&self.db_context.get_connection()).await?; - Ok(in_use) - } - - async fn get_by_id(&self, entity_id: i32) -> Result, DbErr> { - in_use::Entity::find_by_id(entity_id) - .one(&self.db_context.get_connection()) - .await - } - - async fn get_all(&self) -> Result, DbErr> { - in_use::Entity::find() - .all(&self.db_context.get_connection()) - .await - } - - async fn update(&self, entity: in_use::Model) -> Result { - in_use::ActiveModel { - project_id: Unchanged(entity.project_id), - session_id: Set(entity.session_id), - latest_activity: Set(entity.latest_activity), - } - .update(&self.db_context.get_connection()) - .await - } - - async fn delete(&self, entity_id: i32) -> Result { - let in_use = self.get_by_id(entity_id).await?; - match in_use { - None => Err(DbErr::RecordNotFound("No record was deleted".into())), - Some(in_use) => { - in_use::Entity::delete_by_id(entity_id) - .exec(&self.db_context.get_connection()) - .await?; - Ok(in_use) - } - } - } -} - -#[cfg(test)] -#[path = "../../tests/contexts/in_use_context.rs"] -mod in_use_context_tests; diff --git a/src/contexts/context_impls/mod.rs b/src/contexts/context_impls/mod.rs deleted file mode 100644 index e0b0fcb..0000000 --- a/src/contexts/context_impls/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -pub mod access_context; -pub mod in_use_context; -pub mod postgres_database_context; -pub mod project_context; -pub mod query_context; -pub mod session_context; -pub mod sqlite_database_context; -pub mod user_context; - -pub use access_context::AccessContext; -pub use in_use_context::InUseContext; -pub use postgres_database_context::PostgresDatabaseContext; -pub use project_context::ProjectContext; -pub use query_context::QueryContext; -pub use session_context::SessionContext; -pub use sqlite_database_context::SQLiteDatabaseContext; -pub use user_context::UserContext; diff --git a/src/contexts/context_impls/project_context.rs b/src/contexts/context_impls/project_context.rs deleted file mode 100644 index fa64d97..0000000 --- a/src/contexts/context_impls/project_context.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::contexts::context_traits::{ - DatabaseContextTrait, EntityContextTrait, ProjectContextTrait, -}; -use crate::entities::{access, project, query}; - -use crate::api::server::protobuf::ProjectInfo; -use async_trait::async_trait; -use sea_orm::{ - ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, IntoActiveModel, JoinType, ModelTrait, - QueryFilter, QuerySelect, RelationTrait, Set, Unchanged, -}; -use std::sync::Arc; - -pub struct ProjectContext { - db_context: Arc, -} - -#[async_trait] -impl ProjectContextTrait for ProjectContext { - async fn get_project_info_by_uid(&self, uid: i32) -> Result, DbErr> { - //join project, access and role tables - access::Entity::find() - .select_only() - .column_as(project::Column::Id, "project_id") - .column_as(project::Column::Name, "project_name") - .column_as(project::Column::OwnerId, "project_owner_id") - .column_as(access::Column::Role, "user_role_on_project") - .join(JoinType::InnerJoin, access::Relation::Project.def()) - .join(JoinType::InnerJoin, access::Relation::Role.def()) - .group_by(project::Column::Id) - .group_by(access::Column::Role) - .filter(access::Column::UserId.eq(uid)) - .into_model::() - .all(&self.db_context.get_connection()) - .await - } -} - -impl ProjectContext { - pub fn new(db_context: Arc) -> ProjectContext { - ProjectContext { db_context } - } -} - -#[async_trait] -impl EntityContextTrait for ProjectContext { - /// Used for creating a project::Model entity - /// # Example - /// ``` - /// let project = project::Model { - /// id: Default::default(), - /// name: "project::Model name".to_owned(), - /// components_info: "{}".to_owned().parse().unwrap(), - /// owner_id: 1 - /// }; - /// let project_context: ProjectContext = ProjectContext::new(...); - /// project_context.create(project); - /// ``` - async fn create(&self, entity: project::Model) -> Result { - let project = project::ActiveModel { - id: Default::default(), - name: Set(entity.name), - components_info: Set(entity.components_info), - owner_id: Set(entity.owner_id), - }; - let project: project::Model = project.insert(&self.db_context.get_connection()).await?; - Ok(project) - } - - /// Returns a single project entity (Uses primary key) - /// # Example - /// ``` - /// let project_context: ProjectContext = ProjectContext::new(...); - /// let project = project_context.get_by_id(1).unwrap(); - /// ``` - async fn get_by_id(&self, entity_id: i32) -> Result, DbErr> { - project::Entity::find_by_id(entity_id) - .one(&self.db_context.get_connection()) - .await - } - - /// Returns a all project entities (Uses primary key) - /// # Example - /// ``` - /// let project_context: ProjectContext = ProjectContext::new(...); - /// let project = project_context.get_all().unwrap(); - /// ``` - async fn get_all(&self) -> Result, DbErr> { - project::Entity::find() - .all(&self.db_context.get_connection()) - .await - } - - /// Updates a single project entity - /// # Example - /// ``` - /// let update_project = project::Model { - /// name: "new name", - /// ..original_project - /// }; - /// - /// let project_context: ProjectContext = ProjectContext::new(...); - /// let project = project_context.update(update_project).unwrap(); - /// ``` - async fn update(&self, entity: project::Model) -> Result { - let existing_project = self.get_by_id(entity.id).await?; - - return match existing_project { - None => Err(DbErr::RecordNotUpdated), - Some(existing_project) => { - let queries: Vec = existing_project - .find_related(query::Entity) - .all(&self.db_context.get_connection()) - .await?; - for q in queries.iter() { - let mut aq = q.clone().into_active_model(); - aq.outdated = Set(true); - aq.update(&self.db_context.get_connection()).await?; - } - project::ActiveModel { - id: Unchanged(entity.id), - name: Set(entity.name), - components_info: Set(entity.components_info), - owner_id: Unchanged(entity.id), - } - .update(&self.db_context.get_connection()) - .await - } - }; - } - - /// Returns and deletes a single project entity - /// # Example - /// ``` - /// let project_context: ProjectContext = ProjectContext::new(...); - /// let project = project_context.delete().unwrap(); - /// ``` - async fn delete(&self, entity_id: i32) -> Result { - let project = self.get_by_id(entity_id).await?; - match project { - None => Err(DbErr::RecordNotFound("No record was deleted".into())), - Some(project) => { - project::Entity::delete_by_id(entity_id) - .exec(&self.db_context.get_connection()) - .await?; - Ok(project) - } - } - } -} - -#[cfg(test)] -#[path = "../../tests/contexts/project_context.rs"] -mod project_context_tests; diff --git a/src/contexts/context_impls/query_context.rs b/src/contexts/context_impls/query_context.rs deleted file mode 100644 index 4656871..0000000 --- a/src/contexts/context_impls/query_context.rs +++ /dev/null @@ -1,132 +0,0 @@ -use crate::contexts::context_traits::{ - DatabaseContextTrait, EntityContextTrait, QueryContextTrait, -}; -use crate::entities::query; -use sea_orm::prelude::async_trait::async_trait; -use sea_orm::ActiveValue::{Set, Unchanged}; -use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, NotSet, QueryFilter}; -use std::sync::Arc; - -pub struct QueryContext { - db_context: Arc, -} -#[async_trait] -impl QueryContextTrait for QueryContext { - async fn get_all_by_project_id(&self, project_id: i32) -> Result, DbErr> { - query::Entity::find() - .filter(query::Column::ProjectId.eq(project_id)) - .all(&self.db_context.get_connection()) - .await - } -} - -impl QueryContext { - pub fn new(db_context: Arc) -> QueryContext { - QueryContext { db_context } - } -} - -#[async_trait] -impl EntityContextTrait for QueryContext { - /// Used for creating a query entity - /// ## Example - /// ``` - /// let model : Model = { - /// id: Default::default(), - /// string: "query_string".into(), - /// project_id: 1, - /// result: "query_result".into(), - /// out_dated: true - /// } - /// let context : QueryContext = QueryContext::new(...); - /// context.create(model); - /// ``` - async fn create(&self, entity: query::Model) -> Result { - let query = query::ActiveModel { - id: Default::default(), - string: Set(entity.string), - project_id: Set(entity.project_id), - result: NotSet, - outdated: NotSet, - }; - let query = query.insert(&self.db_context.get_connection()).await?; - Ok(query) - } - - /// Returns a single query entity (uses primary key) - /// ## Example - /// ``` - /// let context : QueryContext = QueryContext::new(...); - /// let model : Model = context.get_by_id(1).unwrap(); - /// assert_eq!(model.string,"query_string".into()); - /// ``` - async fn get_by_id(&self, entity_id: i32) -> Result, DbErr> { - query::Entity::find_by_id(entity_id) - .one(&self.db_context.get_connection()) - .await - } - - /// Returns all the query entities - /// ## Example - /// ``` - /// let context : QueryContext = QueryContext::new(...); - /// let model : vec = context.get_all().unwrap(); - /// assert_eq!(model.len(),5); - /// ``` - async fn get_all(&self) -> Result, DbErr> { - query::Entity::find() - .all(&self.db_context.get_connection()) - .await - } - - /// Updates and returns the given user entity - /// ## Example - /// ``` - /// let context : QueryContext = QueryContext::new(...); - /// let query = context.get_by_id(1).unwrap(); - /// let updated_query = Model { - /// id: query.id, - /// string: query.string, - /// project_id: query.project_id, - /// result: query.result, - /// out_dated: false - /// } - /// assert_eq!(context.update(updated_query).unwrap(),Model { - /// id: 1, - /// string: "query_string".into(), - /// project_id: 1, - /// result: "query_result".into(), - /// out_dated: false - /// } - /// ``` - /// ## Note - /// The user entity's id will never be changed. If this behavior is wanted, delete the old user and create a one. - async fn update(&self, entity: query::Model) -> Result { - query::ActiveModel { - id: Unchanged(entity.id), - string: Set(entity.string), - result: Set(entity.result), - outdated: Set(entity.outdated), - project_id: Unchanged(entity.project_id), - } - .update(&self.db_context.get_connection()) - .await - } - - async fn delete(&self, entity_id: i32) -> Result { - let query = self.get_by_id(entity_id).await?; - match query { - None => Err(DbErr::RecordNotFound("No record was deleted".into())), - Some(query) => { - query::Entity::delete_by_id(entity_id) - .exec(&self.db_context.get_connection()) - .await?; - Ok(query) - } - } - } -} - -#[cfg(test)] -#[path = "../../tests/contexts/query_context.rs"] -mod query_context_tests; diff --git a/src/contexts/context_impls/session_context.rs b/src/contexts/context_impls/session_context.rs deleted file mode 100644 index 874431a..0000000 --- a/src/contexts/context_impls/session_context.rs +++ /dev/null @@ -1,182 +0,0 @@ -use crate::api::auth::TokenType; -use crate::contexts::context_traits::{ - DatabaseContextTrait, EntityContextTrait, SessionContextTrait, -}; -use crate::entities::session; -use chrono::Local; -use sea_orm::prelude::async_trait::async_trait; -use sea_orm::ActiveValue::{Set, Unchanged}; -use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, NotSet, QueryFilter}; -use std::sync::Arc; - -pub struct SessionContext { - db_context: Arc, -} - -#[async_trait] -impl SessionContextTrait for SessionContext { - async fn get_by_token( - &self, - token_type: TokenType, - token: String, - ) -> Result, DbErr> { - match token_type { - TokenType::AccessToken => { - session::Entity::find() - .filter(session::Column::AccessToken.eq(token)) - .one(&self.db_context.get_connection()) - .await - } - TokenType::RefreshToken => { - session::Entity::find() - .filter(session::Column::RefreshToken.eq(token)) - .one(&self.db_context.get_connection()) - .await - } - } - } - - async fn delete_by_token( - &self, - token_type: TokenType, - token: String, - ) -> Result { - let session = self - .get_by_token(token_type, token) - .await? - .ok_or(DbErr::RecordNotFound( - "No session found with the provided access token".into(), - ))?; - - session::Entity::delete_by_id(session.id) - .exec(&self.db_context.get_connection()) - .await?; - - Ok(session) - } -} - -impl SessionContext { - pub fn new(db_context: Arc) -> Self { - SessionContext { db_context } - } -} - -#[async_trait] -impl EntityContextTrait for SessionContext { - /// Creates a new session in the contexts based on the provided model. - /// # Example - /// ```rust - /// use crate::entities::session::{Entity, Model}; - /// - /// let new_session = Model { - /// id: 1, - /// token: Uuid::parse_str("4473240f-2acb-422f-bd1a-5214554ed0e0").unwrap(), - /// created_at: Local::now().naive_utc(), - /// user_id, - /// }; - /// let created_session = session_context.create(model).await.unwrap(); - /// ``` - async fn create(&self, entity: session::Model) -> Result { - let session = session::ActiveModel { - id: Default::default(), - refresh_token: Set(entity.refresh_token), - access_token: Set(entity.access_token), - user_id: Set(entity.user_id), - updated_at: NotSet, - }; - - session.insert(&self.db_context.get_connection()).await - } - - /// Returns a session by searching for its id. - /// # Example - /// ```rust - /// let session: Result, DbErr> = session_context.get_by_id(id).await; - /// ``` - async fn get_by_id(&self, id: i32) -> Result, DbErr> { - session::Entity::find_by_id(id) - .one(&self.db_context.get_connection()) - .await - } - - /// Returns all models in a vector. - /// # Example - /// ```rust - /// let session: Result, DbErr> = session_context.get_all().await; - /// ``` - async fn get_all(&self) -> Result, DbErr> { - session::Entity::find() - .all(&self.db_context.get_connection()) - .await - } - - /// Updates a model in the contexts based on the provided model. - /// # **Example** - /// ## ***Model in contexts*** - /// ### Model table ### - /// | id | token | created_at | user_id | - /// |----|--------------------------------------|---------------------------|---------| - /// | 1 | 25b14ea1-7b78-4714-b3d0-35d9f56e6cb3 | 2023-09-22T12:42:13+02:00 | 2 | - /// ## ***Rust code*** - /// ```rust - /// use crate::entities::session::{Entity, Model}; - /// - /// let new_session = Model { - /// id: 1, - /// token: Uuid::parse_str("4473240f-2acb-422f-bd1a-5214554ed0e0").unwrap(), - /// created_at: Local::now().naive_utc(), - /// user_id: 2, - /// }; - /// let created_session = session_context.create(model).await.unwrap(); - /// ``` - /// ## ***Result*** - /// ### Model table ### - /// | id | token | created_at | user_id | - /// |----|--------------------------------------|---------------------------|---------| - /// | 1 | 4473240f-2acb-422f-bd1a-5214554ed0e0 | 2023-10-24T13:49:16+02:00 | 2 | - async fn update(&self, entity: session::Model) -> Result { - session::ActiveModel { - id: Unchanged(entity.id), - refresh_token: Set(entity.refresh_token), - access_token: Set(entity.access_token), - user_id: Unchanged(entity.user_id), - updated_at: Set(Local::now().naive_local()), - } - .update(&self.db_context.get_connection()) - .await - } - - /// Deletes a model in the contexts with a specific id. - /// # **Example** - /// ## ***Model in contexts*** - /// ### Model table ### - /// | id | token | created_at | user_id | - /// |----|--------------------------------------|---------------------------|---------| - /// | 1 | 25b14ea1-7b78-4714-b3d0-35d9f56e6cb3 | 2023-10-24T14:03:37+02:00 | 2 | - /// ## ***Rust code*** - /// ```rust - /// let deleted_session = session_context.delete(1).await.unwrap(); - /// ``` - /// ## ***Result*** - /// ### Model table ### - /// | id | token | created_at | user_id | - /// |----|-------|------------|---------| - /// | | | | | - async fn delete(&self, id: i32) -> Result { - let session = self.get_by_id(id).await?; - match session { - None => Err(DbErr::RecordNotFound("No record was deleted".into())), - Some(session) => { - session::Entity::delete_by_id(id) - .exec(&self.db_context.get_connection()) - .await?; - Ok(session) - } - } - } -} - -#[cfg(test)] -#[path = "../../tests/contexts/session_context.rs"] -mod session_context_tests; diff --git a/src/contexts/context_impls/user_context.rs b/src/contexts/context_impls/user_context.rs deleted file mode 100644 index 8f0ee8e..0000000 --- a/src/contexts/context_impls/user_context.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::contexts::context_traits::{DatabaseContextTrait, EntityContextTrait, UserContextTrait}; -use crate::entities::user; -use sea_orm::prelude::async_trait::async_trait; -use sea_orm::ActiveValue::{Set, Unchanged}; -use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter}; -use std::sync::Arc; - -pub struct UserContext { - db_context: Arc, -} - -#[async_trait] -impl UserContextTrait for UserContext { - async fn get_by_username(&self, username: String) -> Result, DbErr> { - user::Entity::find() - .filter(user::Column::Username.eq(username)) - .one(&self.db_context.get_connection()) - .await - } - async fn get_by_email(&self, email: String) -> Result, DbErr> { - user::Entity::find() - .filter(user::Column::Email.eq(email)) - .one(&self.db_context.get_connection()) - .await - } - - async fn get_by_ids(&self, ids: Vec) -> Result, DbErr> { - user::Entity::find() - .filter(user::Column::Id.is_in(ids)) - .all(&self.db_context.get_connection()) - .await - } -} - -impl UserContext { - pub fn new(db_context: Arc) -> UserContext { - UserContext { db_context } - } -} - -#[async_trait] -impl EntityContextTrait for UserContext { - /// Used for creating a User entity - /// # Example - /// ``` - /// let user : Model = { - /// id: Default::default(), - /// email: "anders@aau.dk".into(), - /// username: "Anders".into(), - /// password: "qwerty".into() - /// } - /// let context : UserContext = UserContext::new(...); - /// context.create(user); - /// ``` - async fn create(&self, entity: user::Model) -> Result { - let user = user::ActiveModel { - id: Default::default(), - email: Set(entity.email), - username: Set(entity.username), - password: Set(entity.password), - }; - let user = user.insert(&self.db_context.get_connection()).await?; - Ok(user) - } - - /// Returns a single user entity (uses primary key) - /// # Example - /// ``` - /// let context : UserContext = UserContext::new(...); - /// let user : Model = context.get_by_id(1).unwrap(); - /// assert_eq!(user.username,"Anders".into()); - /// ``` - async fn get_by_id(&self, entity_id: i32) -> Result, DbErr> { - user::Entity::find_by_id(entity_id) - .one(&self.db_context.get_connection()) - .await - } - - /// Returns all the user entities - /// # Example - /// ``` - /// let context : UserContext = UserContext::new(...); - /// let user : vec = context.get_all().unwrap(); - /// assert_eq!(user.len(),1); - /// ``` - async fn get_all(&self) -> Result, DbErr> { - user::Entity::find() - .all(&self.db_context.get_connection()) - .await - } - - /// Updates and returns the given user entity - /// # Example - /// ``` - /// let context : UserContext = UserContext::new(...); - /// let user = context.get_by_id(1).unwrap(); - /// let updated_user = Model { - /// id: user.id, - /// email: "anders@student.aau.dk".into(), - /// username: "andersAnden", - /// password: user.password - /// } - /// assert_eq!(context.update(updated_user).unwrap(),Model { - /// id: 1, - /// email: "anders@student.aau.dk".into(), - /// username: "andersAnden".into(), - /// password:"qwerty".into(); - /// } - /// ``` - /// # Note - /// The user entity's id will never be changed. If this behavior is wanted, delete the old user and create a new one. - async fn update(&self, entity: user::Model) -> Result { - user::ActiveModel { - id: Unchanged(entity.id), - email: Set(entity.email), - username: Set(entity.username), - password: Set(entity.password), - } - .update(&self.db_context.get_connection()) - .await - } - - /// Returns and deletes a user entity by id - /// - /// # Example - /// ``` - /// let context : UserContext = UserContext::new(...); - /// let user = context.get_by_id(1).unwrap(); - /// let deleted_user = Model { - /// id: user.id, - /// email: "anders@student.aau.dk".into(), - /// username: "andersAnden", - /// password: user.password - /// } - async fn delete(&self, entity_id: i32) -> Result { - let user = self.get_by_id(entity_id).await?; - match user { - None => Err(DbErr::RecordNotFound("No record was deleted".into())), - Some(user) => { - user::Entity::delete_by_id(entity_id) - .exec(&self.db_context.get_connection()) - .await?; - Ok(user) - } - } - } -} - -#[cfg(test)] -#[path = "../../tests/contexts/user_context.rs"] -mod user_context_tests; diff --git a/src/contexts/context_traits/access_context_trait.rs b/src/contexts/context_traits/access_context_trait.rs deleted file mode 100644 index 36d08e0..0000000 --- a/src/contexts/context_traits/access_context_trait.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::api::server::protobuf::AccessInfo; -use crate::contexts::context_traits::EntityContextTrait; -use crate::entities::access; -use async_trait::async_trait; -use sea_orm::DbErr; - -#[async_trait] -pub trait AccessContextTrait: EntityContextTrait { - /// Searches for an access entity by `User` and `Project` id, - /// returning [`Some`] if any entity was found, [`None`] otherwise - /// # Errors - /// Errors on failed connection, execution error or constraint violations. - async fn get_access_by_uid_and_project_id( - &self, - uid: i32, - project_id: i32, - ) -> Result, DbErr>; - /// Returns all [`access::Model`] that are associated with a given `Project`` - async fn get_access_by_project_id(&self, project_id: i32) -> Result, DbErr>; -} diff --git a/src/contexts/context_traits/in_use_context_trait.rs b/src/contexts/context_traits/in_use_context_trait.rs deleted file mode 100644 index 84181ed..0000000 --- a/src/contexts/context_traits/in_use_context_trait.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::contexts::context_traits::EntityContextTrait; -use crate::entities::in_use; - -pub trait InUseContextTrait: EntityContextTrait {} diff --git a/src/contexts/context_traits/mod.rs b/src/contexts/context_traits/mod.rs deleted file mode 100644 index a35698e..0000000 --- a/src/contexts/context_traits/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -pub mod access_context_trait; -pub mod database_context_trait; -pub mod entity_context_trait; -pub mod in_use_context_trait; -pub mod project_context_trait; -pub mod query_context_trait; -pub mod session_context_trait; -pub mod user_context_trait; - -pub use access_context_trait::AccessContextTrait; -pub use database_context_trait::DatabaseContextTrait; -pub use entity_context_trait::EntityContextTrait; -pub use in_use_context_trait::InUseContextTrait; -pub use project_context_trait::ProjectContextTrait; -pub use query_context_trait::QueryContextTrait; -pub use session_context_trait::SessionContextTrait; -pub use user_context_trait::UserContextTrait; diff --git a/src/contexts/context_traits/project_context_trait.rs b/src/contexts/context_traits/project_context_trait.rs deleted file mode 100644 index d6ec6db..0000000 --- a/src/contexts/context_traits/project_context_trait.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::api::server::protobuf::ProjectInfo; -use crate::contexts::context_traits::EntityContextTrait; -use crate::entities::project; -use async_trait::async_trait; -use sea_orm::DbErr; - -#[async_trait] -pub trait ProjectContextTrait: EntityContextTrait { - /// Returns the projects owned by a given user id - /// # Errors - /// Errors on failed connection, execution error or constraint violations. - async fn get_project_info_by_uid(&self, uid: i32) -> Result, DbErr>; -} diff --git a/src/contexts/context_traits/query_context_trait.rs b/src/contexts/context_traits/query_context_trait.rs deleted file mode 100644 index ba50e87..0000000 --- a/src/contexts/context_traits/query_context_trait.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::contexts::context_traits::EntityContextTrait; -use crate::entities::query; -use async_trait::async_trait; -use sea_orm::DbErr; - -#[async_trait] -pub trait QueryContextTrait: EntityContextTrait { - /// Returns the queries associated with a given project id - async fn get_all_by_project_id(&self, project_id: i32) -> Result, DbErr>; -} diff --git a/src/contexts/context_traits/session_context_trait.rs b/src/contexts/context_traits/session_context_trait.rs deleted file mode 100644 index 5139cea..0000000 --- a/src/contexts/context_traits/session_context_trait.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::api::auth::TokenType; -use crate::contexts::context_traits::EntityContextTrait; -use crate::entities::session; -use async_trait::async_trait; -use sea_orm::DbErr; - -#[async_trait] -pub trait SessionContextTrait: EntityContextTrait { - /// Searches for a token by `Access` or `Refresh` token, - /// returning [`Some`] if one is found, [`None`] otherwise - /// # Errors - /// Errors on failed connection, execution error or constraint violations. - async fn get_by_token( - &self, - token_type: TokenType, - token: String, - ) -> Result, DbErr>; - /// Searches for a token by `Access` or `Refresh` token, deleting and returning it - /// # Errors - /// Errors on failed connection, execution error or constraint violations. - async fn delete_by_token( - &self, - token_type: TokenType, - token: String, - ) -> Result; -} diff --git a/src/contexts/context_traits/user_context_trait.rs b/src/contexts/context_traits/user_context_trait.rs deleted file mode 100644 index 8cf410b..0000000 --- a/src/contexts/context_traits/user_context_trait.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::contexts::context_traits::EntityContextTrait; -use crate::entities::user; -use async_trait::async_trait; -use sea_orm::DbErr; - -#[async_trait] -pub trait UserContextTrait: EntityContextTrait { - /// Searches for a `User` by username, returning [`Some`] if one is found, [`None`] otherwise. - /// # Errors - /// Errors on failed connection, execution error or constraint violations. - /// # Notes - /// Since usernames are unique, it is guaranteed that at most one user with the given username exists. - async fn get_by_username(&self, username: String) -> Result, DbErr>; - /// Searches for a `User` by email address, returning [`Some`] if one is found, [`None`] otherwise. - /// # Errors - /// Errors on failed connection, execution error or constraint violations. - /// # Notes - /// Since email address' are unique, it is guaranteed that at most one user with the given email address exists. - async fn get_by_email(&self, email: String) -> Result, DbErr>; - /// Returns all the user entities with the given ids - /// # Example - /// ``` - /// let context : UserContext = UserContext::new(...); - /// let model : vec = context.get_by_ids(vec![1,2]).unwrap(); - /// assert_eq!(model.len(),2); - /// ``` - async fn get_by_ids(&self, ids: Vec) -> Result, DbErr>; -} diff --git a/src/contexts/context_traits/database_context_trait.rs b/src/contexts/db_centexts/database_context.rs similarity index 100% rename from src/contexts/context_traits/database_context_trait.rs rename to src/contexts/db_centexts/database_context.rs diff --git a/src/contexts/db_centexts/mod.rs b/src/contexts/db_centexts/mod.rs new file mode 100644 index 0000000..78bcc9f --- /dev/null +++ b/src/contexts/db_centexts/mod.rs @@ -0,0 +1,7 @@ +pub mod database_context; +pub mod postgres_context; +pub mod sqlite_context; + +pub use database_context::DatabaseContextTrait; +pub use postgres_context::PostgresDatabaseContext; +pub use sqlite_context::SQLiteDatabaseContext; diff --git a/src/contexts/context_impls/postgres_database_context.rs b/src/contexts/db_centexts/postgres_context.rs similarity index 67% rename from src/contexts/context_impls/postgres_database_context.rs rename to src/contexts/db_centexts/postgres_context.rs index 848bf3e..76b3383 100644 --- a/src/contexts/context_impls/postgres_database_context.rs +++ b/src/contexts/db_centexts/postgres_context.rs @@ -1,22 +1,22 @@ -use crate::contexts::context_traits::DatabaseContextTrait; +use crate::contexts::DatabaseContextTrait; use async_trait::async_trait; use migration::{Migrator, MigratorTrait}; use sea_orm::{ConnectionTrait, Database, DatabaseConnection, DbBackend, DbErr}; use std::sync::Arc; + #[derive(Debug)] pub struct PostgresDatabaseContext { pub(crate) db_connection: DatabaseConnection, } impl PostgresDatabaseContext { - pub async fn new(connection_string: &str) -> Result { + pub async fn new(connection_string: &str) -> Result { let db = Database::connect(connection_string).await?; - let db = match db.get_database_backend() { - DbBackend::Postgres => db, - _ => panic!("Expected postgresql connection string"), - }; + if db.get_database_backend() != DbBackend::Postgres { + panic!("Expected postgresql connection string"); + } - Ok(PostgresDatabaseContext { db_connection: db }) + Ok(Self { db_connection: db }) } } diff --git a/src/contexts/context_impls/sqlite_database_context.rs b/src/contexts/db_centexts/sqlite_context.rs similarity index 63% rename from src/contexts/context_impls/sqlite_database_context.rs rename to src/contexts/db_centexts/sqlite_context.rs index 62719c1..91a6e84 100644 --- a/src/contexts/context_impls/sqlite_database_context.rs +++ b/src/contexts/db_centexts/sqlite_context.rs @@ -1,8 +1,7 @@ -use crate::contexts::context_traits::DatabaseContextTrait; +use crate::contexts::DatabaseContextTrait; use migration::{Migrator, MigratorTrait}; use sea_orm::prelude::async_trait::async_trait; use sea_orm::{ConnectionTrait, Database, DatabaseConnection, DbBackend, DbErr}; -use std::fmt::Debug; use std::sync::Arc; #[derive(Debug)] @@ -11,15 +10,14 @@ pub struct SQLiteDatabaseContext { } impl SQLiteDatabaseContext { - pub async fn new(connection_string: &str) -> Result { + pub async fn new(connection_string: &str) -> Result { let db = Database::connect(connection_string).await?; - let db = match db.get_database_backend() { - DbBackend::Sqlite => db, - _ => panic!("Expected sqlite connection string"), - }; + if db.get_database_backend() != DbBackend::Sqlite { + panic!("Expected sqlite connection string"); + } - Ok(SQLiteDatabaseContext { db_connection: db }) + Ok(Self { db_connection: db }) } } @@ -28,7 +26,7 @@ impl DatabaseContextTrait for SQLiteDatabaseContext { async fn reset(&self) -> Result, DbErr> { Migrator::fresh(&self.db_connection).await?; - Ok(Arc::new(SQLiteDatabaseContext { + Ok(Arc::new(Self { db_connection: self.get_connection(), })) } diff --git a/src/contexts/context_traits/entity_context_trait.rs b/src/contexts/entity_context_trait.rs similarity index 89% rename from src/contexts/context_traits/entity_context_trait.rs rename to src/contexts/entity_context_trait.rs index 5bbb3f9..e018e1b 100644 --- a/src/contexts/context_traits/entity_context_trait.rs +++ b/src/contexts/entity_context_trait.rs @@ -1,12 +1,12 @@ //! The base trait for all database entities. Exposes basic CRUD functionality for. -//! Some specific entities might need additional functionality, but that should implemented in entity-specific traits. +//! Some specific entities might need additional functionality, but that should be implemented in entity-specific traits. use sea_orm::prelude::async_trait::async_trait; -use sea_orm::DbErr; +use sea_orm::{DbErr, ModelTrait}; #[async_trait] /// The base trait for all database entities. Exposes basic CRUD functionality for. -/// Some specific entities might need additional functionality, but that should implemented in entity-specific traits. -pub trait EntityContextTrait: Send + Sync { +/// Some specific entities might need additional functionality, but that should be implemented in entity-specific traits. +pub trait EntityContextTrait: Send + Sync { /// Inserts an entity into the database /// # Errors /// Errors on failed connection, execution error or constraint violations. diff --git a/src/contexts/in_use_context.rs b/src/contexts/in_use_context.rs new file mode 100644 index 0000000..56b7142 --- /dev/null +++ b/src/contexts/in_use_context.rs @@ -0,0 +1,341 @@ +use crate::contexts::{DatabaseContextTrait, EntityContextTrait}; +use crate::entities::in_use; +use async_trait::async_trait; +use chrono::Utc; +use sea_orm::{ActiveModelTrait, DbErr, EntityTrait, Set, Unchanged}; +use std::sync::Arc; + +pub trait InUseContextTrait: EntityContextTrait {} + +pub struct InUseContext { + db_context: Arc, +} + +impl InUseContextTrait for InUseContext {} + +impl InUseContext { + pub fn new(db_context: Arc) -> InUseContext { + InUseContext { db_context } + } +} +#[async_trait] +impl EntityContextTrait for InUseContext { + /// Used for creating a Model entity + async fn create(&self, entity: in_use::Model) -> Result { + in_use::ActiveModel { + project_id: Set(entity.project_id), + session_id: Set(entity.session_id), + latest_activity: Set(Utc::now().naive_local()), + } + .insert(&self.db_context.get_connection()) + .await + } + + async fn get_by_id(&self, entity_id: i32) -> Result, DbErr> { + in_use::Entity::find_by_id(entity_id) + .one(&self.db_context.get_connection()) + .await + } + + async fn get_all(&self) -> Result, DbErr> { + in_use::Entity::find() + .all(&self.db_context.get_connection()) + .await + } + + async fn update(&self, entity: in_use::Model) -> Result { + in_use::ActiveModel { + project_id: Unchanged(entity.project_id), + session_id: Set(entity.session_id), + latest_activity: Set(entity.latest_activity), + } + .update(&self.db_context.get_connection()) + .await + } + + async fn delete(&self, entity_id: i32) -> Result { + let model = self + .get_by_id(entity_id) + .await? + .ok_or(DbErr::RecordNotFound("No record was deleted".into()))?; + in_use::Entity::delete_by_id(entity_id) + .exec(&self.db_context.get_connection()) + .await?; + Ok(model) + } +} + +#[cfg(test)] +mod tests { + use super::super::helpers::*; + use crate::{ + contexts::EntityContextTrait, + contexts::InUseContext, + entities::{in_use, project, session, user}, + to_active_models, + }; + use chrono::{Duration, Utc}; + use sea_orm::{entity::prelude::*, IntoActiveModel}; + use std::matches; + use std::ops::Add; + + async fn seed_db() -> ( + InUseContext, + in_use::Model, + session::Model, + project::Model, + user::Model, + ) { + let db_context = get_reset_database_context().await; + + let in_use_context = InUseContext::new(db_context); + + let user = create_users(1)[0].clone(); + let project = create_projects(1, user.id)[0].clone(); + let session = create_sessions(1, user.id)[0].clone(); + let in_use = create_in_uses(1, project.id, session.id)[0].clone(); + + user::Entity::insert(user.clone().into_active_model()) + .exec(&in_use_context.db_context.get_connection()) + .await + .unwrap(); + project::Entity::insert(project.clone().into_active_model()) + .exec(&in_use_context.db_context.get_connection()) + .await + .unwrap(); + session::Entity::insert(session.clone().into_active_model()) + .exec(&in_use_context.db_context.get_connection()) + .await + .unwrap(); + + (in_use_context, in_use, session, project, user) + } + + #[tokio::test] + async fn create_test() { + let (in_use_context, mut in_use, _, _, _) = seed_db().await; + + let inserted_in_use = in_use_context.create(in_use.clone()).await.unwrap(); + + in_use.latest_activity = inserted_in_use.latest_activity; + + let fetched_in_use = in_use::Entity::find_by_id(inserted_in_use.clone().project_id) + .one(&in_use_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + assert_eq!(in_use, inserted_in_use); + assert_eq!(in_use, fetched_in_use); + } + + #[tokio::test] + async fn create_default_latest_activity_test() { + let t_min = Utc::now().timestamp(); + + let (in_use_context, in_use, _, _, _) = seed_db().await; + + let inserted_in_use = in_use_context.create(in_use.clone()).await.unwrap(); + + let fetched_in_use = in_use::Entity::find_by_id(inserted_in_use.project_id) + .one(&in_use_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + let t_max = Utc::now().timestamp(); + + let t_actual = fetched_in_use.clone().latest_activity.timestamp(); + + assert!(t_min <= t_actual && t_actual <= t_max) + } + + #[tokio::test] + async fn get_by_id_test() { + let (in_use_context, in_use, _, _, _) = seed_db().await; + + in_use::Entity::insert(in_use.clone().into_active_model()) + .exec(&in_use_context.db_context.get_connection()) + .await + .unwrap(); + + let fetched_in_use = in_use_context + .get_by_id(in_use.project_id) + .await + .unwrap() + .unwrap(); + + assert_eq!(fetched_in_use, in_use) + } + + #[tokio::test] + async fn get_by_non_existing_id_test() { + let (in_use_context, _in_use, _, _, _) = seed_db().await; + + let in_use = in_use_context.get_by_id(1).await; + + assert!(in_use.unwrap().is_none()) + } + + #[tokio::test] + async fn get_all_test() { + let (in_use_context, _in_use, session, project, _user) = seed_db().await; + + let in_uses = create_in_uses(1, project.id, session.id); + + in_use::Entity::insert_many(to_active_models!(in_uses.clone())) + .exec(&in_use_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(in_use_context.get_all().await.unwrap().len(), 1); + } + + #[tokio::test] + async fn get_all_empty_test() { + let (in_use_context, _, _, _, _) = seed_db().await; + + let in_uses = in_use_context.get_all().await.unwrap(); + + assert_eq!(0, in_uses.len()) + } + + #[tokio::test] + async fn update_test() { + let (in_use_context, in_use, _, _, _) = seed_db().await; + + in_use::Entity::insert(in_use.clone().into_active_model()) + .exec(&in_use_context.db_context.get_connection()) + .await + .unwrap(); + + let new_in_use = in_use::Model { ..in_use }; + + let updated_in_use = in_use_context.update(new_in_use.clone()).await.unwrap(); + + let fetched_in_use = in_use::Entity::find_by_id(updated_in_use.project_id) + .one(&in_use_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + assert_eq!(new_in_use, updated_in_use); + assert_eq!(updated_in_use, fetched_in_use); + } + + #[tokio::test] + async fn update_modifies_latest_activity_test() { + let (in_use_context, in_use, _, _, _) = seed_db().await; + + in_use::Entity::insert(in_use.clone().into_active_model()) + .exec(&in_use_context.db_context.get_connection()) + .await + .unwrap(); + + let new_in_use = in_use::Model { + latest_activity: in_use.clone().latest_activity.add(Duration::seconds(1)), + ..in_use + }; + + let updated_in_use = in_use_context.update(new_in_use.clone()).await.unwrap(); + + assert_ne!(in_use, updated_in_use); + assert_ne!(in_use, new_in_use); + } + + #[tokio::test] + async fn update_modifies_session_id_test() { + let (in_use_context, in_use, _, _, _) = seed_db().await; + + in_use::Entity::insert(in_use.clone().into_active_model()) + .exec(&in_use_context.db_context.get_connection()) + .await + .unwrap(); + + let mut session2 = create_sessions(1, in_use.session_id)[0].clone(); + session2.id = in_use.session_id + 1; + session2.refresh_token = "new_refresh_token".to_string(); + session2.access_token = "new_access_token".to_string(); + + session::Entity::insert(session2.clone().into_active_model()) + .exec(&in_use_context.db_context.get_connection()) + .await + .unwrap(); + + let new_in_use = in_use::Model { + session_id: in_use.session_id + 1, + ..in_use + }; + + let updated_in_use = in_use_context.update(new_in_use.clone()).await.unwrap(); + + assert_ne!(in_use, updated_in_use); + assert_ne!(in_use, new_in_use); + } + + #[tokio::test] + async fn update_does_not_modify_project_id_test() { + let (in_use_context, in_use, _, _, _) = seed_db().await; + + in_use::Entity::insert(in_use.clone().into_active_model()) + .exec(&in_use_context.db_context.get_connection()) + .await + .unwrap(); + + let updated_in_use = in_use::Model { + project_id: in_use.project_id + 1, + ..in_use.clone() + }; + + let updated_in_use = in_use_context.update(updated_in_use.clone()).await; + + assert!(matches!( + updated_in_use.unwrap_err(), + DbErr::RecordNotUpdated + )); + } + + #[tokio::test] + async fn update_non_existing_id_test() { + let (in_use_context, in_use, _, _, _) = seed_db().await; + + let updated_in_use = in_use_context.update(in_use.clone()).await; + + assert!(matches!( + updated_in_use.unwrap_err(), + DbErr::RecordNotUpdated + )); + } + + #[tokio::test] + async fn delete_test() { + let (in_use_context, in_use, _, _, _) = seed_db().await; + + in_use::Entity::insert(in_use.clone().into_active_model()) + .exec(&in_use_context.db_context.get_connection()) + .await + .unwrap(); + + let deleted_in_use = in_use_context.delete(in_use.project_id).await.unwrap(); + + let all_in_uses = in_use::Entity::find() + .all(&in_use_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(in_use, deleted_in_use); + assert!(all_in_uses.is_empty()); + } + + #[tokio::test] + async fn delete_non_existing_id_test() { + let (in_use_context, _, _, _, _) = seed_db().await; + + let deleted_in_use = in_use_context.delete(1).await; + + assert!(matches!( + deleted_in_use.unwrap_err(), + DbErr::RecordNotFound(_) + )) + } +} diff --git a/src/contexts/mod.rs b/src/contexts/mod.rs index f48d865..b01b91f 100644 --- a/src/contexts/mod.rs +++ b/src/contexts/mod.rs @@ -1,3 +1,141 @@ -pub mod context_collection; -pub mod context_impls; -pub mod context_traits; +mod access_context; +mod context_collection; +mod db_centexts; +mod entity_context_trait; +mod in_use_context; +mod project_context; +mod query_context; +mod session_context; +mod user_context; + +pub use access_context::*; +pub use context_collection::*; +pub use db_centexts::*; +pub use entity_context_trait::*; +pub use in_use_context::*; +pub use project_context::*; +pub use query_context::*; +pub use session_context::*; +pub use user_context::*; + +#[cfg(test)] +mod helpers { + use crate::contexts::*; + use crate::entities::{access, in_use, project, query, session, user}; + use dotenv::dotenv; + use sea_orm::{ConnectionTrait, Database, DbBackend}; + use std::env; + use std::sync::Arc; + + pub async fn get_reset_database_context() -> Arc { + dotenv().ok(); + + let url = + env::var("TEST_DATABASE_URL").expect("TEST_DATABASE_URL must be set to run tests."); + let db = Database::connect(&url).await.unwrap(); + let db_context: Arc = match db.get_database_backend() { + DbBackend::Sqlite => Arc::new(SQLiteDatabaseContext::new(&url).await.unwrap()), + DbBackend::Postgres => Arc::new(PostgresDatabaseContext::new(&url).await.unwrap()), + _ => panic!("Database protocol not supported"), + }; + + db_context.reset().await.unwrap() + } + + /// + /// + /// # Arguments + /// + /// * `amount`: + /// * `model`: + /// + /// returns: Vec + /// + /// # Examples + /// + /// ``` + /// let vector: Vec = create_entities(3,|x| UserModel { + /// id: &x+i, + /// email: format!("mail{}@mail.dk",&x), + /// username: format!("username{}", &x), + /// password: format!("qwerty{}", &x), + /// ); + /// ``` + + pub fn create_entities(amount: i32, project_creator: F) -> Vec + where + F: Fn(i32) -> M, + { + let mut vector: Vec = vec![]; + for i in 0..amount { + vector.push(project_creator(i)); + } + vector + } + + pub fn create_users(amount: i32) -> Vec { + create_entities(amount, |i| user::Model { + id: i + 1, + email: format!("mail{}@mail.dk", &i), + username: format!("username{}", &i), + password: format!("qwerty{}", &i), + }) + } + + pub fn create_projects(amount: i32, user_id: i32) -> Vec { + create_entities(amount, |i| project::Model { + id: i + 1, + name: format!("name {}", i), + components_info: "{}".to_owned().parse().unwrap(), + owner_id: user_id, + }) + } + + pub fn create_accesses(amount: i32, user_id: i32, project_id: i32) -> Vec { + create_entities(amount, |i| access::Model { + id: i + 1, + role: "Reader".into(), + project_id: project_id + i, + user_id: user_id + i, + }) + } + + pub fn create_sessions(amount: i32, user_id: i32) -> Vec { + create_entities(amount, |i| session::Model { + id: i + 1, + refresh_token: "test_refresh_token".to_string() + format!("{}", i).as_str(), + access_token: "test_access_token".to_string() + format!("{}", i).as_str(), + user_id, + updated_at: Default::default(), + }) + } + + pub fn create_in_uses(amount: i32, project_id: i32, session_id: i32) -> Vec { + create_entities(amount, |i| in_use::Model { + project_id: project_id + i, + session_id, + latest_activity: Default::default(), + }) + } + + pub fn create_queries(amount: i32, project_id: i32) -> Vec { + create_entities(amount, |i| query::Model { + id: i + 1, + string: "".to_string(), + result: None, + outdated: true, + project_id, + }) + } + + #[macro_export] + macro_rules! to_active_models { + ($vec:expr) => {{ + let mut models = Vec::new(); + for model in $vec { + models.push(model.into_active_model()); + } + models + }}; + } +} diff --git a/src/contexts/project_context.rs b/src/contexts/project_context.rs new file mode 100644 index 0000000..7be8683 --- /dev/null +++ b/src/contexts/project_context.rs @@ -0,0 +1,565 @@ +use crate::entities::{access, project, query}; + +use crate::api::server::protobuf::ProjectInfo; +use crate::contexts::{DatabaseContextTrait, EntityContextTrait}; +use async_trait::async_trait; +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, IntoActiveModel, JoinType, ModelTrait, + QueryFilter, QuerySelect, RelationTrait, Set, Unchanged, +}; +use std::sync::Arc; + +#[async_trait] +pub trait ProjectContextTrait: EntityContextTrait { + /// Returns the projects owned by a given user id + /// # Errors + /// Errors on failed connection, execution error or constraint violations. + async fn get_project_info_by_uid(&self, uid: i32) -> Result, DbErr>; +} + +pub struct ProjectContext { + db_context: Arc, +} + +#[async_trait] +impl ProjectContextTrait for ProjectContext { + async fn get_project_info_by_uid(&self, uid: i32) -> Result, DbErr> { + //join project, access and role tables + access::Entity::find() + .select_only() + .column_as(project::Column::Id, "project_id") + .column_as(project::Column::Name, "project_name") + .column_as(project::Column::OwnerId, "project_owner_id") + .column_as(access::Column::Role, "user_role_on_project") + .join(JoinType::InnerJoin, access::Relation::Project.def()) + .join(JoinType::InnerJoin, access::Relation::Role.def()) + .group_by(project::Column::Id) + .group_by(access::Column::Role) + .filter(access::Column::UserId.eq(uid)) + .into_model::() + .all(&self.db_context.get_connection()) + .await + } +} + +impl ProjectContext { + pub fn new(db_context: Arc) -> ProjectContext { + ProjectContext { db_context } + } +} + +#[async_trait] +impl EntityContextTrait for ProjectContext { + /// Used for creating a project::Model entity + /// # Example + /// ``` + /// let project = project::Model { + /// id: Default::default(), + /// name: "project::Model name".to_owned(), + /// components_info: "{}".to_owned().parse().unwrap(), + /// owner_id: 1 + /// }; + /// let project_context: ProjectContext = ProjectContext::new(...); + /// project_context.create(project); + /// ``` + async fn create(&self, entity: project::Model) -> Result { + project::ActiveModel { + id: Default::default(), + name: Set(entity.name), + components_info: Set(entity.components_info), + owner_id: Set(entity.owner_id), + } + .insert(&self.db_context.get_connection()) + .await + } + + /// Returns a single project entity (Uses primary key) + /// # Example + /// ``` + /// let project_context: ProjectContext = ProjectContext::new(...); + /// let project = project_context.get_by_id(1).unwrap(); + /// ``` + async fn get_by_id(&self, entity_id: i32) -> Result, DbErr> { + project::Entity::find_by_id(entity_id) + .one(&self.db_context.get_connection()) + .await + } + + /// Returns a all project entities (Uses primary key) + /// # Example + /// ``` + /// let project_context: ProjectContext = ProjectContext::new(...); + /// let project = project_context.get_all().unwrap(); + /// ``` + async fn get_all(&self) -> Result, DbErr> { + project::Entity::find() + .all(&self.db_context.get_connection()) + .await + } + + /// Updates a single project entity + /// # Example + /// ``` + /// let update_project = project::Model { + /// name: "new name", + /// ..original_project + /// }; + /// + /// let project_context: ProjectContext = ProjectContext::new(...); + /// let project = project_context.update(update_project).unwrap(); + /// ``` + async fn update(&self, entity: project::Model) -> Result { + let existing_project = self + .get_by_id(entity.id) + .await? + .ok_or(DbErr::RecordNotUpdated)?; + + let queries: Vec = existing_project + .find_related(query::Entity) + .all(&self.db_context.get_connection()) + .await?; + for q in queries.iter() { + let mut aq = q.clone().into_active_model(); + aq.outdated = Set(true); + aq.update(&self.db_context.get_connection()).await?; + } + project::ActiveModel { + id: Unchanged(entity.id), + name: Set(entity.name), + components_info: Set(entity.components_info), + owner_id: Unchanged(entity.id), + } + .update(&self.db_context.get_connection()) + .await + } + + /// Returns and deletes a single project entity + /// # Example + /// ``` + /// let project_context: ProjectContext = ProjectContext::new(...); + /// let project = project_context.delete().unwrap(); + /// ``` + async fn delete(&self, entity_id: i32) -> Result { + let project = self + .get_by_id(entity_id) + .await? + .ok_or(DbErr::RecordNotFound("No record was deleted".into()))?; + project::Entity::delete_by_id(entity_id) + .exec(&self.db_context.get_connection()) + .await?; + Ok(project) + } +} + +#[cfg(test)] +mod tests { + use super::super::helpers::*; + use crate::{ + contexts::EntityContextTrait, + contexts::ProjectContext, + entities::{access, in_use, project, query, session, user}, + to_active_models, + }; + use sea_orm::error::DbErr; + use sea_orm::{entity::prelude::*, IntoActiveModel}; + use std::matches; + + async fn seed_db() -> (ProjectContext, project::Model, user::Model) { + let db_context = get_reset_database_context().await; + + let project_context = ProjectContext::new(db_context); + + let user = create_users(1)[0].clone(); + let project = create_projects(1, user.id)[0].clone(); + + user::Entity::insert(user.clone().into_active_model()) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + + (project_context, project, user) + } + + #[tokio::test] + async fn create_test() { + let (project_context, project, _) = seed_db().await; + + let created_project = project_context.create(project.clone()).await.unwrap(); + + let fetched_project = project::Entity::find_by_id(created_project.id) + .one(&project_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + assert_eq!(project, created_project); + assert_eq!(fetched_project, created_project); + } + + #[tokio::test] + async fn create_auto_increment_test() { + let (project_context, project, _) = seed_db().await; + + let projects = create_projects(2, project.owner_id); + + let created_project1 = project_context.create(projects[0].clone()).await.unwrap(); + let created_project2 = project_context.create(projects[1].clone()).await.unwrap(); + + let fetched_project1 = project::Entity::find_by_id(created_project1.id) + .one(&project_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + let fetched_project2 = project::Entity::find_by_id(created_project2.id) + .one(&project_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + assert_ne!(fetched_project1.id, fetched_project2.id); + assert_ne!(created_project1.id, created_project2.id); + assert_eq!(created_project1.id, fetched_project1.id); + assert_eq!(created_project2.id, fetched_project2.id); + } + + #[tokio::test] + async fn get_by_id_test() { + let (project_context, project, _) = seed_db().await; + + project::Entity::insert(project.clone().into_active_model()) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + + let fetched_project = project_context + .get_by_id(project.id) + .await + .unwrap() + .unwrap(); + + assert_eq!(project, fetched_project); + } + + #[tokio::test] + async fn get_by_non_existing_id_test() { + let (project_context, _, _) = seed_db().await; + + let fetched_project = project_context.get_by_id(1).await.unwrap(); + + assert!(fetched_project.is_none()); + } + + #[tokio::test] + async fn get_all_test() { + let (project_context, _, user) = seed_db().await; + + let new_projects = create_projects(3, user.id); + + project::Entity::insert_many(to_active_models!(new_projects.clone())) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(project_context.get_all().await.unwrap().len(), 3); + + let mut sorted = new_projects.clone(); + sorted.sort_by_key(|k| k.id); + + for (i, project) in sorted.into_iter().enumerate() { + assert_eq!(project, new_projects[i]); + } + } + + #[tokio::test] + async fn get_all_empty_test() { + let (project_context, _, _) = seed_db().await; + + let result = project_context.get_all().await.unwrap(); + let empty_projects: Vec = vec![]; + + assert_eq!(empty_projects, result); + } + + #[tokio::test] + async fn update_test() { + let (project_context, project, _) = seed_db().await; + + project::Entity::insert(project.clone().into_active_model()) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + + let new_project = project::Model { ..project }; + + let updated_project = project_context.update(new_project.clone()).await.unwrap(); + + let fetched_project = project::Entity::find_by_id(updated_project.id) + .one(&project_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + assert_eq!(new_project, updated_project); + assert_eq!(updated_project, fetched_project); + } + + #[tokio::test] + async fn update_modifies_name_test() { + let (project_context, project, _) = seed_db().await; + + let project = project::Model { + name: "project1".into(), + ..project.clone() + }; + + project::Entity::insert(project.clone().into_active_model()) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + + let new_project = project::Model { + name: "project2".into(), + ..project.clone() + }; + + let updated_project = project_context.update(new_project.clone()).await.unwrap(); + + assert_ne!(project, updated_project); + assert_ne!(project, new_project); + } + + #[tokio::test] + async fn update_modifies_components_info_test() { + let (project_context, project, _) = seed_db().await; + + let project = project::Model { + components_info: "{\"a\":1}".to_owned().parse().unwrap(), + ..project.clone() + }; + + project::Entity::insert(project.clone().into_active_model()) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + + let new_project = project::Model { + components_info: "{\"a\":2}".to_owned().parse().unwrap(), + ..project.clone() + }; + + let updated_project = project_context.update(new_project.clone()).await.unwrap(); + + assert_ne!(project, updated_project); + assert_ne!(project, new_project); + } + + #[tokio::test] + async fn update_does_not_modify_id_test() { + let (project_context, project, _) = seed_db().await; + + project::Entity::insert(project.clone().into_active_model()) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + + let new_project = project::Model { + id: &project.id + 1, + ..project.clone() + }; + + let res = project_context.update(new_project.clone()).await; + + assert!(matches!(res.unwrap_err(), DbErr::RecordNotUpdated)); + } + + #[tokio::test] + async fn update_does_not_modify_owner_id_test() { + let (project_context, project, _) = seed_db().await; + + project::Entity::insert(project.clone().into_active_model()) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + + let new_project = project::Model { + owner_id: &project.owner_id + 1, + ..project.clone() + }; + + let res = project_context.update(new_project.clone()).await.unwrap(); + + assert_eq!(project, res); + } + + #[tokio::test] + async fn update_check_query_outdated_test() { + let (project_context, project, _) = seed_db().await; + + let mut query = create_queries(1, project.id)[0].clone(); + + query.outdated = false; + + project::Entity::insert(project.clone().into_active_model()) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + + query::Entity::insert(query.clone().into_active_model()) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + + let new_project = project::Model { ..project }; + + let updated_project = project_context.update(new_project.clone()).await.unwrap(); + + let fetched_query = query::Entity::find_by_id(updated_project.id) + .one(&project_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + assert!(fetched_query.outdated); + } + + #[tokio::test] + async fn update_non_existing_id_test() { + let (project_context, project, _) = seed_db().await; + + let updated_project = project_context.update(project.clone()).await; + + assert!(matches!( + updated_project.unwrap_err(), + DbErr::RecordNotUpdated + )); + } + + #[tokio::test] + async fn delete_test() { + // Setting up contexts and user context + let (project_context, project, _) = seed_db().await; + + project::Entity::insert(project.clone().into_active_model()) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + + let deleted_project = project_context.delete(project.id).await.unwrap(); + + let all_projects = project::Entity::find() + .all(&project_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(project, deleted_project); + assert_eq!(all_projects.len(), 0); + } + + #[tokio::test] + async fn delete_cascade_query_test() { + let (project_context, project, _) = seed_db().await; + + let query = create_queries(1, project.clone().id)[0].clone(); + + project::Entity::insert(project.clone().into_active_model()) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + query::Entity::insert(query.clone().into_active_model()) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + + project_context.delete(project.id).await.unwrap(); + + let all_queries = query::Entity::find() + .all(&project_context.db_context.get_connection()) + .await + .unwrap(); + let all_projects = project::Entity::find() + .all(&project_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(all_queries.len(), 0); + assert_eq!(all_projects.len(), 0); + } + + #[tokio::test] + async fn delete_cascade_access_test() { + let (project_context, project, _) = seed_db().await; + + let access = create_accesses(1, 1, project.clone().id)[0].clone(); + + project::Entity::insert(project.clone().into_active_model()) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + access::Entity::insert(access.clone().into_active_model()) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + + project_context.delete(project.id).await.unwrap(); + + let all_projects = project::Entity::find() + .all(&project_context.db_context.get_connection()) + .await + .unwrap(); + let all_accesses = access::Entity::find() + .all(&project_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(all_projects.len(), 0); + assert_eq!(all_accesses.len(), 0); + } + + #[tokio::test] + async fn delete_cascade_in_use_test() { + let (project_context, project, user) = seed_db().await; + + let session = create_sessions(1, user.clone().id)[0].clone(); + let in_use = create_in_uses(1, project.clone().id, 1)[0].clone(); + + session::Entity::insert(session.clone().into_active_model()) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + project::Entity::insert(project.clone().into_active_model()) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + in_use::Entity::insert(in_use.clone().into_active_model()) + .exec(&project_context.db_context.get_connection()) + .await + .unwrap(); + + project_context.delete(project.id).await.unwrap(); + + let all_projects = project::Entity::find() + .all(&project_context.db_context.get_connection()) + .await + .unwrap(); + let all_in_uses = in_use::Entity::find() + .all(&project_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(all_projects.len(), 0); + assert_eq!(all_in_uses.len(), 0); + } + + #[tokio::test] + async fn delete_non_existing_id_test() { + let (project_context, _, _) = seed_db().await; + + let deleted_project = project_context.delete(1).await; + + assert!(matches!( + deleted_project.unwrap_err(), + DbErr::RecordNotFound(_) + )); + } +} diff --git a/src/contexts/query_context.rs b/src/contexts/query_context.rs new file mode 100644 index 0000000..152e81b --- /dev/null +++ b/src/contexts/query_context.rs @@ -0,0 +1,449 @@ +use crate::contexts::{DatabaseContextTrait, EntityContextTrait}; +use crate::entities::query; +use sea_orm::prelude::async_trait::async_trait; +use sea_orm::ActiveValue::{Set, Unchanged}; +use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, NotSet, QueryFilter}; +use std::sync::Arc; + +#[async_trait] +pub trait QueryContextTrait: EntityContextTrait { + /// Returns the queries associated with a given project id + async fn get_all_by_project_id(&self, project_id: i32) -> Result, DbErr>; +} + +pub struct QueryContext { + db_context: Arc, +} +#[async_trait] +impl QueryContextTrait for QueryContext { + async fn get_all_by_project_id(&self, project_id: i32) -> Result, DbErr> { + query::Entity::find() + .filter(query::Column::ProjectId.eq(project_id)) + .all(&self.db_context.get_connection()) + .await + } +} + +impl QueryContext { + pub fn new(db_context: Arc) -> QueryContext { + QueryContext { db_context } + } +} + +#[async_trait] +impl EntityContextTrait for QueryContext { + /// Used for creating a query entity + /// ## Example + /// ``` + /// let model : Model = { + /// id: Default::default(), + /// string: "query_string".into(), + /// project_id: 1, + /// result: "query_result".into(), + /// out_dated: true + /// } + /// let context : QueryContext = QueryContext::new(...); + /// context.create(model); + /// ``` + async fn create(&self, entity: query::Model) -> Result { + query::ActiveModel { + id: Default::default(), + string: Set(entity.string), + project_id: Set(entity.project_id), + result: NotSet, + outdated: NotSet, + } + .insert(&self.db_context.get_connection()) + .await + } + + /// Returns a single query entity (uses primary key) + /// ## Example + /// ``` + /// let context : QueryContext = QueryContext::new(...); + /// let model : Model = context.get_by_id(1).unwrap(); + /// assert_eq!(model.string,"query_string".into()); + /// ``` + async fn get_by_id(&self, entity_id: i32) -> Result, DbErr> { + query::Entity::find_by_id(entity_id) + .one(&self.db_context.get_connection()) + .await + } + + /// Returns all the query entities + /// ## Example + /// ``` + /// let context : QueryContext = QueryContext::new(...); + /// let model : vec = context.get_all().unwrap(); + /// assert_eq!(model.len(),5); + /// ``` + async fn get_all(&self) -> Result, DbErr> { + query::Entity::find() + .all(&self.db_context.get_connection()) + .await + } + + /// Updates and returns the given user entity + /// ## Example + /// ``` + /// let context : QueryContext = QueryContext::new(...); + /// let query = context.get_by_id(1).unwrap(); + /// let updated_query = Model { + /// id: query.id, + /// string: query.string, + /// project_id: query.project_id, + /// result: query.result, + /// out_dated: false + /// } + /// assert_eq!(context.update(updated_query).unwrap(),Model { + /// id: 1, + /// string: "query_string".into(), + /// project_id: 1, + /// result: "query_result".into(), + /// out_dated: false + /// } + /// ``` + /// ## Note + /// The user entity's id will never be changed. If this behavior is wanted, delete the old user and create a one. + async fn update(&self, entity: query::Model) -> Result { + query::ActiveModel { + id: Unchanged(entity.id), + string: Set(entity.string), + result: Set(entity.result), + outdated: Set(entity.outdated), + project_id: Unchanged(entity.project_id), + } + .update(&self.db_context.get_connection()) + .await + } + + async fn delete(&self, entity_id: i32) -> Result { + let query = self + .get_by_id(entity_id) + .await? + .ok_or(DbErr::RecordNotFound("No record was deleted".into()))?; + query::Entity::delete_by_id(entity_id) + .exec(&self.db_context.get_connection()) + .await?; + Ok(query) + } +} + +#[cfg(test)] +mod tests { + use super::super::helpers::{ + create_projects, create_queries, create_users, get_reset_database_context, + }; + use crate::{ + contexts::EntityContextTrait, + contexts::QueryContext, + entities::{project, query, user}, + to_active_models, + }; + use sea_orm::{entity::prelude::*, IntoActiveModel}; + + async fn seed_db() -> (QueryContext, query::Model, project::Model) { + let db_context = get_reset_database_context().await; + + let query_context = QueryContext::new(db_context); + + let user = create_users(1)[0].clone(); + let project = create_projects(1, user.id)[0].clone(); + let query = create_queries(1, project.id)[0].clone(); + + user::Entity::insert(user.clone().into_active_model()) + .exec(&query_context.db_context.get_connection()) + .await + .unwrap(); + project::Entity::insert(project.clone().into_active_model()) + .exec(&query_context.db_context.get_connection()) + .await + .unwrap(); + + (query_context, query, project) + } + + #[tokio::test] + async fn create_test() { + let (query_context, query, _) = seed_db().await; + + let created_query = query_context.create(query.clone()).await.unwrap(); + + let fetched_query = query::Entity::find_by_id(created_query.id) + .one(&query_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + // Assert if the fetched access is the same as the created access + assert_eq!(query, created_query); + assert_eq!(fetched_query, created_query); + } + + #[tokio::test] + async fn create_default_outdated_test() { + let (query_context, query, _) = seed_db().await; + + let _inserted_query = query_context.create(query.clone()).await.unwrap(); + + let fetched_query = query::Entity::find_by_id(query.project_id) + .one(&query_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + assert!(fetched_query.outdated) + } + + #[tokio::test] + async fn create_auto_increment_test() { + let (query_context, query, _) = seed_db().await; + + let created_query1 = query_context.create(query.clone()).await.unwrap(); + let created_query2 = query_context.create(query.clone()).await.unwrap(); + + let fetched_query1 = query::Entity::find_by_id(created_query1.id) + .one(&query_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + let fetched_query2 = query::Entity::find_by_id(created_query2.id) + .one(&query_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + assert_ne!(fetched_query1.id, fetched_query2.id); + assert_ne!(created_query1.id, created_query2.id); + assert_eq!(created_query1.id, fetched_query1.id); + assert_eq!(created_query2.id, fetched_query2.id); + } + + #[tokio::test] + async fn get_by_id_test() { + let (query_context, query, _) = seed_db().await; + + query::Entity::insert(query.clone().into_active_model()) + .exec(&query_context.db_context.get_connection()) + .await + .unwrap(); + + let fetched_in_use = query_context + .get_by_id(query.project_id) + .await + .unwrap() + .unwrap(); + + assert_eq!(fetched_in_use, query) + } + + #[tokio::test] + async fn get_by_non_existing_id_test() { + let (query_context, _, _) = seed_db().await; + + let query = query_context.get_by_id(1).await; + + assert!(query.unwrap().is_none()) + } + + #[tokio::test] + async fn get_all_test() { + let (query_context, _, project) = seed_db().await; + + let queries = create_queries(10, project.id); + + query::Entity::insert_many(to_active_models!(queries.clone())) + .exec(&query_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(query_context.get_all().await.unwrap().len(), 10); + + let mut sorted = queries.clone(); + sorted.sort_by_key(|k| k.project_id); + + for (i, query) in sorted.into_iter().enumerate() { + assert_eq!(query, queries[i]); + } + } + + #[tokio::test] + async fn get_all_empty_test() { + let (query_context, _, _) = seed_db().await; + + let queries = query_context.get_all().await.unwrap(); + + assert_eq!(0, queries.len()) + } + + #[tokio::test] + async fn update_test() { + let (query_context, query, _) = seed_db().await; + + query::Entity::insert(query.clone().into_active_model()) + .exec(&query_context.db_context.get_connection()) + .await + .unwrap(); + + let new_query = query::Model { ..query }; + + let updated_query = query_context.update(new_query.clone()).await.unwrap(); + + let fetched_query = query::Entity::find_by_id(updated_query.project_id) + .one(&query_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + assert_eq!(new_query, updated_query); + assert_eq!(updated_query, fetched_query); + } + + #[tokio::test] + async fn update_modifies_string_test() { + let (query_context, query, _) = seed_db().await; + + query::Entity::insert(query.clone().into_active_model()) + .exec(&query_context.db_context.get_connection()) + .await + .unwrap(); + + let new_query = query::Model { + string: query.clone().string + "123", + ..query.clone() + }; + + let updated_query = query_context.update(new_query.clone()).await.unwrap(); + + assert_ne!(query, updated_query); + assert_ne!(query, new_query); + } + + #[tokio::test] + async fn update_modifies_outdated_test() { + let (query_context, query, _) = seed_db().await; + + query::Entity::insert(query.clone().into_active_model()) + .exec(&query_context.db_context.get_connection()) + .await + .unwrap(); + + let new_query = query::Model { + outdated: !query.clone().outdated, + ..query.clone() + }; + + let updated_query = query_context.update(new_query.clone()).await.unwrap(); + + assert_ne!(query, updated_query); + assert_ne!(query, new_query); + } + + #[tokio::test] + async fn update_modifies_result_test() { + let (query_context, mut query, _) = seed_db().await; + + query.result = Some("{}".to_owned().parse().unwrap()); + + query::Entity::insert(query.clone().into_active_model()) + .exec(&query_context.db_context.get_connection()) + .await + .unwrap(); + + let new_query = query::Model { + result: None, + ..query.clone() + }; + + let updated_query = query_context.update(new_query.clone()).await.unwrap(); + + assert_ne!(query, updated_query); + assert_ne!(query, new_query); + } + + #[tokio::test] + async fn update_does_not_modify_id_test() { + let (query_context, query, _) = seed_db().await; + + query::Entity::insert(query.clone().into_active_model()) + .exec(&query_context.db_context.get_connection()) + .await + .unwrap(); + + let new_query = query::Model { + id: query.id + 1, + ..query.clone() + }; + + let updated_query = query_context.update(new_query.clone()).await; + + assert!(matches!( + updated_query.unwrap_err(), + DbErr::RecordNotUpdated + )); + } + + #[tokio::test] + async fn update_does_not_modify_project_id_test() { + let (query_context, query, _) = seed_db().await; + + query::Entity::insert(query.clone().into_active_model()) + .exec(&query_context.db_context.get_connection()) + .await + .unwrap(); + + let new_query = query::Model { + project_id: query.project_id + 1, + ..query.clone() + }; + + let updated_query = query_context.update(new_query.clone()).await.unwrap(); + + assert_eq!(query, updated_query); + } + + #[tokio::test] + async fn update_non_existing_id_test() { + let (query_context, query, _) = seed_db().await; + + let updated_query = query_context.update(query.clone()).await; + + assert!(matches!( + updated_query.unwrap_err(), + DbErr::RecordNotUpdated + )); + } + + #[tokio::test] + async fn delete_test() { + let (query_context, query, _) = seed_db().await; + + query::Entity::insert(query.clone().into_active_model()) + .exec(&query_context.db_context.get_connection()) + .await + .unwrap(); + + let deleted_query = query_context.delete(query.project_id).await.unwrap(); + + let all_queries = query::Entity::find() + .all(&query_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(query, deleted_query); + assert!(all_queries.is_empty()); + } + + #[tokio::test] + async fn delete_non_existing_id_test() { + let (query_context, _, _) = seed_db().await; + + let deleted_query = query_context.delete(1).await; + + assert!(matches!( + deleted_query.unwrap_err(), + DbErr::RecordNotFound(_) + )) + } +} diff --git a/src/contexts/session_context.rs b/src/contexts/session_context.rs new file mode 100644 index 0000000..b41c50f --- /dev/null +++ b/src/contexts/session_context.rs @@ -0,0 +1,621 @@ +use crate::api::auth::TokenType; +use crate::contexts::{DatabaseContextTrait, EntityContextTrait}; +use crate::entities::session; +use chrono::Local; +use sea_orm::prelude::async_trait::async_trait; +use sea_orm::ActiveValue::{Set, Unchanged}; +use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, NotSet, QueryFilter}; +use std::sync::Arc; + +#[async_trait] +pub trait SessionContextTrait: EntityContextTrait { + /// Searches for a token by `Access` or `Refresh` token, + /// returning [`Some`] if one is found, [`None`] otherwise + /// # Errors + /// Errors on failed connection, execution error or constraint violations. + async fn get_by_token( + &self, + token_type: TokenType, + token: String, + ) -> Result, DbErr>; + /// Searches for a token by `Access` or `Refresh` token, deleting and returning it + /// # Errors + /// Errors on failed connection, execution error or constraint violations. + async fn delete_by_token( + &self, + token_type: TokenType, + token: String, + ) -> Result; +} + +pub struct SessionContext { + db_context: Arc, +} + +#[async_trait] +impl SessionContextTrait for SessionContext { + async fn get_by_token( + &self, + token_type: TokenType, + token: String, + ) -> Result, DbErr> { + match token_type { + TokenType::AccessToken => { + session::Entity::find() + .filter(session::Column::AccessToken.eq(token)) + .one(&self.db_context.get_connection()) + .await + } + TokenType::RefreshToken => { + session::Entity::find() + .filter(session::Column::RefreshToken.eq(token)) + .one(&self.db_context.get_connection()) + .await + } + } + } + + async fn delete_by_token( + &self, + token_type: TokenType, + token: String, + ) -> Result { + let session = self + .get_by_token(token_type, token) + .await? + .ok_or(DbErr::RecordNotFound( + "No session found with the provided access token".into(), + ))?; + + session::Entity::delete_by_id(session.id) + .exec(&self.db_context.get_connection()) + .await + .map(|_| session) + } +} + +impl SessionContext { + pub fn new(db_context: Arc) -> Self { + SessionContext { db_context } + } +} + +#[async_trait] +impl EntityContextTrait for SessionContext { + /// Creates a new session in the contexts based on the provided model. + /// # Example + /// ```rust + /// use crate::entities::session::{Entity, Model}; + /// + /// let new_session = Model { + /// id: 1, + /// token: Uuid::parse_str("4473240f-2acb-422f-bd1a-5214554ed0e0").unwrap(), + /// created_at: Local::now().naive_utc(), + /// user_id, + /// }; + /// let created_session = session_context.create(model).await.unwrap(); + /// ``` + async fn create(&self, entity: session::Model) -> Result { + session::ActiveModel { + id: Default::default(), + refresh_token: Set(entity.refresh_token), + access_token: Set(entity.access_token), + user_id: Set(entity.user_id), + updated_at: NotSet, + } + .insert(&self.db_context.get_connection()) + .await + } + + /// Returns a session by searching for its id. + /// # Example + /// ```rust + /// let session: Result, DbErr> = session_context.get_by_id(id).await; + /// ``` + async fn get_by_id(&self, id: i32) -> Result, DbErr> { + session::Entity::find_by_id(id) + .one(&self.db_context.get_connection()) + .await + } + + /// Returns all models in a vector. + /// # Example + /// ```rust + /// let session: Result, DbErr> = session_context.get_all().await; + /// ``` + async fn get_all(&self) -> Result, DbErr> { + session::Entity::find() + .all(&self.db_context.get_connection()) + .await + } + + /// Updates a model in the contexts based on the provided model. + /// # **Example** + /// ## ***Model in contexts*** + /// ### Model table ### + /// | id | token | created_at | user_id | + /// |----|--------------------------------------|---------------------------|---------| + /// | 1 | 25b14ea1-7b78-4714-b3d0-35d9f56e6cb3 | 2023-09-22T12:42:13+02:00 | 2 | + /// ## ***Rust code*** + /// ```rust + /// use crate::entities::session::{Entity, Model}; + /// + /// let new_session = Model { + /// id: 1, + /// token: Uuid::parse_str("4473240f-2acb-422f-bd1a-5214554ed0e0").unwrap(), + /// created_at: Local::now().naive_utc(), + /// user_id: 2, + /// }; + /// let created_session = session_context.create(model).await.unwrap(); + /// ``` + /// ## ***Result*** + /// ### Model table ### + /// | id | token | created_at | user_id | + /// |----|--------------------------------------|---------------------------|---------| + /// | 1 | 4473240f-2acb-422f-bd1a-5214554ed0e0 | 2023-10-24T13:49:16+02:00 | 2 | + async fn update(&self, entity: session::Model) -> Result { + session::ActiveModel { + id: Unchanged(entity.id), + refresh_token: Set(entity.refresh_token), + access_token: Set(entity.access_token), + user_id: Unchanged(entity.user_id), + updated_at: Set(Local::now().naive_local()), + } + .update(&self.db_context.get_connection()) + .await + } + + /// Deletes a model in the contexts with a specific id. + /// # **Example** + /// ## ***Model in contexts*** + /// ### Model table ### + /// | id | token | created_at | user_id | + /// |----|--------------------------------------|---------------------------|---------| + /// | 1 | 25b14ea1-7b78-4714-b3d0-35d9f56e6cb3 | 2023-10-24T14:03:37+02:00 | 2 | + /// ## ***Rust code*** + /// ```rust + /// let deleted_session = session_context.delete(1).await.unwrap(); + /// ``` + /// ## ***Result*** + /// ### Model table ### + /// | id | token | created_at | user_id | + /// |----|-------|------------|---------| + /// | | | | | + async fn delete(&self, id: i32) -> Result { + let session = self + .get_by_id(id) + .await? + .ok_or(DbErr::RecordNotFound("No record was deleted".into()))?; + session::Entity::delete_by_id(id) + .exec(&self.db_context.get_connection()) + .await + .map(|_| session) + } +} + +#[cfg(test)] +mod tests { + use super::super::helpers::*; + use crate::api::auth::TokenType; + use sea_orm::{entity::prelude::*, IntoActiveModel}; + use std::ops::Add; + + use crate::{ + contexts::SessionContext, + contexts::{EntityContextTrait, SessionContextTrait}, + entities::{in_use, project, session, user}, + to_active_models, + }; + + use chrono::{Duration, Utc}; + + async fn seed_db() -> (SessionContext, session::Model, user::Model, project::Model) { + let db_context = get_reset_database_context().await; + + let session_context = SessionContext::new(db_context); + + let user = create_users(1)[0].clone(); + let project = create_projects(1, user.id)[0].clone(); + let session = create_sessions(1, user.id)[0].clone(); + + user::Entity::insert(user.clone().into_active_model()) + .exec(&session_context.db_context.get_connection()) + .await + .unwrap(); + project::Entity::insert(project.clone().into_active_model()) + .exec(&session_context.db_context.get_connection()) + .await + .unwrap(); + + (session_context, session, user, project) + } + + #[tokio::test] + async fn create_test() { + // Setting up a sqlite contexts in memory. + let (session_context, mut session, _, _) = seed_db().await; + + let created_session = session_context.create(session.clone()).await.unwrap(); + + session.updated_at = created_session.updated_at; + + let fetched_session = session::Entity::find_by_id(created_session.id) + .one(&session_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + assert_eq!(session, created_session); + assert_eq!(fetched_session, created_session); + } + + #[tokio::test] + async fn create_default_created_at_test() { + let t_min = Utc::now().timestamp(); + + let (session_context, session, _, _) = seed_db().await; + + let _created_session = session_context.create(session.clone()).await.unwrap(); + + let fetched_session = session::Entity::find_by_id(1) + .one(&session_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + let t_max = Utc::now().timestamp(); + let t_actual = fetched_session.clone().updated_at.timestamp(); + + assert!(t_min <= t_actual && t_actual <= t_max) + } + + #[tokio::test] + async fn create_auto_increment_test() { + // Setting up contexts and session context + let (session_context, _, user, _) = seed_db().await; + + let sessions = create_sessions(2, user.id); + + // Creates the sessions in the contexts using the 'create' function + let created_session1 = session_context.create(sessions[0].clone()).await.unwrap(); + let created_session2 = session_context.create(sessions[1].clone()).await.unwrap(); + + let fetched_session1 = session::Entity::find_by_id(created_session1.id) + .one(&session_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + let fetched_session2 = session::Entity::find_by_id(created_session2.id) + .one(&session_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + // Assert if the new_session, created_session, and fetched_session are the same + assert_ne!(fetched_session1.id, fetched_session2.id); + assert_ne!(created_session1.id, created_session2.id); + assert_eq!(created_session1.id, fetched_session1.id); + assert_eq!(created_session2.id, fetched_session2.id); + } + + #[tokio::test] + async fn create_non_unique_refresh_token_test() { + let (session_context, _, _, user) = seed_db().await; + + let mut sessions = create_sessions(2, user.id); + + sessions[1].refresh_token = sessions[0].refresh_token.clone(); + + let _created_session1 = session_context.create(sessions[0].clone()).await.unwrap(); + let created_session2 = session_context.create(sessions[1].clone()).await; + + assert!(matches!( + created_session2.unwrap_err().sql_err(), + Some(SqlErr::UniqueConstraintViolation(_)) + )); + } + + #[tokio::test] + async fn get_by_id_test() { + let (session_context, session, _, _) = seed_db().await; + + session::Entity::insert(session.clone().into_active_model()) + .exec(&session_context.db_context.get_connection()) + .await + .unwrap(); + + let fetched_session = session_context + .get_by_id(session.id) + .await + .unwrap() + .unwrap(); + + assert_eq!(session, fetched_session); + } + + #[tokio::test] + async fn get_by_non_existing_id_test() { + let (session_context, _, _, _) = seed_db().await; + + let fetched_session = session_context.get_by_id(1).await.unwrap(); + + assert!(fetched_session.is_none()); + } + + #[tokio::test] + async fn get_all_test() { + let (session_context, _, user, _) = seed_db().await; + + let new_sessions = create_sessions(3, user.id); + + session::Entity::insert_many(to_active_models!(new_sessions.clone())) + .exec(&session_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(session_context.get_all().await.unwrap().len(), 3); + + let mut sorted: Vec = new_sessions.clone(); + sorted.sort_by_key(|k| k.id); + + for (i, session) in sorted.into_iter().enumerate() { + assert_eq!(session, new_sessions[i]); + } + } + + #[tokio::test] + async fn get_all_empty_test() { + let (session_context, _, _, _) = seed_db().await; + + let result = session_context.get_all().await.unwrap(); + let empty_accesses: Vec = vec![]; + + assert_eq!(empty_accesses, result); + } + + #[tokio::test] + async fn update_test() { + let (session_context, session, _, _) = seed_db().await; + + session::Entity::insert(session.clone().into_active_model()) + .exec(&session_context.db_context.get_connection()) + .await + .unwrap(); + + //A session has nothing to update + let mut new_session = session::Model { ..session }; + + let mut updated_session = session_context.update(new_session.clone()).await.unwrap(); + + let fetched_session = session::Entity::find_by_id(updated_session.id) + .one(&session_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + new_session.updated_at = fetched_session.updated_at; + updated_session.updated_at = fetched_session.updated_at; + + assert_eq!(new_session, updated_session); + assert_eq!(updated_session, fetched_session); + } + + #[tokio::test] + async fn update_does_not_modify_id_test() { + let (session_context, session, _, _) = seed_db().await; + session::Entity::insert(session.clone().into_active_model()) + .exec(&session_context.db_context.get_connection()) + .await + .unwrap(); + + let updated_session = session::Model { + id: &session.id + 1, + ..session.clone() + }; + let res = session_context.update(updated_session.clone()).await; + + assert!(matches!(res.unwrap_err(), DbErr::RecordNotUpdated)); + } + + #[tokio::test] + async fn update_does_modifies_updated_at_automatically_test() { + let (session_context, mut session, _, _) = seed_db().await; + session::Entity::insert(session.clone().into_active_model()) + .exec(&session_context.db_context.get_connection()) + .await + .unwrap(); + + let updated_session = session::Model { + updated_at: session.clone().updated_at.add(Duration::seconds(1)), + ..session.clone() + }; + let res = session_context + .update(updated_session.clone()) + .await + .unwrap(); + + assert!(session.updated_at < res.updated_at); + + session.updated_at = res.updated_at; + + assert_eq!(session, res); + } + + #[tokio::test] + async fn update_does_not_modify_user_id_test() { + let (session_context, mut session, _, _) = seed_db().await; + session::Entity::insert(session.clone().into_active_model()) + .exec(&session_context.db_context.get_connection()) + .await + .unwrap(); + + let updated_session = session::Model { + user_id: &session.user_id + 1, + ..session.clone() + }; + let res = session_context + .update(updated_session.clone()) + .await + .unwrap(); + + session.updated_at = res.updated_at; + + assert_eq!(session, res); + } + + #[tokio::test] + async fn update_non_existing_id_test() { + let (session_context, session, _, _) = seed_db().await; + + let updated_session = session_context.update(session.clone()).await; + + assert!(matches!( + updated_session.unwrap_err(), + DbErr::RecordNotUpdated + )); + } + + #[tokio::test] + async fn delete_test() { + let (session_context, session, _, _) = seed_db().await; + + session::Entity::insert(session.clone().into_active_model()) + .exec(&session_context.db_context.get_connection()) + .await + .unwrap(); + + let deleted_session = session_context.delete(session.id).await.unwrap(); + + let all_sessions = session::Entity::find() + .all(&session_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(session, deleted_session); + assert!(all_sessions.is_empty()); + } + + #[tokio::test] + async fn delete_cascade_in_use_test() { + let (session_context, session, _, project) = seed_db().await; + + let in_use = create_in_uses(1, project.id, session.id)[0].clone(); + + session::Entity::insert(session.clone().into_active_model()) + .exec(&session_context.db_context.get_connection()) + .await + .unwrap(); + in_use::Entity::insert(in_use.clone().into_active_model()) + .exec(&session_context.db_context.get_connection()) + .await + .unwrap(); + + session_context.delete(session.id).await.unwrap(); + + let all_sessions = session::Entity::find() + .all(&session_context.db_context.get_connection()) + .await + .unwrap(); + let all_in_uses = in_use::Entity::find() + .all(&session_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(all_sessions.len(), 0); + assert_eq!(all_in_uses.len(), 0); + } + + #[tokio::test] + async fn delete_non_existing_id_test() { + let (session_context, _, _, _) = seed_db().await; + + let deleted_session = session_context.delete(1).await; + + assert!(matches!( + deleted_session.unwrap_err(), + DbErr::RecordNotFound(_) + )); + } + + #[tokio::test] + async fn get_by_token_refresh_test() { + let (session_context, session, _, _) = seed_db().await; + + session::Entity::insert(session.clone().into_active_model()) + .exec(&session_context.db_context.get_connection()) + .await + .unwrap(); + + let fetched_session = session_context + .get_by_token(TokenType::RefreshToken, session.refresh_token.clone()) + .await + .unwrap(); + + assert_eq!( + fetched_session.unwrap().refresh_token, + session.refresh_token + ); + } + + #[tokio::test] + async fn get_by_token_access_test() { + let (session_context, session, _, _) = seed_db().await; + + session::Entity::insert(session.clone().into_active_model()) + .exec(&session_context.db_context.get_connection()) + .await + .unwrap(); + + let fetched_session = session_context + .get_by_token(TokenType::AccessToken, session.access_token.clone()) + .await + .unwrap(); + + assert_eq!(fetched_session.unwrap().access_token, session.access_token); + } + + #[tokio::test] + async fn delete_by_token_refresh_test() { + let (session_context, session, _, _) = seed_db().await; + + session::Entity::insert(session.clone().into_active_model()) + .exec(&session_context.db_context.get_connection()) + .await + .unwrap(); + + session_context + .delete_by_token(TokenType::RefreshToken, session.refresh_token.clone()) + .await + .unwrap(); + + let fetched_session = session_context + .get_by_token(TokenType::RefreshToken, session.refresh_token.clone()) + .await + .unwrap(); + + assert!(fetched_session.is_none()); + } + + #[tokio::test] + async fn delete_by_token_access_test() { + let (session_context, session, _, _) = seed_db().await; + + session::Entity::insert(session.clone().into_active_model()) + .exec(&session_context.db_context.get_connection()) + .await + .unwrap(); + + session_context + .delete_by_token(TokenType::AccessToken, session.access_token.clone()) + .await + .unwrap(); + + let fetched_session = session_context + .get_by_token(TokenType::AccessToken, session.access_token.clone()) + .await + .unwrap(); + + assert!(fetched_session.is_none()); + } +} diff --git a/src/contexts/user_context.rs b/src/contexts/user_context.rs new file mode 100644 index 0000000..a44dd84 --- /dev/null +++ b/src/contexts/user_context.rs @@ -0,0 +1,704 @@ +use crate::contexts::{DatabaseContextTrait, EntityContextTrait}; +use crate::entities::user; +use sea_orm::prelude::async_trait::async_trait; +use sea_orm::ActiveValue::{Set, Unchanged}; +use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter}; +use std::sync::Arc; + +#[async_trait] +pub trait UserContextTrait: EntityContextTrait { + /// Searches for a `User` by username, returning [`Some`] if one is found, [`None`] otherwise. + /// # Errors + /// Errors on failed connection, execution error or constraint violations. + /// # Notes + /// Since usernames are unique, it is guaranteed that at most one user with the given username exists. + async fn get_by_username(&self, username: String) -> Result, DbErr>; + /// Searches for a `User` by email address, returning [`Some`] if one is found, [`None`] otherwise. + /// # Errors + /// Errors on failed connection, execution error or constraint violations. + /// # Notes + /// Since email address' are unique, it is guaranteed that at most one user with the given email address exists. + async fn get_by_email(&self, email: String) -> Result, DbErr>; + /// Returns all the user entities with the given ids + /// # Example + /// ``` + /// let context : UserContext = UserContext::new(...); + /// let model : vec = context.get_by_ids(vec![1,2]).unwrap(); + /// assert_eq!(model.len(),2); + /// ``` + async fn get_by_ids(&self, ids: Vec) -> Result, DbErr>; +} + +pub struct UserContext { + db_context: Arc, +} + +#[async_trait] +impl UserContextTrait for UserContext { + async fn get_by_username(&self, username: String) -> Result, DbErr> { + user::Entity::find() + .filter(user::Column::Username.eq(username)) + .one(&self.db_context.get_connection()) + .await + } + async fn get_by_email(&self, email: String) -> Result, DbErr> { + user::Entity::find() + .filter(user::Column::Email.eq(email)) + .one(&self.db_context.get_connection()) + .await + } + + async fn get_by_ids(&self, ids: Vec) -> Result, DbErr> { + user::Entity::find() + .filter(user::Column::Id.is_in(ids)) + .all(&self.db_context.get_connection()) + .await + } +} + +impl UserContext { + pub fn new(db_context: Arc) -> UserContext { + UserContext { db_context } + } +} + +#[async_trait] +impl EntityContextTrait for UserContext { + /// Used for creating a User entity + /// # Example + /// ``` + /// let user : Model = { + /// id: Default::default(), + /// email: "anders@aau.dk".into(), + /// username: "Anders".into(), + /// password: "qwerty".into() + /// } + /// let context : UserContext = UserContext::new(...); + /// context.create(user); + /// ``` + async fn create(&self, entity: user::Model) -> Result { + user::ActiveModel { + id: Default::default(), + email: Set(entity.email), + username: Set(entity.username), + password: Set(entity.password), + } + .insert(&self.db_context.get_connection()) + .await + } + + /// Returns a single user entity (uses primary key) + /// # Example + /// ``` + /// let context : UserContext = UserContext::new(...); + /// let user : Model = context.get_by_id(1).unwrap(); + /// assert_eq!(user.username,"Anders".into()); + /// ``` + async fn get_by_id(&self, entity_id: i32) -> Result, DbErr> { + user::Entity::find_by_id(entity_id) + .one(&self.db_context.get_connection()) + .await + } + + /// Returns all the user entities + /// # Example + /// ``` + /// let context : UserContext = UserContext::new(...); + /// let user : vec = context.get_all().unwrap(); + /// assert_eq!(user.len(),1); + /// ``` + async fn get_all(&self) -> Result, DbErr> { + user::Entity::find() + .all(&self.db_context.get_connection()) + .await + } + + /// Updates and returns the given user entity + /// # Example + /// ``` + /// let context : UserContext = UserContext::new(...); + /// let user = context.get_by_id(1).unwrap(); + /// let updated_user = Model { + /// id: user.id, + /// email: "anders@student.aau.dk".into(), + /// username: "andersAnden", + /// password: user.password + /// } + /// assert_eq!(context.update(updated_user).unwrap(),Model { + /// id: 1, + /// email: "anders@student.aau.dk".into(), + /// username: "andersAnden".into(), + /// password:"qwerty".into(); + /// } + /// ``` + /// # Note + /// The user entity's id will never be changed. If this behavior is wanted, delete the old user and create a new one. + async fn update(&self, entity: user::Model) -> Result { + user::ActiveModel { + id: Unchanged(entity.id), + email: Set(entity.email), + username: Set(entity.username), + password: Set(entity.password), + } + .update(&self.db_context.get_connection()) + .await + } + + /// Returns and deletes a user entity by id + /// + /// # Example + /// ``` + /// let context : UserContext = UserContext::new(...); + /// let user = context.get_by_id(1).unwrap(); + /// let deleted_user = Model { + /// id: user.id, + /// email: "anders@student.aau.dk".into(), + /// username: "andersAnden", + /// password: user.password + /// } + async fn delete(&self, entity_id: i32) -> Result { + let user = self + .get_by_id(entity_id) + .await? + .ok_or(DbErr::RecordNotFound("No record was deleted".into()))?; + user::Entity::delete_by_id(entity_id) + .exec(&self.db_context.get_connection()) + .await + .map(|_| user) + } +} + +#[cfg(test)] +mod tests { + use crate::contexts::helpers::*; + use crate::{ + contexts::UserContext, + contexts::{EntityContextTrait, UserContextTrait}, + entities::{access, project, session, user}, + to_active_models, + }; + use sea_orm::{entity::prelude::*, IntoActiveModel}; + use std::matches; + + async fn seed_db() -> (UserContext, user::Model) { + let db_context = get_reset_database_context().await; + + let user_context = UserContext::new(db_context); + + let user = create_users(1)[0].clone(); + + (user_context, user) + } + + // Test the functionality of the 'create' function, which creates a user in the contexts + #[tokio::test] + async fn create_test() { + // Setting up contexts and user context + let (user_context, user) = seed_db().await; + + // Creates the user in the contexts using the 'create' function + let created_user = user_context.create(user.clone()).await.unwrap(); + + let fetched_user = user::Entity::find_by_id(created_user.id) + .one(&user_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + // Assert if the new_user, created_user, and fetched_user are the same + assert_eq!(user, created_user); + assert_eq!(created_user, fetched_user); + } + + #[tokio::test] + async fn create_non_unique_username_test() { + // Setting up contexts and user context + let (user_context, user) = seed_db().await; + + // Creates a model of the user which will be created + let mut users = create_users(2); + + users[0].username = user.clone().username; + users[1].username = user.clone().username; + + // Creates the user in the contexts using the 'create' function + let _created_user1 = user_context.create(users[0].clone()).await.unwrap(); + let created_user2 = user_context.create(users[1].clone()).await; + + assert!(matches!( + created_user2.unwrap_err().sql_err(), + Some(SqlErr::UniqueConstraintViolation(_)) + )); + } + + #[tokio::test] + async fn create_non_unique_email_test() { + // Setting up contexts and user context + let (user_context, user) = seed_db().await; + + // Creates a model of the user which will be created + let mut users = create_users(2); + + users[0].email = user.clone().email; + users[1].email = user.clone().email; + + // Creates the user in the contexts using the 'create' function + let _created_user1 = user_context.create(users[0].clone()).await.unwrap(); + let created_user2 = user_context.create(users[1].clone()).await; + + // Assert if the new_user, created_user, and fetched_user are the same + assert!(matches!( + created_user2.unwrap_err().sql_err(), + Some(SqlErr::UniqueConstraintViolation(_)) + )); + } + + #[tokio::test] + async fn create_auto_increment_test() { + // Setting up contexts and user context + let (user_context, user) = seed_db().await; + + let mut users = create_users(2); + + users[0].id = user.clone().id; + users[1].id = user.clone().id; + + // Creates the user in the contexts using the 'create' function + let created_user1 = user_context.create(users[0].clone()).await.unwrap(); + let created_user2 = user_context.create(users[1].clone()).await.unwrap(); + + let fetched_user1 = user::Entity::find_by_id(created_user1.id) + .one(&user_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + let fetched_user2 = user::Entity::find_by_id(created_user2.id) + .one(&user_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + // Assert if the new_user, created_user, and fetched_user are the same + assert_ne!(fetched_user1.id, fetched_user2.id); + assert_ne!(created_user1.id, created_user2.id); + assert_eq!(created_user1.id, fetched_user1.id); + assert_eq!(created_user2.id, fetched_user2.id); + } + + #[tokio::test] + async fn get_by_id_test() { + // Setting up contexts and user context + let (user_context, user) = seed_db().await; + + // Creates the user in the contexts using the 'create' function + user::Entity::insert(user.clone().into_active_model()) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + + // Fetches the user created using the 'get_by_id' function + let fetched_user = user_context.get_by_id(user.id).await.unwrap().unwrap(); + + // Assert if the new_user, created_user, and fetched_user are the same + assert_eq!(user, fetched_user); + } + + #[tokio::test] + async fn get_by_non_existing_id_test() { + // Setting up contexts and user context + let (user_context, _) = seed_db().await; + + // Fetches the user created using the 'get_by_id' function + let fetched_user = user_context.get_by_id(1).await.unwrap(); + + assert!(fetched_user.is_none()); + } + + #[tokio::test] + async fn get_all_test() { + // Setting up contexts and user context + let (user_context, _) = seed_db().await; + + let users = create_users(10); + let active_users_vec = to_active_models!(users.clone()); + + user::Entity::insert_many(active_users_vec) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(user_context.get_all().await.unwrap().len(), 10); + + let mut sorted = users.clone(); + sorted.sort_by_key(|k| k.id); + + for (i, user) in sorted.into_iter().enumerate() { + assert_eq!(user, users[i]); + } + } + + #[tokio::test] + async fn get_all_empty_test() { + // Setting up contexts and user context + let (user_context, _) = seed_db().await; + + let result = user_context.get_all().await.unwrap(); + let empty_users: Vec = vec![]; + + assert_eq!(empty_users, result); + } + + #[tokio::test] + async fn update_test() { + // Setting up contexts and user context + let (user_context, user) = seed_db().await; + + user::Entity::insert(user.clone().into_active_model()) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + + let new_user = user::Model { ..user }; + + let updated_user = user_context.update(new_user.clone()).await.unwrap(); + + let fetched_user = user::Entity::find_by_id(updated_user.id) + .one(&user_context.db_context.get_connection()) + .await + .unwrap() + .unwrap(); + + assert_eq!(new_user, updated_user); + assert_eq!(updated_user, fetched_user); + } + + #[tokio::test] + async fn update_modifies_username_test() { + let (user_context, user) = seed_db().await; + + let user = user::Model { + username: "tester1".into(), + ..user.clone() + }; + + user::Entity::insert(user.clone().into_active_model()) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + + let new_user = user::Model { + username: "tester2".into(), + ..user.clone() + }; + + let updated_user = user_context.update(new_user.clone()).await.unwrap(); + + assert_ne!(user, updated_user); + assert_ne!(user, new_user); + } + + #[tokio::test] + async fn update_modifies_email_test() { + let (user_context, user) = seed_db().await; + + let user = user::Model { + email: "tester1@mail.dk".into(), + ..user.clone() + }; + + user::Entity::insert(user.clone().into_active_model()) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + + let new_user = user::Model { + email: "tester2@mail.dk".into(), + ..user.clone() + }; + + let updated_user = user_context.update(new_user.clone()).await.unwrap(); + + assert_ne!(user, updated_user); + assert_ne!(user, new_user); + } + + #[tokio::test] + async fn update_modifies_password_test() { + let (user_context, user) = seed_db().await; + + let user = user::Model { + password: "12345".into(), + ..user.clone() + }; + + user::Entity::insert(user.clone().into_active_model()) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + + let new_user = user::Model { + password: "123456".into(), + ..user.clone() + }; + + let updated_user = user_context.update(new_user.clone()).await.unwrap(); + + assert_ne!(user, updated_user); + assert_ne!(user, new_user); + } + + #[tokio::test] + async fn update_does_not_modify_id_test() { + let (user_context, user) = seed_db().await; + + user::Entity::insert(user.clone().into_active_model()) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + + let updated_user = user::Model { + id: user.id + 1, + ..user + }; + + let res = user_context.update(updated_user.clone()).await; + + assert!(matches!(res.unwrap_err(), DbErr::RecordNotUpdated)); + } + + #[tokio::test] + async fn update_non_unique_username_test() { + // Setting up contexts and user context + let (user_context, _) = seed_db().await; + + let users = create_users(2); + + user::Entity::insert_many(to_active_models!(users.clone())) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + + let new_user = user::Model { + username: users[1].clone().username, + ..users[0].clone() + }; + + let updated_user = user_context.update(new_user.clone()).await; + + // Assert if the new_user, created_user, and fetched_user are the same + assert!(matches!( + updated_user.unwrap_err().sql_err(), + Some(SqlErr::UniqueConstraintViolation(_)) + )); + } + + #[tokio::test] + async fn update_non_unique_email_test() { + // Setting up contexts and user context + let (user_context, _) = seed_db().await; + + // Creates a model of the user which will be created + let users = create_users(2); + + user::Entity::insert_many(to_active_models!(users.clone())) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + + let new_user = user::Model { + email: users[1].clone().email, + ..users[0].clone() + }; + + let updated_user = user_context.update(new_user.clone()).await; + + // Assert if the new_user, created_user, and fetched_user are the same + assert!(matches!( + updated_user.unwrap_err().sql_err(), + Some(SqlErr::UniqueConstraintViolation(_)) + )); + } + + #[tokio::test] + async fn update_non_existing_id_test() { + // Setting up contexts and user context + let (user_context, user) = seed_db().await; + + let updated_user = user_context.update(user.clone()).await; + + // Assert if the new_user, created_user, and fetched_user are the same + assert!(matches!(updated_user.unwrap_err(), DbErr::RecordNotUpdated)); + } + + #[tokio::test] + async fn delete_test() { + // Setting up contexts and user context + let (user_context, user) = seed_db().await; + + user::Entity::insert(user.clone().into_active_model()) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + + let deleted_user = user_context.delete(user.id).await.unwrap(); + + let all_users = user::Entity::find() + .all(&user_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(user, deleted_user); + assert!(all_users.is_empty()); + } + + #[tokio::test] + async fn delete_cascade_project_test() { + // Setting up contexts and user context + let (user_context, user) = seed_db().await; + + let project = create_projects(1, user.clone().id)[0].clone(); + + user::Entity::insert(user.clone().into_active_model()) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + project::Entity::insert(project.clone().into_active_model()) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + + user_context.delete(user.id).await.unwrap(); + + let all_users = user::Entity::find() + .all(&user_context.db_context.get_connection()) + .await + .unwrap(); + let all_projects = project::Entity::find() + .all(&user_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(all_users.len(), 0); + assert_eq!(all_projects.len(), 0); + } + + #[tokio::test] + async fn delete_cascade_access_test() { + // Setting up contexts and user context + let (user_context, user) = seed_db().await; + + let project = create_projects(1, user.clone().id)[0].clone(); + let access = create_accesses(1, user.clone().id, project.clone().id)[0].clone(); + + user::Entity::insert(user.clone().into_active_model()) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + project::Entity::insert(project.clone().into_active_model()) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + access::Entity::insert(access.clone().into_active_model()) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + + user_context.delete(user.id).await.unwrap(); + + let all_users = user::Entity::find() + .all(&user_context.db_context.get_connection()) + .await + .unwrap(); + let all_projects = project::Entity::find() + .all(&user_context.db_context.get_connection()) + .await + .unwrap(); + let all_accesses = access::Entity::find() + .all(&user_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(all_users.len(), 0); + assert_eq!(all_projects.len(), 0); + assert_eq!(all_accesses.len(), 0); + } + + #[tokio::test] + async fn delete_cascade_session_test() { + // Setting up contexts and user context + let (user_context, user) = seed_db().await; + + let session = create_sessions(1, user.clone().id)[0].clone(); + + user::Entity::insert(user.clone().into_active_model()) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + session::Entity::insert(session.clone().into_active_model()) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + + user_context.delete(user.id).await.unwrap(); + + let all_users = user::Entity::find() + .all(&user_context.db_context.get_connection()) + .await + .unwrap(); + let all_sessions = session::Entity::find() + .all(&user_context.db_context.get_connection()) + .await + .unwrap(); + + assert_eq!(all_users.len(), 0); + assert_eq!(all_sessions.len(), 0); + } + + #[tokio::test] + async fn delete_non_existing_id_test() { + // Setting up contexts and user context + let (user_context, _) = seed_db().await; + + let deleted_user = user_context.delete(1).await; + + // Assert if the new_user, created_user, and fetched_user are the same + assert!(matches!( + deleted_user.unwrap_err(), + DbErr::RecordNotFound(_) + )); + } + + #[tokio::test] + async fn get_by_username_test() { + let (user_context, user) = seed_db().await; + + user::Entity::insert(user.clone().into_active_model()) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + + // Fetches the user created using the 'get_by_username' function + let fetched_user = user_context + .get_by_username(user.username.clone()) + .await + .unwrap(); + + // Assert if the fetched user is the same as the created user + assert_eq!(fetched_user.unwrap().username, user.username); + } + + #[tokio::test] + async fn get_by_email_test() { + let (user_context, user) = seed_db().await; + + user::Entity::insert(user.clone().into_active_model()) + .exec(&user_context.db_context.get_connection()) + .await + .unwrap(); + + let fetched_user = user_context.get_by_email(user.email.clone()).await.unwrap(); + + assert_eq!(fetched_user.unwrap().email, user.email); + } +} diff --git a/src/controllers/access_controller.rs b/src/controllers/access_controller.rs new file mode 100644 index 0000000..6d47805 --- /dev/null +++ b/src/controllers/access_controller.rs @@ -0,0 +1,835 @@ +use crate::api::auth::RequestExt; +use crate::api::server::protobuf::create_access_request::User; +use crate::api::server::protobuf::{ + CreateAccessRequest, DeleteAccessRequest, ListAccessInfoRequest, ListAccessInfoResponse, + UpdateAccessRequest, +}; +use crate::contexts::{AccessContextTrait, ContextCollection, UserContextTrait}; +use crate::entities::{access, user}; +use async_trait::async_trait; +use std::sync::Arc; +use tonic::{Request, Response, Status}; + +#[async_trait] +pub trait AccessControllerTrait: Send + Sync { + /// handles the list_access_info endpoint + /// # Errors + /// If an invalid or non-existent [`ListAccessInfoRequest::project_id`] is provided + async fn list_access_info( + &self, + request: Request, + ) -> Result, Status>; + /// Creates access in the contexts. + /// # Errors + /// Returns an error if the contexts context fails to create the access + async fn create_access( + &self, + request: Request, + ) -> Result, Status>; + + /// Endpoint for updating an access record. + /// + /// Takes [`UpdateAccessRequest`] as input + /// + /// Returns a [`Status`] as response + /// + /// `project_id` and `user_id` is set to 'default' since they won't be updated in the contexts. + async fn update_access( + &self, + request: Request, + ) -> Result, Status>; + + /// Deletes the an Access from the contexts. This has no sideeffects. + /// + /// # Errors + /// This function will return an error if the access does not exist in the contexts. + async fn delete_access( + &self, + request: Request, + ) -> Result, Status>; +} + +pub struct AccessController { + contexts: ContextCollection, +} + +impl AccessController { + pub fn new(contexts: ContextCollection) -> Self { + AccessController { contexts } + } +} +#[async_trait] +impl AccessControllerTrait for AccessController { + async fn list_access_info( + &self, + request: Request, + ) -> Result, Status> { + let message = request.get_ref().clone(); + + let uid = request + .uid() + .map_err(|err| { + Status::internal(format!( + "could not stringify user id in request metadata, internal error {}", + err + )) + })? + .ok_or(Status::internal("Could not get uid from request metadata"))?; + + self.contexts + .access_context + .get_access_by_uid_and_project_id(uid, message.project_id) + .await + .map_err(|error| Status::internal(error.to_string()))? + .ok_or(Status::permission_denied( + "User does not have access to model", + ))?; + self.contexts + .access_context + .get_access_by_project_id(message.project_id) + .await + .map_err(|error| Status::internal(error.to_string())) + .and_then(|access_info_list| { + if access_info_list.is_empty() { + Err(Status::not_found("No access found for given user")) + } else { + Ok(Response::new(ListAccessInfoResponse { access_info_list })) + } + }) + } + + async fn create_access( + &self, + request: Request, + ) -> Result, Status> { + let message = request.get_ref().clone(); + + let uid = request + .uid() + .map_err(|err| { + Status::internal(format!( + "could not stringify user id in request metadata, internal error {}", + err + )) + })? + .ok_or(Status::internal("Could not get uid from request metadata"))?; + + // Check if the requester has access to model with role 'Editor' + check_editor_role_helper( + Arc::clone(&self.contexts.access_context), + uid, + message.project_id, + ) + .await?; + + if let Some(user) = message.user { + let user_from_db = + create_access_find_user_helper(Arc::clone(&self.contexts.user_context), user) + .await?; + + let access = access::Model { + id: Default::default(), + role: message.role.to_string(), + project_id: message.project_id, + user_id: user_from_db.id, + }; + + match self.contexts.access_context.create(access).await { + Ok(_) => Ok(Response::new(())), + Err(error) => Err(Status::internal(error.to_string())), + } + } else { + Err(Status::invalid_argument("No user identification provided")) + } + } + + async fn update_access( + &self, + request: Request, + ) -> Result, Status> { + let message = request.get_ref().clone(); + + let uid = request + .uid() + .map_err(|err| { + Status::internal(format!( + "could not stringify user id in request metadata, internal error {}", + err + )) + })? + .ok_or(Status::internal("Could not get uid from request metadata"))?; + + let user_access = self + .contexts + .access_context + .get_by_id(message.id) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::not_found("No access entity found for user".to_string()))?; + + check_editor_role_helper( + Arc::clone(&self.contexts.access_context), + uid, + user_access.project_id, + ) + .await?; + + let model = self + .contexts + .project_context + .get_by_id(user_access.project_id) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::not_found("No model found for access".to_string()))?; + + // Check that the requester is not trying to update the owner's access + if model.owner_id == message.id { + return Err(Status::permission_denied( + "Requester does not have permission to update access for this user", + )); + } + + let access = access::Model { + id: message.id, + role: message.role, + project_id: Default::default(), + user_id: Default::default(), + }; + + self.contexts + .access_context + .update(access) + .await + .map(|_| Response::new(())) + .map_err(|error| Status::internal(error.to_string())) + } + + async fn delete_access( + &self, + request: Request, + ) -> Result, Status> { + let message = request.get_ref().clone(); + + let uid = request + .uid() + .map_err(|err| { + Status::internal(format!( + "could not stringify user id in request metadata, inner error: {}", + err + )) + })? + .ok_or(Status::internal("Could not get uid from request metadata"))?; + + let user_access = self + .contexts + .access_context + .get_by_id(message.id) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::not_found("No access entity found for user".to_string()))?; + + check_editor_role_helper( + Arc::clone(&self.contexts.access_context), + uid, + user_access.project_id, + ) + .await?; + + let model = self + .contexts + .project_context + .get_by_id(user_access.project_id) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::not_found("No model found for access".to_string()))?; + + // Check that the requester is not trying to delete the owner's access + if model.owner_id == message.id { + return Err(Status::permission_denied( + "You cannot delete the access entity for this user", + )); + } + + match self.contexts.access_context.delete(message.id).await { + Ok(_) => Ok(Response::new(())), + Err(error) => match error { + sea_orm::DbErr::RecordNotFound(message) => Err(Status::not_found(message)), + _ => Err(Status::internal(error.to_string())), + }, + } + } +} +async fn check_editor_role_helper( + access_context: Arc, + user_id: i32, + project_id: i32, +) -> Result<(), Status> { + let access = access_context + .get_access_by_uid_and_project_id(user_id, project_id) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| { + Status::permission_denied("User does not have access to model".to_string()) + })?; + + // Check if the requester has role 'Editor' + if access.role != "Editor" { + Err(Status::permission_denied( + "User does not have 'Editor' role for this model", + )) + } else { + Ok(()) + } +} + +async fn create_access_find_user_helper( + user_context: Arc, + user: User, +) -> Result { + match user { + User::UserId(user_id) => Ok(user_context + .get_by_id(user_id) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::not_found("No user found with given id"))?), + + User::Username(username) => Ok(user_context + .get_by_username(username) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::not_found("No user found with given username"))?), + + User::Email(email) => Ok(user_context + .get_by_email(email) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::not_found("No user found with given email"))?), + } +} + +#[cfg(test)] +mod tests { + use super::super::helpers::{disguise_context_mocks, get_mock_contexts}; + use crate::api::server::protobuf::create_access_request::User; + use crate::api::server::protobuf::{ + AccessInfo, CreateAccessRequest, DeleteAccessRequest, ListAccessInfoRequest, + UpdateAccessRequest, + }; + use crate::controllers::AccessController; + use crate::controllers::AccessControllerTrait; + use crate::entities::{access, project, user}; + use mockall::predicate; + use sea_orm::DbErr; + use std::str::FromStr; + use tonic::{metadata, Code, Request}; + + #[tokio::test] + async fn create_invalid_access_returns_err() { + let mut mock_contexts = get_mock_contexts(); + + let access = access::Model { + id: Default::default(), + role: "Editor".to_string(), + project_id: 1, + user_id: 1, + }; + + mock_contexts + .access_context_mock + .expect_create() + .with(predicate::eq(access.clone())) + .returning(move |_| Err(DbErr::RecordNotInserted)); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: Default::default(), + role: "Editor".to_owned(), + user_id: 1, + project_id: 1, + })) + }); + + mock_contexts + .user_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(user::Model { + id: 1, + email: Default::default(), + username: "test".to_string(), + password: "test".to_string(), + })) + }); + + let mut request = Request::new(CreateAccessRequest { + role: "Editor".to_string(), + project_id: 1, + user: Some(User::UserId(1)), + }); + + request.metadata_mut().insert( + "uid", + tonic::metadata::MetadataValue::from_str("1").unwrap(), + ); + + let contexts = disguise_context_mocks(mock_contexts); + let access_logic = AccessController::new(contexts); + + let res = access_logic.create_access(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::Internal); + } + + #[tokio::test] + async fn create_access_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + + let access = access::Model { + id: Default::default(), + role: "Editor".to_string(), + project_id: 1, + user_id: 1, + }; + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: Default::default(), + role: "Editor".to_string(), + user_id: 1, + project_id: 1, + })) + }); + + mock_contexts + .access_context_mock + .expect_create() + .with(predicate::eq(access.clone())) + .returning(move |_| Ok(access.clone())); + + mock_contexts + .user_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(user::Model { + id: 1, + email: Default::default(), + username: "test".to_string(), + password: "test".to_string(), + })) + }); + + let mut request = Request::new(CreateAccessRequest { + role: "Editor".to_string(), + project_id: 1, + user: Some(User::UserId(1)), + }); + + request.metadata_mut().insert( + "uid", + tonic::metadata::MetadataValue::from_str("1").unwrap(), + ); + + let contexts = disguise_context_mocks(mock_contexts); + let access_logic = AccessController::new(contexts); + + let res = access_logic.create_access(request).await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn update_invalid_access_returns_err() { + let mut mock_contexts = get_mock_contexts(); + + let access = access::Model { + id: 2, + role: "Editor".to_string(), + project_id: Default::default(), + user_id: Default::default(), + }; + + mock_contexts + .access_context_mock + .expect_update() + .with(predicate::eq(access.clone())) + .returning(move |_| Err(DbErr::RecordNotUpdated)); + + mock_contexts + .access_context_mock + .expect_get_by_id() + .with(predicate::eq(2)) + .returning(move |_| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + project_id: 1, + user_id: 2, + })) + }); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + project_id: 1, + user_id: 1, + })) + }); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(project::Model { + id: 1, + name: "test".to_string(), + owner_id: 1, + components_info: Default::default(), + })) + }); + + let mut request = Request::new(UpdateAccessRequest { + id: 2, + role: "Editor".to_string(), + }); + + request.metadata_mut().insert( + "uid", + tonic::metadata::MetadataValue::from_str("1").unwrap(), + ); + + let contexts = disguise_context_mocks(mock_contexts); + let access_logic = AccessController::new(contexts); + + let res = access_logic.update_access(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::Internal); + } + + #[tokio::test] + async fn update_access_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + + let access = access::Model { + id: 2, + role: "Editor".to_string(), + project_id: Default::default(), + user_id: Default::default(), + }; + + mock_contexts + .access_context_mock + .expect_update() + .with(predicate::eq(access.clone())) + .returning(move |_| Ok(access.clone())); + + mock_contexts + .access_context_mock + .expect_get_by_id() + .with(predicate::eq(2)) + .returning(move |_| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + project_id: 1, + user_id: 2, + })) + }); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + project_id: 1, + user_id: 1, + })) + }); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(project::Model { + id: 1, + name: "test".to_string(), + owner_id: 1, + components_info: Default::default(), + })) + }); + + let mut request = Request::new(UpdateAccessRequest { + id: 2, + role: "Editor".to_string(), + }); + + request.metadata_mut().insert( + "uid", + tonic::metadata::MetadataValue::from_str("1").unwrap(), + ); + + let contexts = disguise_context_mocks(mock_contexts); + let access_logic = AccessController::new(contexts); + + let res = access_logic.update_access(request).await; + + print!("{:?}", res); + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn delete_invalid_access_returns_err() { + let mut mock_contexts = get_mock_contexts(); + + mock_contexts + .access_context_mock + .expect_delete() + .with(predicate::eq(2)) + .returning(move |_| Err(DbErr::RecordNotFound("".to_string()))); + + mock_contexts + .access_context_mock + .expect_get_by_id() + .with(predicate::eq(2)) + .returning(move |_| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + project_id: 1, + user_id: 2, + })) + }); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + project_id: 1, + user_id: 1, + })) + }); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(project::Model { + id: 1, + name: "test".to_string(), + owner_id: 1, + components_info: Default::default(), + })) + }); + + let mut request = Request::new(DeleteAccessRequest { id: 2 }); + + request.metadata_mut().insert( + "uid", + tonic::metadata::MetadataValue::from_str("1").unwrap(), + ); + + let contexts = disguise_context_mocks(mock_contexts); + let access_logic = AccessController::new(contexts); + + let res = access_logic.delete_access(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::NotFound); + } + + #[tokio::test] + async fn delete_access_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + + let access = access::Model { + id: 2, + role: "Editor".to_string(), + project_id: Default::default(), + user_id: Default::default(), + }; + + mock_contexts + .access_context_mock + .expect_delete() + .with(predicate::eq(2)) + .returning(move |_| Ok(access.clone())); + + mock_contexts + .access_context_mock + .expect_get_by_id() + .with(predicate::eq(2)) + .returning(move |_| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + project_id: 1, + user_id: 2, + })) + }); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + project_id: 1, + user_id: 1, + })) + }); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(project::Model { + id: 1, + name: "test".to_string(), + owner_id: 1, + components_info: Default::default(), + })) + }); + + let mut request = Request::new(DeleteAccessRequest { id: 2 }); + + request.metadata_mut().insert( + "uid", + tonic::metadata::MetadataValue::from_str("1").unwrap(), + ); + + let contexts = disguise_context_mocks(mock_contexts); + let access_logic = AccessController::new(contexts); + + let res = access_logic.delete_access(request).await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn list_access_info_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + + let mut request: Request = + Request::new(ListAccessInfoRequest { project_id: 1 }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let access = AccessInfo { + id: 1, + role: "Editor".to_string(), + project_id: 1, + user_id: 1, + }; + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + project_id: Default::default(), + user_id: Default::default(), + })) + }); + + mock_contexts + .access_context_mock + .expect_get_access_by_project_id() + .returning(move |_| Ok(vec![access.clone()])); + + let contexts = disguise_context_mocks(mock_contexts); + let access_logic = AccessController::new(contexts); + + let res = access_logic.list_access_info(request).await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn list_access_info_returns_not_found() { + let mut mock_contexts = get_mock_contexts(); + + let mut request = Request::new(ListAccessInfoRequest { project_id: 1 }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let access = access::Model { + id: 1, + role: "Editor".to_string(), + project_id: 1, + user_id: 1, + }; + + mock_contexts + .access_context_mock + .expect_get_access_by_project_id() + .returning(move |_| Ok(vec![])); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .returning(move |_, _| Ok(Some(access.clone()))); + + let contexts = disguise_context_mocks(mock_contexts); + let access_logic = AccessController::new(contexts); + + let res = access_logic.list_access_info(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::NotFound); + } + + #[tokio::test] + async fn list_access_info_returns_no_permission() { + let mut request = Request::new(ListAccessInfoRequest { project_id: 1 }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let mut mock_contexts = get_mock_contexts(); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .returning(move |_, _| Ok(None)); + + let contexts = disguise_context_mocks(mock_contexts); + let access_logic = AccessController::new(contexts); + + let res = access_logic.list_access_info(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::PermissionDenied); + } +} diff --git a/src/controllers/controller_collection.rs b/src/controllers/controller_collection.rs index d2374ef..5ffbc95 100644 --- a/src/controllers/controller_collection.rs +++ b/src/controllers/controller_collection.rs @@ -1,5 +1,5 @@ use crate::api::server::protobuf::ecdar_backend_server::EcdarBackend; -use crate::controllers::controller_traits::*; +use crate::controllers::*; use std::sync::Arc; #[derive(Clone)] diff --git a/src/controllers/controller_impls/access_controller.rs b/src/controllers/controller_impls/access_controller.rs deleted file mode 100644 index 816b9e0..0000000 --- a/src/controllers/controller_impls/access_controller.rs +++ /dev/null @@ -1,309 +0,0 @@ -use crate::api::auth::RequestExt; -use crate::api::server::protobuf::create_access_request::User; -use crate::api::server::protobuf::{ - CreateAccessRequest, DeleteAccessRequest, ListAccessInfoRequest, ListAccessInfoResponse, - UpdateAccessRequest, -}; -use crate::contexts::context_collection::ContextCollection; -use crate::contexts::context_traits::{AccessContextTrait, UserContextTrait}; -use crate::controllers::controller_traits::AccessControllerTrait; -use crate::entities::{access, user}; -use async_trait::async_trait; -use std::sync::Arc; -use tonic::{Code, Request, Response, Status}; - -pub struct AccessController { - contexts: ContextCollection, -} - -impl AccessController { - pub fn new(contexts: ContextCollection) -> Self { - AccessController { contexts } - } -} -#[async_trait] -impl AccessControllerTrait for AccessController { - async fn list_access_info( - &self, - request: Request, - ) -> Result, Status> { - let message = request.get_ref().clone(); - - let uid = request - .uid() - .map_err(|err| { - Status::internal(format!( - "could not stringify user id in request metadata, internal error {}", - err - )) - })? - .ok_or(Status::internal("Could not get uid from request metadata"))?; - - match self - .contexts - .access_context - .get_access_by_uid_and_project_id(uid, message.project_id) - .await - { - Ok(access) => { - if access.is_none() { - return Err(Status::new( - Code::PermissionDenied, - "User does not have access to model", - )); - } - } - Err(error) => return Err(Status::new(Code::Internal, error.to_string())), - }; - - match self - .contexts - .access_context - .get_access_by_project_id(message.project_id) - .await - { - Ok(access_info_list) => { - if access_info_list.is_empty() { - return Err(Status::new( - Code::NotFound, - "No access found for given user", - )); - } else { - Ok(Response::new(ListAccessInfoResponse { access_info_list })) - } - } - Err(error) => Err(Status::new(Code::Internal, error.to_string())), - } - } - - async fn create_access( - &self, - request: Request, - ) -> Result, Status> { - let message = request.get_ref().clone(); - - let uid = request - .uid() - .map_err(|err| { - Status::internal(format!( - "could not stringify user id in request metadata, internal error {}", - err - )) - })? - .ok_or(Status::internal("Could not get uid from request metadata"))?; - - // Check if the requester has access to model with role 'Editor' - check_editor_role_helper( - Arc::clone(&self.contexts.access_context), - uid, - message.project_id, - ) - .await?; - - if let Some(user) = message.user { - let user_from_db = - create_access_find_user_helper(Arc::clone(&self.contexts.user_context), user) - .await?; - - let access = access::Model { - id: Default::default(), - role: message.role.to_string(), - project_id: message.project_id, - user_id: user_from_db.id, - }; - - match self.contexts.access_context.create(access).await { - Ok(_) => Ok(Response::new(())), - Err(error) => Err(Status::new(Code::Internal, error.to_string())), - } - } else { - Err(Status::new( - Code::InvalidArgument, - "No user identification provided", - )) - } - } - - async fn update_access( - &self, - request: Request, - ) -> Result, Status> { - let message = request.get_ref().clone(); - - let uid = request - .uid() - .map_err(|err| { - Status::internal(format!( - "could not stringify user id in request metadata, internal error {}", - err - )) - })? - .ok_or(Status::internal("Could not get uid from request metadata"))?; - - let user_access = self - .contexts - .access_context - .get_by_id(message.id) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| { - Status::new( - Code::NotFound, - "No access entity found for user".to_string(), - ) - })?; - - check_editor_role_helper( - Arc::clone(&self.contexts.access_context), - uid, - user_access.project_id, - ) - .await?; - - let model = self - .contexts - .project_context - .get_by_id(user_access.project_id) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| Status::new(Code::NotFound, "No model found for access".to_string()))?; - - // Check that the requester is not trying to update the owner's access - if model.owner_id == message.id { - return Err(Status::new( - Code::PermissionDenied, - "Requester does not have permission to update access for this user", - )); - } - - let access = access::Model { - id: message.id, - role: message.role, - project_id: Default::default(), - user_id: Default::default(), - }; - - match self.contexts.access_context.update(access).await { - Ok(_) => Ok(Response::new(())), - Err(error) => Err(Status::new(Code::Internal, error.to_string())), - } - } - - async fn delete_access( - &self, - request: Request, - ) -> Result, Status> { - let message = request.get_ref().clone(); - - let uid = request - .uid() - .map_err(|err| { - Status::internal(format!( - "could not stringify user id in request metadata, inner error: {}", - err - )) - })? - .ok_or(Status::internal("Could not get uid from request metadata"))?; - - let user_access = self - .contexts - .access_context - .get_by_id(message.id) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| { - Status::new( - Code::NotFound, - "No access entity found for user".to_string(), - ) - })?; - - check_editor_role_helper( - Arc::clone(&self.contexts.access_context), - uid, - user_access.project_id, - ) - .await?; - - let model = self - .contexts - .project_context - .get_by_id(user_access.project_id) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| Status::new(Code::NotFound, "No model found for access".to_string()))?; - - // Check that the requester is not trying to delete the owner's access - if model.owner_id == message.id { - return Err(Status::new( - Code::PermissionDenied, - "You cannot delete the access entity for this user", - )); - } - - match self.contexts.access_context.delete(message.id).await { - Ok(_) => Ok(Response::new(())), - Err(error) => match error { - sea_orm::DbErr::RecordNotFound(message) => { - Err(Status::new(Code::NotFound, message)) - } - _ => Err(Status::new(Code::Internal, error.to_string())), - }, - } - } -} -async fn check_editor_role_helper( - access_context: Arc, - user_id: i32, - project_id: i32, -) -> Result<(), Status> { - let access = access_context - .get_access_by_uid_and_project_id(user_id, project_id) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| { - Status::new( - Code::PermissionDenied, - "User does not have access to model".to_string(), - ) - })?; - - // Check if the requester has role 'Editor' - if access.role != "Editor" { - return Err(Status::new( - Code::PermissionDenied, - "User does not have 'Editor' role for this model", - )); - } - - Ok(()) -} - -async fn create_access_find_user_helper( - user_context: Arc, - user: User, -) -> Result { - match user { - User::UserId(user_id) => Ok(user_context - .get_by_id(user_id) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| Status::new(Code::NotFound, "No user found with given id"))?), - - User::Username(username) => Ok(user_context - .get_by_username(username) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| Status::new(Code::NotFound, "No user found with given username"))?), - - User::Email(email) => Ok(user_context - .get_by_email(email) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| Status::new(Code::NotFound, "No user found with given email"))?), - } -} - -#[cfg(test)] -#[path = "../../tests/controllers/access_controller.rs"] -mod access_controller_tests; diff --git a/src/controllers/controller_impls/mod.rs b/src/controllers/controller_impls/mod.rs deleted file mode 100644 index 016f5fc..0000000 --- a/src/controllers/controller_impls/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod access_controller; -pub mod project_controller; -pub mod query_controller; -pub mod reveaal_controller; -pub mod session_controller; -pub mod user_controller; - -pub use access_controller::AccessController; -pub use project_controller::ProjectController; -pub use query_controller::QueryController; -pub use reveaal_controller::ReveaalController; -pub use session_controller::SessionController; -pub use user_controller::UserController; diff --git a/src/controllers/controller_impls/project_controller.rs b/src/controllers/controller_impls/project_controller.rs deleted file mode 100644 index 54a3ba2..0000000 --- a/src/controllers/controller_impls/project_controller.rs +++ /dev/null @@ -1,494 +0,0 @@ -use crate::api::auth::{RequestExt, TokenType}; -use crate::api::server::protobuf::{ - CreateProjectRequest, CreateProjectResponse, DeleteProjectRequest, GetProjectRequest, - GetProjectResponse, ListProjectsInfoResponse, Project, Query, UpdateProjectRequest, -}; -use crate::contexts::context_collection::ContextCollection; -use crate::controllers::controller_traits::ProjectControllerTrait; -use crate::entities::{access, in_use, project}; -use async_trait::async_trait; -use chrono::{Duration, Utc}; -use sea_orm::SqlErr; -use tonic::{Code, Request, Response, Status}; - -const IN_USE_DURATION_MINUTES: i64 = 10; - -pub struct ProjectController { - contexts: ContextCollection, -} - -impl ProjectController { - pub fn new(contexts: ContextCollection) -> Self { - ProjectController { contexts } - } -} - -#[async_trait] -impl ProjectControllerTrait for ProjectController { - async fn get_project( - &self, - request: Request, - ) -> Result, Status> { - let message = request.get_ref().clone(); - - let project_id = message.id; - - let uid = request - .uid() - .map_err(|err| { - Status::internal(format!( - "could not stringify user id in request metadata, internal error {}", - err - )) - })? - .ok_or(Status::internal("Could not get uid from request metadata"))?; - - let access = self - .contexts - .access_context - .get_access_by_uid_and_project_id(uid, project_id) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| { - Status::new( - Code::PermissionDenied, - "User does not have access to project", - ) - })?; - - let project = self - .contexts - .project_context - .get_by_id(project_id) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| Status::new(Code::Internal, "Model not found"))?; - - let project = Project { - id: project.id, - name: project.name, - components_info: serde_json::from_value(project.components_info).map_err(|err| { - Status::internal(format!( - "failed to parse components info object, internal error: {}", - err - )) - })?, - owner_id: project.owner_id, - }; - - let mut in_use_bool = true; - match self.contexts.in_use_context.get_by_id(project_id).await { - Ok(Some(in_use)) => { - // If project is not in use and user is an Editor, update the in use with the users session. - if in_use.latest_activity - <= (Utc::now().naive_utc() - Duration::minutes(IN_USE_DURATION_MINUTES)) - { - in_use_bool = false; - - if access.role == "Editor" { - let session = self - .contexts - .session_context - .get_by_token( - TokenType::AccessToken, - request - .token_string() - .map_err(|err| Status::internal(format!("could not stringify user id in request metadata, internal error {}",err)))? - .ok_or(Status::invalid_argument("failed to get token from request metadata"))?, - ) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| { - Status::new( - Code::Unauthenticated, - "No session found with given access token", - ) - })?; - - let in_use = in_use::Model { - project_id: in_use.project_id, - session_id: session.id, - latest_activity: Utc::now().naive_utc(), - }; - - self.contexts - .in_use_context - .update(in_use) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))?; - } - } - } - Ok(None) => return Err(Status::new(Code::Internal, "No in use found for project")), - Err(err) => return Err(Status::new(Code::Internal, err.to_string())), - } - - let queries = self - .contexts - .query_context - .get_all_by_project_id(project_id) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))?; - - let queries = queries - .into_iter() - .map(|query| { - let result = serde_json::from_value(query.result.unwrap_or_else(|| "".into()))?; - - Ok(Query { - id: query.id, - project_id: query.project_id, - query: query.string, - result, - outdated: query.outdated, - }) - }) - .collect::, serde_json::Error>>() - .map_err(|err| { - Status::internal(format!( - "failed to parse json result, inner error: {}", - err - )) - })?; - - Ok(Response::new(GetProjectResponse { - project: Some(project), - queries, - in_use: in_use_bool, - })) - } - - async fn create_project( - &self, - request: Request, - ) -> Result, Status> { - let message = request.get_ref().clone(); - let uid = request - .uid() - .map_err(|err| { - Status::internal(format!( - "could not stringify user id in request metadata, internal error {}", - err - )) - })? - .ok_or(Status::internal("Could not get uid from request metadata"))?; - - let components_info = match message.clone().components_info { - Some(components_info) => serde_json::to_value(components_info).map_err(|err| { - Status::internal(format!( - "failed to parse components info object, internal error: {}", - err - )) - })?, - None => return Err(Status::invalid_argument("No components info provided")), - }; - - let mut project = project::Model { - id: Default::default(), - name: message.clone().name, - components_info, - owner_id: uid, - }; - - project = match self.contexts.project_context.create(project).await { - Ok(project) => project, - Err(error) => { - return match error.sql_err() { - Some(SqlErr::UniqueConstraintViolation(e)) => { - let error_msg = match e.to_lowercase() { - _ if e.contains("name") => "A project with that name already exists", - _ => "Model already exists", - }; - println!("{}", e); - Err(Status::already_exists(error_msg)) - } - Some(SqlErr::ForeignKeyConstraintViolation(e)) => { - let error_msg = match e.to_lowercase() { - _ if e.contains("owner_id") => "No user with that id exists", - _ => "Could not create project", - }; - println!("{}", e); - Err(Status::invalid_argument(error_msg)) - } - _ => Err(Status::internal(error.to_string())), - }; - } - }; - - let access = access::Model { - id: Default::default(), - role: "Editor".to_string(), //todo!("Use role enum") - project_id: project.clone().id, - user_id: uid, - }; - - let session = self - .contexts - .session_context - .get_by_token( - TokenType::AccessToken, - request - .token_string() - .map_err(|err| { - Status::internal(format!( - "could not stringify user id in request metadata, internal error {}", - err - )) - })? - .ok_or(Status::internal( - "failed to get token from request metadata", - ))?, - ) - .await - .map_err(|_err| Status::internal("failed to query database"))? - .ok_or(Status::not_found("token not found"))?; - - let in_use = in_use::Model { - project_id: project.clone().id, - session_id: session.id, - latest_activity: Default::default(), - }; - - self.contexts - .in_use_context - .create(in_use) - .await - .map_err(|err| { - Status::internal(format!("a database error occured, internal error: {}", err)) - })?; - self.contexts - .access_context - .create(access) - .await - .map_err(|err| { - Status::internal(format!("a database error occured, internal error: {}", err)) - })?; - - Ok(Response::new(CreateProjectResponse { id: project.id })) - } - - async fn update_project( - &self, - request: Request, - ) -> Result, Status> { - let message = request.get_ref().clone(); - let uid = request - .uid() - .map_err(|err| { - Status::internal(format!( - "could not stringify user id in request metadata, internal error {}", - err - )) - })? - .ok_or(Status::internal("Could not get uid from request metadata"))?; - - // Check if the project exists - let project = match self.contexts.project_context.get_by_id(message.id).await { - Ok(Some(project)) => project, - Ok(None) => return Err(Status::not_found("No project found with given id")), - Err(error) => return Err(Status::internal(error.to_string())), - }; - - // Check if the user has access to the project - match self - .contexts - .access_context - .get_access_by_uid_and_project_id(uid, project.id) - .await - { - Ok(access) => { - let mut is_editor = false; - let access = match access { - Some(access) => { - is_editor = access.role == "Editor"; - Some(access) - } - None => None, - }; - - if !is_editor || access.is_none() { - return Err(Status::permission_denied( - "You do not have permission to update this project", - )); - } - } - Err(error) => return Err(Status::internal(error.to_string())), - }; - - // Get user session - let session = match self - .contexts - .session_context - .get_by_token( - TokenType::AccessToken, - request - .token_string() - .map_err(|err| { - Status::internal(format!( - "could not stringify user id in request metadata, internal error {}", - err - )) - })? - .ok_or(Status::internal( - "failed to get token from request metadata", - ))?, - ) - .await - { - Ok(Some(session)) => session, - Ok(None) => { - return Err(Status::unauthenticated( - "No session found with given access token", - )); - } - Err(error) => return Err(Status::internal(error.to_string())), - }; - - // Get in_use for project - match self.contexts.in_use_context.get_by_id(project.id).await { - Ok(Some(in_use)) => { - // Check if in_use latest activity is older than the max allowed - if in_use.latest_activity - > (Utc::now().naive_utc() - Duration::minutes(IN_USE_DURATION_MINUTES)) - && in_use.session_id != session.id - { - return Err(Status::failed_precondition( - "Model is currently in use by another session", - )); - } - - let new_in_use = in_use::Model { - project_id: in_use.project_id, - session_id: session.id, - latest_activity: Utc::now().naive_utc(), - }; - - match self.contexts.in_use_context.update(new_in_use).await { - Ok(_) => (), - Err(error) => return Err(Status::internal(error.to_string())), - } - } - Ok(None) => return Err(Status::internal("No in_use found for project")), - Err(error) => return Err(Status::internal(error.to_string())), - }; - - let new_project = project::Model { - id: project.id, - name: match message.clone().name { - Some(name) => name, - None => project.name, - }, - components_info: match message.clone().components_info { - Some(components_info) => serde_json::to_value(components_info).map_err(|err| { - Status::internal(format!( - "failed to parse components info object, internal error: {}", - err - )) - })?, - None => project.components_info, - }, - owner_id: match message.clone().owner_id { - Some(new_owner_id) => { - if project.owner_id == uid { - new_owner_id - } else { - return Err(Status::permission_denied( - "You do not have permission to change the owner of this project", - )); - } - } - None => project.owner_id, - }, - }; - - match self.contexts.project_context.update(new_project).await { - Ok(_) => Ok(Response::new(())), - Err(error) => Err(Status::new(Code::Internal, error.to_string())), - } - } - - async fn delete_project( - &self, - request: Request, - ) -> Result, Status> { - let uid = request - .uid() - .map_err(|err| { - Status::internal(format!( - "could not stringify user id in request metadata, internal error {}", - err - )) - })? - .ok_or(Status::internal("Could not get uid from request metadata"))?; - let project_id = request.get_ref().id; - - let project = match self.contexts.project_context.get_by_id(project_id).await { - Ok(Some(project)) => project, - Ok(None) => { - return Err(Status::new( - Code::NotFound, - "No project found with given id", - )); - } - Err(err) => return Err(Status::new(Code::Internal, err.to_string())), - }; - - // Check if user is owner and thereby has permission to delete project - if project.owner_id != uid { - return Err(Status::new( - Code::PermissionDenied, - "You do not have permission to delete this project", - )); - } - - match self.contexts.project_context.delete(project_id).await { - Ok(_) => Ok(Response::new(())), - Err(error) => match error { - sea_orm::DbErr::RecordNotFound(message) => { - Err(Status::new(Code::NotFound, message)) - } - _ => Err(Status::new(Code::Internal, error.to_string())), - }, - } - } - - async fn list_projects_info( - &self, - request: Request<()>, - ) -> Result, Status> { - let uid = request - .uid() - .map_err(|err| { - Status::internal(format!( - "could not stringify user id in request metadata, internal error {}", - err - )) - })? - .ok_or(Status::internal("Could not get uid from request metadata"))?; - - match self - .contexts - .project_context - .get_project_info_by_uid(uid) - .await - { - Ok(project_info_list) => { - if project_info_list.is_empty() { - return Err(Status::new( - Code::NotFound, - "No access found for given user", - )); - } else { - Ok(Response::new(ListProjectsInfoResponse { - project_info_list, - })) - } - } - Err(error) => Err(Status::new(Code::Internal, error.to_string())), - } - } -} - -#[cfg(test)] -#[path = "../../tests/controllers/project_controller.rs"] -mod project_controller_tests; diff --git a/src/controllers/controller_impls/query_controller.rs b/src/controllers/controller_impls/query_controller.rs deleted file mode 100644 index 8244146..0000000 --- a/src/controllers/controller_impls/query_controller.rs +++ /dev/null @@ -1,307 +0,0 @@ -use crate::api::auth::RequestExt; -use crate::api::server::protobuf::{ - CreateQueryRequest, DeleteQueryRequest, QueryRequest, SendQueryRequest, SendQueryResponse, - UpdateQueryRequest, -}; -use crate::contexts::context_collection::ContextCollection; -use crate::controllers::controller_traits::QueryControllerTrait; -use crate::entities::query; -use crate::services::service_collection::ServiceCollection; -use async_trait::async_trait; -use tonic::{Code, Request, Response, Status}; - -pub struct QueryController { - contexts: ContextCollection, - services: ServiceCollection, -} - -impl QueryController { - pub fn new(contexts: ContextCollection, services: ServiceCollection) -> Self { - Self { contexts, services } - } -} - -#[async_trait] -impl QueryControllerTrait for QueryController { - async fn create_query( - &self, - request: Request, - ) -> Result, Status> { - let query_request = request.get_ref(); - - let access = self - .contexts - .access_context - .get_access_by_uid_and_project_id( - request - .uid() - .map_err(|err| { - Status::invalid_argument(format!( - "could not stringify user id in request metadata, inner error {}", - err - )) - })? - .ok_or(Status::invalid_argument( - "failed to get user id from request metadata", - ))?, - query_request.project_id, - ) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| { - Status::new( - Code::PermissionDenied, - "User does not have access to project", - ) - })?; - - if access.role != "Editor" { - return Err(Status::new( - Code::PermissionDenied, - "Role does not have permission to create query", - )); - } - - let query = query::Model { - id: Default::default(), - string: query_request.string.to_string(), - result: Default::default(), - outdated: Default::default(), - project_id: query_request.project_id, - }; - - match self.contexts.query_context.create(query).await { - Ok(_) => Ok(Response::new(())), - Err(error) => Err(Status::new(Code::Internal, error.to_string())), - } - } - - async fn update_query( - &self, - request: Request, - ) -> Result, Status> { - let message = request.get_ref().clone(); - - let old_query_res = self - .contexts - .query_context - .get_by_id(message.id) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))?; - - let old_query = match old_query_res { - Some(oq) => oq, - None => return Err(Status::new(Code::NotFound, "Query not found".to_string())), - }; - - let access = self - .contexts - .access_context - .get_access_by_uid_and_project_id( - request - .uid() - .map_err(|err| { - Status::internal(format!( - "could not stringify user id in request metadata, internal error {}", - err - )) - })? - .ok_or(Status::internal( - "failed to get user id from request metadata", - ))?, - old_query.project_id, - ) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| { - Status::new( - Code::PermissionDenied, - "User does not have access to project", - ) - })?; - - if access.role != "Editor" { - return Err(Status::new( - Code::PermissionDenied, - "Role does not have permission to update query", - )); - } - - let query = query::Model { - id: message.id, - project_id: Default::default(), - string: message.string, - result: old_query.result, - outdated: old_query.outdated, - }; - - match self.contexts.query_context.update(query).await { - Ok(_) => Ok(Response::new(())), - Err(error) => Err(Status::new(Code::Internal, error.to_string())), - } - } - - async fn delete_query( - &self, - request: Request, - ) -> Result, Status> { - let message = request.get_ref(); - - let query = self - .contexts - .query_context - .get_by_id(message.id) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| Status::new(Code::NotFound, "Query not found"))?; - - let access = self - .contexts - .access_context - .get_access_by_uid_and_project_id( - request - .uid() - .map_err(|err| { - Status::internal(format!( - "could not stringify user id in request metadata, internal error {}", - err - )) - })? - .ok_or(Status::internal( - "failed to get user id from request metadata", - ))?, - query.project_id, - ) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| { - Status::new( - Code::PermissionDenied, - "User does not have access to project", - ) - })?; - - if access.role != "Editor" { - return Err(Status::new( - Code::PermissionDenied, - "Role does not have permission to update query", - )); - } - - match self.contexts.query_context.delete(message.id).await { - Ok(_) => Ok(Response::new(())), - Err(error) => match error { - sea_orm::DbErr::RecordNotFound(message) => { - Err(Status::new(Code::NotFound, message)) - } - _ => Err(Status::new(Code::Internal, error.to_string())), - }, - } - } - - async fn send_query( - &self, - request: Request, - ) -> Result, Status> { - let message = request.get_ref(); - - let uid = request - .uid() - .map_err(|err| { - Status::internal(format!( - "could not stringify user id in request metadata, internal error {}", - err - )) - })? - .ok_or(Status::internal( - "failed to get user id from request metadata", - ))?; - - // Verify user access - self.contexts - .access_context - .get_access_by_uid_and_project_id(uid, message.project_id) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| { - Status::new( - Code::PermissionDenied, - "User does not have access to project", - ) - })?; - - // Get project from contexts - let project = self - .contexts - .project_context - .get_by_id(message.project_id) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| Status::new(Code::NotFound, "Model not found"))?; - - // Get query from contexts - let query = self - .contexts - .query_context - .get_by_id(message.id) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| Status::new(Code::NotFound, "Query not found"))?; - - // Construct query request to send to Reveaal - let query_request = Request::new(QueryRequest { - user_id: uid, - query_id: message.id, - query: query.string.clone(), - components_info: serde_json::from_value(project.components_info).map_err(|err| { - Status::internal(format!( - "error parsing query result, internal error: {}", - err - )) - })?, - settings: Default::default(), //TODO - }); - - // Run query on Reveaal - let query_result = self - .services - .reveaal_service - .send_query(query_request) - .await?; - - // Update query result in contexts - self.contexts - .query_context - .update(query::Model { - id: query.id, - string: query.string.clone(), - result: Some( - serde_json::to_value( - query_result - .get_ref() - .result - .clone() - .ok_or(Status::internal("failed to get query result"))?, //TODO better error message ? - ) - .map_err(|err| { - Status::internal(format!( - "error parsing query result, internal error: {}", - err - )) - })?, - ), - outdated: false, - project_id: query.project_id, - }) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))?; - - Ok(Response::new(SendQueryResponse { - response: Some(query_result.into_inner()), - })) - } -} - -#[cfg(test)] -#[path = "../../tests/controllers/query_controller.rs"] -mod query_controller_tests; diff --git a/src/controllers/controller_impls/session_controller.rs b/src/controllers/controller_impls/session_controller.rs deleted file mode 100644 index a6b6b06..0000000 --- a/src/controllers/controller_impls/session_controller.rs +++ /dev/null @@ -1,197 +0,0 @@ -use crate::api::auth::{RequestExt, Token, TokenError, TokenType}; -use crate::api::server::protobuf::get_auth_token_request::{user_credentials, UserCredentials}; -use crate::api::server::protobuf::{GetAuthTokenRequest, GetAuthTokenResponse}; -use crate::contexts::context_collection::ContextCollection; -use crate::controllers::controller_traits::SessionControllerTrait; -use crate::entities::{session, user}; -use crate::services::service_collection::ServiceCollection; -use async_trait::async_trait; -use sea_orm::DbErr; -use tonic::{Code, Request, Response, Status}; - -pub struct SessionController { - contexts: ContextCollection, - services: ServiceCollection, -} - -impl SessionController { - pub fn new(contexts: ContextCollection, services: ServiceCollection) -> Self { - Self { contexts, services } - } - - async fn user_from_user_credentials( - &self, - user_credentials: UserCredentials, - ) -> Result, DbErr> { - match user_credentials.user { - Some(user_credentials::User::Username(username)) => { - Ok(self.contexts.user_context.get_by_username(username).await?) - } - Some(user_credentials::User::Email(email)) => { - Ok(self.contexts.user_context.get_by_email(email).await?) - } - None => Ok(None), - } - } - - /// Updates the session given by refresh token in the contexts. - /// Returns the new access and refresh token i.e. a tuple `(Token, Token)` where the 0th element is the access token and the 1st element refresh token. - pub async fn update_session(&self, refresh_token: String) -> Result<(Token, Token), Status> { - let session = match self - .contexts - .session_context - .get_by_token(TokenType::RefreshToken, refresh_token) - .await - { - Ok(Some(session)) => session, - Ok(None) => { - return Err(Status::unauthenticated( - "No session found with given refresh token", - )); - } - Err(err) => return Err(Status::internal(err.to_string())), - }; - - let uid = session.user_id.to_string(); - - let access_token = Token::access(&uid)?; - let refresh_token = Token::refresh(&uid)?; - - self.contexts - .session_context - .update(session::Model { - id: session.id, - access_token: access_token.to_string(), - refresh_token: refresh_token.to_string(), - updated_at: Default::default(), - user_id: session.user_id, - }) - .await - .map_err(|err| { - Status::internal(format!( - "a database error occurred, internal message: {}", - err - )) - })?; - - Ok((access_token, refresh_token)) - } -} - -#[async_trait] -impl SessionControllerTrait for SessionController { - async fn delete_session(&self, request: Request<()>) -> Result, Status> { - let access_token = request - .token_string() - .map_err(|err| { - Status::internal(format!( - "failed to convert token to string, internal error: {}", - err - )) - })? - .ok_or(Status::unauthenticated("No access token provided"))?; - - match self - .contexts - .session_context - .delete_by_token(TokenType::AccessToken, access_token) - .await - { - Ok(_) => Ok(Response::new(())), - Err(error) => Err(Status::new(Code::Internal, error.to_string())), - } - } - - async fn get_auth_token( - &self, - request: Request, - ) -> Result, Status> { - let message = request.get_ref().clone(); - - let (access_token, refresh_token) = match message.user_credentials { - None => { - let refresh_token = Token::from_str( - TokenType::RefreshToken, - request - .token_str() - .map_err(|err| { - Status::internal(format!( - "failed to convert token to string, internal error: {}", - err - )) - })? - .ok_or(Status::unauthenticated("No refresh token provided"))?, - ); - - // Validate refresh token - match refresh_token.validate() { - Ok(_) => (), - Err(TokenError::ExpiredSignature) => { - // Delete session if expired - let _ = self - .contexts - .session_context - .delete_by_token(TokenType::RefreshToken, refresh_token.to_string()) - .await; - - return Err(Status::from(TokenError::ExpiredSignature)); - } - Err(err) => return Err(Status::from(err)), - } - - self.update_session(refresh_token.to_string()).await? - } - Some(user_credentials) => { - let input_password = user_credentials.password.clone(); - let user = self - .user_from_user_credentials(user_credentials) - .await - .map_err(|err| Status::internal(err.to_string()))? - .ok_or_else(|| Status::unauthenticated("Wrong username or password"))?; - - // Check if password in request matches users password - if !self - .services - .hashing_service - .verify_password(input_password, user.password.as_str()) - .map_err(|__err| Status::internal("failed to verify password"))? - { - return Err(Status::unauthenticated("Wrong username or password")); - } - - let uid = user.id.to_string(); - - let access_token = Token::access(&uid)?; - let refresh_token = Token::refresh(&uid)?; - - self.contexts - .session_context - .create(session::Model { - id: Default::default(), - access_token: access_token.to_string(), - refresh_token: refresh_token.to_string(), - updated_at: Default::default(), - user_id: uid.parse().map_err(|err| { - Status::internal(format!( - "failed to parse user id, internal error: {}", - err - )) - })?, - }) - .await - .map_err(|err| Status::internal(err.to_string()))?; - - (access_token, refresh_token) - } - }; - - Ok(Response::new(GetAuthTokenResponse { - access_token: access_token.to_string(), - refresh_token: refresh_token.to_string(), - })) - } -} - -#[cfg(test)] -#[path = "../../tests/controllers/session_controller.rs"] -mod session_controller_tests; diff --git a/src/controllers/controller_impls/user_controller.rs b/src/controllers/controller_impls/user_controller.rs deleted file mode 100644 index b406553..0000000 --- a/src/controllers/controller_impls/user_controller.rs +++ /dev/null @@ -1,207 +0,0 @@ -use crate::api::auth::RequestExt; -use crate::api::server::protobuf::get_users_response::UserInfo; -use crate::api::server::protobuf::{ - CreateUserRequest, GetUsersRequest, GetUsersResponse, UpdateUserRequest, -}; -use crate::contexts::context_collection::ContextCollection; -use crate::controllers::controller_traits::UserControllerTrait; -use crate::entities::user; -use crate::services::service_collection::ServiceCollection; -use async_trait::async_trait; -use regex::Regex; -use sea_orm::SqlErr; -use tonic::{Code, Request, Response, Status}; - -pub struct UserController { - contexts: ContextCollection, - services: ServiceCollection, -} - -impl UserController { - pub fn new(contexts: ContextCollection, services: ServiceCollection) -> Self { - UserController { contexts, services } - } - - /// Returns true if the given email is a valid format. - #[allow(clippy::expect_used)] - fn is_valid_email(&self, email: &str) -> bool { - Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") - .expect("failed to compile regex") - .is_match(email) - } - - /// Returns true if the given username is a valid format, i.e. only contains letters and numbers and a length from 3 to 32. - #[allow(clippy::expect_used)] - fn is_valid_username(&self, username: &str) -> bool { - Regex::new(r"^[a-zA-Z0-9_]{3,32}$") - .expect("failed to compile regex") - .is_match(username) - } -} - -#[async_trait] -impl UserControllerTrait for UserController { - async fn create_user( - &self, - request: Request, - ) -> Result, Status> { - let message = request.into_inner().clone(); - - if !self.is_valid_username(message.clone().username.as_str()) { - return Err(Status::new(Code::InvalidArgument, "Invalid username")); - } - - if !self.is_valid_email(message.clone().email.as_str()) { - return Err(Status::new(Code::InvalidArgument, "Invalid email")); - } - - let hashed_password = self - .services - .hashing_service - .hash_password(message.clone().password) - .map_err(|_err| Status::internal("failed to hash password"))?; - - let user = user::Model { - id: Default::default(), - username: message.clone().username, - password: hashed_password, - email: message.clone().email, - }; - - match self.contexts.user_context.create(user).await { - Ok(_) => Ok(Response::new(())), - Err(e) => match e.sql_err() { - Some(SqlErr::UniqueConstraintViolation(e)) => { - let error_msg = match e.to_lowercase() { - _ if e.contains("username") => "A user with that username already exists", - _ if e.contains("email") => "A user with that email already exists", - _ => "User already exists", - }; - Err(Status::new(Code::AlreadyExists, error_msg)) - } - _ => Err(Status::new(Code::Internal, "Could not create user")), - }, - } - } - - /// Updates a user record in the contexts. - /// # Errors - /// Returns an error if the contexts context fails to update the user or - /// if the uid could not be parsed from the request metadata. - async fn update_user( - &self, - request: Request, - ) -> Result, Status> { - let message = request.get_ref().clone(); - - let uid = request - .uid() - .map_err(|err| { - Status::internal(format!( - "could not stringify user id in request metadata, internal error {}", - err - )) - })? - .ok_or(Status::internal("Could not get uid from request metadata"))?; - - // Get user from contexts - let user = self - .contexts - .user_context - .get_by_id(uid) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| Status::new(Code::Internal, "No user found with given uid"))?; - - // Record to be inserted in contexts - let new_user = user::Model { - id: uid, - username: match message.clone().username { - Some(username) => { - if self.is_valid_username(username.as_str()) { - username - } else { - return Err(Status::new(Code::InvalidArgument, "Invalid username")); - } - } - None => user.username, - }, - email: match message.clone().email { - Some(email) => { - if self.is_valid_email(email.as_str()) { - email - } else { - return Err(Status::new(Code::InvalidArgument, "Invalid email")); - } - } - None => user.email, - }, - password: match message.clone().password { - Some(password) => self - .services - .hashing_service - .hash_password(password) - .map_err(|_err| Status::internal("failed to hash password"))?, - None => user.password, - }, - }; - - // Update user in contexts - match self.contexts.user_context.update(new_user).await { - Ok(_) => Ok(Response::new(())), - Err(error) => Err(Status::new(Code::Internal, error.to_string())), - } - } - - /// Deletes a user from the contexts. - /// # Errors - /// Returns an error if the contexts context fails to delete the user or - /// if the uid could not be parsed from the request metadata. - async fn delete_user(&self, request: Request<()>) -> Result, Status> { - let uid = request - .uid() - .map_err(|err| { - Status::internal(format!( - "could not stringify user id in request metadata, internal error {}", - err - )) - })? - .ok_or(Status::internal("Could not get uid from request metadata"))?; - - // Delete user from contexts - match self.contexts.user_context.delete(uid).await { - Ok(_) => Ok(Response::new(())), - Err(error) => Err(Status::new(Code::Internal, error.to_string())), - } - } - - /// Gets users from the contexts. - /// If no users exits with the given ids, an empty list is returned. - async fn get_users( - &self, - request: Request, - ) -> Result, Status> { - let ids = request.get_ref().ids.clone(); - - let users = self - .contexts - .user_context - .get_by_ids(ids) - .await - .map_err(|err| Status::internal(err.to_string()))?; - - let users_info = users - .into_iter() - .map(|user| UserInfo { - id: user.id, - username: user.username, - }) - .collect::>(); - - Ok(Response::new(GetUsersResponse { users: users_info })) - } -} - -#[cfg(test)] -#[path = "../../tests/controllers/user_controller.rs"] -mod user_controller_tests; diff --git a/src/controllers/controller_traits/access_controller_trait.rs b/src/controllers/controller_traits/access_controller_trait.rs deleted file mode 100644 index babc3e6..0000000 --- a/src/controllers/controller_traits/access_controller_trait.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::api::server::protobuf::{ - CreateAccessRequest, DeleteAccessRequest, ListAccessInfoRequest, ListAccessInfoResponse, - UpdateAccessRequest, -}; -use async_trait::async_trait; -use tonic::{Request, Response, Status}; - -#[async_trait] -pub trait AccessControllerTrait: Send + Sync { - /// handles the list_access_info endpoint - /// # Errors - /// If an invalid or non-existent [`ListAccessInfoRequest::project_id`] is provided - async fn list_access_info( - &self, - request: Request, - ) -> Result, Status>; - /// Creates an access in the contexts. - /// # Errors - /// Returns an error if the contexts context fails to create the access - async fn create_access( - &self, - request: Request, - ) -> Result, Status>; - - /// Endpoint for updating an access record. - /// - /// Takes [`UpdateAccessRequest`] as input - /// - /// Returns a [`Status`] as response - /// - /// `project_id` and `user_id` is set to 'default' since they won't be updated in the contexts. - async fn update_access( - &self, - request: Request, - ) -> Result, Status>; - - /// Deletes the an Access from the contexts. This has no sideeffects. - /// - /// # Errors - /// This function will return an error if the access does not exist in the contexts. - async fn delete_access( - &self, - request: Request, - ) -> Result, Status>; -} diff --git a/src/controllers/controller_traits/mod.rs b/src/controllers/controller_traits/mod.rs deleted file mode 100644 index 202ed99..0000000 --- a/src/controllers/controller_traits/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod access_controller_trait; -mod project_controller_trait; -mod query_controller_trait; -mod session_controller_trait; -mod user_controller_trait; - -pub use access_controller_trait::AccessControllerTrait; -pub use project_controller_trait::ProjectControllerTrait; -pub use query_controller_trait::QueryControllerTrait; -pub use session_controller_trait::SessionControllerTrait; -pub use user_controller_trait::UserControllerTrait; diff --git a/src/controllers/controller_traits/project_controller_trait.rs b/src/controllers/controller_traits/project_controller_trait.rs deleted file mode 100644 index 0aac153..0000000 --- a/src/controllers/controller_traits/project_controller_trait.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::api::server::protobuf::{ - CreateProjectRequest, CreateProjectResponse, DeleteProjectRequest, GetProjectRequest, - GetProjectResponse, ListProjectsInfoResponse, UpdateProjectRequest, -}; -use async_trait::async_trait; -use tonic::{Request, Response, Status}; - -#[async_trait] -pub trait ProjectControllerTrait: Send + Sync { - /// Gets a project and its queries from the contexts. - /// - /// If the project is not in use, it will now be in use by the requester's session, - /// given that they are an Editor. - async fn get_project( - &self, - request: Request, - ) -> Result, Status>; - - /// Creates a project from [`CreateProjectRequest`] - /// # Errors - /// Errors on invalid JSON, invalid user id or if a project already exists - async fn create_project( - &self, - request: Request, - ) -> Result, Status>; - - /// Updates a Model in the contexts given its id. - /// - /// # Errors - /// This function will return an error if the project does not exist in the contexts - /// or if the user does not have access to the project with role 'Editor'. - async fn update_project( - &self, - request: Request, - ) -> Result, Status>; - - /// Deletes a Model from the contexts. - /// - /// # Errors - /// This function will return an error if the project does not exist in the contexts - /// or if the user is not the project owner. - async fn delete_project( - &self, - request: Request, - ) -> Result, Status>; - - async fn list_projects_info( - &self, - request: Request<()>, - ) -> Result, Status>; -} diff --git a/src/controllers/controller_traits/query_controller_trait.rs b/src/controllers/controller_traits/query_controller_trait.rs deleted file mode 100644 index 5d232ca..0000000 --- a/src/controllers/controller_traits/query_controller_trait.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::api::server::protobuf::{ - CreateQueryRequest, DeleteQueryRequest, SendQueryRequest, SendQueryResponse, UpdateQueryRequest, -}; -use async_trait::async_trait; -use tonic::{Request, Response, Status}; - -#[async_trait] -pub trait QueryControllerTrait: Send + Sync { - /// Creates a query in the contexts - /// # Errors - /// Returns an error if the contexts context fails to create the query or - async fn create_query( - &self, - request: Request, - ) -> Result, Status>; - - /// Endpoint for updating a query record. - /// # Errors - /// Errors on non existent entity, parsing error or invalid rights - async fn update_query( - &self, - request: Request, - ) -> Result, Status>; - - /// Deletes a query record in the contexts. - /// # Errors - /// Returns an error if the provided query_id is not found in the contexts. - async fn delete_query( - &self, - request: Request, - ) -> Result, Status>; - - /// Sends a query to be run on Reveaal. - /// After query is run the result is stored in the contexts. - /// - /// Returns the response that is received from Reveaal. - async fn send_query( - &self, - request: Request, - ) -> Result, Status>; -} diff --git a/src/controllers/controller_traits/session_controller_trait.rs b/src/controllers/controller_traits/session_controller_trait.rs deleted file mode 100644 index 312cddf..0000000 --- a/src/controllers/controller_traits/session_controller_trait.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::api::server::protobuf::{GetAuthTokenRequest, GetAuthTokenResponse}; -use async_trait::async_trait; -use tonic::{Request, Response, Status}; - -#[async_trait] -pub trait SessionControllerTrait: Send + Sync { - /// Deletes the requester's session, found by their access token. - /// - /// Returns the response that is received from Reveaal. - async fn delete_session(&self, _request: Request<()>) -> Result, Status>; - - /// This method is used to get a new access and refresh token for a user. - /// - /// # Errors - /// This function will return an error if the user does not exist in the contexts, - /// if the password in the request does not match the user's password, - /// or if no user is provided in the request. - async fn get_auth_token( - &self, - request: Request, - ) -> Result, Status>; -} diff --git a/src/controllers/controller_traits/user_controller_trait.rs b/src/controllers/controller_traits/user_controller_trait.rs deleted file mode 100644 index 28ed8f6..0000000 --- a/src/controllers/controller_traits/user_controller_trait.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::api::server::protobuf::{ - CreateUserRequest, GetUsersRequest, GetUsersResponse, UpdateUserRequest, -}; -use async_trait::async_trait; -use tonic::{Request, Response, Status}; - -#[async_trait] -pub trait UserControllerTrait: Send + Sync { - async fn create_user( - &self, - request: Request, - ) -> Result, Status>; - async fn update_user( - &self, - request: Request, - ) -> Result, Status>; - async fn delete_user(&self, request: Request<()>) -> Result, Status>; - async fn get_users( - &self, - request: Request, - ) -> Result, Status>; -} diff --git a/src/controllers/mod.rs b/src/controllers/mod.rs index ad896bd..46c87b4 100644 --- a/src/controllers/mod.rs +++ b/src/controllers/mod.rs @@ -1,3 +1,207 @@ -pub mod controller_collection; -pub mod controller_impls; -pub mod controller_traits; +mod access_controller; +mod controller_collection; +mod project_controller; +mod query_controller; +mod reveaal_controller; +mod session_controller; +mod user_controller; + +pub use access_controller::*; +pub use controller_collection::*; +pub use project_controller::*; +pub use query_controller::*; +pub use reveaal_controller::*; +pub use session_controller::*; +pub use user_controller::*; + +#[cfg(test)] +mod helpers { + use crate::api::auth::TokenType; + use crate::api::server::protobuf::{ + AccessInfo, ProjectInfo, QueryRequest, QueryResponse, SimulationStartRequest, + SimulationStepRequest, SimulationStepResponse, UserTokenResponse, + }; + use crate::contexts::*; + use crate::entities::{access, in_use, project, query, session, user}; + use crate::services::*; + use async_trait::async_trait; + use mockall::mock; + use sea_orm::DbErr; + use std::sync::Arc; + use tonic::{Request, Response, Status}; + + pub fn get_mock_contexts() -> MockContexts { + MockContexts { + access_context_mock: MockAccessContext::new(), + in_use_context_mock: MockInUseContext::new(), + project_context_mock: MockProjectContext::new(), + query_context_mock: MockQueryContext::new(), + session_context_mock: MockSessionContext::new(), + user_context_mock: MockUserContext::new(), + } + } + + pub fn get_mock_services() -> MockServices { + MockServices { + hashing_service_mock: MockHashingService::new(), + reveaal_service_mock: MockReveaalService::new(), + } + } + + pub fn disguise_context_mocks(mock_services: MockContexts) -> ContextCollection { + ContextCollection { + access_context: Arc::new(mock_services.access_context_mock), + in_use_context: Arc::new(mock_services.in_use_context_mock), + project_context: Arc::new(mock_services.project_context_mock), + query_context: Arc::new(mock_services.query_context_mock), + session_context: Arc::new(mock_services.session_context_mock), + user_context: Arc::new(mock_services.user_context_mock), + } + } + + pub fn disguise_service_mocks(mock_services: MockServices) -> ServiceCollection { + ServiceCollection { + hashing_service: Arc::new(mock_services.hashing_service_mock), + reveaal_service: Arc::new(mock_services.reveaal_service_mock), + } + } + + pub struct MockContexts { + pub(crate) access_context_mock: MockAccessContext, + pub(crate) in_use_context_mock: MockInUseContext, + pub(crate) project_context_mock: MockProjectContext, + pub(crate) query_context_mock: MockQueryContext, + pub(crate) session_context_mock: MockSessionContext, + pub(crate) user_context_mock: MockUserContext, + } + + pub struct MockServices { + pub(crate) hashing_service_mock: MockHashingService, + pub(crate) reveaal_service_mock: MockReveaalService, + } + + mock! { + pub AccessContext {} + #[async_trait] + impl EntityContextTrait for AccessContext { + async fn create(&self, entity: access::Model) -> Result; + async fn get_by_id(&self, entity_id: i32) -> Result, DbErr>; + async fn get_all(&self) -> Result, DbErr>; + async fn update(&self, entity: access::Model) -> Result; + async fn delete(&self, entity_id: i32) -> Result; + } + #[async_trait] + impl AccessContextTrait for AccessContext { + async fn get_access_by_uid_and_project_id( + &self, + uid: i32, + project_id: i32, + ) -> Result, DbErr>; + + async fn get_access_by_project_id( + &self, + project_id: i32, + ) -> Result, DbErr>; + } + } + + mock! { + pub InUseContext {} + #[async_trait] + impl EntityContextTrait for InUseContext { + async fn create(&self, entity: in_use::Model) -> Result; + async fn get_by_id(&self, entity_id: i32) -> Result, DbErr>; + async fn get_all(&self) -> Result, DbErr>; + async fn update(&self, entity: in_use::Model) -> Result; + async fn delete(&self, entity_id: i32) -> Result; + } + #[async_trait] + impl InUseContextTrait for InUseContext {} + } + + mock! { + pub ProjectContext {} + #[async_trait] + impl EntityContextTrait for ProjectContext { + async fn create(&self, entity: project::Model) -> Result; + async fn get_by_id(&self, entity_id: i32) -> Result, DbErr>; + async fn get_all(&self) -> Result, DbErr>; + async fn update(&self, entity: project::Model) -> Result; + async fn delete(&self, entity_id: i32) -> Result; + } + #[async_trait] + impl ProjectContextTrait for ProjectContext { + async fn get_project_info_by_uid(&self, uid: i32) -> Result, DbErr>; + } + } + + mock! { + pub QueryContext {} + #[async_trait] + impl EntityContextTrait for QueryContext { + async fn create(&self, entity: query::Model) -> Result; + async fn get_by_id(&self, entity_id: i32) -> Result, DbErr>; + async fn get_all(&self) -> Result, DbErr>; + async fn update(&self, entity: query::Model) -> Result; + async fn delete(&self, entity_id: i32) -> Result; + } + #[async_trait] + impl QueryContextTrait for QueryContext { + async fn get_all_by_project_id(&self, project_id: i32) -> Result, DbErr>; + } + } + + mock! { + pub SessionContext {} + #[async_trait] + impl EntityContextTrait for SessionContext { + async fn create(&self, entity: session::Model) -> Result; + async fn get_by_id(&self, entity_id: i32) -> Result, DbErr>; + async fn get_all(&self) -> Result, DbErr>; + async fn update(&self, entity: session::Model) -> Result; + async fn delete(&self, entity_id: i32) -> Result; + } + #[async_trait] + impl SessionContextTrait for SessionContext { + async fn get_by_token(&self, token_type: TokenType, token: String) -> Result, DbErr>; + async fn delete_by_token(&self, token_type: TokenType, token: String) -> Result; + } + } + + mock! { + pub UserContext {} + #[async_trait] + impl EntityContextTrait for UserContext { + async fn create(&self, entity: user::Model) -> Result; + async fn get_by_id(&self, entity_id: i32) -> Result, DbErr>; + async fn get_all(&self) -> Result, DbErr>; + async fn update(&self, entity: user::Model) -> Result; + async fn delete(&self, entity_id: i32) -> Result; + } + #[async_trait] + impl UserContextTrait for UserContext { + async fn get_by_username(&self, username: String) -> Result, DbErr>; + async fn get_by_email(&self, email: String) -> Result, DbErr>; + async fn get_by_ids(&self, ids: Vec) -> Result, DbErr>; + } + } + + mock! { + pub ReveaalService{} + #[async_trait] + impl ReveaalServiceTrait for ReveaalService { + async fn get_user_token(&self,request: Request<()>) -> Result, Status>; + async fn send_query(&self,request: Request) -> Result, Status>; + async fn start_simulation(&self, request: Request) -> Result, Status>; + async fn take_simulation_step(&self, request: Request) -> Result, Status>; + } + } + + mock! { + pub HashingService {} + impl HashingService for HashingService { + fn hash_password(&self, password: String) -> Result; + fn verify_password(&self, password: String, hash: &str) -> Result; + } + } +} diff --git a/src/controllers/project_controller.rs b/src/controllers/project_controller.rs new file mode 100644 index 0000000..3d80880 --- /dev/null +++ b/src/controllers/project_controller.rs @@ -0,0 +1,2045 @@ +use crate::api::auth::{RequestExt, TokenType}; +use crate::api::server::protobuf::{ + CreateProjectRequest, CreateProjectResponse, DeleteProjectRequest, GetProjectRequest, + GetProjectResponse, ListProjectsInfoResponse, Project, Query, UpdateProjectRequest, +}; +use crate::contexts::ContextCollection; +use crate::entities::{access, in_use, project}; +use async_trait::async_trait; +use chrono::{Duration, Utc}; +use sea_orm::SqlErr; +use tonic::{Request, Response, Status}; + +const IN_USE_DURATION_MINUTES: i64 = 10; + +#[async_trait] +pub trait ProjectControllerTrait: Send + Sync { + /// Gets a project and its queries from the contexts. + /// + /// If the project is not in use, it will now be in use by the requester's session, + /// given that they are an Editor. + async fn get_project( + &self, + request: Request, + ) -> Result, Status>; + + /// Creates a project from [`CreateProjectRequest`] + /// # Errors + /// Errors on invalid JSON, invalid user id or if a project already exists + async fn create_project( + &self, + request: Request, + ) -> Result, Status>; + + /// Updates a Model in the contexts given its id. + /// + /// # Errors + /// This function will return an error if the project does not exist in the contexts + /// or if the user does not have access to the project with role 'Editor'. + async fn update_project( + &self, + request: Request, + ) -> Result, Status>; + + /// Deletes a Model from the contexts. + /// + /// # Errors + /// This function will return an error if the project does not exist in the contexts + /// or if the user is not the project owner. + async fn delete_project( + &self, + request: Request, + ) -> Result, Status>; + + async fn list_projects_info( + &self, + request: Request<()>, + ) -> Result, Status>; +} + +pub struct ProjectController { + contexts: ContextCollection, +} + +impl ProjectController { + pub fn new(contexts: ContextCollection) -> Self { + ProjectController { contexts } + } +} + +#[async_trait] +impl ProjectControllerTrait for ProjectController { + async fn get_project( + &self, + request: Request, + ) -> Result, Status> { + let message = request.get_ref(); + + let project_id = message.id; + + let uid = request + .uid() + .map_err(|err| { + Status::internal(format!( + "could not stringify user id in request metadata, internal error {}", + err + )) + })? + .ok_or(Status::internal("Could not get uid from request metadata"))?; + + let access = self + .contexts + .access_context + .get_access_by_uid_and_project_id(uid, project_id) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::permission_denied("User does not have access to project"))?; + + let project = self + .contexts + .project_context + .get_by_id(project_id) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::internal("Model not found"))?; + + let project = Project { + id: project.id, + name: project.name, + components_info: serde_json::from_value(project.components_info).map_err(|err| { + Status::internal(format!( + "failed to parse components info object, internal error: {}", + err + )) + })?, + owner_id: project.owner_id, + }; + + let mut in_use_bool = true; + match self.contexts.in_use_context.get_by_id(project_id).await { + Ok(Some(in_use)) => { + // If project is not in use and user is an Editor, update the in use with the users session. + if in_use.latest_activity + <= (Utc::now().naive_utc() - Duration::minutes(IN_USE_DURATION_MINUTES)) + { + in_use_bool = false; + + if access.role == "Editor" { + let session = self + .contexts + .session_context + .get_by_token( + TokenType::AccessToken, + request + .token_string() + .map_err(|err| Status::internal(format!("could not stringify user id in request metadata, internal error {}",err)))? + .ok_or(Status::invalid_argument("failed to get token from request metadata"))?, + ) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| { + Status::unauthenticated( + "No session found with given access token", + ) + })?; + + let in_use = in_use::Model { + project_id: in_use.project_id, + session_id: session.id, + latest_activity: Utc::now().naive_utc(), + }; + + self.contexts + .in_use_context + .update(in_use) + .await + .map_err(|err| Status::internal(err.to_string()))?; + } + } + } + Ok(None) => return Err(Status::internal("No in use found for project")), + Err(err) => return Err(Status::internal(err.to_string())), + } + + let queries = self + .contexts + .query_context + .get_all_by_project_id(project_id) + .await + .map_err(|err| Status::internal(err.to_string()))?; + + let queries = queries + .into_iter() + .map(|query| { + let result = serde_json::from_value(query.result.unwrap_or_else(|| "".into()))?; + + Ok(Query { + id: query.id, + project_id: query.project_id, + query: query.string, + result, + outdated: query.outdated, + }) + }) + .collect::, serde_json::Error>>() + .map_err(|err| { + Status::internal(format!( + "failed to parse json result, inner error: {}", + err + )) + })?; + + Ok(Response::new(GetProjectResponse { + project: Some(project), + queries, + in_use: in_use_bool, + })) + } + + async fn create_project( + &self, + request: Request, + ) -> Result, Status> { + let message = request.get_ref().clone(); + let uid = request + .uid() + .map_err(|err| { + Status::internal(format!( + "could not stringify user id in request metadata, internal error {}", + err + )) + })? + .ok_or(Status::internal("Could not get uid from request metadata"))?; + + let components_info = match message.clone().components_info { + Some(components_info) => serde_json::to_value(components_info).map_err(|err| { + Status::internal(format!( + "failed to parse components info object, internal error: {}", + err + )) + })?, + None => return Err(Status::invalid_argument("No components info provided")), + }; + + let mut project = project::Model { + id: Default::default(), + name: message.clone().name, + components_info, + owner_id: uid, + }; + + project = match self.contexts.project_context.create(project).await { + Ok(project) => project, + Err(error) => { + return match error.sql_err() { + Some(SqlErr::UniqueConstraintViolation(e)) => { + let error_msg = match e.to_lowercase() { + _ if e.contains("name") => "A project with that name already exists", + _ => "Model already exists", + }; + println!("{}", e); + Err(Status::already_exists(error_msg)) + } + Some(SqlErr::ForeignKeyConstraintViolation(e)) => { + let error_msg = match e.to_lowercase() { + _ if e.contains("owner_id") => "No user with that id exists", + _ => "Could not create project", + }; + println!("{}", e); + Err(Status::invalid_argument(error_msg)) + } + _ => Err(Status::internal(error.to_string())), + }; + } + }; + + let access = access::Model { + id: Default::default(), + role: "Editor".to_string(), //todo!("Use role enum") + project_id: project.clone().id, + user_id: uid, + }; + + let session = self + .contexts + .session_context + .get_by_token( + TokenType::AccessToken, + request + .token_string() + .map_err(|err| { + Status::internal(format!( + "could not stringify user id in request metadata, internal error {}", + err + )) + })? + .ok_or(Status::internal( + "failed to get token from request metadata", + ))?, + ) + .await + .map_err(|_err| Status::internal("failed to query database"))? + .ok_or(Status::not_found("token not found"))?; + + let in_use = in_use::Model { + project_id: project.clone().id, + session_id: session.id, + latest_activity: Default::default(), + }; + + self.contexts + .in_use_context + .create(in_use) + .await + .map_err(|err| { + Status::internal(format!("a database error occured, internal error: {}", err)) + })?; + self.contexts + .access_context + .create(access) + .await + .map_err(|err| { + Status::internal(format!("a database error occured, internal error: {}", err)) + })?; + + Ok(Response::new(CreateProjectResponse { id: project.id })) + } + + async fn update_project( + &self, + request: Request, + ) -> Result, Status> { + let message = request.get_ref().clone(); + let uid = request + .uid() + .map_err(|err| { + Status::internal(format!( + "could not stringify user id in request metadata, internal error {}", + err + )) + })? + .ok_or(Status::internal("Could not get uid from request metadata"))?; + + // Check if the project exists + let project = self + .contexts + .project_context + .get_by_id(message.id) + .await + .map_err(|error| Status::internal(error.to_string()))? + .ok_or(Status::not_found("No project found with given id"))?; + + // Check if the user has access to the project + match self + .contexts + .access_context + .get_access_by_uid_and_project_id(uid, project.id) + .await + { + Ok(access) => { + let mut is_editor = false; + let access = match access { + Some(access) => { + is_editor = access.role == "Editor"; + Some(access) + } + None => None, + }; + + if !is_editor || access.is_none() { + return Err(Status::permission_denied( + "You do not have permission to update this project", + )); + } + } + Err(error) => return Err(Status::internal(error.to_string())), + }; + + // Get user session + let session = match self + .contexts + .session_context + .get_by_token( + TokenType::AccessToken, + request + .token_string() + .map_err(|err| { + Status::internal(format!( + "could not stringify user id in request metadata, internal error {}", + err + )) + })? + .ok_or(Status::internal( + "failed to get token from request metadata", + ))?, + ) + .await + { + Ok(Some(session)) => session, + Ok(None) => { + return Err(Status::unauthenticated( + "No session found with given access token", + )); + } + Err(error) => return Err(Status::internal(error.to_string())), + }; + + // Get in_use for project + match self.contexts.in_use_context.get_by_id(project.id).await { + Ok(Some(in_use)) => { + // Check if in_use latest activity is older than the max allowed + if in_use.latest_activity + > (Utc::now().naive_utc() - Duration::minutes(IN_USE_DURATION_MINUTES)) + && in_use.session_id != session.id + { + return Err(Status::failed_precondition( + "Model is currently in use by another session", + )); + } + + let new_in_use = in_use::Model { + project_id: in_use.project_id, + session_id: session.id, + latest_activity: Utc::now().naive_utc(), + }; + + match self.contexts.in_use_context.update(new_in_use).await { + Ok(_) => (), + Err(error) => return Err(Status::internal(error.to_string())), + } + } + Ok(None) => return Err(Status::internal("No in_use found for project")), + Err(error) => return Err(Status::internal(error.to_string())), + }; + + let new_project = project::Model { + id: project.id, + name: match message.clone().name { + Some(name) => name, + None => project.name, + }, + components_info: match message.clone().components_info { + Some(components_info) => serde_json::to_value(components_info).map_err(|err| { + Status::internal(format!( + "failed to parse components info object, internal error: {}", + err + )) + })?, + None => project.components_info, + }, + owner_id: match message.clone().owner_id { + Some(new_owner_id) => { + if project.owner_id == uid { + new_owner_id + } else { + return Err(Status::permission_denied( + "You do not have permission to change the owner of this project", + )); + } + } + None => project.owner_id, + }, + }; + + match self.contexts.project_context.update(new_project).await { + Ok(_) => Ok(Response::new(())), + Err(error) => Err(Status::internal(error.to_string())), + } + } + + async fn delete_project( + &self, + request: Request, + ) -> Result, Status> { + let uid = request + .uid() + .map_err(|err| { + Status::internal(format!( + "could not stringify user id in request metadata, internal error {}", + err + )) + })? + .ok_or(Status::internal("Could not get uid from request metadata"))?; + let project_id = request.get_ref().id; + + let project = self + .contexts + .project_context + .get_by_id(project_id) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or(Status::not_found("No project found with given id"))?; + + // Check if user is owner and thereby has permission to delete project + if project.owner_id != uid { + return Err(Status::permission_denied( + "You do not have permission to delete this project", + )); + } + + match self.contexts.project_context.delete(project_id).await { + Ok(_) => Ok(Response::new(())), + Err(error) => match error { + sea_orm::DbErr::RecordNotFound(message) => Err(Status::not_found(message)), + _ => Err(Status::internal(error.to_string())), + }, + } + } + + async fn list_projects_info( + &self, + request: Request<()>, + ) -> Result, Status> { + let uid = request + .uid() + .map_err(|err| { + Status::internal(format!( + "could not stringify user id in request metadata, internal error {}", + err + )) + })? + .ok_or(Status::internal("Could not get uid from request metadata"))?; + + match self + .contexts + .project_context + .get_project_info_by_uid(uid) + .await + { + Ok(project_info_list) => { + if project_info_list.is_empty() { + Err(Status::not_found("No access found for given user")) + } else { + Ok(Response::new(ListProjectsInfoResponse { + project_info_list, + })) + } + } + Err(error) => Err(Status::internal(error.to_string())), + } + } +} + +#[cfg(test)] +mod tests { + use super::super::helpers::disguise_context_mocks; + use super::super::helpers::get_mock_contexts; + use crate::controllers::ProjectController; + use crate::controllers::ProjectControllerTrait; + use crate::{ + api::{ + auth::TokenType, + server::protobuf::{ + component::Rep, Component, ComponentsInfo, CreateProjectRequest, + DeleteProjectRequest, GetProjectRequest, ProjectInfo, UpdateProjectRequest, + }, + }, + entities::{access, in_use, project, query, session}, + }; + use chrono::Utc; + use mockall::predicate; + use sea_orm::DbErr; + use std::str::FromStr; + use tonic::{metadata, Code, Request}; + + #[tokio::test] + async fn create_project_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + + let uid = 0; + + let components_info = ComponentsInfo { + components: vec![], + components_hash: 0, + }; + + let project = project::Model { + id: Default::default(), + name: Default::default(), + components_info: serde_json::to_value(components_info.clone()).unwrap(), + owner_id: uid, + }; + + let access = access::Model { + id: Default::default(), + role: "Editor".to_string(), + user_id: uid, + project_id: project.id, + }; + + let session = session::Model { + id: Default::default(), + refresh_token: "refresh_token".to_string(), + access_token: "access_token".to_string(), + updated_at: Default::default(), + user_id: uid, + }; + + let in_use = in_use::Model { + project_id: project.id, + session_id: session.id, + latest_activity: Default::default(), + }; + + mock_contexts + .project_context_mock + .expect_create() + .with(predicate::eq(project.clone())) + .returning(move |_| Ok(project.clone())); + + mock_contexts + .access_context_mock + .expect_create() + .with(predicate::eq(access.clone())) + .returning(move |_| Ok(access.clone())); + + mock_contexts + .session_context_mock + .expect_get_by_token() + .with( + predicate::eq(TokenType::AccessToken), + predicate::eq("access_token".to_string()), + ) + .returning(move |_, _| Ok(Some(session.clone()))); + + mock_contexts + .in_use_context_mock + .expect_create() + .with(predicate::eq(in_use.clone())) + .returning(move |_| Ok(in_use.clone())); + + let mut request = Request::new(CreateProjectRequest { + name: Default::default(), + components_info: Option::from(components_info), + }); + + request + .metadata_mut() + .insert("uid", uid.to_string().parse().unwrap()); + + request.metadata_mut().insert( + "authorization", + metadata::MetadataValue::from_str("Bearer access_token").unwrap(), + ); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.create_project(request).await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn create_project_existing_name_returns_err() { + let mut mock_contexts = get_mock_contexts(); + + let uid = 0; + + let project = project::Model { + id: Default::default(), + name: "project".to_string(), + components_info: Default::default(), + owner_id: uid, + }; + + mock_contexts + .project_context_mock + .expect_create() + .with(predicate::eq(project.clone())) + .returning(move |_| Err(DbErr::RecordNotInserted)); //todo!("Needs to be a SqlError with UniqueConstraintViolation with 'name' in message) + + let mut request = Request::new(CreateProjectRequest { + name: "project".to_string(), + components_info: Default::default(), + }); + + request + .metadata_mut() + .insert("uid", uid.to_string().parse().unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.create_project(request).await; + + assert_eq!(res.unwrap_err().code(), Code::InvalidArgument); //todo!("Needs to be code AlreadyExists when mocked Error is corrected) + } + + #[tokio::test] + async fn get_project_user_has_access_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + + let project = project::Model { + id: Default::default(), + name: "project".to_string(), + components_info: Default::default(), + owner_id: 0, + }; + + let access = access::Model { + id: Default::default(), + role: "Editor".to_string(), + project_id: 1, + user_id: 1, + }; + + let in_use = in_use::Model { + project_id: Default::default(), + session_id: 0, + latest_activity: Utc::now().naive_utc(), + }; + + let queries: Vec = vec![]; + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(0), predicate::eq(0)) + .returning(move |_, _| Ok(Some(access.clone()))); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(Some(project.clone()))); + + mock_contexts + .in_use_context_mock + .expect_get_by_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(Some(in_use.clone()))); + + mock_contexts + .query_context_mock + .expect_get_all_by_project_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(queries.clone())); + + let mut request = Request::new(GetProjectRequest { id: 0 }); + + request.metadata_mut().insert("uid", "0".parse().unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.get_project(request).await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn delete_not_owner_returns_err() { + let mut mock_contexts = get_mock_contexts(); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(project::Model { + id: 1, + name: Default::default(), + components_info: Default::default(), + owner_id: 2, + })) + }); + + let mut request = Request::new(DeleteProjectRequest { id: 1 }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.delete_project(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::PermissionDenied); + } + + #[tokio::test] + async fn delete_invalid_project_returns_err() { + let mut mock_contexts = get_mock_contexts(); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(2)) + .returning(move |_| Ok(None)); + + let mut request = Request::new(DeleteProjectRequest { id: 2 }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.delete_project(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::NotFound); + } + + #[tokio::test] + async fn delete_project_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(project::Model { + id: 1, + name: Default::default(), + components_info: Default::default(), + owner_id: 1, + })) + }); + + mock_contexts + .project_context_mock + .expect_delete() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(project::Model { + id: 1, + name: Default::default(), + components_info: Default::default(), + owner_id: 1, + }) + }); + + let mut request = Request::new(DeleteProjectRequest { id: 1 }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.delete_project(request).await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn get_project_user_has_no_access_returns_err() { + let mut mock_contexts = get_mock_contexts(); + + let project = project::Model { + id: Default::default(), + name: "project".to_string(), + components_info: Default::default(), + owner_id: 0, + }; + + let in_use = in_use::Model { + project_id: Default::default(), + session_id: 0, + latest_activity: Default::default(), + }; + + let queries: Vec = vec![]; + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(0), predicate::eq(0)) + .returning(move |_, _| Ok(None)); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(Some(project.clone()))); + + mock_contexts + .in_use_context_mock + .expect_get_by_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(Some(in_use.clone()))); + + mock_contexts + .query_context_mock + .expect_get_all_by_project_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(queries.clone())); + + let mut request = Request::new(GetProjectRequest { id: 0 }); + + request.metadata_mut().insert("uid", "0".parse().unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.get_project(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::PermissionDenied); + } + + #[tokio::test] + async fn get_project_is_in_use_is_true() { + let mut mock_contexts = get_mock_contexts(); + + let project = project::Model { + id: Default::default(), + name: "project".to_string(), + components_info: Default::default(), + owner_id: 0, + }; + + let access = access::Model { + id: Default::default(), + role: "Editor".to_string(), + project_id: 1, + user_id: 1, + }; + + let in_use = in_use::Model { + project_id: Default::default(), + session_id: 0, + latest_activity: Utc::now().naive_utc(), + }; + + let queries: Vec = vec![]; + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(0), predicate::eq(0)) + .returning(move |_, _| Ok(Some(access.clone()))); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(Some(project.clone()))); + + mock_contexts + .in_use_context_mock + .expect_get_by_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(Some(in_use.clone()))); + + mock_contexts + .query_context_mock + .expect_get_all_by_project_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(queries.clone())); + + let mut request = Request::new(GetProjectRequest { id: 0 }); + + request.metadata_mut().insert("uid", "0".parse().unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.get_project(request).await; + + assert!(res.unwrap().get_ref().in_use); + } + + #[tokio::test] + async fn get_project_is_in_use_is_false() { + let mut mock_contexts = get_mock_contexts(); + + let project = project::Model { + id: Default::default(), + name: "project".to_string(), + components_info: Default::default(), + owner_id: 0, + }; + + let access = access::Model { + id: Default::default(), + role: "Editor".to_string(), + project_id: 1, + user_id: 1, + }; + + let in_use = in_use::Model { + project_id: 0, + session_id: 0, + latest_activity: Default::default(), + }; + + let updated_in_use = in_use::Model { + project_id: 0, + session_id: 1, + latest_activity: Default::default(), + }; + + let session = session::Model { + id: 0, + refresh_token: "refresh_token".to_owned(), + access_token: "access_token".to_owned(), + updated_at: Default::default(), + user_id: Default::default(), + }; + + let queries: Vec = vec![]; + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(0), predicate::eq(0)) + .returning(move |_, _| Ok(Some(access.clone()))); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(Some(project.clone()))); + + mock_contexts + .in_use_context_mock + .expect_get_by_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(Some(in_use.clone()))); + + mock_contexts + .session_context_mock + .expect_get_by_token() + .with( + predicate::eq(TokenType::AccessToken), + predicate::eq("access_token".to_owned()), + ) + .returning(move |_, _| Ok(Some(session.clone()))); + + mock_contexts + .query_context_mock + .expect_get_all_by_project_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(queries.clone())); + + mock_contexts + .in_use_context_mock + .expect_update() + .returning(move |_| Ok(updated_in_use.clone())); + + let mut request = Request::new(GetProjectRequest { id: 0 }); + + request + .metadata_mut() + .insert("authorization", "Bearer access_token".parse().unwrap()); + request.metadata_mut().insert("uid", "0".parse().unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.get_project(request).await; + + assert!(!res.unwrap().get_ref().in_use); + } + + #[tokio::test] + async fn get_project_project_has_no_queries_queries_are_empty() { + let mut mock_contexts = get_mock_contexts(); + + let project = project::Model { + id: Default::default(), + name: "project".to_string(), + components_info: Default::default(), + owner_id: 0, + }; + + let access = access::Model { + id: Default::default(), + role: "Editor".to_string(), + project_id: 1, + user_id: 1, + }; + + let in_use = in_use::Model { + project_id: Default::default(), + session_id: 0, + latest_activity: Utc::now().naive_utc(), + }; + + let queries: Vec = vec![]; + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(0), predicate::eq(0)) + .returning(move |_, _| Ok(Some(access.clone()))); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(Some(project.clone()))); + + mock_contexts + .in_use_context_mock + .expect_get_by_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(Some(in_use.clone()))); + + mock_contexts + .query_context_mock + .expect_get_all_by_project_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(queries.clone())); + + let mut request = Request::new(GetProjectRequest { id: 0 }); + + request.metadata_mut().insert("uid", "0".parse().unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.get_project(request).await; + + assert!(res.unwrap().get_ref().queries.is_empty()); + } + + #[tokio::test] + async fn get_project_query_has_no_result_query_is_empty() { + let mut mock_contexts = get_mock_contexts(); + + let project = project::Model { + id: Default::default(), + name: "project".to_string(), + components_info: Default::default(), + owner_id: 0, + }; + + let access = access::Model { + id: Default::default(), + role: "Editor".to_string(), + project_id: 1, + user_id: 1, + }; + + let in_use = in_use::Model { + project_id: Default::default(), + session_id: 0, + latest_activity: Utc::now().naive_utc(), + }; + + let query = query::Model { + id: 0, + project_id: 1, + string: "query".to_owned(), + result: None, + outdated: false, + }; + + let queries: Vec = vec![query]; + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(0), predicate::eq(0)) + .returning(move |_, _| Ok(Some(access.clone()))); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(Some(project.clone()))); + + mock_contexts + .in_use_context_mock + .expect_get_by_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(Some(in_use.clone()))); + + mock_contexts + .query_context_mock + .expect_get_all_by_project_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(queries.clone())); + + let mut request = Request::new(GetProjectRequest { id: 0 }); + + request.metadata_mut().insert("uid", "0".parse().unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.get_project(request).await; + + assert!(res.unwrap().get_ref().queries[0].result.is_empty()); + } + + #[tokio::test] + async fn list_projects_info_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + + let project_info = ProjectInfo { + project_id: 1, + project_name: "project::Model name".to_owned(), + project_owner_id: 1, + user_role_on_project: "Editor".to_owned(), + }; + + mock_contexts + .project_context_mock + .expect_get_project_info_by_uid() + .with(predicate::eq(1)) + .returning(move |_| Ok(vec![project_info.clone()])); + + let mut list_projects_info_request = Request::new(()); + + list_projects_info_request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic + .list_projects_info(list_projects_info_request) + .await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn list_projects_info_returns_err() { + let mut mock_contexts = get_mock_contexts(); + + mock_contexts + .project_context_mock + .expect_get_project_info_by_uid() + .with(predicate::eq(1)) + .returning(move |_| Ok(vec![])); + + let mut list_projects_info_request = Request::new(()); + + list_projects_info_request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic + .list_projects_info(list_projects_info_request) + .await; + + assert!(res.is_err()); + } + + #[tokio::test] + async fn update_name_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + + let user_id = 1; + let project_id = 1; + let new_project_name = "new_name".to_string(); + + let mut update_project_request = Request::new(UpdateProjectRequest { + id: project_id, + name: Some(new_project_name.clone()), + components_info: None, + owner_id: None, + }); + + update_project_request.metadata_mut().insert( + "authorization", + metadata::MetadataValue::from_str("Bearer access_token").unwrap(), + ); + + update_project_request.metadata_mut().insert( + "uid", + metadata::MetadataValue::from_str(user_id.to_string().as_str()).unwrap(), + ); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(project_id)) + .returning(move |_| { + Ok(Some(project::Model { + id: project_id, + name: "old_name".to_owned(), + components_info: Default::default(), + owner_id: user_id, + })) + }); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(project_id)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + user_id, + project_id, + role: "Editor".to_string(), + })) + }); + + mock_contexts + .session_context_mock + .expect_get_by_token() + .with( + predicate::eq(TokenType::AccessToken), + predicate::eq("access_token".to_string()), + ) + .returning(move |_, _| { + Ok(Some(session::Model { + id: 1, + refresh_token: "refresh_token".to_string(), + access_token: "access_token".to_string(), + updated_at: Default::default(), + user_id, + })) + }); + + mock_contexts + .project_context_mock + .expect_update() + .returning(move |_| { + Ok(project::Model { + id: project_id, + name: new_project_name.clone(), + components_info: Default::default(), + owner_id: user_id, + }) + }); + + mock_contexts + .in_use_context_mock + .expect_get_by_id() + .returning(move |_| { + Ok(Some(in_use::Model { + project_id, + session_id: 1, + latest_activity: Utc::now().naive_utc(), + })) + }); + + mock_contexts + .in_use_context_mock + .expect_update() + .returning(move |_| { + Ok(in_use::Model { + project_id: 1, + session_id: 1, + latest_activity: Utc::now().naive_utc(), + }) + }); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.update_project(update_project_request).await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn update_components_info_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + + let user_id = 1; + let project_id = 1; + let components_info_non_json = ComponentsInfo { + components: vec![Component { + rep: Some(Rep::Json("a".to_owned())), + }], + components_hash: 1234456, + }; + let components_info = serde_json::to_value(components_info_non_json.clone()).unwrap(); + + let mut update_project_request = Request::new(UpdateProjectRequest { + id: project_id, + name: None, + components_info: Some(components_info_non_json.clone()), + owner_id: None, + }); + + update_project_request.metadata_mut().insert( + "authorization", + metadata::MetadataValue::from_str("Bearer access_token").unwrap(), + ); + + update_project_request.metadata_mut().insert( + "uid", + metadata::MetadataValue::from_str(user_id.to_string().as_str()).unwrap(), + ); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(project_id)) + .returning(move |_| { + Ok(Some(project::Model { + id: project_id, + name: Default::default(), + components_info: Default::default(), + owner_id: user_id, + })) + }); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(project_id)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + user_id, + project_id, + role: "Editor".to_string(), + })) + }); + + mock_contexts + .session_context_mock + .expect_get_by_token() + .with( + predicate::eq(TokenType::AccessToken), + predicate::eq("access_token".to_string()), + ) + .returning(move |_, _| { + Ok(Some(session::Model { + id: 1, + refresh_token: "refresh_token".to_string(), + access_token: "access_token".to_string(), + updated_at: Default::default(), + user_id, + })) + }); + + mock_contexts + .project_context_mock + .expect_update() + .returning(move |_| { + Ok(project::Model { + id: project_id, + name: Default::default(), + components_info: components_info.clone(), + owner_id: user_id, + }) + }); + + mock_contexts + .in_use_context_mock + .expect_get_by_id() + .returning(move |_| { + Ok(Some(in_use::Model { + project_id, + session_id: 1, + latest_activity: Utc::now().naive_utc(), + })) + }); + + mock_contexts + .in_use_context_mock + .expect_update() + .returning(move |_| { + Ok(in_use::Model { + project_id: 1, + session_id: 1, + latest_activity: Utc::now().naive_utc(), + }) + }); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.update_project(update_project_request).await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn update_owner_id_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + + let user_id = 1; + let project_id = 1; + let new_owner_id = 2; + + let mut update_project_request = Request::new(UpdateProjectRequest { + id: project_id, + name: None, + components_info: None, + owner_id: Some(new_owner_id), + }); + + update_project_request.metadata_mut().insert( + "authorization", + metadata::MetadataValue::from_str("Bearer access_token").unwrap(), + ); + + update_project_request.metadata_mut().insert( + "uid", + metadata::MetadataValue::from_str(user_id.to_string().as_str()).unwrap(), + ); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(project_id)) + .returning(move |_| { + Ok(Some(project::Model { + id: project_id, + name: Default::default(), + components_info: Default::default(), + owner_id: user_id, + })) + }); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(project_id)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + user_id, + project_id, + role: "Editor".to_string(), + })) + }); + + mock_contexts + .session_context_mock + .expect_get_by_token() + .with( + predicate::eq(TokenType::AccessToken), + predicate::eq("access_token".to_string()), + ) + .returning(move |_, _| { + Ok(Some(session::Model { + id: 1, + refresh_token: "refresh_token".to_string(), + access_token: "access_token".to_string(), + updated_at: Default::default(), + user_id, + })) + }); + + mock_contexts + .project_context_mock + .expect_update() + .returning(move |_| { + Ok(project::Model { + id: project_id, + name: Default::default(), + components_info: Default::default(), + owner_id: new_owner_id, + }) + }); + + mock_contexts + .in_use_context_mock + .expect_get_by_id() + .returning(move |_| { + Ok(Some(in_use::Model { + project_id, + session_id: 1, + latest_activity: Utc::now().naive_utc(), + })) + }); + + mock_contexts + .in_use_context_mock + .expect_update() + .returning(move |_| { + Ok(in_use::Model { + project_id: 1, + session_id: 1, + latest_activity: Utc::now().naive_utc(), + }) + }); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.update_project(update_project_request).await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn update_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + + let user_id = 1; + let project_id = 1; + let new_project_name = "new_name".to_string(); + let new_components_info_non_json = ComponentsInfo { + components: vec![Component { + rep: Some(Rep::Json("a".to_owned())), + }], + components_hash: 1234456, + }; + let new_components_info = + serde_json::to_value(new_components_info_non_json.clone()).unwrap(); + let new_owner_id = 2; + + let mut update_project_request = Request::new(UpdateProjectRequest { + id: project_id, + name: Some(new_project_name.clone()), + components_info: Some(new_components_info_non_json.clone()), + owner_id: Some(new_owner_id), + }); + + update_project_request.metadata_mut().insert( + "authorization", + metadata::MetadataValue::from_str("Bearer access_token").unwrap(), + ); + + update_project_request.metadata_mut().insert( + "uid", + metadata::MetadataValue::from_str(user_id.to_string().as_str()).unwrap(), + ); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(project_id)) + .returning(move |_| { + Ok(Some(project::Model { + id: project_id, + name: "old_name".to_owned(), + components_info: serde_json::to_value("{\"old_components\":1}").unwrap(), + owner_id: user_id, + })) + }); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(project_id)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + user_id, + project_id, + role: "Editor".to_string(), + })) + }); + + mock_contexts + .session_context_mock + .expect_get_by_token() + .with( + predicate::eq(TokenType::AccessToken), + predicate::eq("access_token".to_string()), + ) + .returning(move |_, _| { + Ok(Some(session::Model { + id: 1, + refresh_token: "refresh_token".to_string(), + access_token: "access_token".to_string(), + updated_at: Default::default(), + user_id, + })) + }); + + mock_contexts + .project_context_mock + .expect_update() + .returning(move |_| { + Ok(project::Model { + id: project_id, + name: new_project_name.clone(), + components_info: new_components_info.clone(), + owner_id: new_owner_id, + }) + }); + + mock_contexts + .in_use_context_mock + .expect_get_by_id() + .returning(move |_| { + Ok(Some(in_use::Model { + project_id, + session_id: 1, + latest_activity: Utc::now().naive_utc(), + })) + }); + + mock_contexts + .in_use_context_mock + .expect_update() + .returning(move |_| { + Ok(in_use::Model { + project_id: 1, + session_id: 1, + latest_activity: Utc::now().naive_utc(), + }) + }); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.update_project(update_project_request).await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn update_owner_not_owner_returns_err() { + let mut mock_contexts = get_mock_contexts(); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(project::Model { + id: 1, + name: Default::default(), + components_info: Default::default(), + owner_id: 2, + })) + }); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + user_id: 1, + project_id: 1, + role: "Editor".to_owned(), + })) + }); + + mock_contexts + .session_context_mock + .expect_get_by_token() + .with( + predicate::eq(TokenType::AccessToken), + predicate::eq("access_token".to_string()), + ) + .returning(move |_, _| { + Ok(Some(session::Model { + id: 1, + refresh_token: "refresh_token".to_string(), + access_token: "access_token".to_string(), + updated_at: Default::default(), + user_id: 1, + })) + }); + + mock_contexts + .in_use_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(in_use::Model { + session_id: 1, + latest_activity: Default::default(), + project_id: 1, + })) + }); + + mock_contexts + .in_use_context_mock + .expect_update() + .returning(move |_| { + Ok(in_use::Model { + session_id: 1, + latest_activity: Default::default(), + project_id: 1, + }) + }); + + let mut request = Request::new(UpdateProjectRequest { + id: 1, + name: None, + components_info: None, + owner_id: Some(1), + }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + request.metadata_mut().insert( + "authorization", + metadata::MetadataValue::from_str("access_token").unwrap(), + ); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.update_project(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::PermissionDenied); + } + + #[tokio::test] + async fn update_no_in_use_returns_err() { + let mut mock_contexts = get_mock_contexts(); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(project::Model { + id: 1, + name: Default::default(), + components_info: Default::default(), + owner_id: 1, + })) + }); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + user_id: 1, + project_id: 1, + role: "Editor".to_owned(), + })) + }); + + mock_contexts + .session_context_mock + .expect_get_by_token() + .with( + predicate::eq(TokenType::AccessToken), + predicate::eq("access_token".to_string()), + ) + .returning(move |_, _| { + Ok(Some(session::Model { + id: 1, + refresh_token: "refresh_token".to_string(), + access_token: "access_token".to_string(), + updated_at: Default::default(), + user_id: 1, + })) + }); + + mock_contexts + .in_use_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(in_use::Model { + session_id: 2, + latest_activity: Utc::now().naive_utc(), + project_id: 1, + })) + }); + + let mut request = Request::new(UpdateProjectRequest { + id: 1, + name: None, + components_info: None, + owner_id: None, + }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + request.metadata_mut().insert( + "authorization", + metadata::MetadataValue::from_str("access_token").unwrap(), + ); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.update_project(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::FailedPrecondition); + } + + #[tokio::test] + async fn update_no_access_returns_err() { + let mut mock_contexts = get_mock_contexts(); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(project::Model { + id: 1, + name: Default::default(), + components_info: Default::default(), + owner_id: 1, + })) + }); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| Ok(None)); + + let mut request = Request::new(UpdateProjectRequest { + id: 1, + name: None, + components_info: None, + owner_id: None, + }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.update_project(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::PermissionDenied); + } + + #[tokio::test] + async fn update_incorrect_role_returns_err() { + let mut mock_contexts = get_mock_contexts(); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(project::Model { + id: 1, + name: Default::default(), + components_info: Default::default(), + owner_id: 1, + })) + }); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + user_id: 1, + project_id: 1, + role: "Viewer".to_owned(), + })) + }); + + let mut request = Request::new(UpdateProjectRequest { + id: 1, + name: None, + components_info: None, + owner_id: None, + }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.update_project(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::PermissionDenied); + } + + #[tokio::test] + async fn update_no_session_returns_err() { + let mut mock_contexts = get_mock_contexts(); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(project::Model { + id: 1, + name: Default::default(), + components_info: Default::default(), + owner_id: 1, + })) + }); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + user_id: 1, + project_id: 1, + role: "Editor".to_owned(), + })) + }); + + mock_contexts + .session_context_mock + .expect_get_by_token() + .with( + predicate::eq(TokenType::AccessToken), + predicate::eq("access_token".to_string()), + ) + .returning(move |_, _| Ok(None)); + + let mut request = Request::new(UpdateProjectRequest { + id: 1, + name: None, + components_info: None, + owner_id: None, + }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + request.metadata_mut().insert( + "authorization", + metadata::MetadataValue::from_str("access_token").unwrap(), + ); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.update_project(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::Unauthenticated); + } + + #[tokio::test] + async fn update_no_project_returns_err() { + let mut mock_contexts = get_mock_contexts(); + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(2)) + .returning(move |_| Ok(None)); + + let mut request = Request::new(UpdateProjectRequest { + id: 2, + name: None, + components_info: None, + owner_id: None, + }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let project_logic = ProjectController::new(contexts); + + let res = project_logic.update_project(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::NotFound); + } +} diff --git a/src/controllers/query_controller.rs b/src/controllers/query_controller.rs new file mode 100644 index 0000000..be22139 --- /dev/null +++ b/src/controllers/query_controller.rs @@ -0,0 +1,913 @@ +use crate::api::auth::RequestExt; +use crate::api::server::protobuf::{ + CreateQueryRequest, DeleteQueryRequest, QueryRequest, SendQueryRequest, SendQueryResponse, + UpdateQueryRequest, +}; +use crate::contexts::ContextCollection; +use crate::entities::query; +use crate::services::ServiceCollection; +use async_trait::async_trait; +use tonic::{Request, Response, Status}; + +#[async_trait] +pub trait QueryControllerTrait: Send + Sync { + /// Creates a query in the contexts + /// # Errors + /// Returns an error if the contexts context fails to create the query or + async fn create_query( + &self, + request: Request, + ) -> Result, Status>; + + /// Endpoint for updating a query record. + /// # Errors + /// Errors on non existent entity, parsing error or invalid rights + async fn update_query( + &self, + request: Request, + ) -> Result, Status>; + + /// Deletes a query record in the contexts. + /// # Errors + /// Returns an error if the provided query_id is not found in the contexts. + async fn delete_query( + &self, + request: Request, + ) -> Result, Status>; + + /// Sends a query to be run on Reveaal. + /// After query is run the result is stored in the contexts. + /// + /// Returns the response that is received from Reveaal. + async fn send_query( + &self, + request: Request, + ) -> Result, Status>; +} + +pub struct QueryController { + contexts: ContextCollection, + services: ServiceCollection, +} + +impl QueryController { + pub fn new(contexts: ContextCollection, services: ServiceCollection) -> Self { + Self { contexts, services } + } +} + +#[async_trait] +impl QueryControllerTrait for QueryController { + async fn create_query( + &self, + request: Request, + ) -> Result, Status> { + let query_request = request.get_ref(); + + let access = self + .contexts + .access_context + .get_access_by_uid_and_project_id( + request + .uid() + .map_err(|err| { + Status::invalid_argument(format!( + "could not stringify user id in request metadata, inner error {}", + err + )) + })? + .ok_or(Status::invalid_argument( + "failed to get user id from request metadata", + ))?, + query_request.project_id, + ) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::permission_denied("User does not have access to project"))?; + + if access.role != "Editor" { + return Err(Status::permission_denied( + "Role does not have permission to create query", + )); + } + + let query = query::Model { + id: Default::default(), + string: query_request.string.to_string(), + result: Default::default(), + outdated: Default::default(), + project_id: query_request.project_id, + }; + + self.contexts + .query_context + .create(query) + .await + .map(|_| Response::new(())) + .map_err(|e| Status::internal(e.to_string())) + } + + async fn update_query( + &self, + request: Request, + ) -> Result, Status> { + let message = request.get_ref().clone(); + + let old_query_res = self + .contexts + .query_context + .get_by_id(message.id) + .await + .map_err(|err| Status::internal(err.to_string()))?; + + let old_query = old_query_res.ok_or(Status::not_found("Query not found"))?; + + let access = self + .contexts + .access_context + .get_access_by_uid_and_project_id( + request + .uid() + .map_err(|err| { + Status::internal(format!( + "could not stringify user id in request metadata, internal error {}", + err + )) + })? + .ok_or(Status::internal( + "failed to get user id from request metadata", + ))?, + old_query.project_id, + ) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::permission_denied("User does not have access to project"))?; + + if access.role != "Editor" { + return Err(Status::permission_denied( + "Role does not have permission to update query", + )); + } + + let query = query::Model { + id: message.id, + project_id: Default::default(), + string: message.string, + result: old_query.result, + outdated: old_query.outdated, + }; + + self.contexts + .query_context + .update(query) + .await + .map(|_| Response::new(())) + .map_err(|e| Status::internal(e.to_string())) + } + + async fn delete_query( + &self, + request: Request, + ) -> Result, Status> { + let message = request.get_ref(); + + let query = self + .contexts + .query_context + .get_by_id(message.id) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::not_found("Query not found"))?; + + let access = self + .contexts + .access_context + .get_access_by_uid_and_project_id( + request + .uid() + .map_err(|err| { + Status::internal(format!( + "could not stringify user id in request metadata, internal error {}", + err + )) + })? + .ok_or(Status::internal( + "failed to get user id from request metadata", + ))?, + query.project_id, + ) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::permission_denied("User does not have access to project"))?; + + if access.role != "Editor" { + return Err(Status::permission_denied( + "Role does not have permission to update query", + )); + } + + match self.contexts.query_context.delete(message.id).await { + Ok(_) => Ok(Response::new(())), + Err(error) => match error { + sea_orm::DbErr::RecordNotFound(message) => Err(Status::not_found(message)), + _ => Err(Status::internal(error.to_string())), + }, + } + } + + async fn send_query( + &self, + request: Request, + ) -> Result, Status> { + let message = request.get_ref(); + + let uid = request + .uid() + .map_err(|err| { + Status::internal(format!( + "could not stringify user id in request metadata, internal error {}", + err + )) + })? + .ok_or(Status::internal( + "failed to get user id from request metadata", + ))?; + + // Verify user access + self.contexts + .access_context + .get_access_by_uid_and_project_id(uid, message.project_id) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::permission_denied("User does not have access to project"))?; + + // Get project from contexts + let project = self + .contexts + .project_context + .get_by_id(message.project_id) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::not_found("Model not found"))?; + + // Get query from contexts + let query = self + .contexts + .query_context + .get_by_id(message.id) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::not_found("Query not found"))?; + + // Construct query request to send to Reveaal + let query_request = Request::new(QueryRequest { + user_id: uid, + query_id: message.id, + query: query.string.clone(), + components_info: serde_json::from_value(project.components_info).map_err(|err| { + Status::internal(format!( + "error parsing query result, internal error: {}", + err + )) + })?, + settings: Default::default(), //TODO + }); + + // Run query on Reveaal + let query_result = self + .services + .reveaal_service + .send_query(query_request) + .await?; + + // Update query result in contexts + self.contexts + .query_context + .update(query::Model { + id: query.id, + string: query.string.clone(), + result: Some( + serde_json::to_value( + query_result + .get_ref() + .result + .clone() + .ok_or(Status::internal("failed to get query result"))?, //TODO better error message ? + ) + .map_err(|err| { + Status::internal(format!( + "error parsing query result, internal error: {}", + err + )) + })?, + ), + outdated: false, + project_id: query.project_id, + }) + .await + .map_err(|err| Status::internal(err.to_string()))?; + + Ok(Response::new(SendQueryResponse { + response: Some(query_result.into_inner()), + })) + } +} + +#[cfg(test)] +mod tests { + use super::super::helpers::{ + disguise_context_mocks, disguise_service_mocks, get_mock_contexts, get_mock_services, + }; + use crate::api::server::protobuf::query_response::{self, Result}; + use crate::api::server::protobuf::{ + CreateQueryRequest, DeleteQueryRequest, QueryResponse, SendQueryRequest, UpdateQueryRequest, + }; + use crate::controllers::QueryController; + use crate::controllers::QueryControllerTrait; + use crate::entities::{access, project, query}; + use mockall::predicate; + use sea_orm::DbErr; + use std::str::FromStr; + use tonic::{metadata, Code, Request, Response}; + + #[tokio::test] + async fn create_invalid_query_returns_err() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let query = query::Model { + id: Default::default(), + string: "".to_string(), + result: Default::default(), + project_id: 1, + outdated: Default::default(), + }; + + let access = access::Model { + id: Default::default(), + role: "Editor".to_string(), + project_id: 1, + user_id: 1, + }; + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| Ok(Some(access.clone()))); + + mock_contexts + .query_context_mock + .expect_create() + .with(predicate::eq(query.clone())) + .returning(move |_| Err(DbErr::RecordNotInserted)); + + let mut request = Request::new(CreateQueryRequest { + string: "".to_string(), + project_id: 1, + }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let query_logic = QueryController::new(contexts, services); + + let res = query_logic.create_query(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::Internal); + } + + #[tokio::test] + async fn create_query_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let query = query::Model { + id: Default::default(), + string: "".to_string(), + result: Default::default(), + project_id: 1, + outdated: Default::default(), + }; + + let access = access::Model { + id: Default::default(), + role: "Editor".to_string(), + project_id: 1, + user_id: 1, + }; + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| Ok(Some(access.clone()))); + + mock_contexts + .query_context_mock + .expect_create() + .with(predicate::eq(query.clone())) + .returning(move |_| Ok(query.clone())); + + let mut request = Request::new(CreateQueryRequest { + string: "".to_string(), + project_id: 1, + }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let query_logic = QueryController::new(contexts, services); + + let res = query_logic.create_query(request).await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn update_invalid_query_returns_err() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let old_query = query::Model { + id: 1, + string: "".to_string(), + result: None, + project_id: Default::default(), + outdated: true, + }; + + let query = query::Model { + string: "updated".to_string(), + ..old_query.clone() + }; + + let access = access::Model { + id: 1, + role: "Editor".to_string(), + project_id: Default::default(), + user_id: 1, + }; + + mock_contexts + .query_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| Ok(Some(old_query.clone()))); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(0)) + .returning(move |_, _| Ok(Some(access.clone()))); + + mock_contexts + .query_context_mock + .expect_update() + .with(predicate::eq(query.clone())) + .returning(move |_| Err(DbErr::RecordNotUpdated)); + + let mut request = Request::new(UpdateQueryRequest { + id: 1, + string: "updated".to_string(), + }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let query_logic = QueryController::new(contexts, services); + + let res = query_logic.update_query(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::Internal); + } + + #[tokio::test] + async fn update_query_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let old_query = query::Model { + id: 1, + string: "".to_string(), + result: None, + project_id: Default::default(), + outdated: true, + }; + + let query = query::Model { + string: "updated".to_string(), + ..old_query.clone() + }; + + let access = access::Model { + id: Default::default(), + role: "Editor".to_string(), + project_id: Default::default(), + user_id: 1, + }; + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(0)) + .returning(move |_, _| Ok(Some(access.clone()))); + + mock_contexts + .query_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| Ok(Some(old_query.clone()))); + + mock_contexts + .query_context_mock + .expect_update() + .with(predicate::eq(query.clone())) + .returning(move |_| Ok(query.clone())); + + let mut request = Request::new(UpdateQueryRequest { + id: 1, + string: "updated".to_string(), + }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let query_logic = QueryController::new(contexts, services); + + let res = query_logic.update_query(request).await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn delete_invalid_query_returns_err() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let access = access::Model { + id: Default::default(), + role: "Editor".to_string(), + project_id: Default::default(), + user_id: 1, + }; + + let query = query::Model { + id: 1, + string: "".to_string(), + result: Default::default(), + project_id: Default::default(), + outdated: Default::default(), + }; + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(0)) + .returning(move |_, _| Ok(Some(access.clone()))); + + mock_contexts + .query_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| Ok(Some(query.clone()))); + + mock_contexts + .query_context_mock + .expect_delete() + .with(predicate::eq(1)) + .returning(move |_| Err(DbErr::RecordNotFound("".to_string()))); + + let mut request = Request::new(DeleteQueryRequest { id: 1 }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let query_logic = QueryController::new(contexts, services); + + let res = query_logic.delete_query(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::NotFound); + } + + #[tokio::test] + async fn delete_query_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let query = query::Model { + id: 1, + string: "".to_string(), + result: Default::default(), + project_id: Default::default(), + outdated: Default::default(), + }; + + let query_clone = query.clone(); + + let access = access::Model { + id: Default::default(), + role: "Editor".to_string(), + project_id: Default::default(), + user_id: 1, + }; + + mock_contexts + .query_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| Ok(Some(query.clone()))); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(0)) + .returning(move |_, _| Ok(Some(access.clone()))); + + mock_contexts + .query_context_mock + .expect_delete() + .with(predicate::eq(1)) + .returning(move |_| Ok(query_clone.clone())); + + let mut request = Request::new(DeleteQueryRequest { id: 1 }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let query_logic = QueryController::new(contexts, services); + + let res = query_logic.delete_query(request).await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn create_query_invalid_role_returns_err() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let query = query::Model { + id: 1, + string: "".to_string(), + result: Default::default(), + project_id: Default::default(), + outdated: Default::default(), + }; + + let access = access::Model { + id: Default::default(), + role: "Viewer".to_string(), + project_id: Default::default(), + user_id: 1, + }; + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| Ok(Some(access.clone()))); + + mock_contexts + .query_context_mock + .expect_create() + .with(predicate::eq(query.clone())) + .returning(move |_| Ok(query.clone())); + + let mut request = Request::new(CreateQueryRequest { + string: "".to_string(), + project_id: 1, + }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let query_logic = QueryController::new(contexts, services); + + let res = query_logic.create_query(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::PermissionDenied); + } + + #[tokio::test] + async fn delete_query_invalid_role_returns_err() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let query = query::Model { + id: 1, + string: "".to_string(), + result: Default::default(), + project_id: Default::default(), + outdated: Default::default(), + }; + + let query_clone = query.clone(); + + let access = access::Model { + id: Default::default(), + role: "Viewer".to_string(), + project_id: Default::default(), + user_id: 1, + }; + + mock_contexts + .query_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| Ok(Some(query.clone()))); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(0)) + .returning(move |_, _| Ok(Some(access.clone()))); + + mock_contexts + .query_context_mock + .expect_delete() + .with(predicate::eq(1)) + .returning(move |_| Ok(query_clone.clone())); + + let mut request = Request::new(DeleteQueryRequest { id: 1 }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let query_logic = QueryController::new(contexts, services); + + let res = query_logic.delete_query(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::PermissionDenied); + } + + #[tokio::test] + async fn update_query_invalid_role_returns_err() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let old_query = query::Model { + id: 1, + string: "".to_string(), + result: None, + project_id: Default::default(), + outdated: true, + }; + + let query = query::Model { + string: "updated".to_string(), + ..old_query.clone() + }; + + let access = access::Model { + id: Default::default(), + role: "Viewer".to_string(), + project_id: Default::default(), + user_id: 1, + }; + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(0)) + .returning(move |_, _| Ok(Some(access.clone()))); + + mock_contexts + .query_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| Ok(Some(old_query.clone()))); + + mock_contexts + .query_context_mock + .expect_update() + .with(predicate::eq(query.clone())) + .returning(move |_| Ok(query.clone())); + + let mut request = Request::new(UpdateQueryRequest { + id: 1, + string: "updated".to_string(), + }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let query_logic = QueryController::new(contexts, services); + + let res = query_logic.update_query(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::PermissionDenied); + } + + #[tokio::test] + async fn send_query_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + let mut mock_services = get_mock_services(); + + let query = query::Model { + id: Default::default(), + string: "".to_string(), + result: Default::default(), + project_id: Default::default(), + outdated: Default::default(), + }; + + let access = access::Model { + id: Default::default(), + role: "Editor".to_string(), + project_id: Default::default(), + user_id: 1, + }; + + let project = project::Model { + id: Default::default(), + name: "project".to_string(), + components_info: Default::default(), + owner_id: 0, + }; + + let query_response = QueryResponse { + query_id: Default::default(), + info: Default::default(), + result: Some(Result::Success(query_response::Success {})), + }; + + let updated_query = query::Model { + result: Some(serde_json::to_value(query_response.clone().result).unwrap()), + ..query.clone() + }; + + mock_contexts + .project_context_mock + .expect_get_by_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(Some(project.clone()))); + + mock_contexts + .access_context_mock + .expect_get_access_by_uid_and_project_id() + .with(predicate::eq(1), predicate::eq(0)) + .returning(move |_, _| Ok(Some(access.clone()))); + + mock_contexts + .query_context_mock + .expect_get_by_id() + .with(predicate::eq(0)) + .returning(move |_| Ok(Some(query.clone()))); + + mock_services + .reveaal_service_mock + .expect_send_query() + .returning(move |_| Ok(Response::new(query_response.clone()))); + + mock_contexts + .query_context_mock + .expect_update() + .with(predicate::eq(updated_query.clone())) + .returning(move |_| Ok(updated_query.clone())); + + let mut request = Request::new(SendQueryRequest { + id: Default::default(), + project_id: Default::default(), + }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let query_logic = QueryController::new(contexts, services); + + let res = query_logic.send_query(request).await; + + assert!(res.is_ok()); + } +} diff --git a/src/controllers/controller_impls/reveaal_controller.rs b/src/controllers/reveaal_controller.rs similarity index 86% rename from src/controllers/controller_impls/reveaal_controller.rs rename to src/controllers/reveaal_controller.rs index d7fcfe5..c6f0f3e 100644 --- a/src/controllers/controller_impls/reveaal_controller.rs +++ b/src/controllers/reveaal_controller.rs @@ -1,9 +1,8 @@ -use crate::api::server::protobuf::ecdar_backend_server::EcdarBackend; use crate::api::server::protobuf::{ - QueryRequest, QueryResponse, SimulationStartRequest, SimulationStepRequest, - SimulationStepResponse, UserTokenResponse, + ecdar_backend_server::EcdarBackend, QueryRequest, QueryResponse, SimulationStartRequest, + SimulationStepRequest, SimulationStepResponse, UserTokenResponse, }; -use crate::services::service_collection::ServiceCollection; +use crate::services::ServiceCollection; use async_trait::async_trait; use tonic::{Request, Response, Status}; diff --git a/src/controllers/session_controller.rs b/src/controllers/session_controller.rs new file mode 100644 index 0000000..f84df38 --- /dev/null +++ b/src/controllers/session_controller.rs @@ -0,0 +1,551 @@ +use crate::api::auth::{RequestExt, Token, TokenError, TokenType}; +use crate::api::server::protobuf::get_auth_token_request::user_credentials::User; +use crate::api::server::protobuf::{ + get_auth_token_request::UserCredentials, GetAuthTokenRequest, GetAuthTokenResponse, +}; +use crate::contexts::ContextCollection; +use crate::entities::{session, user}; +use crate::services::ServiceCollection; +use async_trait::async_trait; +use sea_orm::DbErr; +use tonic::{Request, Response, Status}; + +#[async_trait] +pub trait SessionControllerTrait: Send + Sync { + /// Deletes the requester's session, found by their access token. + /// + /// Returns the response that is received from Reveaal. + async fn delete_session(&self, _request: Request<()>) -> Result, Status>; + + /// This method is used to get a new access and refresh token for a user. + /// + /// # Errors + /// This function will return an error if the user does not exist in the contexts, + /// if the password in the request does not match the user's password, + /// or if no user is provided in the request. + async fn get_auth_token( + &self, + request: Request, + ) -> Result, Status>; +} + +pub struct SessionController { + contexts: ContextCollection, + services: ServiceCollection, +} + +impl SessionController { + pub fn new(contexts: ContextCollection, services: ServiceCollection) -> Self { + Self { contexts, services } + } + + async fn user_from_user_credentials( + &self, + user_credentials: UserCredentials, + ) -> Result, DbErr> { + let m = user_credentials.user.map(|u| match u { + User::Username(username) => self.contexts.user_context.get_by_username(username), + User::Email(email) => self.contexts.user_context.get_by_email(email), + }); + match m { + Some(l) => Ok(l.await?), + None => Ok(None), + } + } + + /// Updates the session given by refresh token in the contexts. + /// Returns the new access and refresh token i.e. a tuple `(Token, Token)` where the 0th element is the access token and the 1st element refresh token. + pub async fn update_session(&self, refresh_token: String) -> Result<(Token, Token), Status> { + let session = match self + .contexts + .session_context + .get_by_token(TokenType::RefreshToken, refresh_token) + .await + { + Ok(Some(session)) => session, + Ok(None) => { + return Err(Status::unauthenticated( + "No session found with given refresh token", + )); + } + Err(err) => return Err(Status::internal(err.to_string())), + }; + + let uid = session.user_id.to_string(); + + let access_token = Token::access(&uid)?; + let refresh_token = Token::refresh(&uid)?; + + self.contexts + .session_context + .update(session::Model { + id: session.id, + access_token: access_token.to_string(), + refresh_token: refresh_token.to_string(), + updated_at: Default::default(), + user_id: session.user_id, + }) + .await + .map_err(|err| { + Status::internal(format!( + "a database error occurred, internal message: {}", + err + )) + })?; + + Ok((access_token, refresh_token)) + } +} + +#[async_trait] +impl SessionControllerTrait for SessionController { + async fn delete_session(&self, request: Request<()>) -> Result, Status> { + let access_token = request + .token_string() + .map_err(|err| { + Status::internal(format!( + "failed to convert token to string, internal error: {}", + err + )) + })? + .ok_or(Status::unauthenticated("No access token provided"))?; + + self.contexts + .session_context + .delete_by_token(TokenType::AccessToken, access_token) + .await + .map(|_| Response::new(())) + .map_err(|e| Status::internal(e.to_string())) + } + + async fn get_auth_token( + &self, + request: Request, + ) -> Result, Status> { + let message = request.get_ref().clone(); + + let (access_token, refresh_token) = match message.user_credentials { + None => { + let refresh_token = Token::from_str( + TokenType::RefreshToken, + request + .token_str() + .map_err(|err| { + Status::internal(format!( + "failed to convert token to string, internal error: {}", + err + )) + })? + .ok_or(Status::unauthenticated("No refresh token provided"))?, + ); + + // Validate refresh token + match refresh_token.validate() { + Ok(_) => (), + Err(TokenError::ExpiredSignature) => { + // Delete session if expired + let _ = self + .contexts + .session_context + .delete_by_token(TokenType::RefreshToken, refresh_token.to_string()) + .await; + + return Err(Status::from(TokenError::ExpiredSignature)); + } + Err(err) => return Err(Status::from(err)), + } + + self.update_session(refresh_token.to_string()).await? + } + Some(user_credentials) => { + let input_password = user_credentials.password.clone(); + let user = self + .user_from_user_credentials(user_credentials) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::unauthenticated("Wrong username or password"))?; + + // Check if password in request matches users password + if !self + .services + .hashing_service + .verify_password(input_password, user.password.as_str()) + .map_err(|__err| Status::internal("failed to verify password"))? + { + return Err(Status::unauthenticated("Wrong username or password")); + } + + let uid = user.id.to_string(); + + let access_token = Token::access(&uid)?; + let refresh_token = Token::refresh(&uid)?; + + self.contexts + .session_context + .create(session::Model { + id: Default::default(), + access_token: access_token.to_string(), + refresh_token: refresh_token.to_string(), + updated_at: Default::default(), + user_id: uid.parse().map_err(|err| { + Status::internal(format!( + "failed to parse user id, internal error: {}", + err + )) + })?, + }) + .await + .map_err(|err| Status::internal(err.to_string()))?; + + (access_token, refresh_token) + } + }; + + Ok(Response::new(GetAuthTokenResponse { + access_token: access_token.to_string(), + refresh_token: refresh_token.to_string(), + })) + } +} + +#[cfg(test)] +mod tests { + use mockall::predicate; + use std::env; + use std::str::FromStr; + + use super::super::helpers::{ + disguise_context_mocks, disguise_service_mocks, get_mock_contexts, get_mock_services, + }; + use crate::entities::{session, user}; + + use crate::api::auth::{Token, TokenType}; + use crate::api::server::protobuf::get_auth_token_request::{user_credentials, UserCredentials}; + use crate::api::server::protobuf::GetAuthTokenRequest; + use crate::controllers::SessionController; + use crate::controllers::SessionControllerTrait; + use sea_orm::DbErr; + use tonic::{metadata, Code, Request}; + + #[tokio::test] + async fn update_session_no_session_exists_creates_session_returns_err() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + mock_contexts + .session_context_mock + .expect_get_by_token() + .returning(move |_, _| Ok(None)); + + mock_contexts + .session_context_mock + .expect_update() + .returning(move |_| Err(DbErr::RecordNotInserted)); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let session_logic = SessionController::new(contexts, services); + + let res = session_logic + .update_session("old_refresh_token".to_string()) + .await; + + assert_eq!(res.unwrap_err().code(), Code::Unauthenticated); + } + + #[tokio::test] + async fn update_session_returns_new_tokens_when_session_exists() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let refresh_token = "refresh_token".to_string(); + + mock_contexts + .session_context_mock + .expect_get_by_token() + .times(1) + .returning(|_, _| { + Ok(Some(session::Model { + id: 0, + access_token: "old_access_token".to_string(), + refresh_token: "old_refresh_token".to_string(), + updated_at: Default::default(), + user_id: 1, + })) + }); + + mock_contexts + .session_context_mock + .expect_update() + .times(1) + .returning(move |_| { + Ok(session::Model { + id: 0, + refresh_token: "refresh_token".to_string(), + access_token: "access_token".to_string(), + updated_at: Default::default(), + user_id: 1, + }) + }); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let session_logic = SessionController::new(contexts, services); + + let result = session_logic.update_session(refresh_token).await; + + assert!(result.is_ok()); + let (access_token, refresh_token) = result.unwrap(); + assert_ne!(access_token.to_string(), "old_access_token"); + assert_ne!(refresh_token.to_string(), "old_refresh_token"); + } + + #[tokio::test] + async fn update_session_returns_error_when_no_session_found() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let refresh_token = "refresh_token".to_string(); + + mock_contexts + .session_context_mock + .expect_get_by_token() + .times(1) + .returning(|_, _| Ok(None)); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let session_logic = SessionController::new(contexts, services); + + let result = session_logic.update_session(refresh_token).await; + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().code(), Code::Unauthenticated); + } + + #[tokio::test] + async fn update_session_returns_error_when_database_error_occurs() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let refresh_token = "refresh_token".to_string(); + + mock_contexts + .session_context_mock + .expect_get_by_token() + .times(1) + .returning(|_, _| Err(DbErr::RecordNotFound("".to_string()))); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let session_logic = SessionController::new(contexts, services); + + let result = session_logic.update_session(refresh_token).await; + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().code(), Code::Internal); + } + + #[tokio::test] + async fn get_auth_token_from_credentials_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + let mut mock_services = get_mock_services(); + + let request = GetAuthTokenRequest { + user_credentials: Option::from(UserCredentials { + password: "Password123".to_string(), + user: Option::from(user_credentials::User::Username("Example".to_string())), + }), + }; + + mock_contexts + .user_context_mock + .expect_get_by_username() + .returning(move |_| { + Ok(Option::from(user::Model { + id: 1, + email: "".to_string(), + username: "Example".to_string(), + password: "".to_string(), + })) + }); + + mock_services + .hashing_service_mock + .expect_verify_password() + .returning(move |_, _| Ok(true)); + + mock_contexts + .session_context_mock + .expect_create() + .returning(move |_| { + Ok(session::Model { + id: 0, + refresh_token: "refresh_token".to_string(), + access_token: "access_token".to_string(), + updated_at: Default::default(), + user_id: 1, + }) + }); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let session_logic = SessionController::new(contexts, services); + + let response = session_logic + .get_auth_token(Request::new(request)) + .await + .unwrap(); + + assert!(!response.get_ref().refresh_token.is_empty()); + assert!(!response.get_ref().access_token.is_empty()); + } + + #[tokio::test] + async fn get_auth_token_from_token_returns_ok() { + env::set_var("REFRESH_TOKEN_HS512_SECRET", "refresh_secret"); + + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let mut request = Request::new(GetAuthTokenRequest { + user_credentials: None, + }); + + let refresh_token = Token::new(TokenType::RefreshToken, "1").unwrap(); + + request.metadata_mut().insert( + "authorization", + metadata::MetadataValue::from_str(format!("Bearer {}", refresh_token).as_str()) + .unwrap(), + ); + + mock_contexts + .session_context_mock + .expect_get_by_token() + .returning(move |_, _| { + Ok(Option::from(session::Model { + id: 0, + refresh_token: "refresh_token".to_string(), + access_token: "access_token".to_string(), + updated_at: Default::default(), + user_id: 1, + })) + }); + + mock_contexts + .session_context_mock + .expect_update() + .returning(move |_| { + Ok(session::Model { + id: 0, + refresh_token: "refresh_token".to_string(), + access_token: "access_token".to_string(), + updated_at: Default::default(), + user_id: 1, + }) + }); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let session_logic = SessionController::new(contexts, services); + + let response = session_logic.get_auth_token(request).await.unwrap(); + + assert!(!response.get_ref().refresh_token.is_empty()); + assert!(!response.get_ref().access_token.is_empty()); + } + + #[tokio::test] + async fn get_auth_token_from_invalid_token_returns_err() { + let mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let mut request = Request::new(GetAuthTokenRequest { + user_credentials: None, + }); + + request.metadata_mut().insert( + "authorization", + metadata::MetadataValue::from_str("invalid token").unwrap(), + ); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let session_logic = SessionController::new(contexts, services); + + let response = session_logic.get_auth_token(request).await; + + assert_eq!(response.unwrap_err().code(), Code::Unauthenticated); + } + + #[tokio::test] + async fn delete_session_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + mock_contexts + .session_context_mock + .expect_delete_by_token() + .with( + predicate::eq(TokenType::AccessToken), + predicate::eq("test_token".to_string()), + ) + .returning(move |_, _| { + Ok(session::Model { + id: 1, + refresh_token: Default::default(), + access_token: "test_token".to_string(), + updated_at: Default::default(), + user_id: Default::default(), + }) + }); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let session_logic = SessionController::new(contexts, services); + + let mut request = Request::new(()); + request.metadata_mut().insert( + "authorization", + metadata::MetadataValue::from_str("Bearer test_token").unwrap(), + ); + + let res = session_logic.delete_session(request).await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn delete_session_no_session_returns_err() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + mock_contexts + .session_context_mock + .expect_delete_by_token() + .with( + predicate::eq(TokenType::AccessToken), + predicate::eq("test_token".to_string()), + ) + .returning(move |_, _| { + Err(DbErr::RecordNotFound( + "No session found with the provided access token".to_string(), + )) + }); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let session_logic = SessionController::new(contexts, services); + + let mut request = Request::new(()); + request.metadata_mut().insert( + "authorization", + metadata::MetadataValue::from_str("Bearer test_token").unwrap(), + ); + + let res = session_logic.delete_session(request).await; + + assert_eq!(res.unwrap_err().code(), Code::Internal); + } +} diff --git a/src/controllers/user_controller.rs b/src/controllers/user_controller.rs new file mode 100644 index 0000000..02cd041 --- /dev/null +++ b/src/controllers/user_controller.rs @@ -0,0 +1,635 @@ +use crate::api::auth::RequestExt; +use crate::api::server::protobuf::{ + get_users_response::UserInfo, CreateUserRequest, GetUsersRequest, GetUsersResponse, + UpdateUserRequest, +}; +use crate::contexts::ContextCollection; +use crate::entities::user; +use crate::services::ServiceCollection; +use async_trait::async_trait; +use regex::Regex; +use sea_orm::SqlErr; +use tonic::{Request, Response, Status}; + +#[async_trait] +pub trait UserControllerTrait: Send + Sync { + async fn create_user( + &self, + request: Request, + ) -> Result, Status>; + async fn update_user( + &self, + request: Request, + ) -> Result, Status>; + async fn delete_user(&self, request: Request<()>) -> Result, Status>; + async fn get_users( + &self, + request: Request, + ) -> Result, Status>; +} + +pub struct UserController { + contexts: ContextCollection, + services: ServiceCollection, +} + +impl UserController { + pub fn new(contexts: ContextCollection, services: ServiceCollection) -> Self { + UserController { contexts, services } + } + + /// Returns true if the given email is a valid format. + fn is_valid_email(&self, email: &str) -> bool { + Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") + .expect("failed to compile regex") + .is_match(email) + } + + /// Returns true if the given username is a valid format, i.e. only contains letters and numbers and a length from 3 to 32. + fn is_valid_username(&self, username: &str) -> bool { + Regex::new(r"^[a-zA-Z0-9_]{3,32}$") + .expect("failed to compile regex") + .is_match(username) + } +} + +#[async_trait] +impl UserControllerTrait for UserController { + async fn create_user( + &self, + request: Request, + ) -> Result, Status> { + let message = request.into_inner().clone(); + + if !self.is_valid_username(message.clone().username.as_str()) { + return Err(Status::invalid_argument("Invalid username")); + } + + if !self.is_valid_email(message.clone().email.as_str()) { + return Err(Status::invalid_argument("Invalid email")); + } + + let hashed_password = self + .services + .hashing_service + .hash_password(message.clone().password) + .map_err(|_err| Status::internal("failed to hash password"))?; + + let user = user::Model { + id: Default::default(), + username: message.clone().username, + password: hashed_password, + email: message.clone().email, + }; + + match self.contexts.user_context.create(user).await { + Ok(_) => Ok(Response::new(())), + Err(e) => match e.sql_err() { + Some(SqlErr::UniqueConstraintViolation(e)) => { + let error_msg = match e.to_lowercase() { + _ if e.contains("username") => "A user with that username already exists", + _ if e.contains("email") => "A user with that email already exists", + _ => "User already exists", + }; + Err(Status::already_exists(error_msg)) + } + _ => Err(Status::internal("Could not create user")), + }, + } + } + + /// Updates a user record in the contexts. + /// # Errors + /// Returns an error if the contexts context fails to update the user or + /// if the uid could not be parsed from the request metadata. + async fn update_user( + &self, + request: Request, + ) -> Result, Status> { + let message = request.get_ref().clone(); + + let uid = request + .uid() + .map_err(|err| { + Status::internal(format!( + "could not stringify user id in request metadata, internal error {}", + err + )) + })? + .ok_or(Status::internal("Could not get uid from request metadata"))?; + + // Get user from contexts + let user = self + .contexts + .user_context + .get_by_id(uid) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::internal("No user found with given uid"))?; + + // Record to be inserted in contexts + let new_user = user::Model { + id: uid, + username: match message.username { + Some(username) => { + if self.is_valid_username(username.as_str()) { + username + } else { + return Err(Status::invalid_argument("Invalid username")); + } + } + None => user.username, + }, + email: match message.email { + Some(email) => { + if self.is_valid_email(email.as_str()) { + email + } else { + return Err(Status::invalid_argument("Invalid email")); + } + } + None => user.email, + }, + password: match message.password { + Some(password) => self + .services + .hashing_service + .hash_password(password) + .map_err(|_err| Status::internal("failed to hash password"))?, + None => user.password, + }, + }; + + // Update user in contexts + match self.contexts.user_context.update(new_user).await { + Ok(_) => Ok(Response::new(())), + Err(error) => Err(Status::internal(error.to_string())), + } + } + + /// Deletes a user from the contexts. + /// # Errors + /// Returns an error if the contexts context fails to delete the user or + /// if the uid could not be parsed from the request metadata. + async fn delete_user(&self, request: Request<()>) -> Result, Status> { + let uid = request + .uid() + .map_err(|err| { + Status::internal(format!( + "could not stringify user id in request metadata, internal error {}", + err + )) + })? + .ok_or(Status::internal("Could not get uid from request metadata"))?; + + // Delete user from contexts + self.contexts + .user_context + .delete(uid) + .await + .map(|_| Response::new(())) + .map_err(|e| Status::internal(e.to_string())) + } + + /// Gets users from the contexts. + /// If no users exist with the given ids, an empty list is returned. + async fn get_users( + &self, + request: Request, + ) -> Result, Status> { + let ids = request.get_ref().ids.clone(); + + let users = self + .contexts + .user_context + .get_by_ids(ids) + .await + .map_err(|err| Status::internal(err.to_string()))?; + + let users_info = users + .into_iter() + .map(|user| UserInfo { + id: user.id, + username: user.username, + }) + .collect::>(); + + Ok(Response::new(GetUsersResponse { users: users_info })) + } +} + +#[cfg(test)] +mod tests { + use super::super::helpers::{ + disguise_context_mocks, disguise_service_mocks, get_mock_contexts, get_mock_services, + }; + use crate::api::server::protobuf::{CreateUserRequest, GetUsersRequest, UpdateUserRequest}; + use crate::controllers::UserController; + use crate::controllers::UserControllerTrait; + use crate::entities::user; + use mockall::predicate; + use sea_orm::DbErr; + use std::str::FromStr; + use tonic::{metadata, Code, Request}; + + #[tokio::test] + async fn delete_user_nonexistent_user_returns_err() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + mock_contexts + .user_context_mock + .expect_delete() + .with(predicate::eq(1)) + .returning(|_| Err(DbErr::RecordNotFound("".into()))); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let user_logic = UserController::new(contexts, services); + + let mut delete_request = Request::new(()); + + // Insert uid into request metadata + delete_request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let delete_response = user_logic.delete_user(delete_request).await.unwrap_err(); + let expected_response_code = Code::Internal; + + assert_eq!(delete_response.code(), expected_response_code); + } + + #[tokio::test] + async fn delete_user_existing_user_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let user = user::Model { + id: 1, + email: "".to_string(), + username: "".to_string(), + password: "".to_string(), + }; + + mock_contexts + .user_context_mock + .expect_delete() + .with(predicate::eq(1)) + .returning(move |_| Ok(user.clone())); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let user_logic = UserController::new(contexts, services); + + let mut delete_request = Request::new(()); + + // Insert uid into request metadata + delete_request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let delete_response = user_logic.delete_user(delete_request).await; + + assert!(delete_response.is_ok()); + } + + #[tokio::test] + async fn create_user_nonexistent_user_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + let mut mock_services = get_mock_services(); + + let password = "Password123".to_string(); + + let user = user::Model { + id: Default::default(), + email: "anders21@student.aau.dk".to_string(), + username: "anders".to_string(), + password: password.clone(), + }; + + let create_user_request = Request::new(CreateUserRequest { + email: "anders21@student.aau.dk".to_string(), + username: "anders".to_string(), + password: password.clone(), + }); + + mock_services + .hashing_service_mock + .expect_hash_password() + .returning(move |_| Ok(password.clone())); + + mock_contexts + .user_context_mock + .expect_create() + .with(predicate::eq(user.clone())) + .returning(move |_| Ok(user.clone())); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let user_logic = UserController::new(contexts, services); + + let create_user_response = user_logic.create_user(create_user_request).await; + assert!(create_user_response.is_ok()); + } + + #[tokio::test] + async fn create_user_duplicate_email_returns_error() { + let mut mock_contexts = get_mock_contexts(); + let mut mock_services = get_mock_services(); + + let password = "Password123".to_string(); + + let user = user::Model { + id: Default::default(), + email: "anders21@student.aau.dk".to_string(), + username: "anders".to_string(), + password: password.clone(), + }; + + let create_user_request = Request::new(CreateUserRequest { + email: "anders21@student.aau.dk".to_string(), + username: "anders".to_string(), + password: password.clone(), + }); + + mock_services + .hashing_service_mock + .expect_hash_password() + .returning(move |_| Ok(password.clone())); + + mock_contexts + .user_context_mock + .expect_create() + .with(predicate::eq(user.clone())) + .returning(move |_| Err(DbErr::RecordNotInserted)); //todo!("Needs to be a SqlError with UniqueConstraintViolation with 'email' in message) + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let user_logic = UserController::new(contexts, services); + + let res = user_logic.create_user(create_user_request).await; + assert_eq!(res.unwrap_err().code(), Code::Internal); //todo!("Needs to be code AlreadyExists when mocked Error is corrected) + } + + #[tokio::test] + async fn create_user_invalid_email_returns_error() { + let mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let user_logic = UserController::new(contexts, services); + + let create_user_request = Request::new(CreateUserRequest { + email: "invalid-email".to_string(), + username: "newuser".to_string(), + password: "123".to_string(), + }); + + let res = user_logic.create_user(create_user_request).await; + assert_eq!(res.unwrap_err().code(), Code::InvalidArgument); + } + + #[tokio::test] + async fn create_user_duplicate_username_returns_error() { + let mut mock_contexts = get_mock_contexts(); + let mut mock_services = get_mock_services(); + + let password = "Password123".to_string(); + + let user = user::Model { + id: Default::default(), + email: "anders21@student.aau.dk".to_string(), + username: "anders".to_string(), + password: password.clone(), + }; + + let create_user_request = Request::new(CreateUserRequest { + email: "anders21@student.aau.dk".to_string(), + username: "anders".to_string(), + password: password.clone(), + }); + + mock_services + .hashing_service_mock + .expect_hash_password() + .returning(move |_| Ok(password.clone())); + + mock_contexts + .user_context_mock + .expect_create() + .with(predicate::eq(user.clone())) + .returning(move |_| Err(DbErr::RecordNotInserted)); //todo!("Needs to be a SqlError with UniqueConstraintViolation with 'username' in message) + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let user_logic = UserController::new(contexts, services); + + let res = user_logic.create_user(create_user_request).await; + assert_eq!(res.unwrap_err().code(), Code::Internal); //todo!("Needs to be code AlreadyExists when mocked Error is corrected) + } + + #[tokio::test] + async fn create_user_invalid_username_returns_error() { + let mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let user_logic = UserController::new(contexts, services); + + let create_user_request = Request::new(CreateUserRequest { + email: "valid@email.com".to_string(), + username: "ØØØØØ".to_string(), + password: "123".to_string(), + }); + + let res = user_logic.create_user(create_user_request).await; + assert_eq!(res.unwrap_err().code(), Code::InvalidArgument); + } + + #[tokio::test] + async fn create_user_valid_request_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + let mut mock_services = get_mock_services(); + + let password = "Password123".to_string(); + + let user = user::Model { + id: Default::default(), + email: "newuser@example.com".to_string(), + username: "newuser".to_string(), + password: password.clone(), + }; + + let create_user_request = Request::new(CreateUserRequest { + email: "newuser@example.com".to_string(), + username: "newuser".to_string(), + password: password.clone(), + }); + + mock_services + .hashing_service_mock + .expect_hash_password() + .returning(move |_| Ok(password.clone())); + + mock_contexts + .user_context_mock + .expect_create() + .with(predicate::eq(user.clone())) + .returning(move |_| Ok(user.clone())); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let user_logic = UserController::new(contexts, services); + + let create_user_response = user_logic.create_user(create_user_request).await; + assert!(create_user_response.is_ok()); + } + + #[tokio::test] + async fn update_user_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + let mut mock_services = get_mock_services(); + + let old_user = user::Model { + id: 1, + email: "olduser@example.com".to_string(), + username: "old_username".to_string(), + password: "StrongPassword123".to_string(), + }; + + let new_user = user::Model { + id: 1, + email: "newuser@example.com".to_string(), + username: "new_username".to_string(), + password: "g76df2gd7hd837g8hjd8723hd8gd823d82d3".to_string(), + }; + + mock_contexts + .user_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| Ok(Some(old_user.clone()))); + + mock_services + .hashing_service_mock + .expect_hash_password() + .with(predicate::eq("StrongPassword123".to_string())) + .returning(move |_| Ok("g76df2gd7hd837g8hjd8723hd8gd823d82d3".to_string())); + + mock_contexts + .user_context_mock + .expect_update() + .with(predicate::eq(new_user.clone())) + .returning(move |_| Ok(new_user.clone())); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let user_logic = UserController::new(contexts, services); + + let mut update_user_request = Request::new(UpdateUserRequest { + email: Some("newuser@example.com".to_string()), + username: Some("new_username".to_string()), + password: Some("StrongPassword123".to_string()), + }); + + update_user_request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let update_user_response = user_logic.update_user(update_user_request).await; + + assert!(update_user_response.is_ok()) + } + + #[tokio::test] + async fn update_user_non_existant_user_returns_err() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + mock_contexts + .user_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| Err(DbErr::RecordNotFound("".to_string()))); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let user_logic = UserController::new(contexts, services); + + let mut update_user_request = Request::new(UpdateUserRequest { + email: Some("new_test@test".to_string()), + username: Some("new_test_user".to_string()), + password: Some("new_test_pass".to_string()), + }); + + update_user_request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let res = user_logic.update_user(update_user_request).await; + + assert_eq!(res.unwrap_err().code(), Code::Internal); + } + + #[tokio::test] + async fn get_users_returns_ok() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let users = vec![ + user::Model { + id: 1, + email: "".to_string(), + username: "".to_string(), + password: "".to_string(), + }, + user::Model { + id: 2, + email: "".to_string(), + username: "".to_string(), + password: "".to_string(), + }, + ]; + + mock_contexts + .user_context_mock + .expect_get_by_ids() + .returning(move |_| Ok(users.clone())); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let user_logic = UserController::new(contexts, services); + + let get_users_request = Request::new(GetUsersRequest { ids: vec![1, 2] }); + + let get_users_response = user_logic.get_users(get_users_request).await.unwrap(); + + assert_eq!(get_users_response.get_ref().users.len(), 2); + } + + #[tokio::test] + async fn get_users_returns_empty_array() { + let mut mock_contexts = get_mock_contexts(); + let mock_services = get_mock_services(); + + let users: Vec = vec![]; + + mock_contexts + .user_context_mock + .expect_get_by_ids() + .returning(move |_| Ok(users.clone())); + + let contexts = disguise_context_mocks(mock_contexts); + let services = disguise_service_mocks(mock_services); + let user_logic = UserController::new(contexts, services); + + let get_users_request = Request::new(GetUsersRequest { ids: vec![1, 2] }); + + let get_users_response = user_logic.get_users(get_users_request).await.unwrap(); + + assert_eq!(get_users_response.get_ref().users.len(), 0); + } +} diff --git a/ecdar_api_macros/src/lib.rs b/src/lib.rs similarity index 97% rename from ecdar_api_macros/src/lib.rs rename to src/lib.rs index 222bd8d..1862e56 100644 --- a/ecdar_api_macros/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,7 @@ pub fn endpoints(_attr: TokenStream, item: TokenStream) -> TokenStream { .as_ref() .map(|(_, items)| { items.iter().filter_map(|item| { - if let syn::Item::Impl(impl_item) = item { + if let Item::Impl(impl_item) = item { Some( impl_item .trait_ @@ -75,7 +75,7 @@ pub fn endpoints(_attr: TokenStream, item: TokenStream) -> TokenStream { .items .iter() .filter_map(|item| match item { - syn::ImplItem::Fn(function) => { + ImplItem::Fn(function) => { Some(function.sig.ident.to_string().to_case(Case::Pascal)) } _ => None, @@ -96,7 +96,7 @@ pub fn endpoints(_attr: TokenStream, item: TokenStream) -> TokenStream { })) } } - .into(); + .into(); // It's cursed, but what could be expected from traversing an AST without some kind of pattern. // A method of getting the reference to the "EcdarApiAuth" implementation diff --git a/src/main.rs b/src/main.rs index a397c1b..e17fef7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ //! # Description //! This project serves as an API server between an ECDAR frontend and Reveaal //! -//! The project is currently being developed at [Github](https://github.com/ECDAR-AAU-SW-P5/) -//! Ecdar-API serves as the intermediary between the [Ecdar frontend](https://github.com/ECDAR-AAU-SW-P5/Ecdar-GUI-Web) and the [Ecdar backend](https://github.com/ECDAR-AAU-SW-P5/Reveaal) (Reveaal). Its core functionality revolves around storing and managing entities such as users and projects, allowing the backend to focus solely on computations. +//! The project is currently being developed at [Github](https://github.com/Ecdar/) +//! Ecdar-API serves as the intermediary between the [Ecdar frontend](https://github.com/Ecdar/Ecdar-GUI-Web) and the [Ecdar backend](https://github.com/Ecdar/Reveaal) (Reveaal). Its core functionality revolves around storing and managing entities such as users and projects, allowing the backend to focus solely on computations. //! //! # Notes //! Currently, the only supported databases are `PostgreSQL` and `SQLite` @@ -11,15 +11,7 @@ mod contexts; mod controllers; mod entities; mod services; -mod tests; -use crate::contexts::context_collection::ContextCollection; -use crate::contexts::context_impls::*; -use crate::contexts::context_traits::DatabaseContextTrait; -use crate::controllers::controller_collection::ControllerCollection; -use crate::controllers::controller_impls::*; -use crate::services::service_collection::ServiceCollection; -use crate::services::service_impls::{HashingService, ReveaalService}; use api::server::start_grpc_server; use dotenv::dotenv; use sea_orm::{ConnectionTrait, Database, DbBackend}; @@ -28,8 +20,14 @@ use std::error::Error; use std::sync::Arc; #[tokio::main] -#[allow(clippy::expect_used)] async fn main() -> Result<(), Box> { + use crate::contexts::{ + AccessContext, ContextCollection, DatabaseContextTrait, InUseContext, + PostgresDatabaseContext, ProjectContext, QueryContext, SQLiteDatabaseContext, + SessionContext, UserContext, + }; + use crate::controllers::*; + use crate::services::{DefaultHashing, ReveaalService, ServiceCollection}; dotenv().ok(); let reveaal_addr = env::var("REVEAAL_ADDRESS").expect("Expected REVEAAL_ADDRESS to be set."); @@ -52,7 +50,7 @@ async fn main() -> Result<(), Box> { }; let services = ServiceCollection { - hashing_service: Arc::new(HashingService), + hashing_service: Arc::new(DefaultHashing), reveaal_service: Arc::new(ReveaalService::new(&reveaal_addr)), }; diff --git a/src/services/service_impls/hashing_service.rs b/src/services/hashing_service.rs similarity index 52% rename from src/services/service_impls/hashing_service.rs rename to src/services/hashing_service.rs index 112eb84..d56b87f 100644 --- a/src/services/service_impls/hashing_service.rs +++ b/src/services/hashing_service.rs @@ -1,9 +1,13 @@ -use crate::services::service_traits::hashing_service_trait::HashingServiceTrait; use bcrypt::{hash, verify, BcryptError, DEFAULT_COST}; -pub struct HashingService; +pub trait HashingService: Send + Sync { + fn hash_password(&self, password: String) -> Result; + fn verify_password(&self, password: String, hash: &str) -> Result; +} + +pub struct DefaultHashing; -impl HashingServiceTrait for HashingService { +impl HashingService for DefaultHashing { fn hash_password(&self, password: String) -> Result { hash(password, DEFAULT_COST) } diff --git a/src/services/mod.rs b/src/services/mod.rs index 22340aa..0c9455b 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,3 +1,7 @@ -pub mod service_collection; -pub mod service_impls; -pub mod service_traits; +mod hashing_service; +mod reveaal_service; +mod service_collection; + +pub use hashing_service::*; +pub use reveaal_service::*; +pub use service_collection::*; diff --git a/src/services/reveaal_service.rs b/src/services/reveaal_service.rs new file mode 100644 index 0000000..9656831 --- /dev/null +++ b/src/services/reveaal_service.rs @@ -0,0 +1,111 @@ +use crate::api::server::protobuf::{ + ecdar_backend_client::EcdarBackendClient, QueryRequest, QueryResponse, SimulationStartRequest, + SimulationStepRequest, SimulationStepResponse, UserTokenResponse, +}; +use async_trait::async_trait; +use tonic::{transport::Channel, Request, Response, Status}; + +#[async_trait] +pub trait ReveaalServiceTrait: Send + Sync { + async fn get_user_token( + &self, + request: Request<()>, + ) -> Result, Status>; + async fn send_query( + &self, + request: Request, + ) -> Result, Status>; + async fn start_simulation( + &self, + request: Request, + ) -> Result, Status>; + async fn take_simulation_step( + &self, + request: Request, + ) -> Result, Status>; +} + +pub struct ReveaalService { + address: String, +} + +impl ReveaalService { + pub fn new(address: &str) -> Self { + Self { + address: address.to_string(), + } + } + + async fn get_connection(&self) -> Result, Status> { + EcdarBackendClient::connect(self.address.clone()) + .await + .map_err(|err| Status::internal(format!("{err}"))) + } +} + +#[async_trait] +impl ReveaalServiceTrait for ReveaalService { + async fn get_user_token( + &self, + request: Request<()>, + ) -> Result, Status> { + self.get_connection().await?.get_user_token(request).await + } + + async fn send_query( + &self, + request: Request, + ) -> Result, Status> { + self.get_connection().await?.send_query(request).await + } + + async fn start_simulation( + &self, + request: Request, + ) -> Result, Status> { + self.get_connection().await?.start_simulation(request).await + } + + async fn take_simulation_step( + &self, + request: Request, + ) -> Result, Status> { + self.get_connection() + .await? + .take_simulation_step(request) + .await + } +} + +#[cfg(test)] +mod tests { + // use crate::api::server::server::QueryResponse; + // use wiremock_grpc::generate; + // use wiremock_grpc::*; + // + // generate!("EcdarBackend", MyMockServer); + + #[ignore] + #[tokio::test] + async fn send_query_test_correct_query_returns_ok() { + //todo!("Somehow QueryResponse does not implement prost::message::Message even though it does. + // supposedly a versioning error between wiremock_grpc, tonic, and prost") + + // let mut server = MyMockServer::start_default().await; + // + // let request1 = server.setup( + // MockBuilder::when() + // .path("EcdarBackend/SendQuery") + // .then() + // .return_status(Code::Ok) + // .return_body(|| QueryResponse { + // query_id: 0, + // info: vec![], + // result: None, + // }), + // ); + + //... + //https://crates.io/crates/wiremock-grpc + } +} diff --git a/src/services/service_collection.rs b/src/services/service_collection.rs index b487c85..feee2ee 100644 --- a/src/services/service_collection.rs +++ b/src/services/service_collection.rs @@ -1,8 +1,8 @@ -use crate::services::service_traits::{HashingServiceTrait, ReveaalServiceTrait}; +use crate::services::{HashingService, ReveaalServiceTrait}; use std::sync::Arc; #[derive(Clone)] pub struct ServiceCollection { - pub(crate) hashing_service: Arc, + pub(crate) hashing_service: Arc, pub(crate) reveaal_service: Arc, } diff --git a/src/services/service_impls/mod.rs b/src/services/service_impls/mod.rs deleted file mode 100644 index 3b93ad7..0000000 --- a/src/services/service_impls/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod hashing_service; -mod reveaal_service; - -pub use hashing_service::HashingService; -pub use reveaal_service::ReveaalService; diff --git a/src/services/service_impls/reveaal_service.rs b/src/services/service_impls/reveaal_service.rs deleted file mode 100644 index 3b20a82..0000000 --- a/src/services/service_impls/reveaal_service.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::api::server::protobuf::ecdar_backend_client::EcdarBackendClient; -use crate::api::server::protobuf::{ - QueryRequest, QueryResponse, SimulationStartRequest, SimulationStepRequest, - SimulationStepResponse, UserTokenResponse, -}; -use crate::services::service_traits::ReveaalServiceTrait; -use async_trait::async_trait; -use tonic::transport::Channel; -use tonic::{Request, Response, Status}; - -pub struct ReveaalService { - address: String, -} - -impl ReveaalService { - pub fn new(address: &str) -> Self { - Self { - address: address.to_string(), - } - } - - async fn get_connection(&self) -> Result, Status> { - EcdarBackendClient::connect(self.address.clone()) - .await - .map_err(|err| Status::internal(format!("{err}"))) - } -} - -#[async_trait] -impl ReveaalServiceTrait for ReveaalService { - async fn get_user_token( - &self, - request: Request<()>, - ) -> Result, Status> { - self.get_connection().await?.get_user_token(request).await - } - - async fn send_query( - &self, - request: Request, - ) -> Result, Status> { - self.get_connection().await?.send_query(request).await - } - - async fn start_simulation( - &self, - request: Request, - ) -> Result, Status> { - self.get_connection().await?.start_simulation(request).await - } - - async fn take_simulation_step( - &self, - request: Request, - ) -> Result, Status> { - self.get_connection() - .await? - .take_simulation_step(request) - .await - } -} diff --git a/src/services/service_traits/hashing_service_trait.rs b/src/services/service_traits/hashing_service_trait.rs deleted file mode 100644 index 5b36e95..0000000 --- a/src/services/service_traits/hashing_service_trait.rs +++ /dev/null @@ -1,6 +0,0 @@ -use bcrypt::BcryptError; - -pub trait HashingServiceTrait: Send + Sync { - fn hash_password(&self, password: String) -> Result; - fn verify_password(&self, password: String, hash: &str) -> Result; -} diff --git a/src/services/service_traits/mod.rs b/src/services/service_traits/mod.rs deleted file mode 100644 index 0761798..0000000 --- a/src/services/service_traits/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod hashing_service_trait; -pub mod reveaal_service_trait; - -pub use hashing_service_trait::HashingServiceTrait; -pub use reveaal_service_trait::ReveaalServiceTrait; diff --git a/src/services/service_traits/reveaal_service_trait.rs b/src/services/service_traits/reveaal_service_trait.rs deleted file mode 100644 index 66fcfde..0000000 --- a/src/services/service_traits/reveaal_service_trait.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::api::server::protobuf::{ - QueryRequest, QueryResponse, SimulationStartRequest, SimulationStepRequest, - SimulationStepResponse, UserTokenResponse, -}; -use async_trait::async_trait; -use tonic::{Request, Response, Status}; - -#[async_trait] -pub trait ReveaalServiceTrait: Send + Sync { - async fn get_user_token( - &self, - request: Request<()>, - ) -> Result, Status>; - async fn send_query( - &self, - request: Request, - ) -> Result, Status>; - async fn start_simulation( - &self, - request: Request, - ) -> Result, Status>; - async fn take_simulation_step( - &self, - request: Request, - ) -> Result, Status>; -} diff --git a/src/tests/api/auth.rs b/src/tests/api/auth.rs deleted file mode 100644 index 10e8f10..0000000 --- a/src/tests/api/auth.rs +++ /dev/null @@ -1,141 +0,0 @@ -#[cfg(test)] -mod auth { - use crate::api::auth::{RequestExt, Token, TokenError, TokenType}; - use std::{env, str::FromStr}; - use tonic::{metadata::MetadataValue, Request}; - - #[tokio::test] - async fn request_token_trims_bearer() { - let token = "Bearer 1234567890"; - let mut request = Request::new(()); - request - .metadata_mut() - .insert("authorization", MetadataValue::from_str(token).unwrap()); - - let result = request.token_str().unwrap().unwrap(); - - assert_eq!(result, token.trim_start_matches("Bearer ")); - } - - #[tokio::test] - async fn request_token_no_token_returns_none() { - let request = Request::new(()); - let result = request.token_str().unwrap(); - - assert!(result.is_none()); - } - - #[tokio::test] - async fn token_new_access_returns_token() { - env::set_var("ACCESS_TOKEN_HS512_SECRET", "access_secret"); - - let uid = "1"; - let result = Token::new(TokenType::AccessToken, uid); - - assert!(result.is_ok()); - } - - #[tokio::test] - async fn token_new_refresh_returns_token() { - env::set_var("REFRESH_TOKEN_HS512_SECRET", "refresh_secret"); - - let uid = "1"; - let result = Token::new(TokenType::RefreshToken, uid); - - assert!(result.is_ok()); - } - - #[tokio::test] - async fn validate_token_valid_access_returns_tokendata() { - env::set_var("ACCESS_TOKEN_HS512_SECRET", "access_secret"); - - let token = Token::new(TokenType::AccessToken, "1").unwrap(); - let result = token.validate(); - - assert!(result.is_ok()); - } - - #[tokio::test] - async fn validate_token_valid_refresh_returns_tokendata() { - env::set_var("REFRESH_TOKEN_HS512_SECRET", "refresh_secret"); - - let token = Token::new(TokenType::RefreshToken, "1").unwrap(); - let result = token.validate(); - - assert!(result.is_ok()); - } - - #[tokio::test] - async fn validate_token_invalid_returns_err() { - env::set_var("ACCESS_TOKEN_HS512_SECRET", "access_secret"); - env::set_var("REFRESH_TOKEN_HS512_SECRET", "refresh_secret"); - - let result_access = Token::from_str(TokenType::AccessToken, "invalid_token").validate(); - let result_refresh = Token::from_str(TokenType::RefreshToken, "invalid_token").validate(); - - assert_eq!(result_access.unwrap_err(), TokenError::InvalidToken); - assert_eq!(result_refresh.unwrap_err(), TokenError::InvalidToken); - } - - #[tokio::test] - async fn token_type_access_returns_access() { - env::set_var("ACCESS_TOKEN_HS512_SECRET", "access_secret"); - - let token = Token::new(TokenType::AccessToken, "1").unwrap(); - let result = token.token_type(); - - assert_eq!(result, TokenType::AccessToken); - } - - #[tokio::test] - async fn token_type_refresh_returns_refresh() { - env::set_var("REFRESH_TOKEN_HS512_SECRET", "refresh_secret"); - - let token = Token::new(TokenType::RefreshToken, "1").unwrap(); - let result = token.token_type(); - - assert_eq!(result, TokenType::RefreshToken); - } - - #[tokio::test] - async fn token_to_string_returns_string() { - env::set_var("ACCESS_TOKEN_HS512_SECRET", "access_secret"); - - let token = Token::new(TokenType::AccessToken, "1").unwrap(); - let result = token.to_string(); - - assert_eq!(result, token.as_str()); - } - - #[tokio::test] - async fn token_as_str_returns_string() { - env::set_var("ACCESS_TOKEN_HS512_SECRET", "access_secret"); - - let token = Token::new(TokenType::AccessToken, "1").unwrap(); - let result = token.as_str(); - - assert_eq!(result, token.to_string()); - } - - #[tokio::test] - async fn token_from_str_returns_token() { - env::set_var("ACCESS_TOKEN_HS512_SECRET", "access_secret"); - - let token = Token::new(TokenType::AccessToken, "1").unwrap(); - let token_from_str = Token::from_str(TokenType::AccessToken, token.as_str()); - - let result = token_from_str.validate(); - - assert!(result.is_ok()); - } - - #[tokio::test] - async fn token_from_str_invalid_returns_err() { - env::set_var("ACCESS_TOKEN_HS512_SECRET", "access_secret"); - - let token = Token::from_str(TokenType::AccessToken, "invalid_token"); - let result = token.validate(); - - assert!(result.is_err()); - } -} diff --git a/src/tests/api/mod.rs b/src/tests/api/mod.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/tests/api/mod.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/tests/contexts/access_context.rs b/src/tests/contexts/access_context.rs deleted file mode 100644 index 41207c0..0000000 --- a/src/tests/contexts/access_context.rs +++ /dev/null @@ -1,393 +0,0 @@ -use crate::api::server::protobuf::AccessInfo; -use crate::contexts::context_traits::{AccessContextTrait, EntityContextTrait}; -use crate::tests::contexts::helpers::{ - create_accesses, create_projects, create_users, get_reset_database_context, -}; -use crate::{ - contexts::context_impls::AccessContext, - entities::{access, project, user}, - to_active_models, -}; -use sea_orm::{entity::prelude::*, IntoActiveModel}; - -async fn seed_db() -> (AccessContext, access::Model, user::Model, project::Model) { - let db_context = get_reset_database_context().await; - - let access_context = AccessContext::new(db_context); - - let user = create_users(1)[0].clone(); - let project = create_projects(1, user.id)[0].clone(); - let access = create_accesses(1, user.id, project.id)[0].clone(); - - user::Entity::insert(user.clone().into_active_model()) - .exec(&access_context.db_context.get_connection()) - .await - .unwrap(); - project::Entity::insert(project.clone().into_active_model()) - .exec(&access_context.db_context.get_connection()) - .await - .unwrap(); - - (access_context, access, user, project) -} - -// Test the functionality of the 'create' function, which creates a access in the contexts -#[tokio::test] -async fn create_test() { - let (access_context, access, _, _) = seed_db().await; - - let created_access = access_context.create(access.clone()).await.unwrap(); - - let fetched_access = access::Entity::find_by_id(created_access.id) - .one(&access_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - // Assert if the fetched access is the same as the created access - assert_eq!(access, created_access); - assert_eq!(fetched_access, created_access); -} - -#[tokio::test] -async fn create_check_unique_pair_project_id_user_id_test() { - let (access_context, access, _, _) = seed_db().await; - - let _created_access_1 = access_context.create(access.clone()).await.unwrap(); - let _created_access_2 = access_context.create(access.clone()).await; - - assert!(matches!( - _created_access_2.unwrap_err().sql_err(), - Some(SqlErr::UniqueConstraintViolation(_)) - )); -} - -#[tokio::test] -async fn create_invalid_role_test() { - let (access_context, mut access, _, _) = seed_db().await; - - access.role = "abc".into(); - - let created_access = access_context.create(access.clone()).await; - - assert!(matches!( - created_access.unwrap_err().sql_err(), - Some(SqlErr::ForeignKeyConstraintViolation(_)) - )); -} - -#[tokio::test] -async fn create_auto_increment_test() { - let (access_context, _, user, project_1) = seed_db().await; - - let mut project_2 = create_projects(1, user.id)[0].clone(); - project_2.id = project_1.id + 1; - project_2.name = "project_2".to_string(); - - project::Entity::insert(project_2.into_active_model()) - .exec(&access_context.db_context.get_connection()) - .await - .unwrap(); - - let access_1 = access::Model { - id: 0, - role: "Editor".to_string(), - project_id: 1, - user_id: user.id, - }; - - let access_2 = access::Model { - id: 0, - role: "Editor".to_string(), - project_id: 2, - user_id: user.id, - }; - - let created_access1 = access_context.create(access_1.clone()).await.unwrap(); - let created_access2 = access_context.create(access_2.clone()).await.unwrap(); - - let fetched_access1 = access::Entity::find_by_id(created_access1.id) - .one(&access_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - let fetched_access2 = access::Entity::find_by_id(created_access2.id) - .one(&access_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - assert_ne!(fetched_access1.id, fetched_access2.id); - assert_ne!(created_access1.id, created_access2.id); - assert_eq!(created_access1.id, fetched_access1.id); - assert_eq!(created_access2.id, fetched_access2.id); -} - -#[tokio::test] -async fn get_by_id_test() { - let (access_context, access, _, _) = seed_db().await; - - access::Entity::insert(access.clone().into_active_model()) - .exec(&access_context.db_context.get_connection()) - .await - .unwrap(); - - // Fetches the access created using the 'get_by_id' function - let fetched_access = access_context.get_by_id(access.id).await.unwrap().unwrap(); - - // Assert if the fetched access is the same as the created access - assert_eq!(access, fetched_access); -} - -#[tokio::test] -async fn get_by_non_existing_id_test() { - let (access_context, _, _, _) = seed_db().await; - - let fetched_access = access_context.get_by_id(1).await.unwrap(); - - assert!(fetched_access.is_none()); -} - -#[tokio::test] -async fn get_all_test() { - let (access_context, _, user, project) = seed_db().await; - - // Creates a model of the access which will be created - let new_accesses = create_accesses(1, user.id, project.id); - - // Creates the access in the contexts using the 'create' function - access::Entity::insert_many(to_active_models!(new_accesses.clone())) - .exec(&access_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(access_context.get_all().await.unwrap().len(), 1); - - let mut sorted: Vec = new_accesses.clone(); - sorted.sort_by_key(|k| k.id); - - for (i, access) in sorted.into_iter().enumerate() { - assert_eq!(access, new_accesses[i]); - } -} - -#[tokio::test] -async fn get_all_empty_test() { - let (access_context, _, _, _) = seed_db().await; - - let result = access_context.get_all().await.unwrap(); - let empty_accesses: Vec = vec![]; - - assert_eq!(empty_accesses, result); -} - -#[tokio::test] -async fn update_test() { - let (access_context, access, _, _) = seed_db().await; - - access::Entity::insert(access.clone().into_active_model()) - .exec(&access_context.db_context.get_connection()) - .await - .unwrap(); - - let new_access = access::Model { ..access }; - - let updated_access = access_context.update(new_access.clone()).await.unwrap(); - - let fetched_access = access::Entity::find_by_id(updated_access.id) - .one(&access_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - assert_eq!(new_access, updated_access); - assert_eq!(updated_access, fetched_access); -} - -#[tokio::test] -async fn update_modifies_role_test() { - let (access_context, access, _, _) = seed_db().await; - - let access = access::Model { - role: "Editor".into(), - ..access - }; - - access::Entity::insert(access.clone().into_active_model()) - .exec(&access_context.db_context.get_connection()) - .await - .unwrap(); - - let new_access = access::Model { - role: "Commenter".into(), - ..access - }; - - let updated_access = access_context.update(new_access.clone()).await.unwrap(); - - assert_ne!(access, updated_access); - assert_ne!(access, new_access); -} - -#[tokio::test] -async fn update_does_not_modify_id_test() { - let (access_context, access, _, _) = seed_db().await; - access::Entity::insert(access.clone().into_active_model()) - .exec(&access_context.db_context.get_connection()) - .await - .unwrap(); - - let updated_access = access::Model { - id: &access.id + 1, - ..access.clone() - }; - let res = access_context.update(updated_access.clone()).await; - - assert!(matches!(res.unwrap_err(), DbErr::RecordNotUpdated)); -} - -#[tokio::test] -async fn update_does_not_modify_project_id_test() { - let (access_context, access, _, _) = seed_db().await; - - access::Entity::insert(access.clone().into_active_model()) - .exec(&access_context.db_context.get_connection()) - .await - .unwrap(); - - let updated_access = access::Model { - project_id: &access.project_id + 1, - ..access.clone() - }; - let res = access_context.update(updated_access.clone()).await.unwrap(); - - assert_eq!(access, res); -} - -#[tokio::test] -async fn update_does_not_modify_user_id_test() { - let (access_context, access, _, _) = seed_db().await; - - access::Entity::insert(access.clone().into_active_model()) - .exec(&access_context.db_context.get_connection()) - .await - .unwrap(); - - let updated_access = access::Model { - user_id: &access.user_id + 1, - ..access.clone() - }; - let res = access_context.update(updated_access.clone()).await.unwrap(); - - assert_eq!(access, res); -} - -#[tokio::test] -async fn update_invalid_role_test() { - let (access_context, mut access, _, _) = seed_db().await; - - access::Entity::insert(access.clone().into_active_model()) - .exec(&access_context.db_context.get_connection()) - .await - .unwrap(); - - access.role = "abc".into(); - - let updated_access = access_context.update(access.clone()).await; - - assert!(matches!( - updated_access.unwrap_err().sql_err(), - Some(SqlErr::ForeignKeyConstraintViolation(_)) - )); -} - -#[tokio::test] -async fn update_non_existing_id_test() { - let (access_context, access, _, _) = seed_db().await; - - let updated_access = access_context.update(access.clone()).await; - - assert!(matches!( - updated_access.unwrap_err(), - DbErr::RecordNotUpdated - )); -} - -#[tokio::test] -async fn delete_test() { - let (access_context, access, _, _) = seed_db().await; - - access::Entity::insert(access.clone().into_active_model()) - .exec(&access_context.db_context.get_connection()) - .await - .unwrap(); - - let deleted_access = access_context.delete(access.id).await.unwrap(); - - let all_accesses = access::Entity::find() - .all(&access_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(access, deleted_access); - assert!(all_accesses.is_empty()); -} - -#[tokio::test] -async fn delete_non_existing_id_test() { - let (access_context, _, _, _) = seed_db().await; - - let deleted_access = access_context.delete(1).await; - - assert!(matches!( - deleted_access.unwrap_err(), - DbErr::RecordNotFound(_) - )); -} - -#[tokio::test] -async fn get_by_uid_and_project_id_test() { - let (access_context, expected_access, user, project) = seed_db().await; - - access::Entity::insert(expected_access.clone().into_active_model()) - .exec(&access_context.db_context.get_connection()) - .await - .unwrap(); - - let access = access_context - .get_access_by_uid_and_project_id(user.id, project.id) - .await; - - assert_eq!(access.unwrap().unwrap(), expected_access); -} - -#[tokio::test] -async fn get_access_by_project_id_test_returns_ok() { - let (access_context, expected_access, _, model) = seed_db().await; - - let expected_access_access_info_vector = vec![AccessInfo { - id: expected_access.id, - project_id: expected_access.project_id, - user_id: expected_access.user_id, - role: expected_access.role.clone(), - }]; - - access::Entity::insert(expected_access.clone().into_active_model()) - .exec(&access_context.db_context.get_connection()) - .await - .unwrap(); - - let access = access_context.get_access_by_project_id(model.id).await; - - assert!(access.unwrap() == expected_access_access_info_vector); -} - -#[tokio::test] -async fn get_access_by_project_id_test_returns_empty() { - let (access_context, _, _, model) = seed_db().await; - - let access = access_context.get_access_by_project_id(model.id).await; - - assert!(access.unwrap().is_empty()); -} diff --git a/src/tests/contexts/helpers.rs b/src/tests/contexts/helpers.rs deleted file mode 100644 index 2286f5b..0000000 --- a/src/tests/contexts/helpers.rs +++ /dev/null @@ -1,120 +0,0 @@ -#![cfg(test)] - -use crate::contexts::context_impls::{PostgresDatabaseContext, SQLiteDatabaseContext}; -use crate::contexts::context_traits::DatabaseContextTrait; -use crate::entities::{access, in_use, project, query, session, user}; -use dotenv::dotenv; -use sea_orm::{ConnectionTrait, Database, DbBackend}; -use std::env; -use std::sync::Arc; - -pub async fn get_reset_database_context() -> Arc { - dotenv().ok(); - - let url = env::var("TEST_DATABASE_URL").expect("TEST_DATABASE_URL must be set to run tests."); - let db = Database::connect(&url).await.unwrap(); - let db_context: Arc = match db.get_database_backend() { - DbBackend::Sqlite => Arc::new(SQLiteDatabaseContext::new(&url).await.unwrap()), - DbBackend::Postgres => Arc::new(PostgresDatabaseContext::new(&url).await.unwrap()), - _ => panic!("Database protocol not supported"), - }; - - db_context.reset().await.unwrap() -} - -/// -/// -/// # Arguments -/// -/// * `amount`: -/// * `model`: -/// -/// returns: Vec -/// -/// # Examples -/// -/// ``` -/// let vector: Vec = create_entities(3,|x| UserModel { -/// id: &x+i, -/// email: format!("mail{}@mail.dk",&x), -/// username: format!("username{}", &x), -/// password: format!("qwerty{}", &x), -/// ); -/// ``` - -pub fn create_entities(amount: i32, project_creator: F) -> Vec -where - F: Fn(i32) -> M, -{ - let mut vector: Vec = vec![]; - for i in 0..amount { - vector.push(project_creator(i)); - } - vector -} - -pub fn create_users(amount: i32) -> Vec { - create_entities(amount, |i| user::Model { - id: i + 1, - email: format!("mail{}@mail.dk", &i), - username: format!("username{}", &i), - password: format!("qwerty{}", &i), - }) -} - -pub fn create_projects(amount: i32, user_id: i32) -> Vec { - create_entities(amount, |i| project::Model { - id: i + 1, - name: format!("name {}", i), - components_info: "{}".to_owned().parse().unwrap(), - owner_id: user_id, - }) -} - -pub fn create_accesses(amount: i32, user_id: i32, project_id: i32) -> Vec { - create_entities(amount, |i| access::Model { - id: i + 1, - role: "Reader".into(), - project_id: project_id + i, - user_id: user_id + i, - }) -} - -pub fn create_sessions(amount: i32, user_id: i32) -> Vec { - create_entities(amount, |i| session::Model { - id: i + 1, - refresh_token: "test_refresh_token".to_string() + format!("{}", i).as_str(), - access_token: "test_access_token".to_string() + format!("{}", i).as_str(), - user_id, - updated_at: Default::default(), - }) -} - -pub fn create_in_uses(amount: i32, project_id: i32, session_id: i32) -> Vec { - create_entities(amount, |i| in_use::Model { - project_id: project_id + i, - session_id, - latest_activity: Default::default(), - }) -} - -pub fn create_queries(amount: i32, project_id: i32) -> Vec { - create_entities(amount, |i| query::Model { - id: i + 1, - string: "".to_string(), - result: None, - outdated: true, - project_id, - }) -} - -#[macro_export] -macro_rules! to_active_models { - ($vec:expr) => {{ - let mut models = Vec::new(); - for model in $vec { - models.push(model.into_active_model()); - } - models - }}; -} diff --git a/src/tests/contexts/in_use_context.rs b/src/tests/contexts/in_use_context.rs deleted file mode 100644 index fd21241..0000000 --- a/src/tests/contexts/in_use_context.rs +++ /dev/null @@ -1,271 +0,0 @@ -use crate::tests::contexts::helpers::*; -use crate::{ - contexts::context_impls::InUseContext, - contexts::context_traits::EntityContextTrait, - entities::{in_use, project, session, user}, - to_active_models, -}; -use chrono::{Duration, Utc}; -use sea_orm::{entity::prelude::*, IntoActiveModel}; -use std::matches; -use std::ops::Add; - -async fn seed_db() -> ( - InUseContext, - in_use::Model, - session::Model, - project::Model, - user::Model, -) { - let db_context = get_reset_database_context().await; - - let in_use_context = InUseContext::new(db_context); - - let user = create_users(1)[0].clone(); - let project = create_projects(1, user.id)[0].clone(); - let session = create_sessions(1, user.id)[0].clone(); - let in_use = create_in_uses(1, project.id, session.id)[0].clone(); - - user::Entity::insert(user.clone().into_active_model()) - .exec(&in_use_context.db_context.get_connection()) - .await - .unwrap(); - project::Entity::insert(project.clone().into_active_model()) - .exec(&in_use_context.db_context.get_connection()) - .await - .unwrap(); - session::Entity::insert(session.clone().into_active_model()) - .exec(&in_use_context.db_context.get_connection()) - .await - .unwrap(); - - (in_use_context, in_use, session, project, user) -} - -#[tokio::test] -async fn create_test() { - let (in_use_context, mut in_use, _, _, _) = seed_db().await; - - let inserted_in_use = in_use_context.create(in_use.clone()).await.unwrap(); - - in_use.latest_activity = inserted_in_use.latest_activity; - - let fetched_in_use = in_use::Entity::find_by_id(inserted_in_use.clone().project_id) - .one(&in_use_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - assert_eq!(in_use, inserted_in_use); - assert_eq!(in_use, fetched_in_use); -} - -#[tokio::test] -async fn create_default_latest_activity_test() { - let t_min = Utc::now().timestamp(); - - let (in_use_context, in_use, _, _, _) = seed_db().await; - - let inserted_in_use = in_use_context.create(in_use.clone()).await.unwrap(); - - let fetched_in_use = in_use::Entity::find_by_id(inserted_in_use.project_id) - .one(&in_use_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - let t_max = Utc::now().timestamp(); - - let t_actual = fetched_in_use.clone().latest_activity.timestamp(); - - assert!(t_min <= t_actual && t_actual <= t_max) -} - -#[tokio::test] -async fn get_by_id_test() { - let (in_use_context, in_use, _, _, _) = seed_db().await; - - in_use::Entity::insert(in_use.clone().into_active_model()) - .exec(&in_use_context.db_context.get_connection()) - .await - .unwrap(); - - let fetched_in_use = in_use_context - .get_by_id(in_use.project_id) - .await - .unwrap() - .unwrap(); - - assert_eq!(fetched_in_use, in_use) -} - -#[tokio::test] -async fn get_by_non_existing_id_test() { - let (in_use_context, _in_use, _, _, _) = seed_db().await; - - let in_use = in_use_context.get_by_id(1).await; - - assert!(in_use.unwrap().is_none()) -} - -#[tokio::test] -async fn get_all_test() { - let (in_use_context, _in_use, session, project, _user) = seed_db().await; - - let in_uses = create_in_uses(1, project.id, session.id); - - in_use::Entity::insert_many(to_active_models!(in_uses.clone())) - .exec(&in_use_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(in_use_context.get_all().await.unwrap().len(), 1); -} - -#[tokio::test] -async fn get_all_empty_test() { - let (in_use_context, _, _, _, _) = seed_db().await; - - let in_uses = in_use_context.get_all().await.unwrap(); - - assert_eq!(0, in_uses.len()) -} - -#[tokio::test] -async fn update_test() { - let (in_use_context, in_use, _, _, _) = seed_db().await; - - in_use::Entity::insert(in_use.clone().into_active_model()) - .exec(&in_use_context.db_context.get_connection()) - .await - .unwrap(); - - let new_in_use = in_use::Model { ..in_use }; - - let updated_in_use = in_use_context.update(new_in_use.clone()).await.unwrap(); - - let fetched_in_use = in_use::Entity::find_by_id(updated_in_use.project_id) - .one(&in_use_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - assert_eq!(new_in_use, updated_in_use); - assert_eq!(updated_in_use, fetched_in_use); -} - -#[tokio::test] -async fn update_modifies_latest_activity_test() { - let (in_use_context, in_use, _, _, _) = seed_db().await; - - in_use::Entity::insert(in_use.clone().into_active_model()) - .exec(&in_use_context.db_context.get_connection()) - .await - .unwrap(); - - let new_in_use = in_use::Model { - latest_activity: in_use.clone().latest_activity.add(Duration::seconds(1)), - ..in_use - }; - - let updated_in_use = in_use_context.update(new_in_use.clone()).await.unwrap(); - - assert_ne!(in_use, updated_in_use); - assert_ne!(in_use, new_in_use); -} - -#[tokio::test] -async fn update_modifies_session_id_test() { - let (in_use_context, in_use, _, _, _) = seed_db().await; - - in_use::Entity::insert(in_use.clone().into_active_model()) - .exec(&in_use_context.db_context.get_connection()) - .await - .unwrap(); - - let mut session2 = create_sessions(1, in_use.session_id)[0].clone(); - session2.id = in_use.session_id + 1; - session2.refresh_token = "new_refresh_token".to_string(); - session2.access_token = "new_access_token".to_string(); - - session::Entity::insert(session2.clone().into_active_model()) - .exec(&in_use_context.db_context.get_connection()) - .await - .unwrap(); - - let new_in_use = in_use::Model { - session_id: in_use.session_id + 1, - ..in_use - }; - - let updated_in_use = in_use_context.update(new_in_use.clone()).await.unwrap(); - - assert_ne!(in_use, updated_in_use); - assert_ne!(in_use, new_in_use); -} - -#[tokio::test] -async fn update_does_not_modify_project_id_test() { - let (in_use_context, in_use, _, _, _) = seed_db().await; - - in_use::Entity::insert(in_use.clone().into_active_model()) - .exec(&in_use_context.db_context.get_connection()) - .await - .unwrap(); - - let updated_in_use = in_use::Model { - project_id: in_use.project_id + 1, - ..in_use.clone() - }; - - let updated_in_use = in_use_context.update(updated_in_use.clone()).await; - - assert!(matches!( - updated_in_use.unwrap_err(), - DbErr::RecordNotUpdated - )); -} - -#[tokio::test] -async fn update_non_existing_id_test() { - let (in_use_context, in_use, _, _, _) = seed_db().await; - - let updated_in_use = in_use_context.update(in_use.clone()).await; - - assert!(matches!( - updated_in_use.unwrap_err(), - DbErr::RecordNotUpdated - )); -} - -#[tokio::test] -async fn delete_test() { - let (in_use_context, in_use, _, _, _) = seed_db().await; - - in_use::Entity::insert(in_use.clone().into_active_model()) - .exec(&in_use_context.db_context.get_connection()) - .await - .unwrap(); - - let deleted_in_use = in_use_context.delete(in_use.project_id).await.unwrap(); - - let all_in_uses = in_use::Entity::find() - .all(&in_use_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(in_use, deleted_in_use); - assert!(all_in_uses.is_empty()); -} - -#[tokio::test] -async fn delete_non_existing_id_test() { - let (in_use_context, _, _, _, _) = seed_db().await; - - let deleted_in_use = in_use_context.delete(1).await; - - assert!(matches!( - deleted_in_use.unwrap_err(), - DbErr::RecordNotFound(_) - )) -} diff --git a/src/tests/contexts/mod.rs b/src/tests/contexts/mod.rs deleted file mode 100644 index 1630fab..0000000 --- a/src/tests/contexts/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod helpers; diff --git a/src/tests/contexts/project_context.rs b/src/tests/contexts/project_context.rs deleted file mode 100644 index 3d79071..0000000 --- a/src/tests/contexts/project_context.rs +++ /dev/null @@ -1,409 +0,0 @@ -use crate::tests::contexts::helpers::*; -use crate::{ - contexts::context_impls::ProjectContext, - contexts::context_traits::EntityContextTrait, - entities::{access, in_use, project, query, session, user}, - to_active_models, -}; -use sea_orm::error::DbErr; -use sea_orm::{entity::prelude::*, IntoActiveModel}; -use std::matches; - -async fn seed_db() -> (ProjectContext, project::Model, user::Model) { - let db_context = get_reset_database_context().await; - - let project_context = ProjectContext::new(db_context); - - let user = create_users(1)[0].clone(); - let project = create_projects(1, user.id)[0].clone(); - - user::Entity::insert(user.clone().into_active_model()) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - - (project_context, project, user) -} - -#[tokio::test] -async fn create_test() { - let (project_context, project, _) = seed_db().await; - - let created_project = project_context.create(project.clone()).await.unwrap(); - - let fetched_project = project::Entity::find_by_id(created_project.id) - .one(&project_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - assert_eq!(project, created_project); - assert_eq!(fetched_project, created_project); -} - -#[tokio::test] -async fn create_auto_increment_test() { - let (project_context, project, _) = seed_db().await; - - let projects = create_projects(2, project.owner_id); - - let created_project1 = project_context.create(projects[0].clone()).await.unwrap(); - let created_project2 = project_context.create(projects[1].clone()).await.unwrap(); - - let fetched_project1 = project::Entity::find_by_id(created_project1.id) - .one(&project_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - let fetched_project2 = project::Entity::find_by_id(created_project2.id) - .one(&project_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - assert_ne!(fetched_project1.id, fetched_project2.id); - assert_ne!(created_project1.id, created_project2.id); - assert_eq!(created_project1.id, fetched_project1.id); - assert_eq!(created_project2.id, fetched_project2.id); -} - -#[tokio::test] -async fn get_by_id_test() { - let (project_context, project, _) = seed_db().await; - - project::Entity::insert(project.clone().into_active_model()) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - - let fetched_project = project_context - .get_by_id(project.id) - .await - .unwrap() - .unwrap(); - - assert_eq!(project, fetched_project); -} - -#[tokio::test] -async fn get_by_non_existing_id_test() { - let (project_context, _, _) = seed_db().await; - - let fetched_project = project_context.get_by_id(1).await.unwrap(); - - assert!(fetched_project.is_none()); -} - -#[tokio::test] -async fn get_all_test() { - let (project_context, _, user) = seed_db().await; - - let new_projects = create_projects(3, user.id); - - project::Entity::insert_many(to_active_models!(new_projects.clone())) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(project_context.get_all().await.unwrap().len(), 3); - - let mut sorted = new_projects.clone(); - sorted.sort_by_key(|k| k.id); - - for (i, project) in sorted.into_iter().enumerate() { - assert_eq!(project, new_projects[i]); - } -} - -#[tokio::test] -async fn get_all_empty_test() { - let (project_context, _, _) = seed_db().await; - - let result = project_context.get_all().await.unwrap(); - let empty_projects: Vec = vec![]; - - assert_eq!(empty_projects, result); -} - -#[tokio::test] -async fn update_test() { - let (project_context, project, _) = seed_db().await; - - project::Entity::insert(project.clone().into_active_model()) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - - let new_project = project::Model { ..project }; - - let updated_project = project_context.update(new_project.clone()).await.unwrap(); - - let fetched_project = project::Entity::find_by_id(updated_project.id) - .one(&project_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - assert_eq!(new_project, updated_project); - assert_eq!(updated_project, fetched_project); -} - -#[tokio::test] -async fn update_modifies_name_test() { - let (project_context, project, _) = seed_db().await; - - let project = project::Model { - name: "project1".into(), - ..project.clone() - }; - - project::Entity::insert(project.clone().into_active_model()) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - - let new_project = project::Model { - name: "project2".into(), - ..project.clone() - }; - - let updated_project = project_context.update(new_project.clone()).await.unwrap(); - - assert_ne!(project, updated_project); - assert_ne!(project, new_project); -} - -#[tokio::test] -async fn update_modifies_components_info_test() { - let (project_context, project, _) = seed_db().await; - - let project = project::Model { - components_info: "{\"a\":1}".to_owned().parse().unwrap(), - ..project.clone() - }; - - project::Entity::insert(project.clone().into_active_model()) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - - let new_project = project::Model { - components_info: "{\"a\":2}".to_owned().parse().unwrap(), - ..project.clone() - }; - - let updated_project = project_context.update(new_project.clone()).await.unwrap(); - - assert_ne!(project, updated_project); - assert_ne!(project, new_project); -} - -#[tokio::test] -async fn update_does_not_modify_id_test() { - let (project_context, project, _) = seed_db().await; - - project::Entity::insert(project.clone().into_active_model()) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - - let new_project = project::Model { - id: &project.id + 1, - ..project.clone() - }; - - let res = project_context.update(new_project.clone()).await; - - assert!(matches!(res.unwrap_err(), DbErr::RecordNotUpdated)); -} - -#[tokio::test] -async fn update_does_not_modify_owner_id_test() { - let (project_context, project, _) = seed_db().await; - - project::Entity::insert(project.clone().into_active_model()) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - - let new_project = project::Model { - owner_id: &project.owner_id + 1, - ..project.clone() - }; - - let res = project_context.update(new_project.clone()).await.unwrap(); - - assert_eq!(project, res); -} - -#[tokio::test] -async fn update_check_query_outdated_test() { - let (project_context, project, _) = seed_db().await; - - let mut query = create_queries(1, project.id)[0].clone(); - - query.outdated = false; - - project::Entity::insert(project.clone().into_active_model()) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - - query::Entity::insert(query.clone().into_active_model()) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - - let new_project = project::Model { ..project }; - - let updated_project = project_context.update(new_project.clone()).await.unwrap(); - - let fetched_query = query::Entity::find_by_id(updated_project.id) - .one(&project_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - assert!(fetched_query.outdated); -} - -#[tokio::test] -async fn update_non_existing_id_test() { - let (project_context, project, _) = seed_db().await; - - let updated_project = project_context.update(project.clone()).await; - - assert!(matches!( - updated_project.unwrap_err(), - DbErr::RecordNotUpdated - )); -} - -#[tokio::test] -async fn delete_test() { - // Setting up contexts and user context - let (project_context, project, _) = seed_db().await; - - project::Entity::insert(project.clone().into_active_model()) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - - let deleted_project = project_context.delete(project.id).await.unwrap(); - - let all_projects = project::Entity::find() - .all(&project_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(project, deleted_project); - assert_eq!(all_projects.len(), 0); -} - -#[tokio::test] -async fn delete_cascade_query_test() { - let (project_context, project, _) = seed_db().await; - - let query = create_queries(1, project.clone().id)[0].clone(); - - project::Entity::insert(project.clone().into_active_model()) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - query::Entity::insert(query.clone().into_active_model()) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - - project_context.delete(project.id).await.unwrap(); - - let all_queries = query::Entity::find() - .all(&project_context.db_context.get_connection()) - .await - .unwrap(); - let all_projects = project::Entity::find() - .all(&project_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(all_queries.len(), 0); - assert_eq!(all_projects.len(), 0); -} - -#[tokio::test] -async fn delete_cascade_access_test() { - let (project_context, project, _) = seed_db().await; - - let access = create_accesses(1, 1, project.clone().id)[0].clone(); - - project::Entity::insert(project.clone().into_active_model()) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - access::Entity::insert(access.clone().into_active_model()) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - - project_context.delete(project.id).await.unwrap(); - - let all_projects = project::Entity::find() - .all(&project_context.db_context.get_connection()) - .await - .unwrap(); - let all_accesses = access::Entity::find() - .all(&project_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(all_projects.len(), 0); - assert_eq!(all_accesses.len(), 0); -} - -#[tokio::test] -async fn delete_cascade_in_use_test() { - let (project_context, project, user) = seed_db().await; - - let session = create_sessions(1, user.clone().id)[0].clone(); - let in_use = create_in_uses(1, project.clone().id, 1)[0].clone(); - - session::Entity::insert(session.clone().into_active_model()) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - project::Entity::insert(project.clone().into_active_model()) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - in_use::Entity::insert(in_use.clone().into_active_model()) - .exec(&project_context.db_context.get_connection()) - .await - .unwrap(); - - project_context.delete(project.id).await.unwrap(); - - let all_projects = project::Entity::find() - .all(&project_context.db_context.get_connection()) - .await - .unwrap(); - let all_in_uses = in_use::Entity::find() - .all(&project_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(all_projects.len(), 0); - assert_eq!(all_in_uses.len(), 0); -} - -#[tokio::test] -async fn delete_non_existing_id_test() { - let (project_context, _, _) = seed_db().await; - - let deleted_project = project_context.delete(1).await; - - assert!(matches!( - deleted_project.unwrap_err(), - DbErr::RecordNotFound(_) - )); -} diff --git a/src/tests/contexts/query_context.rs b/src/tests/contexts/query_context.rs deleted file mode 100644 index 65f6734..0000000 --- a/src/tests/contexts/query_context.rs +++ /dev/null @@ -1,315 +0,0 @@ -use crate::tests::contexts::helpers::{ - create_projects, create_queries, create_users, get_reset_database_context, -}; -use crate::{ - contexts::context_impls::QueryContext, - contexts::context_traits::EntityContextTrait, - entities::{project, query, user}, - to_active_models, -}; -use sea_orm::{entity::prelude::*, IntoActiveModel}; - -async fn seed_db() -> (QueryContext, query::Model, project::Model) { - let db_context = get_reset_database_context().await; - - let query_context = QueryContext::new(db_context); - - let user = create_users(1)[0].clone(); - let project = create_projects(1, user.id)[0].clone(); - let query = create_queries(1, project.id)[0].clone(); - - user::Entity::insert(user.clone().into_active_model()) - .exec(&query_context.db_context.get_connection()) - .await - .unwrap(); - project::Entity::insert(project.clone().into_active_model()) - .exec(&query_context.db_context.get_connection()) - .await - .unwrap(); - - (query_context, query, project) -} - -#[tokio::test] -async fn create_test() { - let (query_context, query, _) = seed_db().await; - - let created_query = query_context.create(query.clone()).await.unwrap(); - - let fetched_query = query::Entity::find_by_id(created_query.id) - .one(&query_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - // Assert if the fetched access is the same as the created access - assert_eq!(query, created_query); - assert_eq!(fetched_query, created_query); -} - -#[tokio::test] -async fn create_default_outdated_test() { - let (query_context, query, _) = seed_db().await; - - let _inserted_query = query_context.create(query.clone()).await.unwrap(); - - let fetched_query = query::Entity::find_by_id(query.project_id) - .one(&query_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - assert!(fetched_query.outdated) -} - -#[tokio::test] -async fn create_auto_increment_test() { - let (query_context, query, _) = seed_db().await; - - let created_query1 = query_context.create(query.clone()).await.unwrap(); - let created_query2 = query_context.create(query.clone()).await.unwrap(); - - let fetched_query1 = query::Entity::find_by_id(created_query1.id) - .one(&query_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - let fetched_query2 = query::Entity::find_by_id(created_query2.id) - .one(&query_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - assert_ne!(fetched_query1.id, fetched_query2.id); - assert_ne!(created_query1.id, created_query2.id); - assert_eq!(created_query1.id, fetched_query1.id); - assert_eq!(created_query2.id, fetched_query2.id); -} - -#[tokio::test] -async fn get_by_id_test() { - let (query_context, query, _) = seed_db().await; - - query::Entity::insert(query.clone().into_active_model()) - .exec(&query_context.db_context.get_connection()) - .await - .unwrap(); - - let fetched_in_use = query_context - .get_by_id(query.project_id) - .await - .unwrap() - .unwrap(); - - assert_eq!(fetched_in_use, query) -} - -#[tokio::test] -async fn get_by_non_existing_id_test() { - let (query_context, _, _) = seed_db().await; - - let query = query_context.get_by_id(1).await; - - assert!(query.unwrap().is_none()) -} - -#[tokio::test] -async fn get_all_test() { - let (query_context, _, project) = seed_db().await; - - let queries = create_queries(10, project.id); - - query::Entity::insert_many(to_active_models!(queries.clone())) - .exec(&query_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(query_context.get_all().await.unwrap().len(), 10); - - let mut sorted = queries.clone(); - sorted.sort_by_key(|k| k.project_id); - - for (i, query) in sorted.into_iter().enumerate() { - assert_eq!(query, queries[i]); - } -} - -#[tokio::test] -async fn get_all_empty_test() { - let (query_context, _, _) = seed_db().await; - - let queries = query_context.get_all().await.unwrap(); - - assert_eq!(0, queries.len()) -} - -#[tokio::test] -async fn update_test() { - let (query_context, query, _) = seed_db().await; - - query::Entity::insert(query.clone().into_active_model()) - .exec(&query_context.db_context.get_connection()) - .await - .unwrap(); - - let new_query = query::Model { ..query }; - - let updated_query = query_context.update(new_query.clone()).await.unwrap(); - - let fetched_query = query::Entity::find_by_id(updated_query.project_id) - .one(&query_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - assert_eq!(new_query, updated_query); - assert_eq!(updated_query, fetched_query); -} - -#[tokio::test] -async fn update_modifies_string_test() { - let (query_context, query, _) = seed_db().await; - - query::Entity::insert(query.clone().into_active_model()) - .exec(&query_context.db_context.get_connection()) - .await - .unwrap(); - - let new_query = query::Model { - string: query.clone().string + "123", - ..query.clone() - }; - - let updated_query = query_context.update(new_query.clone()).await.unwrap(); - - assert_ne!(query, updated_query); - assert_ne!(query, new_query); -} - -#[tokio::test] -async fn update_modifies_outdated_test() { - let (query_context, query, _) = seed_db().await; - - query::Entity::insert(query.clone().into_active_model()) - .exec(&query_context.db_context.get_connection()) - .await - .unwrap(); - - let new_query = query::Model { - outdated: !query.clone().outdated, - ..query.clone() - }; - - let updated_query = query_context.update(new_query.clone()).await.unwrap(); - - assert_ne!(query, updated_query); - assert_ne!(query, new_query); -} - -#[tokio::test] -async fn update_modifies_result_test() { - let (query_context, mut query, _) = seed_db().await; - - query.result = Some("{}".to_owned().parse().unwrap()); - - query::Entity::insert(query.clone().into_active_model()) - .exec(&query_context.db_context.get_connection()) - .await - .unwrap(); - - let new_query = query::Model { - result: None, - ..query.clone() - }; - - let updated_query = query_context.update(new_query.clone()).await.unwrap(); - - assert_ne!(query, updated_query); - assert_ne!(query, new_query); -} - -#[tokio::test] -async fn update_does_not_modify_id_test() { - let (query_context, query, _) = seed_db().await; - - query::Entity::insert(query.clone().into_active_model()) - .exec(&query_context.db_context.get_connection()) - .await - .unwrap(); - - let new_query = query::Model { - id: query.id + 1, - ..query.clone() - }; - - let updated_query = query_context.update(new_query.clone()).await; - - assert!(matches!( - updated_query.unwrap_err(), - DbErr::RecordNotUpdated - )); -} - -#[tokio::test] -async fn update_does_not_modify_project_id_test() { - let (query_context, query, _) = seed_db().await; - - query::Entity::insert(query.clone().into_active_model()) - .exec(&query_context.db_context.get_connection()) - .await - .unwrap(); - - let new_query = query::Model { - project_id: query.project_id + 1, - ..query.clone() - }; - - let updated_query = query_context.update(new_query.clone()).await.unwrap(); - - assert_eq!(query, updated_query); -} - -#[tokio::test] -async fn update_non_existing_id_test() { - let (query_context, query, _) = seed_db().await; - - let updated_query = query_context.update(query.clone()).await; - - assert!(matches!( - updated_query.unwrap_err(), - DbErr::RecordNotUpdated - )); -} - -#[tokio::test] -async fn delete_test() { - let (query_context, query, _) = seed_db().await; - - query::Entity::insert(query.clone().into_active_model()) - .exec(&query_context.db_context.get_connection()) - .await - .unwrap(); - - let deleted_query = query_context.delete(query.project_id).await.unwrap(); - - let all_queries = query::Entity::find() - .all(&query_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(query, deleted_query); - assert!(all_queries.is_empty()); -} - -#[tokio::test] -async fn delete_non_existing_id_test() { - let (query_context, _, _) = seed_db().await; - - let deleted_query = query_context.delete(1).await; - - assert!(matches!( - deleted_query.unwrap_err(), - DbErr::RecordNotFound(_) - )) -} diff --git a/src/tests/contexts/session_context.rs b/src/tests/contexts/session_context.rs deleted file mode 100644 index 5fed383..0000000 --- a/src/tests/contexts/session_context.rs +++ /dev/null @@ -1,422 +0,0 @@ -use crate::{api::auth::TokenType, tests::contexts::helpers::*}; -use sea_orm::{entity::prelude::*, IntoActiveModel}; -use std::ops::Add; - -use crate::{ - contexts::context_impls::SessionContext, - contexts::context_traits::{EntityContextTrait, SessionContextTrait}, - entities::{in_use, project, session, user}, - to_active_models, -}; - -use chrono::{Duration, Utc}; - -async fn seed_db() -> (SessionContext, session::Model, user::Model, project::Model) { - let db_context = get_reset_database_context().await; - - let session_context = SessionContext::new(db_context); - - let user = create_users(1)[0].clone(); - let project = create_projects(1, user.id)[0].clone(); - let session = create_sessions(1, user.id)[0].clone(); - - user::Entity::insert(user.clone().into_active_model()) - .exec(&session_context.db_context.get_connection()) - .await - .unwrap(); - project::Entity::insert(project.clone().into_active_model()) - .exec(&session_context.db_context.get_connection()) - .await - .unwrap(); - - (session_context, session, user, project) -} - -#[tokio::test] -async fn create_test() { - // Setting up a sqlite contexts in memory. - let (session_context, mut session, _, _) = seed_db().await; - - let created_session = session_context.create(session.clone()).await.unwrap(); - - session.updated_at = created_session.updated_at; - - let fetched_session = session::Entity::find_by_id(created_session.id) - .one(&session_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - assert_eq!(session, created_session); - assert_eq!(fetched_session, created_session); -} - -#[tokio::test] -async fn create_default_created_at_test() { - let t_min = Utc::now().timestamp(); - - let (session_context, session, _, _) = seed_db().await; - - let _created_session = session_context.create(session.clone()).await.unwrap(); - - let fetched_session = session::Entity::find_by_id(1) - .one(&session_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - let t_max = Utc::now().timestamp(); - let t_actual = fetched_session.clone().updated_at.timestamp(); - - assert!(t_min <= t_actual && t_actual <= t_max) -} - -#[tokio::test] -async fn create_auto_increment_test() { - // Setting up contexts and session context - let (session_context, _, user, _) = seed_db().await; - - let sessions = create_sessions(2, user.id); - - // Creates the sessions in the contexts using the 'create' function - let created_session1 = session_context.create(sessions[0].clone()).await.unwrap(); - let created_session2 = session_context.create(sessions[1].clone()).await.unwrap(); - - let fetched_session1 = session::Entity::find_by_id(created_session1.id) - .one(&session_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - let fetched_session2 = session::Entity::find_by_id(created_session2.id) - .one(&session_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - // Assert if the new_session, created_session, and fetched_session are the same - assert_ne!(fetched_session1.id, fetched_session2.id); - assert_ne!(created_session1.id, created_session2.id); - assert_eq!(created_session1.id, fetched_session1.id); - assert_eq!(created_session2.id, fetched_session2.id); -} - -#[tokio::test] -async fn create_non_unique_refresh_token_test() { - let (session_context, _, _, user) = seed_db().await; - - let mut sessions = create_sessions(2, user.id); - - sessions[1].refresh_token = sessions[0].refresh_token.clone(); - - let _created_session1 = session_context.create(sessions[0].clone()).await.unwrap(); - let created_session2 = session_context.create(sessions[1].clone()).await; - - assert!(matches!( - created_session2.unwrap_err().sql_err(), - Some(SqlErr::UniqueConstraintViolation(_)) - )); -} - -#[tokio::test] -async fn get_by_id_test() { - let (session_context, session, _, _) = seed_db().await; - - session::Entity::insert(session.clone().into_active_model()) - .exec(&session_context.db_context.get_connection()) - .await - .unwrap(); - - let fetched_session = session_context - .get_by_id(session.id) - .await - .unwrap() - .unwrap(); - - assert_eq!(session, fetched_session); -} - -#[tokio::test] -async fn get_by_non_existing_id_test() { - let (session_context, _, _, _) = seed_db().await; - - let fetched_session = session_context.get_by_id(1).await.unwrap(); - - assert!(fetched_session.is_none()); -} - -#[tokio::test] -async fn get_all_test() { - let (session_context, _, user, _) = seed_db().await; - - let new_sessions = create_sessions(3, user.id); - - session::Entity::insert_many(to_active_models!(new_sessions.clone())) - .exec(&session_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(session_context.get_all().await.unwrap().len(), 3); - - let mut sorted: Vec = new_sessions.clone(); - sorted.sort_by_key(|k| k.id); - - for (i, session) in sorted.into_iter().enumerate() { - assert_eq!(session, new_sessions[i]); - } -} - -#[tokio::test] -async fn get_all_empty_test() { - let (session_context, _, _, _) = seed_db().await; - - let result = session_context.get_all().await.unwrap(); - let empty_accesses: Vec = vec![]; - - assert_eq!(empty_accesses, result); -} - -#[tokio::test] -async fn update_test() { - let (session_context, session, _, _) = seed_db().await; - - session::Entity::insert(session.clone().into_active_model()) - .exec(&session_context.db_context.get_connection()) - .await - .unwrap(); - - //A session has nothing to update - let mut new_session = session::Model { ..session }; - - let mut updated_session = session_context.update(new_session.clone()).await.unwrap(); - - let fetched_session = session::Entity::find_by_id(updated_session.id) - .one(&session_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - new_session.updated_at = fetched_session.updated_at; - updated_session.updated_at = fetched_session.updated_at; - - assert_eq!(new_session, updated_session); - assert_eq!(updated_session, fetched_session); -} - -#[tokio::test] -async fn update_does_not_modify_id_test() { - let (session_context, session, _, _) = seed_db().await; - session::Entity::insert(session.clone().into_active_model()) - .exec(&session_context.db_context.get_connection()) - .await - .unwrap(); - - let updated_session = session::Model { - id: &session.id + 1, - ..session.clone() - }; - let res = session_context.update(updated_session.clone()).await; - - assert!(matches!(res.unwrap_err(), DbErr::RecordNotUpdated)); -} - -#[tokio::test] -async fn update_does_modifies_updated_at_automatically_test() { - let (session_context, mut session, _, _) = seed_db().await; - session::Entity::insert(session.clone().into_active_model()) - .exec(&session_context.db_context.get_connection()) - .await - .unwrap(); - - let updated_session = session::Model { - updated_at: session.clone().updated_at.add(Duration::seconds(1)), - ..session.clone() - }; - let res = session_context - .update(updated_session.clone()) - .await - .unwrap(); - - assert!(session.updated_at < res.updated_at); - - session.updated_at = res.updated_at; - - assert_eq!(session, res); -} - -#[tokio::test] -async fn update_does_not_modify_user_id_test() { - let (session_context, mut session, _, _) = seed_db().await; - session::Entity::insert(session.clone().into_active_model()) - .exec(&session_context.db_context.get_connection()) - .await - .unwrap(); - - let updated_session = session::Model { - user_id: &session.user_id + 1, - ..session.clone() - }; - let res = session_context - .update(updated_session.clone()) - .await - .unwrap(); - - session.updated_at = res.updated_at; - - assert_eq!(session, res); -} - -#[tokio::test] -async fn update_non_existing_id_test() { - let (session_context, session, _, _) = seed_db().await; - - let updated_session = session_context.update(session.clone()).await; - - assert!(matches!( - updated_session.unwrap_err(), - DbErr::RecordNotUpdated - )); -} - -#[tokio::test] -async fn delete_test() { - let (session_context, session, _, _) = seed_db().await; - - session::Entity::insert(session.clone().into_active_model()) - .exec(&session_context.db_context.get_connection()) - .await - .unwrap(); - - let deleted_session = session_context.delete(session.id).await.unwrap(); - - let all_sessions = session::Entity::find() - .all(&session_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(session, deleted_session); - assert!(all_sessions.is_empty()); -} - -#[tokio::test] -async fn delete_cascade_in_use_test() { - let (session_context, session, _, project) = seed_db().await; - - let in_use = create_in_uses(1, project.id, session.id)[0].clone(); - - session::Entity::insert(session.clone().into_active_model()) - .exec(&session_context.db_context.get_connection()) - .await - .unwrap(); - in_use::Entity::insert(in_use.clone().into_active_model()) - .exec(&session_context.db_context.get_connection()) - .await - .unwrap(); - - session_context.delete(session.id).await.unwrap(); - - let all_sessions = session::Entity::find() - .all(&session_context.db_context.get_connection()) - .await - .unwrap(); - let all_in_uses = in_use::Entity::find() - .all(&session_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(all_sessions.len(), 0); - assert_eq!(all_in_uses.len(), 0); -} - -#[tokio::test] -async fn delete_non_existing_id_test() { - let (session_context, _, _, _) = seed_db().await; - - let deleted_session = session_context.delete(1).await; - - assert!(matches!( - deleted_session.unwrap_err(), - DbErr::RecordNotFound(_) - )); -} - -#[tokio::test] -async fn get_by_token_refresh_test() { - let (session_context, session, _, _) = seed_db().await; - - session::Entity::insert(session.clone().into_active_model()) - .exec(&session_context.db_context.get_connection()) - .await - .unwrap(); - - let fetched_session = session_context - .get_by_token(TokenType::RefreshToken, session.refresh_token.clone()) - .await - .unwrap(); - - assert_eq!( - fetched_session.unwrap().refresh_token, - session.refresh_token - ); -} - -#[tokio::test] -async fn get_by_token_access_test() { - let (session_context, session, _, _) = seed_db().await; - - session::Entity::insert(session.clone().into_active_model()) - .exec(&session_context.db_context.get_connection()) - .await - .unwrap(); - - let fetched_session = session_context - .get_by_token(TokenType::AccessToken, session.access_token.clone()) - .await - .unwrap(); - - assert_eq!(fetched_session.unwrap().access_token, session.access_token); -} - -#[tokio::test] -async fn delete_by_token_refresh_test() { - let (session_context, session, _, _) = seed_db().await; - - session::Entity::insert(session.clone().into_active_model()) - .exec(&session_context.db_context.get_connection()) - .await - .unwrap(); - - session_context - .delete_by_token(TokenType::RefreshToken, session.refresh_token.clone()) - .await - .unwrap(); - - let fetched_session = session_context - .get_by_token(TokenType::RefreshToken, session.refresh_token.clone()) - .await - .unwrap(); - - assert!(fetched_session.is_none()); -} - -#[tokio::test] -async fn delete_by_token_access_test() { - let (session_context, session, _, _) = seed_db().await; - - session::Entity::insert(session.clone().into_active_model()) - .exec(&session_context.db_context.get_connection()) - .await - .unwrap(); - - session_context - .delete_by_token(TokenType::AccessToken, session.access_token.clone()) - .await - .unwrap(); - - let fetched_session = session_context - .get_by_token(TokenType::AccessToken, session.access_token.clone()) - .await - .unwrap(); - - assert!(fetched_session.is_none()); -} diff --git a/src/tests/contexts/user_context.rs b/src/tests/contexts/user_context.rs deleted file mode 100644 index 96cadab..0000000 --- a/src/tests/contexts/user_context.rs +++ /dev/null @@ -1,531 +0,0 @@ -use crate::tests::contexts::helpers::*; -use crate::{ - contexts::context_impls::UserContext, - contexts::context_traits::{EntityContextTrait, UserContextTrait}, - entities::{access, project, session, user}, - to_active_models, -}; -use sea_orm::{entity::prelude::*, IntoActiveModel}; -use std::matches; - -async fn seed_db() -> (UserContext, user::Model) { - let db_context = get_reset_database_context().await; - - let user_context = UserContext::new(db_context); - - let user = create_users(1)[0].clone(); - - (user_context, user) -} - -// Test the functionality of the 'create' function, which creates a user in the contexts -#[tokio::test] -async fn create_test() { - // Setting up contexts and user context - let (user_context, user) = seed_db().await; - - // Creates the user in the contexts using the 'create' function - let created_user = user_context.create(user.clone()).await.unwrap(); - - let fetched_user = user::Entity::find_by_id(created_user.id) - .one(&user_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - // Assert if the new_user, created_user, and fetched_user are the same - assert_eq!(user, created_user); - assert_eq!(created_user, fetched_user); -} - -#[tokio::test] -async fn create_non_unique_username_test() { - // Setting up contexts and user context - let (user_context, user) = seed_db().await; - - // Creates a model of the user which will be created - let mut users = create_users(2); - - users[0].username = user.clone().username; - users[1].username = user.clone().username; - - // Creates the user in the contexts using the 'create' function - let _created_user1 = user_context.create(users[0].clone()).await.unwrap(); - let created_user2 = user_context.create(users[1].clone()).await; - - assert!(matches!( - created_user2.unwrap_err().sql_err(), - Some(SqlErr::UniqueConstraintViolation(_)) - )); -} - -#[tokio::test] -async fn create_non_unique_email_test() { - // Setting up contexts and user context - let (user_context, user) = seed_db().await; - - // Creates a model of the user which will be created - let mut users = create_users(2); - - users[0].email = user.clone().email; - users[1].email = user.clone().email; - - // Creates the user in the contexts using the 'create' function - let _created_user1 = user_context.create(users[0].clone()).await.unwrap(); - let created_user2 = user_context.create(users[1].clone()).await; - - // Assert if the new_user, created_user, and fetched_user are the same - assert!(matches!( - created_user2.unwrap_err().sql_err(), - Some(SqlErr::UniqueConstraintViolation(_)) - )); -} - -#[tokio::test] -async fn create_auto_increment_test() { - // Setting up contexts and user context - let (user_context, user) = seed_db().await; - - let mut users = create_users(2); - - users[0].id = user.clone().id; - users[1].id = user.clone().id; - - // Creates the user in the contexts using the 'create' function - let created_user1 = user_context.create(users[0].clone()).await.unwrap(); - let created_user2 = user_context.create(users[1].clone()).await.unwrap(); - - let fetched_user1 = user::Entity::find_by_id(created_user1.id) - .one(&user_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - let fetched_user2 = user::Entity::find_by_id(created_user2.id) - .one(&user_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - // Assert if the new_user, created_user, and fetched_user are the same - assert_ne!(fetched_user1.id, fetched_user2.id); - assert_ne!(created_user1.id, created_user2.id); - assert_eq!(created_user1.id, fetched_user1.id); - assert_eq!(created_user2.id, fetched_user2.id); -} - -#[tokio::test] -async fn get_by_id_test() { - // Setting up contexts and user context - let (user_context, user) = seed_db().await; - - // Creates the user in the contexts using the 'create' function - user::Entity::insert(user.clone().into_active_model()) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - - // Fetches the user created using the 'get_by_id' function - let fetched_user = user_context.get_by_id(user.id).await.unwrap().unwrap(); - - // Assert if the new_user, created_user, and fetched_user are the same - assert_eq!(user, fetched_user); -} - -#[tokio::test] -async fn get_by_non_existing_id_test() { - // Setting up contexts and user context - let (user_context, _) = seed_db().await; - - // Fetches the user created using the 'get_by_id' function - let fetched_user = user_context.get_by_id(1).await.unwrap(); - - assert!(fetched_user.is_none()); -} - -#[tokio::test] -async fn get_all_test() { - // Setting up contexts and user context - let (user_context, _) = seed_db().await; - - let users = create_users(10); - let active_users_vec = to_active_models!(users.clone()); - - user::Entity::insert_many(active_users_vec) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(user_context.get_all().await.unwrap().len(), 10); - - let mut sorted = users.clone(); - sorted.sort_by_key(|k| k.id); - - for (i, user) in sorted.into_iter().enumerate() { - assert_eq!(user, users[i]); - } -} - -#[tokio::test] -async fn get_all_empty_test() { - // Setting up contexts and user context - let (user_context, _) = seed_db().await; - - let result = user_context.get_all().await.unwrap(); - let empty_users: Vec = vec![]; - - assert_eq!(empty_users, result); -} - -#[tokio::test] -async fn update_test() { - // Setting up contexts and user context - let (user_context, user) = seed_db().await; - - user::Entity::insert(user.clone().into_active_model()) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - - let new_user = user::Model { ..user }; - - let updated_user = user_context.update(new_user.clone()).await.unwrap(); - - let fetched_user = user::Entity::find_by_id(updated_user.id) - .one(&user_context.db_context.get_connection()) - .await - .unwrap() - .unwrap(); - - assert_eq!(new_user, updated_user); - assert_eq!(updated_user, fetched_user); -} - -#[tokio::test] -async fn update_modifies_username_test() { - let (user_context, user) = seed_db().await; - - let user = user::Model { - username: "tester1".into(), - ..user.clone() - }; - - user::Entity::insert(user.clone().into_active_model()) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - - let new_user = user::Model { - username: "tester2".into(), - ..user.clone() - }; - - let updated_user = user_context.update(new_user.clone()).await.unwrap(); - - assert_ne!(user, updated_user); - assert_ne!(user, new_user); -} - -#[tokio::test] -async fn update_modifies_email_test() { - let (user_context, user) = seed_db().await; - - let user = user::Model { - email: "tester1@mail.dk".into(), - ..user.clone() - }; - - user::Entity::insert(user.clone().into_active_model()) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - - let new_user = user::Model { - email: "tester2@mail.dk".into(), - ..user.clone() - }; - - let updated_user = user_context.update(new_user.clone()).await.unwrap(); - - assert_ne!(user, updated_user); - assert_ne!(user, new_user); -} - -#[tokio::test] -async fn update_modifies_password_test() { - let (user_context, user) = seed_db().await; - - let user = user::Model { - password: "12345".into(), - ..user.clone() - }; - - user::Entity::insert(user.clone().into_active_model()) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - - let new_user = user::Model { - password: "123456".into(), - ..user.clone() - }; - - let updated_user = user_context.update(new_user.clone()).await.unwrap(); - - assert_ne!(user, updated_user); - assert_ne!(user, new_user); -} - -#[tokio::test] -async fn update_does_not_modify_id_test() { - let (user_context, user) = seed_db().await; - - user::Entity::insert(user.clone().into_active_model()) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - - let updated_user = user::Model { - id: user.id + 1, - ..user - }; - - let res = user_context.update(updated_user.clone()).await; - - assert!(matches!(res.unwrap_err(), DbErr::RecordNotUpdated)); -} - -#[tokio::test] -async fn update_non_unique_username_test() { - // Setting up contexts and user context - let (user_context, _) = seed_db().await; - - let users = create_users(2); - - user::Entity::insert_many(to_active_models!(users.clone())) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - - let new_user = user::Model { - username: users[1].clone().username, - ..users[0].clone() - }; - - let updated_user = user_context.update(new_user.clone()).await; - - // Assert if the new_user, created_user, and fetched_user are the same - assert!(matches!( - updated_user.unwrap_err().sql_err(), - Some(SqlErr::UniqueConstraintViolation(_)) - )); -} - -#[tokio::test] -async fn update_non_unique_email_test() { - // Setting up contexts and user context - let (user_context, _) = seed_db().await; - - // Creates a model of the user which will be created - let users = create_users(2); - - user::Entity::insert_many(to_active_models!(users.clone())) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - - let new_user = user::Model { - email: users[1].clone().email, - ..users[0].clone() - }; - - let updated_user = user_context.update(new_user.clone()).await; - - // Assert if the new_user, created_user, and fetched_user are the same - assert!(matches!( - updated_user.unwrap_err().sql_err(), - Some(SqlErr::UniqueConstraintViolation(_)) - )); -} - -#[tokio::test] -async fn update_non_existing_id_test() { - // Setting up contexts and user context - let (user_context, user) = seed_db().await; - - let updated_user = user_context.update(user.clone()).await; - - // Assert if the new_user, created_user, and fetched_user are the same - assert!(matches!(updated_user.unwrap_err(), DbErr::RecordNotUpdated)); -} - -#[tokio::test] -async fn delete_test() { - // Setting up contexts and user context - let (user_context, user) = seed_db().await; - - user::Entity::insert(user.clone().into_active_model()) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - - let deleted_user = user_context.delete(user.id).await.unwrap(); - - let all_users = user::Entity::find() - .all(&user_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(user, deleted_user); - assert!(all_users.is_empty()); -} - -#[tokio::test] -async fn delete_cascade_project_test() { - // Setting up contexts and user context - let (user_context, user) = seed_db().await; - - let project = create_projects(1, user.clone().id)[0].clone(); - - user::Entity::insert(user.clone().into_active_model()) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - project::Entity::insert(project.clone().into_active_model()) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - - user_context.delete(user.id).await.unwrap(); - - let all_users = user::Entity::find() - .all(&user_context.db_context.get_connection()) - .await - .unwrap(); - let all_projects = project::Entity::find() - .all(&user_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(all_users.len(), 0); - assert_eq!(all_projects.len(), 0); -} - -#[tokio::test] -async fn delete_cascade_access_test() { - // Setting up contexts and user context - let (user_context, user) = seed_db().await; - - let project = create_projects(1, user.clone().id)[0].clone(); - let access = create_accesses(1, user.clone().id, project.clone().id)[0].clone(); - - user::Entity::insert(user.clone().into_active_model()) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - project::Entity::insert(project.clone().into_active_model()) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - access::Entity::insert(access.clone().into_active_model()) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - - user_context.delete(user.id).await.unwrap(); - - let all_users = user::Entity::find() - .all(&user_context.db_context.get_connection()) - .await - .unwrap(); - let all_projects = project::Entity::find() - .all(&user_context.db_context.get_connection()) - .await - .unwrap(); - let all_accesses = access::Entity::find() - .all(&user_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(all_users.len(), 0); - assert_eq!(all_projects.len(), 0); - assert_eq!(all_accesses.len(), 0); -} - -#[tokio::test] -async fn delete_cascade_session_test() { - // Setting up contexts and user context - let (user_context, user) = seed_db().await; - - let session = create_sessions(1, user.clone().id)[0].clone(); - - user::Entity::insert(user.clone().into_active_model()) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - session::Entity::insert(session.clone().into_active_model()) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - - user_context.delete(user.id).await.unwrap(); - - let all_users = user::Entity::find() - .all(&user_context.db_context.get_connection()) - .await - .unwrap(); - let all_sessions = session::Entity::find() - .all(&user_context.db_context.get_connection()) - .await - .unwrap(); - - assert_eq!(all_users.len(), 0); - assert_eq!(all_sessions.len(), 0); -} - -#[tokio::test] -async fn delete_non_existing_id_test() { - // Setting up contexts and user context - let (user_context, _) = seed_db().await; - - let deleted_user = user_context.delete(1).await; - - // Assert if the new_user, created_user, and fetched_user are the same - assert!(matches!( - deleted_user.unwrap_err(), - DbErr::RecordNotFound(_) - )); -} - -#[tokio::test] -async fn get_by_username_test() { - let (user_context, user) = seed_db().await; - - user::Entity::insert(user.clone().into_active_model()) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - - // Fetches the user created using the 'get_by_username' function - let fetched_user = user_context - .get_by_username(user.username.clone()) - .await - .unwrap(); - - // Assert if the fetched user is the same as the created user - assert_eq!(fetched_user.unwrap().username, user.username); -} - -#[tokio::test] -async fn get_by_email_test() { - let (user_context, user) = seed_db().await; - - user::Entity::insert(user.clone().into_active_model()) - .exec(&user_context.db_context.get_connection()) - .await - .unwrap(); - - let fetched_user = user_context.get_by_email(user.email.clone()).await.unwrap(); - - assert_eq!(fetched_user.unwrap().email, user.email); -} diff --git a/src/tests/controllers/access_controller.rs b/src/tests/controllers/access_controller.rs deleted file mode 100644 index 46fbe2d..0000000 --- a/src/tests/controllers/access_controller.rs +++ /dev/null @@ -1,523 +0,0 @@ -use crate::api::server::protobuf::create_access_request::User; -use crate::api::server::protobuf::{ - AccessInfo, CreateAccessRequest, DeleteAccessRequest, ListAccessInfoRequest, - UpdateAccessRequest, -}; -use crate::controllers::controller_impls::AccessController; -use crate::controllers::controller_traits::AccessControllerTrait; -use crate::entities::{access, project, user}; -use crate::tests::controllers::helpers::{disguise_context_mocks, get_mock_contexts}; -use mockall::predicate; -use sea_orm::DbErr; -use std::str::FromStr; -use tonic::{metadata, Code, Request}; - -#[tokio::test] -async fn create_invalid_access_returns_err() { - let mut mock_contexts = get_mock_contexts(); - - let access = access::Model { - id: Default::default(), - role: "Editor".to_string(), - project_id: 1, - user_id: 1, - }; - - mock_contexts - .access_context_mock - .expect_create() - .with(predicate::eq(access.clone())) - .returning(move |_| Err(DbErr::RecordNotInserted)); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(1)) - .returning(move |_, _| { - Ok(Some(access::Model { - id: Default::default(), - role: "Editor".to_owned(), - user_id: 1, - project_id: 1, - })) - }); - - mock_contexts - .user_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| { - Ok(Some(user::Model { - id: 1, - email: Default::default(), - username: "test".to_string(), - password: "test".to_string(), - })) - }); - - let mut request = Request::new(CreateAccessRequest { - role: "Editor".to_string(), - project_id: 1, - user: Some(User::UserId(1)), - }); - - request.metadata_mut().insert( - "uid", - tonic::metadata::MetadataValue::from_str("1").unwrap(), - ); - - let contexts = disguise_context_mocks(mock_contexts); - let access_logic = AccessController::new(contexts); - - let res = access_logic.create_access(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::Internal); -} - -#[tokio::test] -async fn create_access_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - - let access = access::Model { - id: Default::default(), - role: "Editor".to_string(), - project_id: 1, - user_id: 1, - }; - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(1)) - .returning(move |_, _| { - Ok(Some(access::Model { - id: Default::default(), - role: "Editor".to_string(), - user_id: 1, - project_id: 1, - })) - }); - - mock_contexts - .access_context_mock - .expect_create() - .with(predicate::eq(access.clone())) - .returning(move |_| Ok(access.clone())); - - mock_contexts - .user_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| { - Ok(Some(user::Model { - id: 1, - email: Default::default(), - username: "test".to_string(), - password: "test".to_string(), - })) - }); - - let mut request = Request::new(CreateAccessRequest { - role: "Editor".to_string(), - project_id: 1, - user: Some(User::UserId(1)), - }); - - request.metadata_mut().insert( - "uid", - tonic::metadata::MetadataValue::from_str("1").unwrap(), - ); - - let contexts = disguise_context_mocks(mock_contexts); - let access_logic = AccessController::new(contexts); - - let res = access_logic.create_access(request).await; - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn update_invalid_access_returns_err() { - let mut mock_contexts = get_mock_contexts(); - - let access = access::Model { - id: 2, - role: "Editor".to_string(), - project_id: Default::default(), - user_id: Default::default(), - }; - - mock_contexts - .access_context_mock - .expect_update() - .with(predicate::eq(access.clone())) - .returning(move |_| Err(DbErr::RecordNotUpdated)); - - mock_contexts - .access_context_mock - .expect_get_by_id() - .with(predicate::eq(2)) - .returning(move |_| { - Ok(Some(access::Model { - id: 1, - role: "Editor".to_string(), - project_id: 1, - user_id: 2, - })) - }); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(1)) - .returning(move |_, _| { - Ok(Some(access::Model { - id: 1, - role: "Editor".to_string(), - project_id: 1, - user_id: 1, - })) - }); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| { - Ok(Some(project::Model { - id: 1, - name: "test".to_string(), - owner_id: 1, - components_info: Default::default(), - })) - }); - - let mut request = Request::new(UpdateAccessRequest { - id: 2, - role: "Editor".to_string(), - }); - - request.metadata_mut().insert( - "uid", - tonic::metadata::MetadataValue::from_str("1").unwrap(), - ); - - let contexts = disguise_context_mocks(mock_contexts); - let access_logic = AccessController::new(contexts); - - let res = access_logic.update_access(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::Internal); -} - -#[tokio::test] -async fn update_access_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - - let access = access::Model { - id: 2, - role: "Editor".to_string(), - project_id: Default::default(), - user_id: Default::default(), - }; - - mock_contexts - .access_context_mock - .expect_update() - .with(predicate::eq(access.clone())) - .returning(move |_| Ok(access.clone())); - - mock_contexts - .access_context_mock - .expect_get_by_id() - .with(predicate::eq(2)) - .returning(move |_| { - Ok(Some(access::Model { - id: 1, - role: "Editor".to_string(), - project_id: 1, - user_id: 2, - })) - }); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(1)) - .returning(move |_, _| { - Ok(Some(access::Model { - id: 1, - role: "Editor".to_string(), - project_id: 1, - user_id: 1, - })) - }); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| { - Ok(Some(project::Model { - id: 1, - name: "test".to_string(), - owner_id: 1, - components_info: Default::default(), - })) - }); - - let mut request = Request::new(UpdateAccessRequest { - id: 2, - role: "Editor".to_string(), - }); - - request.metadata_mut().insert( - "uid", - tonic::metadata::MetadataValue::from_str("1").unwrap(), - ); - - let contexts = disguise_context_mocks(mock_contexts); - let access_logic = AccessController::new(contexts); - - let res = access_logic.update_access(request).await; - - print!("{:?}", res); - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn delete_invalid_access_returns_err() { - let mut mock_contexts = get_mock_contexts(); - - mock_contexts - .access_context_mock - .expect_delete() - .with(predicate::eq(2)) - .returning(move |_| Err(DbErr::RecordNotFound("".to_string()))); - - mock_contexts - .access_context_mock - .expect_get_by_id() - .with(predicate::eq(2)) - .returning(move |_| { - Ok(Some(access::Model { - id: 1, - role: "Editor".to_string(), - project_id: 1, - user_id: 2, - })) - }); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(1)) - .returning(move |_, _| { - Ok(Some(access::Model { - id: 1, - role: "Editor".to_string(), - project_id: 1, - user_id: 1, - })) - }); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| { - Ok(Some(project::Model { - id: 1, - name: "test".to_string(), - owner_id: 1, - components_info: Default::default(), - })) - }); - - let mut request = Request::new(DeleteAccessRequest { id: 2 }); - - request.metadata_mut().insert( - "uid", - tonic::metadata::MetadataValue::from_str("1").unwrap(), - ); - - let contexts = disguise_context_mocks(mock_contexts); - let access_logic = AccessController::new(contexts); - - let res = access_logic.delete_access(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::NotFound); -} - -#[tokio::test] -async fn delete_access_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - - let access = access::Model { - id: 2, - role: "Editor".to_string(), - project_id: Default::default(), - user_id: Default::default(), - }; - - mock_contexts - .access_context_mock - .expect_delete() - .with(predicate::eq(2)) - .returning(move |_| Ok(access.clone())); - - mock_contexts - .access_context_mock - .expect_get_by_id() - .with(predicate::eq(2)) - .returning(move |_| { - Ok(Some(access::Model { - id: 1, - role: "Editor".to_string(), - project_id: 1, - user_id: 2, - })) - }); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(1)) - .returning(move |_, _| { - Ok(Some(access::Model { - id: 1, - role: "Editor".to_string(), - project_id: 1, - user_id: 1, - })) - }); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| { - Ok(Some(project::Model { - id: 1, - name: "test".to_string(), - owner_id: 1, - components_info: Default::default(), - })) - }); - - let mut request = Request::new(DeleteAccessRequest { id: 2 }); - - request.metadata_mut().insert( - "uid", - tonic::metadata::MetadataValue::from_str("1").unwrap(), - ); - - let contexts = disguise_context_mocks(mock_contexts); - let access_logic = AccessController::new(contexts); - - let res = access_logic.delete_access(request).await; - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn list_access_info_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - - let mut request: Request = - Request::new(ListAccessInfoRequest { project_id: 1 }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let access = AccessInfo { - id: 1, - role: "Editor".to_string(), - project_id: 1, - user_id: 1, - }; - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .returning(move |_, _| { - Ok(Some(access::Model { - id: 1, - role: "Editor".to_string(), - project_id: Default::default(), - user_id: Default::default(), - })) - }); - - mock_contexts - .access_context_mock - .expect_get_access_by_project_id() - .returning(move |_| Ok(vec![access.clone()])); - - let contexts = disguise_context_mocks(mock_contexts); - let access_logic = AccessController::new(contexts); - - let res = access_logic.list_access_info(request).await; - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn list_access_info_returns_not_found() { - let mut mock_contexts = get_mock_contexts(); - - let mut request = Request::new(ListAccessInfoRequest { project_id: 1 }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let access = access::Model { - id: 1, - role: "Editor".to_string(), - project_id: 1, - user_id: 1, - }; - - mock_contexts - .access_context_mock - .expect_get_access_by_project_id() - .returning(move |_| Ok(vec![])); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .returning(move |_, _| Ok(Some(access.clone()))); - - let contexts = disguise_context_mocks(mock_contexts); - let access_logic = AccessController::new(contexts); - - let res = access_logic.list_access_info(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::NotFound); -} - -#[tokio::test] -async fn list_access_info_returns_no_permission() { - let mut request = Request::new(ListAccessInfoRequest { project_id: 1 }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let mut mock_contexts = get_mock_contexts(); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .returning(move |_, _| Ok(None)); - - let contexts = disguise_context_mocks(mock_contexts); - let access_logic = AccessController::new(contexts); - - let res = access_logic.list_access_info(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::PermissionDenied); -} diff --git a/src/tests/controllers/helpers.rs b/src/tests/controllers/helpers.rs deleted file mode 100644 index 49543c3..0000000 --- a/src/tests/controllers/helpers.rs +++ /dev/null @@ -1,194 +0,0 @@ -#![cfg(test)] - -use crate::api::auth::TokenType; -use crate::api::server::protobuf::AccessInfo; -use crate::api::server::protobuf::ProjectInfo; -use crate::api::server::protobuf::{ - QueryRequest, QueryResponse, SimulationStartRequest, SimulationStepRequest, - SimulationStepResponse, UserTokenResponse, -}; -use crate::contexts::context_collection::ContextCollection; -use crate::contexts::context_traits::*; -use crate::entities::{access, in_use, project, query, session, user}; -use crate::services::service_collection::ServiceCollection; -use crate::services::service_traits::*; -use async_trait::async_trait; -use mockall::mock; -use sea_orm::DbErr; -use std::sync::Arc; -use tonic::{Request, Response, Status}; - -pub fn get_mock_contexts() -> MockContexts { - MockContexts { - access_context_mock: MockAccessContext::new(), - in_use_context_mock: MockInUseContext::new(), - project_context_mock: MockProjectContext::new(), - query_context_mock: MockQueryContext::new(), - session_context_mock: MockSessionContext::new(), - user_context_mock: MockUserContext::new(), - } -} - -pub fn get_mock_services() -> MockServices { - MockServices { - hashing_service_mock: MockHashingService::new(), - reveaal_service_mock: MockReveaalService::new(), - } -} - -pub fn disguise_context_mocks(mock_services: MockContexts) -> ContextCollection { - ContextCollection { - access_context: Arc::new(mock_services.access_context_mock), - in_use_context: Arc::new(mock_services.in_use_context_mock), - project_context: Arc::new(mock_services.project_context_mock), - query_context: Arc::new(mock_services.query_context_mock), - session_context: Arc::new(mock_services.session_context_mock), - user_context: Arc::new(mock_services.user_context_mock), - } -} - -pub fn disguise_service_mocks(mock_services: MockServices) -> ServiceCollection { - ServiceCollection { - hashing_service: Arc::new(mock_services.hashing_service_mock), - reveaal_service: Arc::new(mock_services.reveaal_service_mock), - } -} - -pub struct MockContexts { - pub(crate) access_context_mock: MockAccessContext, - pub(crate) in_use_context_mock: MockInUseContext, - pub(crate) project_context_mock: MockProjectContext, - pub(crate) query_context_mock: MockQueryContext, - pub(crate) session_context_mock: MockSessionContext, - pub(crate) user_context_mock: MockUserContext, -} - -pub struct MockServices { - pub(crate) hashing_service_mock: MockHashingService, - pub(crate) reveaal_service_mock: MockReveaalService, -} - -mock! { - pub AccessContext {} - #[async_trait] - impl EntityContextTrait for AccessContext { - async fn create(&self, entity: access::Model) -> Result; - async fn get_by_id(&self, entity_id: i32) -> Result, DbErr>; - async fn get_all(&self) -> Result, DbErr>; - async fn update(&self, entity: access::Model) -> Result; - async fn delete(&self, entity_id: i32) -> Result; - } - #[async_trait] - impl AccessContextTrait for AccessContext { - async fn get_access_by_uid_and_project_id( - &self, - uid: i32, - project_id: i32, - ) -> Result, DbErr>; - - async fn get_access_by_project_id( - &self, - project_id: i32, - ) -> Result, DbErr>; - } -} - -mock! { - pub InUseContext {} - #[async_trait] - impl EntityContextTrait for InUseContext { - async fn create(&self, entity: in_use::Model) -> Result; - async fn get_by_id(&self, entity_id: i32) -> Result, DbErr>; - async fn get_all(&self) -> Result, DbErr>; - async fn update(&self, entity: in_use::Model) -> Result; - async fn delete(&self, entity_id: i32) -> Result; - } - #[async_trait] - impl InUseContextTrait for InUseContext {} -} - -mock! { - pub ProjectContext {} - #[async_trait] - impl EntityContextTrait for ProjectContext { - async fn create(&self, entity: project::Model) -> Result; - async fn get_by_id(&self, entity_id: i32) -> Result, DbErr>; - async fn get_all(&self) -> Result, DbErr>; - async fn update(&self, entity: project::Model) -> Result; - async fn delete(&self, entity_id: i32) -> Result; - } - #[async_trait] - impl ProjectContextTrait for ProjectContext { - async fn get_project_info_by_uid(&self, uid: i32) -> Result, DbErr>; - } -} - -mock! { - pub QueryContext {} - #[async_trait] - impl EntityContextTrait for QueryContext { - async fn create(&self, entity: query::Model) -> Result; - async fn get_by_id(&self, entity_id: i32) -> Result, DbErr>; - async fn get_all(&self) -> Result, DbErr>; - async fn update(&self, entity: query::Model) -> Result; - async fn delete(&self, entity_id: i32) -> Result; - } - #[async_trait] - impl QueryContextTrait for QueryContext { - async fn get_all_by_project_id(&self, project_id: i32) -> Result, DbErr>; - } -} - -mock! { - pub SessionContext {} - #[async_trait] - impl EntityContextTrait for SessionContext { - async fn create(&self, entity: session::Model) -> Result; - async fn get_by_id(&self, entity_id: i32) -> Result, DbErr>; - async fn get_all(&self) -> Result, DbErr>; - async fn update(&self, entity: session::Model) -> Result; - async fn delete(&self, entity_id: i32) -> Result; - } - #[async_trait] - impl SessionContextTrait for SessionContext { - async fn get_by_token(&self, token_type: TokenType, token: String) -> Result, DbErr>; - async fn delete_by_token(&self, token_type: TokenType, token: String) -> Result; - } -} - -mock! { - pub UserContext {} - #[async_trait] - impl EntityContextTrait for UserContext { - async fn create(&self, entity: user::Model) -> Result; - async fn get_by_id(&self, entity_id: i32) -> Result, DbErr>; - async fn get_all(&self) -> Result, DbErr>; - async fn update(&self, entity: user::Model) -> Result; - async fn delete(&self, entity_id: i32) -> Result; - } - #[async_trait] - impl UserContextTrait for UserContext { - async fn get_by_username(&self, username: String) -> Result, DbErr>; - async fn get_by_email(&self, email: String) -> Result, DbErr>; - async fn get_by_ids(&self, ids: Vec) -> Result, DbErr>; - } -} - -mock! { - pub ReveaalService{} - #[async_trait] - impl ReveaalServiceTrait for ReveaalService { - async fn get_user_token(&self,request: Request<()>) -> Result, Status>; - async fn send_query(&self,request: Request) -> Result, Status>; - async fn start_simulation(&self, request: Request) -> Result, Status>; - async fn take_simulation_step(&self, request: Request) -> Result, Status>; - } -} - -mock! { - pub HashingService {} - impl HashingServiceTrait for HashingService { - fn hash_password(&self, password: String) -> Result; - fn verify_password(&self, password: String, hash: &str) -> Result; - } -} diff --git a/src/tests/controllers/mod.rs b/src/tests/controllers/mod.rs deleted file mode 100644 index 1630fab..0000000 --- a/src/tests/controllers/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod helpers; diff --git a/src/tests/controllers/project_controller.rs b/src/tests/controllers/project_controller.rs deleted file mode 100644 index eda3df2..0000000 --- a/src/tests/controllers/project_controller.rs +++ /dev/null @@ -1,1519 +0,0 @@ -use crate::controllers::controller_impls::ProjectController; -use crate::controllers::controller_traits::ProjectControllerTrait; -use crate::tests::controllers::helpers::disguise_context_mocks; -use crate::{ - api::{ - auth::TokenType, - server::protobuf::{ - component::Rep, Component, ComponentsInfo, CreateProjectRequest, DeleteProjectRequest, - GetProjectRequest, ProjectInfo, UpdateProjectRequest, - }, - }, - entities::{access, in_use, project, query, session}, - tests::controllers::helpers::get_mock_contexts, -}; -use chrono::Utc; -use mockall::predicate; -use sea_orm::DbErr; -use std::str::FromStr; -use tonic::{metadata, Code, Request}; - -#[tokio::test] -async fn create_project_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - - let uid = 0; - - let components_info = ComponentsInfo { - components: vec![], - components_hash: 0, - }; - - let project = project::Model { - id: Default::default(), - name: Default::default(), - components_info: serde_json::to_value(components_info.clone()).unwrap(), - owner_id: uid, - }; - - let access = access::Model { - id: Default::default(), - role: "Editor".to_string(), - user_id: uid, - project_id: project.id, - }; - - let session = session::Model { - id: Default::default(), - refresh_token: "refresh_token".to_string(), - access_token: "access_token".to_string(), - updated_at: Default::default(), - user_id: uid, - }; - - let in_use = in_use::Model { - project_id: project.id, - session_id: session.id, - latest_activity: Default::default(), - }; - - mock_contexts - .project_context_mock - .expect_create() - .with(predicate::eq(project.clone())) - .returning(move |_| Ok(project.clone())); - - mock_contexts - .access_context_mock - .expect_create() - .with(predicate::eq(access.clone())) - .returning(move |_| Ok(access.clone())); - - mock_contexts - .session_context_mock - .expect_get_by_token() - .with( - predicate::eq(TokenType::AccessToken), - predicate::eq("access_token".to_string()), - ) - .returning(move |_, _| Ok(Some(session.clone()))); - - mock_contexts - .in_use_context_mock - .expect_create() - .with(predicate::eq(in_use.clone())) - .returning(move |_| Ok(in_use.clone())); - - let mut request = Request::new(CreateProjectRequest { - name: Default::default(), - components_info: Option::from(components_info), - }); - - request - .metadata_mut() - .insert("uid", uid.to_string().parse().unwrap()); - - request.metadata_mut().insert( - "authorization", - metadata::MetadataValue::from_str("Bearer access_token").unwrap(), - ); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.create_project(request).await; - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn create_project_existing_name_returns_err() { - let mut mock_contexts = get_mock_contexts(); - - let uid = 0; - - let project = project::Model { - id: Default::default(), - name: "project".to_string(), - components_info: Default::default(), - owner_id: uid, - }; - - mock_contexts - .project_context_mock - .expect_create() - .with(predicate::eq(project.clone())) - .returning(move |_| Err(DbErr::RecordNotInserted)); //todo!("Needs to be a SqlError with UniqueConstraintViolation with 'name' in message) - - let mut request = Request::new(CreateProjectRequest { - name: "project".to_string(), - components_info: Default::default(), - }); - - request - .metadata_mut() - .insert("uid", uid.to_string().parse().unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.create_project(request).await; - - assert_eq!(res.unwrap_err().code(), Code::InvalidArgument); //todo!("Needs to be code AlreadyExists when mocked Error is corrected) -} - -#[tokio::test] -async fn get_project_user_has_access_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - - let project = project::Model { - id: Default::default(), - name: "project".to_string(), - components_info: Default::default(), - owner_id: 0, - }; - - let access = access::Model { - id: Default::default(), - role: "Editor".to_string(), - project_id: 1, - user_id: 1, - }; - - let in_use = in_use::Model { - project_id: Default::default(), - session_id: 0, - latest_activity: Utc::now().naive_utc(), - }; - - let queries: Vec = vec![]; - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(0), predicate::eq(0)) - .returning(move |_, _| Ok(Some(access.clone()))); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(Some(project.clone()))); - - mock_contexts - .in_use_context_mock - .expect_get_by_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(Some(in_use.clone()))); - - mock_contexts - .query_context_mock - .expect_get_all_by_project_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(queries.clone())); - - let mut request = Request::new(GetProjectRequest { id: 0 }); - - request.metadata_mut().insert("uid", "0".parse().unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.get_project(request).await; - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn delete_not_owner_returns_err() { - let mut mock_contexts = get_mock_contexts(); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| { - Ok(Some(project::Model { - id: 1, - name: Default::default(), - components_info: Default::default(), - owner_id: 2, - })) - }); - - let mut request = Request::new(DeleteProjectRequest { id: 1 }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.delete_project(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::PermissionDenied); -} - -#[tokio::test] -async fn delete_invalid_project_returns_err() { - let mut mock_contexts = get_mock_contexts(); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(2)) - .returning(move |_| Ok(None)); - - let mut request = Request::new(DeleteProjectRequest { id: 2 }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.delete_project(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::NotFound); -} - -#[tokio::test] -async fn delete_project_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| { - Ok(Some(project::Model { - id: 1, - name: Default::default(), - components_info: Default::default(), - owner_id: 1, - })) - }); - - mock_contexts - .project_context_mock - .expect_delete() - .with(predicate::eq(1)) - .returning(move |_| { - Ok(project::Model { - id: 1, - name: Default::default(), - components_info: Default::default(), - owner_id: 1, - }) - }); - - let mut request = Request::new(DeleteProjectRequest { id: 1 }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.delete_project(request).await; - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn get_project_user_has_no_access_returns_err() { - let mut mock_contexts = get_mock_contexts(); - - let project = project::Model { - id: Default::default(), - name: "project".to_string(), - components_info: Default::default(), - owner_id: 0, - }; - - let in_use = in_use::Model { - project_id: Default::default(), - session_id: 0, - latest_activity: Default::default(), - }; - - let queries: Vec = vec![]; - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(0), predicate::eq(0)) - .returning(move |_, _| Ok(None)); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(Some(project.clone()))); - - mock_contexts - .in_use_context_mock - .expect_get_by_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(Some(in_use.clone()))); - - mock_contexts - .query_context_mock - .expect_get_all_by_project_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(queries.clone())); - - let mut request = Request::new(GetProjectRequest { id: 0 }); - - request.metadata_mut().insert("uid", "0".parse().unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.get_project(request).await.unwrap_err(); - - assert!(res.code() == Code::PermissionDenied); -} - -#[tokio::test] -async fn get_project_is_in_use_is_true() { - let mut mock_contexts = get_mock_contexts(); - - let project = project::Model { - id: Default::default(), - name: "project".to_string(), - components_info: Default::default(), - owner_id: 0, - }; - - let access = access::Model { - id: Default::default(), - role: "Editor".to_string(), - project_id: 1, - user_id: 1, - }; - - let in_use = in_use::Model { - project_id: Default::default(), - session_id: 0, - latest_activity: Utc::now().naive_utc(), - }; - - let queries: Vec = vec![]; - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(0), predicate::eq(0)) - .returning(move |_, _| Ok(Some(access.clone()))); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(Some(project.clone()))); - - mock_contexts - .in_use_context_mock - .expect_get_by_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(Some(in_use.clone()))); - - mock_contexts - .query_context_mock - .expect_get_all_by_project_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(queries.clone())); - - let mut request = Request::new(GetProjectRequest { id: 0 }); - - request.metadata_mut().insert("uid", "0".parse().unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.get_project(request).await; - - assert!(res.unwrap().get_ref().in_use); -} - -#[tokio::test] -async fn get_project_is_in_use_is_false() { - let mut mock_contexts = get_mock_contexts(); - - let project = project::Model { - id: Default::default(), - name: "project".to_string(), - components_info: Default::default(), - owner_id: 0, - }; - - let access = access::Model { - id: Default::default(), - role: "Editor".to_string(), - project_id: 1, - user_id: 1, - }; - - let in_use = in_use::Model { - project_id: 0, - session_id: 0, - latest_activity: Default::default(), - }; - - let updated_in_use = in_use::Model { - project_id: 0, - session_id: 1, - latest_activity: Default::default(), - }; - - let session = session::Model { - id: 0, - refresh_token: "refresh_token".to_owned(), - access_token: "access_token".to_owned(), - updated_at: Default::default(), - user_id: Default::default(), - }; - - let queries: Vec = vec![]; - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(0), predicate::eq(0)) - .returning(move |_, _| Ok(Some(access.clone()))); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(Some(project.clone()))); - - mock_contexts - .in_use_context_mock - .expect_get_by_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(Some(in_use.clone()))); - - mock_contexts - .session_context_mock - .expect_get_by_token() - .with( - predicate::eq(TokenType::AccessToken), - predicate::eq("access_token".to_owned()), - ) - .returning(move |_, _| Ok(Some(session.clone()))); - - mock_contexts - .query_context_mock - .expect_get_all_by_project_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(queries.clone())); - - mock_contexts - .in_use_context_mock - .expect_update() - .returning(move |_| Ok(updated_in_use.clone())); - - let mut request = Request::new(GetProjectRequest { id: 0 }); - - request - .metadata_mut() - .insert("authorization", "Bearer access_token".parse().unwrap()); - request.metadata_mut().insert("uid", "0".parse().unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.get_project(request).await; - - assert!(!res.unwrap().get_ref().in_use); -} - -#[tokio::test] -async fn get_project_project_has_no_queries_queries_are_empty() { - let mut mock_contexts = get_mock_contexts(); - - let project = project::Model { - id: Default::default(), - name: "project".to_string(), - components_info: Default::default(), - owner_id: 0, - }; - - let access = access::Model { - id: Default::default(), - role: "Editor".to_string(), - project_id: 1, - user_id: 1, - }; - - let in_use = in_use::Model { - project_id: Default::default(), - session_id: 0, - latest_activity: Utc::now().naive_utc(), - }; - - let queries: Vec = vec![]; - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(0), predicate::eq(0)) - .returning(move |_, _| Ok(Some(access.clone()))); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(Some(project.clone()))); - - mock_contexts - .in_use_context_mock - .expect_get_by_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(Some(in_use.clone()))); - - mock_contexts - .query_context_mock - .expect_get_all_by_project_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(queries.clone())); - - let mut request = Request::new(GetProjectRequest { id: 0 }); - - request.metadata_mut().insert("uid", "0".parse().unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.get_project(request).await; - - assert!(res.unwrap().get_ref().queries.is_empty()); -} - -#[tokio::test] -async fn get_project_query_has_no_result_query_is_empty() { - let mut mock_contexts = get_mock_contexts(); - - let project = project::Model { - id: Default::default(), - name: "project".to_string(), - components_info: Default::default(), - owner_id: 0, - }; - - let access = access::Model { - id: Default::default(), - role: "Editor".to_string(), - project_id: 1, - user_id: 1, - }; - - let in_use = in_use::Model { - project_id: Default::default(), - session_id: 0, - latest_activity: Utc::now().naive_utc(), - }; - - let query = query::Model { - id: 0, - project_id: 1, - string: "query".to_owned(), - result: None, - outdated: false, - }; - - let queries: Vec = vec![query]; - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(0), predicate::eq(0)) - .returning(move |_, _| Ok(Some(access.clone()))); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(Some(project.clone()))); - - mock_contexts - .in_use_context_mock - .expect_get_by_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(Some(in_use.clone()))); - - mock_contexts - .query_context_mock - .expect_get_all_by_project_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(queries.clone())); - - let mut request = Request::new(GetProjectRequest { id: 0 }); - - request.metadata_mut().insert("uid", "0".parse().unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.get_project(request).await; - - assert!(res.unwrap().get_ref().queries[0].result.is_empty()); -} - -#[tokio::test] -async fn list_projects_info_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - - let project_info = ProjectInfo { - project_id: 1, - project_name: "project::Model name".to_owned(), - project_owner_id: 1, - user_role_on_project: "Editor".to_owned(), - }; - - mock_contexts - .project_context_mock - .expect_get_project_info_by_uid() - .with(predicate::eq(1)) - .returning(move |_| Ok(vec![project_info.clone()])); - - let mut list_projects_info_request = Request::new(()); - - list_projects_info_request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic - .list_projects_info(list_projects_info_request) - .await; - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn list_projects_info_returns_err() { - let mut mock_contexts = get_mock_contexts(); - - mock_contexts - .project_context_mock - .expect_get_project_info_by_uid() - .with(predicate::eq(1)) - .returning(move |_| Ok(vec![])); - - let mut list_projects_info_request = Request::new(()); - - list_projects_info_request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic - .list_projects_info(list_projects_info_request) - .await; - - assert!(res.is_err()); -} - -#[tokio::test] -async fn update_name_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - - let user_id = 1; - let project_id = 1; - let new_project_name = "new_name".to_string(); - - let mut update_project_request = Request::new(UpdateProjectRequest { - id: project_id, - name: Some(new_project_name.clone()), - components_info: None, - owner_id: None, - }); - - update_project_request.metadata_mut().insert( - "authorization", - metadata::MetadataValue::from_str("Bearer access_token").unwrap(), - ); - - update_project_request.metadata_mut().insert( - "uid", - metadata::MetadataValue::from_str(user_id.to_string().as_str()).unwrap(), - ); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(project_id)) - .returning(move |_| { - Ok(Some(project::Model { - id: project_id, - name: "old_name".to_owned(), - components_info: Default::default(), - owner_id: user_id, - })) - }); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(project_id)) - .returning(move |_, _| { - Ok(Some(access::Model { - id: 1, - user_id, - project_id, - role: "Editor".to_string(), - })) - }); - - mock_contexts - .session_context_mock - .expect_get_by_token() - .with( - predicate::eq(TokenType::AccessToken), - predicate::eq("access_token".to_string()), - ) - .returning(move |_, _| { - Ok(Some(session::Model { - id: 1, - refresh_token: "refresh_token".to_string(), - access_token: "access_token".to_string(), - updated_at: Default::default(), - user_id, - })) - }); - - mock_contexts - .project_context_mock - .expect_update() - .returning(move |_| { - Ok(project::Model { - id: project_id, - name: new_project_name.clone(), - components_info: Default::default(), - owner_id: user_id, - }) - }); - - mock_contexts - .in_use_context_mock - .expect_get_by_id() - .returning(move |_| { - Ok(Some(in_use::Model { - project_id, - session_id: 1, - latest_activity: Utc::now().naive_utc(), - })) - }); - - mock_contexts - .in_use_context_mock - .expect_update() - .returning(move |_| { - Ok(in_use::Model { - project_id: 1, - session_id: 1, - latest_activity: Utc::now().naive_utc(), - }) - }); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.update_project(update_project_request).await; - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn update_components_info_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - - let user_id = 1; - let project_id = 1; - let components_info_non_json = ComponentsInfo { - components: vec![Component { - rep: Some(Rep::Json("a".to_owned())), - }], - components_hash: 1234456, - }; - let components_info = serde_json::to_value(components_info_non_json.clone()).unwrap(); - - let mut update_project_request = Request::new(UpdateProjectRequest { - id: project_id, - name: None, - components_info: Some(components_info_non_json.clone()), - owner_id: None, - }); - - update_project_request.metadata_mut().insert( - "authorization", - metadata::MetadataValue::from_str("Bearer access_token").unwrap(), - ); - - update_project_request.metadata_mut().insert( - "uid", - metadata::MetadataValue::from_str(user_id.to_string().as_str()).unwrap(), - ); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(project_id)) - .returning(move |_| { - Ok(Some(project::Model { - id: project_id, - name: Default::default(), - components_info: Default::default(), - owner_id: user_id, - })) - }); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(project_id)) - .returning(move |_, _| { - Ok(Some(access::Model { - id: 1, - user_id, - project_id, - role: "Editor".to_string(), - })) - }); - - mock_contexts - .session_context_mock - .expect_get_by_token() - .with( - predicate::eq(TokenType::AccessToken), - predicate::eq("access_token".to_string()), - ) - .returning(move |_, _| { - Ok(Some(session::Model { - id: 1, - refresh_token: "refresh_token".to_string(), - access_token: "access_token".to_string(), - updated_at: Default::default(), - user_id, - })) - }); - - mock_contexts - .project_context_mock - .expect_update() - .returning(move |_| { - Ok(project::Model { - id: project_id, - name: Default::default(), - components_info: components_info.clone(), - owner_id: user_id, - }) - }); - - mock_contexts - .in_use_context_mock - .expect_get_by_id() - .returning(move |_| { - Ok(Some(in_use::Model { - project_id, - session_id: 1, - latest_activity: Utc::now().naive_utc(), - })) - }); - - mock_contexts - .in_use_context_mock - .expect_update() - .returning(move |_| { - Ok(in_use::Model { - project_id: 1, - session_id: 1, - latest_activity: Utc::now().naive_utc(), - }) - }); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.update_project(update_project_request).await; - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn update_owner_id_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - - let user_id = 1; - let project_id = 1; - let new_owner_id = 2; - - let mut update_project_request = Request::new(UpdateProjectRequest { - id: project_id, - name: None, - components_info: None, - owner_id: Some(new_owner_id), - }); - - update_project_request.metadata_mut().insert( - "authorization", - metadata::MetadataValue::from_str("Bearer access_token").unwrap(), - ); - - update_project_request.metadata_mut().insert( - "uid", - metadata::MetadataValue::from_str(user_id.to_string().as_str()).unwrap(), - ); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(project_id)) - .returning(move |_| { - Ok(Some(project::Model { - id: project_id, - name: Default::default(), - components_info: Default::default(), - owner_id: user_id, - })) - }); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(project_id)) - .returning(move |_, _| { - Ok(Some(access::Model { - id: 1, - user_id, - project_id, - role: "Editor".to_string(), - })) - }); - - mock_contexts - .session_context_mock - .expect_get_by_token() - .with( - predicate::eq(TokenType::AccessToken), - predicate::eq("access_token".to_string()), - ) - .returning(move |_, _| { - Ok(Some(session::Model { - id: 1, - refresh_token: "refresh_token".to_string(), - access_token: "access_token".to_string(), - updated_at: Default::default(), - user_id, - })) - }); - - mock_contexts - .project_context_mock - .expect_update() - .returning(move |_| { - Ok(project::Model { - id: project_id, - name: Default::default(), - components_info: Default::default(), - owner_id: new_owner_id, - }) - }); - - mock_contexts - .in_use_context_mock - .expect_get_by_id() - .returning(move |_| { - Ok(Some(in_use::Model { - project_id, - session_id: 1, - latest_activity: Utc::now().naive_utc(), - })) - }); - - mock_contexts - .in_use_context_mock - .expect_update() - .returning(move |_| { - Ok(in_use::Model { - project_id: 1, - session_id: 1, - latest_activity: Utc::now().naive_utc(), - }) - }); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.update_project(update_project_request).await; - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn update_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - - let user_id = 1; - let project_id = 1; - let new_project_name = "new_name".to_string(); - let new_components_info_non_json = ComponentsInfo { - components: vec![Component { - rep: Some(Rep::Json("a".to_owned())), - }], - components_hash: 1234456, - }; - let new_components_info = serde_json::to_value(new_components_info_non_json.clone()).unwrap(); - let new_owner_id = 2; - - let mut update_project_request = Request::new(UpdateProjectRequest { - id: project_id, - name: Some(new_project_name.clone()), - components_info: Some(new_components_info_non_json.clone()), - owner_id: Some(new_owner_id), - }); - - update_project_request.metadata_mut().insert( - "authorization", - metadata::MetadataValue::from_str("Bearer access_token").unwrap(), - ); - - update_project_request.metadata_mut().insert( - "uid", - metadata::MetadataValue::from_str(user_id.to_string().as_str()).unwrap(), - ); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(project_id)) - .returning(move |_| { - Ok(Some(project::Model { - id: project_id, - name: "old_name".to_owned(), - components_info: serde_json::to_value("{\"old_components\":1}").unwrap(), - owner_id: user_id, - })) - }); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(project_id)) - .returning(move |_, _| { - Ok(Some(access::Model { - id: 1, - user_id, - project_id, - role: "Editor".to_string(), - })) - }); - - mock_contexts - .session_context_mock - .expect_get_by_token() - .with( - predicate::eq(TokenType::AccessToken), - predicate::eq("access_token".to_string()), - ) - .returning(move |_, _| { - Ok(Some(session::Model { - id: 1, - refresh_token: "refresh_token".to_string(), - access_token: "access_token".to_string(), - updated_at: Default::default(), - user_id, - })) - }); - - mock_contexts - .project_context_mock - .expect_update() - .returning(move |_| { - Ok(project::Model { - id: project_id, - name: new_project_name.clone(), - components_info: new_components_info.clone(), - owner_id: new_owner_id, - }) - }); - - mock_contexts - .in_use_context_mock - .expect_get_by_id() - .returning(move |_| { - Ok(Some(in_use::Model { - project_id, - session_id: 1, - latest_activity: Utc::now().naive_utc(), - })) - }); - - mock_contexts - .in_use_context_mock - .expect_update() - .returning(move |_| { - Ok(in_use::Model { - project_id: 1, - session_id: 1, - latest_activity: Utc::now().naive_utc(), - }) - }); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.update_project(update_project_request).await; - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn update_owner_not_owner_returns_err() { - let mut mock_contexts = get_mock_contexts(); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| { - Ok(Some(project::Model { - id: 1, - name: Default::default(), - components_info: Default::default(), - owner_id: 2, - })) - }); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(1)) - .returning(move |_, _| { - Ok(Some(access::Model { - id: 1, - user_id: 1, - project_id: 1, - role: "Editor".to_owned(), - })) - }); - - mock_contexts - .session_context_mock - .expect_get_by_token() - .with( - predicate::eq(TokenType::AccessToken), - predicate::eq("access_token".to_string()), - ) - .returning(move |_, _| { - Ok(Some(session::Model { - id: 1, - refresh_token: "refresh_token".to_string(), - access_token: "access_token".to_string(), - updated_at: Default::default(), - user_id: 1, - })) - }); - - mock_contexts - .in_use_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| { - Ok(Some(in_use::Model { - session_id: 1, - latest_activity: Default::default(), - project_id: 1, - })) - }); - - mock_contexts - .in_use_context_mock - .expect_update() - .returning(move |_| { - Ok(in_use::Model { - session_id: 1, - latest_activity: Default::default(), - project_id: 1, - }) - }); - - let mut request = Request::new(UpdateProjectRequest { - id: 1, - name: None, - components_info: None, - owner_id: Some(1), - }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - request.metadata_mut().insert( - "authorization", - metadata::MetadataValue::from_str("access_token").unwrap(), - ); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.update_project(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::PermissionDenied); -} - -#[tokio::test] -async fn update_no_in_use_returns_err() { - let mut mock_contexts = get_mock_contexts(); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| { - Ok(Some(project::Model { - id: 1, - name: Default::default(), - components_info: Default::default(), - owner_id: 1, - })) - }); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(1)) - .returning(move |_, _| { - Ok(Some(access::Model { - id: 1, - user_id: 1, - project_id: 1, - role: "Editor".to_owned(), - })) - }); - - mock_contexts - .session_context_mock - .expect_get_by_token() - .with( - predicate::eq(TokenType::AccessToken), - predicate::eq("access_token".to_string()), - ) - .returning(move |_, _| { - Ok(Some(session::Model { - id: 1, - refresh_token: "refresh_token".to_string(), - access_token: "access_token".to_string(), - updated_at: Default::default(), - user_id: 1, - })) - }); - - mock_contexts - .in_use_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| { - Ok(Some(in_use::Model { - session_id: 2, - latest_activity: Utc::now().naive_utc(), - project_id: 1, - })) - }); - - let mut request = Request::new(UpdateProjectRequest { - id: 1, - name: None, - components_info: None, - owner_id: None, - }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - request.metadata_mut().insert( - "authorization", - metadata::MetadataValue::from_str("access_token").unwrap(), - ); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.update_project(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::FailedPrecondition); -} - -#[tokio::test] -async fn update_no_access_returns_err() { - let mut mock_contexts = get_mock_contexts(); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| { - Ok(Some(project::Model { - id: 1, - name: Default::default(), - components_info: Default::default(), - owner_id: 1, - })) - }); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(1)) - .returning(move |_, _| Ok(None)); - - let mut request = Request::new(UpdateProjectRequest { - id: 1, - name: None, - components_info: None, - owner_id: None, - }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.update_project(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::PermissionDenied); -} - -#[tokio::test] -async fn update_incorrect_role_returns_err() { - let mut mock_contexts = get_mock_contexts(); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| { - Ok(Some(project::Model { - id: 1, - name: Default::default(), - components_info: Default::default(), - owner_id: 1, - })) - }); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(1)) - .returning(move |_, _| { - Ok(Some(access::Model { - id: 1, - user_id: 1, - project_id: 1, - role: "Viewer".to_owned(), - })) - }); - - let mut request = Request::new(UpdateProjectRequest { - id: 1, - name: None, - components_info: None, - owner_id: None, - }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.update_project(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::PermissionDenied); -} - -#[tokio::test] -async fn update_no_session_returns_err() { - let mut mock_contexts = get_mock_contexts(); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| { - Ok(Some(project::Model { - id: 1, - name: Default::default(), - components_info: Default::default(), - owner_id: 1, - })) - }); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(1)) - .returning(move |_, _| { - Ok(Some(access::Model { - id: 1, - user_id: 1, - project_id: 1, - role: "Editor".to_owned(), - })) - }); - - mock_contexts - .session_context_mock - .expect_get_by_token() - .with( - predicate::eq(TokenType::AccessToken), - predicate::eq("access_token".to_string()), - ) - .returning(move |_, _| Ok(None)); - - let mut request = Request::new(UpdateProjectRequest { - id: 1, - name: None, - components_info: None, - owner_id: None, - }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - request.metadata_mut().insert( - "authorization", - metadata::MetadataValue::from_str("access_token").unwrap(), - ); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.update_project(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::Unauthenticated); -} - -#[tokio::test] -async fn update_no_project_returns_err() { - let mut mock_contexts = get_mock_contexts(); - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(2)) - .returning(move |_| Ok(None)); - - let mut request = Request::new(UpdateProjectRequest { - id: 2, - name: None, - components_info: None, - owner_id: None, - }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let project_logic = ProjectController::new(contexts); - - let res = project_logic.update_project(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::NotFound); -} diff --git a/src/tests/controllers/query_controller.rs b/src/tests/controllers/query_controller.rs deleted file mode 100644 index f836bf6..0000000 --- a/src/tests/controllers/query_controller.rs +++ /dev/null @@ -1,595 +0,0 @@ -use crate::api::server::protobuf::query_response::{self, Result}; -use crate::api::server::protobuf::{ - CreateQueryRequest, DeleteQueryRequest, QueryResponse, SendQueryRequest, UpdateQueryRequest, -}; -use crate::controllers::controller_impls::QueryController; -use crate::controllers::controller_traits::QueryControllerTrait; -use crate::entities::{access, project, query}; -use crate::tests::controllers::helpers::{ - disguise_context_mocks, disguise_service_mocks, get_mock_contexts, get_mock_services, -}; -use mockall::predicate; -use sea_orm::DbErr; -use std::str::FromStr; -use tonic::{metadata, Code, Request, Response}; - -#[tokio::test] -async fn create_invalid_query_returns_err() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let query = query::Model { - id: Default::default(), - string: "".to_string(), - result: Default::default(), - project_id: 1, - outdated: Default::default(), - }; - - let access = access::Model { - id: Default::default(), - role: "Editor".to_string(), - project_id: 1, - user_id: 1, - }; - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(1)) - .returning(move |_, _| Ok(Some(access.clone()))); - - mock_contexts - .query_context_mock - .expect_create() - .with(predicate::eq(query.clone())) - .returning(move |_| Err(DbErr::RecordNotInserted)); - - let mut request = Request::new(CreateQueryRequest { - string: "".to_string(), - project_id: 1, - }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let query_logic = QueryController::new(contexts, services); - - let res = query_logic.create_query(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::Internal); -} - -#[tokio::test] -async fn create_query_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let query = query::Model { - id: Default::default(), - string: "".to_string(), - result: Default::default(), - project_id: 1, - outdated: Default::default(), - }; - - let access = access::Model { - id: Default::default(), - role: "Editor".to_string(), - project_id: 1, - user_id: 1, - }; - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(1)) - .returning(move |_, _| Ok(Some(access.clone()))); - - mock_contexts - .query_context_mock - .expect_create() - .with(predicate::eq(query.clone())) - .returning(move |_| Ok(query.clone())); - - let mut request = Request::new(CreateQueryRequest { - string: "".to_string(), - project_id: 1, - }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let query_logic = QueryController::new(contexts, services); - - let res = query_logic.create_query(request).await; - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn update_invalid_query_returns_err() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let old_query = query::Model { - id: 1, - string: "".to_string(), - result: None, - project_id: Default::default(), - outdated: true, - }; - - let query = query::Model { - string: "updated".to_string(), - ..old_query.clone() - }; - - let access = access::Model { - id: 1, - role: "Editor".to_string(), - project_id: Default::default(), - user_id: 1, - }; - - mock_contexts - .query_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| Ok(Some(old_query.clone()))); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(0)) - .returning(move |_, _| Ok(Some(access.clone()))); - - mock_contexts - .query_context_mock - .expect_update() - .with(predicate::eq(query.clone())) - .returning(move |_| Err(DbErr::RecordNotUpdated)); - - let mut request = Request::new(UpdateQueryRequest { - id: 1, - string: "updated".to_string(), - }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let query_logic = QueryController::new(contexts, services); - - let res = query_logic.update_query(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::Internal); -} - -#[tokio::test] -async fn update_query_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let old_query = query::Model { - id: 1, - string: "".to_string(), - result: None, - project_id: Default::default(), - outdated: true, - }; - - let query = query::Model { - string: "updated".to_string(), - ..old_query.clone() - }; - - let access = access::Model { - id: Default::default(), - role: "Editor".to_string(), - project_id: Default::default(), - user_id: 1, - }; - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(0)) - .returning(move |_, _| Ok(Some(access.clone()))); - - mock_contexts - .query_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| Ok(Some(old_query.clone()))); - - mock_contexts - .query_context_mock - .expect_update() - .with(predicate::eq(query.clone())) - .returning(move |_| Ok(query.clone())); - - let mut request = Request::new(UpdateQueryRequest { - id: 1, - string: "updated".to_string(), - }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let query_logic = QueryController::new(contexts, services); - - let res = query_logic.update_query(request).await; - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn delete_invalid_query_returns_err() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let access = access::Model { - id: Default::default(), - role: "Editor".to_string(), - project_id: Default::default(), - user_id: 1, - }; - - let query = query::Model { - id: 1, - string: "".to_string(), - result: Default::default(), - project_id: Default::default(), - outdated: Default::default(), - }; - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(0)) - .returning(move |_, _| Ok(Some(access.clone()))); - - mock_contexts - .query_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| Ok(Some(query.clone()))); - - mock_contexts - .query_context_mock - .expect_delete() - .with(predicate::eq(1)) - .returning(move |_| Err(DbErr::RecordNotFound("".to_string()))); - - let mut request = Request::new(DeleteQueryRequest { id: 1 }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let query_logic = QueryController::new(contexts, services); - - let res = query_logic.delete_query(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::NotFound); -} - -#[tokio::test] -async fn delete_query_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let query = query::Model { - id: 1, - string: "".to_string(), - result: Default::default(), - project_id: Default::default(), - outdated: Default::default(), - }; - - let query_clone = query.clone(); - - let access = access::Model { - id: Default::default(), - role: "Editor".to_string(), - project_id: Default::default(), - user_id: 1, - }; - - mock_contexts - .query_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| Ok(Some(query.clone()))); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(0)) - .returning(move |_, _| Ok(Some(access.clone()))); - - mock_contexts - .query_context_mock - .expect_delete() - .with(predicate::eq(1)) - .returning(move |_| Ok(query_clone.clone())); - - let mut request = Request::new(DeleteQueryRequest { id: 1 }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let query_logic = QueryController::new(contexts, services); - - let res = query_logic.delete_query(request).await; - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn create_query_invalid_role_returns_err() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let query = query::Model { - id: 1, - string: "".to_string(), - result: Default::default(), - project_id: Default::default(), - outdated: Default::default(), - }; - - let access = access::Model { - id: Default::default(), - role: "Viewer".to_string(), - project_id: Default::default(), - user_id: 1, - }; - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(1)) - .returning(move |_, _| Ok(Some(access.clone()))); - - mock_contexts - .query_context_mock - .expect_create() - .with(predicate::eq(query.clone())) - .returning(move |_| Ok(query.clone())); - - let mut request = Request::new(CreateQueryRequest { - string: "".to_string(), - project_id: 1, - }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let query_logic = QueryController::new(contexts, services); - - let res = query_logic.create_query(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::PermissionDenied); -} - -#[tokio::test] -async fn delete_query_invalid_role_returns_err() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let query = query::Model { - id: 1, - string: "".to_string(), - result: Default::default(), - project_id: Default::default(), - outdated: Default::default(), - }; - - let query_clone = query.clone(); - - let access = access::Model { - id: Default::default(), - role: "Viewer".to_string(), - project_id: Default::default(), - user_id: 1, - }; - - mock_contexts - .query_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| Ok(Some(query.clone()))); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(0)) - .returning(move |_, _| Ok(Some(access.clone()))); - - mock_contexts - .query_context_mock - .expect_delete() - .with(predicate::eq(1)) - .returning(move |_| Ok(query_clone.clone())); - - let mut request = Request::new(DeleteQueryRequest { id: 1 }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let query_logic = QueryController::new(contexts, services); - - let res = query_logic.delete_query(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::PermissionDenied); -} - -#[tokio::test] -async fn update_query_invalid_role_returns_err() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let old_query = query::Model { - id: 1, - string: "".to_string(), - result: None, - project_id: Default::default(), - outdated: true, - }; - - let query = query::Model { - string: "updated".to_string(), - ..old_query.clone() - }; - - let access = access::Model { - id: Default::default(), - role: "Viewer".to_string(), - project_id: Default::default(), - user_id: 1, - }; - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(0)) - .returning(move |_, _| Ok(Some(access.clone()))); - - mock_contexts - .query_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| Ok(Some(old_query.clone()))); - - mock_contexts - .query_context_mock - .expect_update() - .with(predicate::eq(query.clone())) - .returning(move |_| Ok(query.clone())); - - let mut request = Request::new(UpdateQueryRequest { - id: 1, - string: "updated".to_string(), - }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let query_logic = QueryController::new(contexts, services); - - let res = query_logic.update_query(request).await.unwrap_err(); - - assert_eq!(res.code(), Code::PermissionDenied); -} - -#[tokio::test] -async fn send_query_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - let mut mock_services = get_mock_services(); - - let query = query::Model { - id: Default::default(), - string: "".to_string(), - result: Default::default(), - project_id: Default::default(), - outdated: Default::default(), - }; - - let access = access::Model { - id: Default::default(), - role: "Editor".to_string(), - project_id: Default::default(), - user_id: 1, - }; - - let project = project::Model { - id: Default::default(), - name: "project".to_string(), - components_info: Default::default(), - owner_id: 0, - }; - - let query_response = QueryResponse { - query_id: Default::default(), - info: Default::default(), - result: Some(Result::Success(query_response::Success {})), - }; - - let updated_query = query::Model { - result: Some(serde_json::to_value(query_response.clone().result).unwrap()), - ..query.clone() - }; - - mock_contexts - .project_context_mock - .expect_get_by_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(Some(project.clone()))); - - mock_contexts - .access_context_mock - .expect_get_access_by_uid_and_project_id() - .with(predicate::eq(1), predicate::eq(0)) - .returning(move |_, _| Ok(Some(access.clone()))); - - mock_contexts - .query_context_mock - .expect_get_by_id() - .with(predicate::eq(0)) - .returning(move |_| Ok(Some(query.clone()))); - - mock_services - .reveaal_service_mock - .expect_send_query() - .returning(move |_| Ok(Response::new(query_response.clone()))); - - mock_contexts - .query_context_mock - .expect_update() - .with(predicate::eq(updated_query.clone())) - .returning(move |_| Ok(updated_query.clone())); - - let mut request = Request::new(SendQueryRequest { - id: Default::default(), - project_id: Default::default(), - }); - - request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let query_logic = QueryController::new(contexts, services); - - let res = query_logic.send_query(request).await; - - assert!(res.is_ok()); -} diff --git a/src/tests/controllers/session_controller.rs b/src/tests/controllers/session_controller.rs deleted file mode 100644 index 99251de..0000000 --- a/src/tests/controllers/session_controller.rs +++ /dev/null @@ -1,337 +0,0 @@ -use mockall::predicate; -use std::env; -use std::str::FromStr; - -use crate::entities::{session, user}; -use crate::tests::controllers::helpers::{ - disguise_context_mocks, disguise_service_mocks, get_mock_contexts, get_mock_services, -}; - -use crate::api::auth::{Token, TokenType}; -use crate::api::server::protobuf::get_auth_token_request::{user_credentials, UserCredentials}; -use crate::api::server::protobuf::GetAuthTokenRequest; -use crate::controllers::controller_impls::SessionController; -use crate::controllers::controller_traits::SessionControllerTrait; -use sea_orm::DbErr; -use tonic::{metadata, Code, Request}; - -#[tokio::test] -async fn update_session_no_session_exists_creates_session_returns_err() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - mock_contexts - .session_context_mock - .expect_get_by_token() - .returning(move |_, _| Ok(None)); - - mock_contexts - .session_context_mock - .expect_update() - .returning(move |_| Err(DbErr::RecordNotInserted)); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let session_logic = SessionController::new(contexts, services); - - let res = session_logic - .update_session("old_refresh_token".to_string()) - .await; - - assert_eq!(res.unwrap_err().code(), Code::Unauthenticated); -} - -#[tokio::test] -async fn update_session_returns_new_tokens_when_session_exists() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let refresh_token = "refresh_token".to_string(); - - mock_contexts - .session_context_mock - .expect_get_by_token() - .times(1) - .returning(|_, _| { - Ok(Some(session::Model { - id: 0, - access_token: "old_access_token".to_string(), - refresh_token: "old_refresh_token".to_string(), - updated_at: Default::default(), - user_id: 1, - })) - }); - - mock_contexts - .session_context_mock - .expect_update() - .times(1) - .returning(move |_| { - Ok(session::Model { - id: 0, - refresh_token: "refresh_token".to_string(), - access_token: "access_token".to_string(), - updated_at: Default::default(), - user_id: 1, - }) - }); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let session_logic = SessionController::new(contexts, services); - - let result = session_logic.update_session(refresh_token).await; - - assert!(result.is_ok()); - let (access_token, refresh_token) = result.unwrap(); - assert_ne!(access_token.to_string(), "old_access_token"); - assert_ne!(refresh_token.to_string(), "old_refresh_token"); -} - -#[tokio::test] -async fn update_session_returns_error_when_no_session_found() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let refresh_token = "refresh_token".to_string(); - - mock_contexts - .session_context_mock - .expect_get_by_token() - .times(1) - .returning(|_, _| Ok(None)); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let session_logic = SessionController::new(contexts, services); - - let result = session_logic.update_session(refresh_token).await; - - assert!(result.is_err()); - assert_eq!(result.unwrap_err().code(), Code::Unauthenticated); -} - -#[tokio::test] -async fn update_session_returns_error_when_database_error_occurs() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let refresh_token = "refresh_token".to_string(); - - mock_contexts - .session_context_mock - .expect_get_by_token() - .times(1) - .returning(|_, _| Err(DbErr::RecordNotFound("".to_string()))); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let session_logic = SessionController::new(contexts, services); - - let result = session_logic.update_session(refresh_token).await; - - assert!(result.is_err()); - assert_eq!(result.unwrap_err().code(), Code::Internal); -} - -#[tokio::test] -async fn get_auth_token_from_credentials_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - let mut mock_services = get_mock_services(); - - let request = GetAuthTokenRequest { - user_credentials: Option::from(UserCredentials { - password: "Password123".to_string(), - user: Option::from(user_credentials::User::Username("Example".to_string())), - }), - }; - - mock_contexts - .user_context_mock - .expect_get_by_username() - .returning(move |_| { - Ok(Option::from(user::Model { - id: 1, - email: "".to_string(), - username: "Example".to_string(), - password: "".to_string(), - })) - }); - - mock_services - .hashing_service_mock - .expect_verify_password() - .returning(move |_, _| Ok(true)); - - mock_contexts - .session_context_mock - .expect_create() - .returning(move |_| { - Ok(session::Model { - id: 0, - refresh_token: "refresh_token".to_string(), - access_token: "access_token".to_string(), - updated_at: Default::default(), - user_id: 1, - }) - }); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let session_logic = SessionController::new(contexts, services); - - let response = session_logic - .get_auth_token(Request::new(request)) - .await - .unwrap(); - - assert!(!response.get_ref().refresh_token.is_empty()); - assert!(!response.get_ref().access_token.is_empty()); -} - -#[tokio::test] -async fn get_auth_token_from_token_returns_ok() { - env::set_var("REFRESH_TOKEN_HS512_SECRET", "refresh_secret"); - - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let mut request = Request::new(GetAuthTokenRequest { - user_credentials: None, - }); - - let refresh_token = Token::new(TokenType::RefreshToken, "1").unwrap(); - - request.metadata_mut().insert( - "authorization", - metadata::MetadataValue::from_str(format!("Bearer {}", refresh_token).as_str()).unwrap(), - ); - - mock_contexts - .session_context_mock - .expect_get_by_token() - .returning(move |_, _| { - Ok(Option::from(session::Model { - id: 0, - refresh_token: "refresh_token".to_string(), - access_token: "access_token".to_string(), - updated_at: Default::default(), - user_id: 1, - })) - }); - - mock_contexts - .session_context_mock - .expect_update() - .returning(move |_| { - Ok(session::Model { - id: 0, - refresh_token: "refresh_token".to_string(), - access_token: "access_token".to_string(), - updated_at: Default::default(), - user_id: 1, - }) - }); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let session_logic = SessionController::new(contexts, services); - - let response = session_logic.get_auth_token(request).await.unwrap(); - - assert!(!response.get_ref().refresh_token.is_empty()); - assert!(!response.get_ref().access_token.is_empty()); -} - -#[tokio::test] -async fn get_auth_token_from_invalid_token_returns_err() { - let mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let mut request = Request::new(GetAuthTokenRequest { - user_credentials: None, - }); - - request.metadata_mut().insert( - "authorization", - metadata::MetadataValue::from_str("invalid token").unwrap(), - ); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let session_logic = SessionController::new(contexts, services); - - let response = session_logic.get_auth_token(request).await; - - assert_eq!(response.unwrap_err().code(), Code::Unauthenticated); -} - -#[tokio::test] -async fn delete_session_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - mock_contexts - .session_context_mock - .expect_delete_by_token() - .with( - predicate::eq(TokenType::AccessToken), - predicate::eq("test_token".to_string()), - ) - .returning(move |_, _| { - Ok(session::Model { - id: 1, - refresh_token: Default::default(), - access_token: "test_token".to_string(), - updated_at: Default::default(), - user_id: Default::default(), - }) - }); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let session_logic = SessionController::new(contexts, services); - - let mut request = Request::new(()); - request.metadata_mut().insert( - "authorization", - metadata::MetadataValue::from_str("Bearer test_token").unwrap(), - ); - - let res = session_logic.delete_session(request).await; - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn delete_session_no_session_returns_err() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - mock_contexts - .session_context_mock - .expect_delete_by_token() - .with( - predicate::eq(TokenType::AccessToken), - predicate::eq("test_token".to_string()), - ) - .returning(move |_, _| { - Err(DbErr::RecordNotFound( - "No session found with the provided access token".to_string(), - )) - }); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let session_logic = SessionController::new(contexts, services); - - let mut request = Request::new(()); - request.metadata_mut().insert( - "authorization", - metadata::MetadataValue::from_str("Bearer test_token").unwrap(), - ); - - let res = session_logic.delete_session(request).await; - - assert_eq!(res.unwrap_err().code(), Code::Internal); -} diff --git a/src/tests/controllers/user_controller.rs b/src/tests/controllers/user_controller.rs deleted file mode 100644 index 8d8c6b4..0000000 --- a/src/tests/controllers/user_controller.rs +++ /dev/null @@ -1,412 +0,0 @@ -use crate::api::server::protobuf::{CreateUserRequest, GetUsersRequest, UpdateUserRequest}; -use crate::controllers::controller_impls::UserController; -use crate::controllers::controller_traits::UserControllerTrait; -use crate::entities::user; -use crate::tests::controllers::helpers::{ - disguise_context_mocks, disguise_service_mocks, get_mock_contexts, get_mock_services, -}; -use mockall::predicate; -use sea_orm::DbErr; -use std::str::FromStr; -use tonic::{metadata, Code, Request}; - -#[tokio::test] -async fn delete_user_nonexistent_user_returns_err() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - mock_contexts - .user_context_mock - .expect_delete() - .with(predicate::eq(1)) - .returning(|_| Err(DbErr::RecordNotFound("".into()))); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let user_logic = UserController::new(contexts, services); - - let mut delete_request = Request::new(()); - - // Insert uid into request metadata - delete_request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let delete_response = user_logic.delete_user(delete_request).await.unwrap_err(); - let expected_response_code = Code::Internal; - - assert_eq!(delete_response.code(), expected_response_code); -} - -#[tokio::test] -async fn delete_user_existing_user_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let user = user::Model { - id: 1, - email: "".to_string(), - username: "".to_string(), - password: "".to_string(), - }; - - mock_contexts - .user_context_mock - .expect_delete() - .with(predicate::eq(1)) - .returning(move |_| Ok(user.clone())); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let user_logic = UserController::new(contexts, services); - - let mut delete_request = Request::new(()); - - // Insert uid into request metadata - delete_request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let delete_response = user_logic.delete_user(delete_request).await; - - assert!(delete_response.is_ok()); -} - -#[tokio::test] -async fn create_user_nonexistent_user_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - let mut mock_services = get_mock_services(); - - let password = "Password123".to_string(); - - let user = user::Model { - id: Default::default(), - email: "anders21@student.aau.dk".to_string(), - username: "anders".to_string(), - password: password.clone(), - }; - - let create_user_request = Request::new(CreateUserRequest { - email: "anders21@student.aau.dk".to_string(), - username: "anders".to_string(), - password: password.clone(), - }); - - mock_services - .hashing_service_mock - .expect_hash_password() - .returning(move |_| Ok(password.clone())); - - mock_contexts - .user_context_mock - .expect_create() - .with(predicate::eq(user.clone())) - .returning(move |_| Ok(user.clone())); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let user_logic = UserController::new(contexts, services); - - let create_user_response = user_logic.create_user(create_user_request).await; - assert!(create_user_response.is_ok()); -} - -#[tokio::test] -async fn create_user_duplicate_email_returns_error() { - let mut mock_contexts = get_mock_contexts(); - let mut mock_services = get_mock_services(); - - let password = "Password123".to_string(); - - let user = user::Model { - id: Default::default(), - email: "anders21@student.aau.dk".to_string(), - username: "anders".to_string(), - password: password.clone(), - }; - - let create_user_request = Request::new(CreateUserRequest { - email: "anders21@student.aau.dk".to_string(), - username: "anders".to_string(), - password: password.clone(), - }); - - mock_services - .hashing_service_mock - .expect_hash_password() - .returning(move |_| Ok(password.clone())); - - mock_contexts - .user_context_mock - .expect_create() - .with(predicate::eq(user.clone())) - .returning(move |_| Err(DbErr::RecordNotInserted)); //todo!("Needs to be a SqlError with UniqueConstraintViolation with 'email' in message) - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let user_logic = UserController::new(contexts, services); - - let res = user_logic.create_user(create_user_request).await; - assert_eq!(res.unwrap_err().code(), Code::Internal); //todo!("Needs to be code AlreadyExists when mocked Error is corrected) -} - -#[tokio::test] -async fn create_user_invalid_email_returns_error() { - let mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let user_logic = UserController::new(contexts, services); - - let create_user_request = Request::new(CreateUserRequest { - email: "invalid-email".to_string(), - username: "newuser".to_string(), - password: "123".to_string(), - }); - - let res = user_logic.create_user(create_user_request).await; - assert_eq!(res.unwrap_err().code(), Code::InvalidArgument); -} - -#[tokio::test] -async fn create_user_duplicate_username_returns_error() { - let mut mock_contexts = get_mock_contexts(); - let mut mock_services = get_mock_services(); - - let password = "Password123".to_string(); - - let user = user::Model { - id: Default::default(), - email: "anders21@student.aau.dk".to_string(), - username: "anders".to_string(), - password: password.clone(), - }; - - let create_user_request = Request::new(CreateUserRequest { - email: "anders21@student.aau.dk".to_string(), - username: "anders".to_string(), - password: password.clone(), - }); - - mock_services - .hashing_service_mock - .expect_hash_password() - .returning(move |_| Ok(password.clone())); - - mock_contexts - .user_context_mock - .expect_create() - .with(predicate::eq(user.clone())) - .returning(move |_| Err(DbErr::RecordNotInserted)); //todo!("Needs to be a SqlError with UniqueConstraintViolation with 'username' in message) - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let user_logic = UserController::new(contexts, services); - - let res = user_logic.create_user(create_user_request).await; - assert_eq!(res.unwrap_err().code(), Code::Internal); //todo!("Needs to be code AlreadyExists when mocked Error is corrected) -} - -#[tokio::test] -async fn create_user_invalid_username_returns_error() { - let mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let user_logic = UserController::new(contexts, services); - - let create_user_request = Request::new(CreateUserRequest { - email: "valid@email.com".to_string(), - username: "ØØØØØ".to_string(), - password: "123".to_string(), - }); - - let res = user_logic.create_user(create_user_request).await; - assert_eq!(res.unwrap_err().code(), Code::InvalidArgument); -} - -#[tokio::test] -async fn create_user_valid_request_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - let mut mock_services = get_mock_services(); - - let password = "Password123".to_string(); - - let user = user::Model { - id: Default::default(), - email: "newuser@example.com".to_string(), - username: "newuser".to_string(), - password: password.clone(), - }; - - let create_user_request = Request::new(CreateUserRequest { - email: "newuser@example.com".to_string(), - username: "newuser".to_string(), - password: password.clone(), - }); - - mock_services - .hashing_service_mock - .expect_hash_password() - .returning(move |_| Ok(password.clone())); - - mock_contexts - .user_context_mock - .expect_create() - .with(predicate::eq(user.clone())) - .returning(move |_| Ok(user.clone())); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let user_logic = UserController::new(contexts, services); - - let create_user_response = user_logic.create_user(create_user_request).await; - assert!(create_user_response.is_ok()); -} - -#[tokio::test] -async fn update_user_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - let mut mock_services = get_mock_services(); - - let old_user = user::Model { - id: 1, - email: "olduser@example.com".to_string(), - username: "old_username".to_string(), - password: "StrongPassword123".to_string(), - }; - - let new_user = user::Model { - id: 1, - email: "newuser@example.com".to_string(), - username: "new_username".to_string(), - password: "g76df2gd7hd837g8hjd8723hd8gd823d82d3".to_string(), - }; - - mock_contexts - .user_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| Ok(Some(old_user.clone()))); - - mock_services - .hashing_service_mock - .expect_hash_password() - .with(predicate::eq("StrongPassword123".to_string())) - .returning(move |_| Ok("g76df2gd7hd837g8hjd8723hd8gd823d82d3".to_string())); - - mock_contexts - .user_context_mock - .expect_update() - .with(predicate::eq(new_user.clone())) - .returning(move |_| Ok(new_user.clone())); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let user_logic = UserController::new(contexts, services); - - let mut update_user_request = Request::new(UpdateUserRequest { - email: Some("newuser@example.com".to_string()), - username: Some("new_username".to_string()), - password: Some("StrongPassword123".to_string()), - }); - - update_user_request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let update_user_response = user_logic.update_user(update_user_request).await; - - assert!(update_user_response.is_ok()) -} - -#[tokio::test] -async fn update_user_non_existant_user_returns_err() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - mock_contexts - .user_context_mock - .expect_get_by_id() - .with(predicate::eq(1)) - .returning(move |_| Err(DbErr::RecordNotFound("".to_string()))); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let user_logic = UserController::new(contexts, services); - - let mut update_user_request = Request::new(UpdateUserRequest { - email: Some("new_test@test".to_string()), - username: Some("new_test_user".to_string()), - password: Some("new_test_pass".to_string()), - }); - - update_user_request - .metadata_mut() - .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - - let res = user_logic.update_user(update_user_request).await; - - assert_eq!(res.unwrap_err().code(), Code::Internal); -} - -#[tokio::test] -async fn get_users_returns_ok() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let users = vec![ - user::Model { - id: 1, - email: "".to_string(), - username: "".to_string(), - password: "".to_string(), - }, - user::Model { - id: 2, - email: "".to_string(), - username: "".to_string(), - password: "".to_string(), - }, - ]; - - mock_contexts - .user_context_mock - .expect_get_by_ids() - .returning(move |_| Ok(users.clone())); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let user_logic = UserController::new(contexts, services); - - let get_users_request = Request::new(GetUsersRequest { ids: vec![1, 2] }); - - let get_users_response = user_logic.get_users(get_users_request).await.unwrap(); - - assert_eq!(get_users_response.get_ref().users.len(), 2); -} - -#[tokio::test] -async fn get_users_returns_empty_array() { - let mut mock_contexts = get_mock_contexts(); - let mock_services = get_mock_services(); - - let users: Vec = vec![]; - - mock_contexts - .user_context_mock - .expect_get_by_ids() - .returning(move |_| Ok(users.clone())); - - let contexts = disguise_context_mocks(mock_contexts); - let services = disguise_service_mocks(mock_services); - let user_logic = UserController::new(contexts, services); - - let get_users_request = Request::new(GetUsersRequest { ids: vec![1, 2] }); - - let get_users_response = user_logic.get_users(get_users_request).await.unwrap(); - - assert_eq!(get_users_response.get_ref().users.len(), 0); -} diff --git a/src/tests/mod.rs b/src/tests/mod.rs deleted file mode 100644 index 4274654..0000000 --- a/src/tests/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod api; -pub mod contexts; -pub mod controllers; -pub mod services; diff --git a/src/tests/services/mod.rs b/src/tests/services/mod.rs deleted file mode 100644 index e53208c..0000000 --- a/src/tests/services/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod reveaal_service; diff --git a/src/tests/services/reveaal_service.rs b/src/tests/services/reveaal_service.rs deleted file mode 100644 index 7d64803..0000000 --- a/src/tests/services/reveaal_service.rs +++ /dev/null @@ -1,29 +0,0 @@ -// use crate::api::server::server::QueryResponse; -// use wiremock_grpc::generate; -// use wiremock_grpc::*; -// -// generate!("EcdarBackend", MyMockServer); - -#[ignore] -#[tokio::test] -async fn send_query_test_correct_query_returns_ok() { - //todo!("Somehow QueryResponse does not implement prost::message::Message even though it does. - // supposedly a versioning error between wiremock_grpc, tonic, and prost") - - // let mut server = MyMockServer::start_default().await; - // - // let request1 = server.setup( - // MockBuilder::when() - // .path("EcdarBackend/SendQuery") - // .then() - // .return_status(Code::Ok) - // .return_body(|| QueryResponse { - // query_id: 0, - // info: vec![], - // result: None, - // }), - // ); - - //... - //https://crates.io/crates/wiremock-grpc -}