Skip to content

Commit

Permalink
Merge pull request #65 from ECDAR-AAU-SW-P5/22-create-model
Browse files Browse the repository at this point in the history
Implement create model endpoint
  • Loading branch information
MadsSR authored Nov 29, 2023
2 parents bacc6ce + fb8df4c commit c61116c
Show file tree
Hide file tree
Showing 15 changed files with 1,075 additions and 873 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ path = "src/main.rs"
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" ] }
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"
Expand Down
6 changes: 6 additions & 0 deletions migration/src/m20231012_094228_create_model_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ impl MigrationTrait for Migration {
.col(ColumnDef::new(Model::Name).string().not_null())
.col(ColumnDef::new(Model::ComponentsInfo).json().not_null())
.col(ColumnDef::new(Model::OwnerId).integer().not_null())
.index(
Index::create()
.col(Model::OwnerId)
.col(Model::Name)
.unique(),
)
.foreign_key(
ForeignKey::create()
.from(Model::Table, Model::OwnerId)
Expand Down
75 changes: 71 additions & 4 deletions src/api/ecdar_api.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::api::context_collection::ContextCollection;
use regex::Regex;
use sea_orm::SqlErr;
use serde_json;
use std::sync::Arc;
use tonic::{Code, Request, Response, Status};

Expand All @@ -19,7 +20,7 @@ use super::server::server::{
SimulationStepRequest, SimulationStepResponse, UpdateAccessRequest, UpdateQueryRequest,
UpdateUserRequest, UserTokenResponse,
};
use crate::entities::{access, query, session, user};
use crate::entities::{access, in_use, model, query, session, user};

#[derive(Clone)]
pub struct ConcreteEcdarApi {
Expand Down Expand Up @@ -56,7 +57,7 @@ pub async fn handle_session(
};
} else {
let mut session = match session_context
.get_by_refresh_token(request.token_string().unwrap())
.get_by_token(TokenType::RefreshToken, request.token_string().unwrap())
.await
{
Ok(Some(session)) => session,
Expand Down Expand Up @@ -109,9 +110,75 @@ impl EcdarApi for ConcreteEcdarApi {

async fn create_model(
&self,
_request: Request<CreateModelRequest>,
request: Request<CreateModelRequest>,
) -> Result<Response<CreateModelResponse>, Status> {
todo!()
let message = request.get_ref().clone();
let uid = request
.uid()
.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).unwrap(),
None => return Err(Status::invalid_argument("No components info provided")),
};

let mut model = model::Model {
id: Default::default(),
name: message.clone().name,
components_info,
owner_id: uid,
};

model = match self.contexts.model_context.create(model).await {
Ok(model) => model,
Err(error) => {
return match error.sql_err() {
Some(SqlErr::UniqueConstraintViolation(e)) => {
let error_msg = match e.to_lowercase() {
_ if e.contains("name") => "A model 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 model",
};
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")
model_id: model.clone().id,
user_id: uid,
};

let session = self
.contexts
.session_context
.get_by_token(TokenType::AccessToken, request.token_string().unwrap())
.await
.unwrap()
.unwrap();

let in_use = in_use::Model {
model_id: model.clone().id,
session_id: session.id,
latest_activity: Default::default(),
};

self.contexts.in_use_context.create(in_use).await.unwrap();
self.contexts.access_context.create(access).await.unwrap();

Ok(Response::new(CreateModelResponse { id: model.id }))
}

async fn update_model(&self, _request: Request<()>) -> Result<Response<()>, Status> {
Expand Down
1 change: 0 additions & 1 deletion src/build.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
fn main() {
tonic_build::compile_protos("Ecdar-ProtoBuf/services.proto").unwrap();
tonic_build::configure()
.type_attribute(
"ComponentsInfo",
Expand Down
22 changes: 17 additions & 5 deletions src/database/access_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::database::entity_context::EntityContextTrait;
use crate::entities::access;
use sea_orm::prelude::async_trait::async_trait;
use sea_orm::ActiveValue::{Set, Unchanged};
use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter};
use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, DbErr, EntityTrait, QueryFilter};
use std::sync::Arc;

pub struct AccessContext {
Expand All @@ -12,15 +12,27 @@ pub struct AccessContext {

#[async_trait]
pub trait AccessContextTrait: EntityContextTrait<access::Model> {
async fn get_access_by_uid(&self, uid: i32) -> Result<Vec<access::Model>, DbErr>;
async fn get_access_by_uid_and_model_id(
&self,
uid: i32,
model_id: i32,
) -> Result<Option<access::Model>, DbErr>;
}

#[async_trait]
impl AccessContextTrait for AccessContext {
async fn get_access_by_uid(&self, uid: i32) -> Result<Vec<access::Model>, DbErr> {
async fn get_access_by_uid_and_model_id(
&self,
uid: i32,
model_id: i32,
) -> Result<Option<access::Model>, DbErr> {
access::Entity::find()
.filter(access::Column::UserId.eq(uid))
.all(&self.db_context.get_connection())
.filter(
Condition::all()
.add(access::Column::UserId.eq(uid))
.add(access::Column::ModelId.eq(model_id)),
)
.one(&self.db_context.get_connection())
.await
}
}
Expand Down
1 change: 1 addition & 0 deletions src/database/model_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ impl ModelContext {
ModelContext { db_context }
}
}

#[async_trait]
impl EntityContextTrait<model::Model> for ModelContext {
/// Used for creating a model::Model entity
Expand Down
29 changes: 21 additions & 8 deletions src/database/session_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use sea_orm::ActiveValue::{Set, Unchanged};
use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, NotSet, QueryFilter};
use std::sync::Arc;

use crate::api::auth::TokenType;
use crate::database::database_context::DatabaseContextTrait;
use crate::database::entity_context::EntityContextTrait;
use crate::entities::session;
Expand All @@ -14,22 +15,34 @@ pub struct SessionContext {

#[async_trait]
pub trait SessionContextTrait: EntityContextTrait<session::Model> {
async fn get_by_refresh_token(
async fn get_by_token(
&self,
refresh_token: String,
token_type: TokenType,
token: String,
) -> Result<Option<session::Model>, DbErr>;
}

#[async_trait]
impl SessionContextTrait for SessionContext {
async fn get_by_refresh_token(
async fn get_by_token(
&self,
refresh_token: String,
token_type: TokenType,
token: String,
) -> Result<Option<session::Model>, DbErr> {
session::Entity::find()
.filter(session::Column::RefreshToken.eq(refresh_token))
.one(&self.db_context.get_connection())
.await
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
}
}
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/tests/api/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![cfg(test)]

use crate::api::auth::TokenType;
use crate::api::context_collection::ContextCollection;
use crate::api::ecdar_api::ConcreteEcdarApi;
use crate::api::hashing_context::HashingContextTrait;
Expand Down Expand Up @@ -73,7 +74,7 @@ mock! {
}
#[async_trait]
impl AccessContextTrait for AccessContext {
async fn get_access_by_uid(&self, uid: i32) -> Result<Vec<access::Model>, DbErr>;
async fn get_access_by_uid_and_model_id(&self, uid: i32, model_id: i32) -> Result<Option<access::Model>, DbErr>;
}
}

Expand Down Expand Up @@ -133,7 +134,7 @@ mock! {
}
#[async_trait]
impl SessionContextTrait for SessionContext {
async fn get_by_refresh_token(&self, refresh_token: String) -> Result<Option<session::Model>, DbErr>;
async fn get_by_token(&self, token_type: TokenType, token: String) -> Result<Option<session::Model>, DbErr>;
}
}

Expand Down
132 changes: 128 additions & 4 deletions src/tests/api/model_logic.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,137 @@
use std::str::FromStr;

use mockall::predicate;
use sea_orm::DbErr;
use tonic::{metadata, Code, Request};

use crate::{
api::server::server::{ecdar_api_server::EcdarApi, DeleteModelRequest, ModelInfo},
entities::model,
tests::api::helpers::{get_mock_concrete_ecdar_api, get_mock_services},
use crate::api::auth::TokenType;
use crate::api::server::server::{
ecdar_api_server::EcdarApi, ComponentsInfo, CreateModelRequest, DeleteModelRequest, ModelInfo,
};
use crate::entities::{access, in_use, model, session};
use crate::tests::api::helpers::{get_mock_concrete_ecdar_api, get_mock_services};

#[tokio::test]
async fn create_model_returns_ok() {
let mut mock_services = get_mock_services();

let uid = 0;

let components_info = ComponentsInfo {
components: vec![],
components_hash: 0,
};

let model = model::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,
model_id: model.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 {
model_id: model.id,
session_id: session.id,
latest_activity: Default::default(),
};

mock_services
.model_context_mock
.expect_create()
.with(predicate::eq(model.clone()))
.returning(move |_| Ok(model.clone()));

mock_services
.access_context_mock
.expect_create()
.with(predicate::eq(access.clone()))
.returning(move |_| Ok(access.clone()));

mock_services
.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_services
.in_use_context_mock
.expect_create()
.with(predicate::eq(in_use.clone()))
.returning(move |_| Ok(in_use.clone()));

let mut request = Request::new(CreateModelRequest {
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 api = get_mock_concrete_ecdar_api(mock_services);

let res = api.create_model(request).await;

assert!(res.is_ok());
}

#[tokio::test]
async fn create_model_existing_name_returns_err() {
let mut mock_services = get_mock_services();

let uid = 0;

let model = model::Model {
id: Default::default(),
name: "model".to_string(),
components_info: Default::default(),
owner_id: uid,
};

mock_services
.model_context_mock
.expect_create()
.with(predicate::eq(model.clone()))
.returning(move |_| Err(DbErr::RecordNotInserted)); //todo!("Needs to be a SqlError with UniqueConstraintViolation with 'name' in message)

let mut request = Request::new(CreateModelRequest {
name: "model".to_string(),
components_info: Default::default(),
});

request
.metadata_mut()
.insert("uid", uid.to_string().parse().unwrap());

let api = get_mock_concrete_ecdar_api(mock_services);

let res = api.create_model(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 delete_not_owner_returns_err() {
Expand Down
Loading

0 comments on commit c61116c

Please sign in to comment.