use std::sync::Arc;
use web_route::ParameterizedRoute;
use web_route::WebRoute;
use super::super::domain::authorization::PotreeRenderResource;
use super::super::domain::authorization::ProjectDashboardResource;
use super::super::domain::not_found_render::NotFound;
use super::super::domain::potree_render::PotreeRender;
use super::super::domain::project_dashboard_render::ProjectDashboard;
use super::error::RenderingServiceError;
use crate::authorization::domain::action::Action;
use crate::authorization::ports::authorization_engine::AuthorizationEngine;
use crate::project::domain::Project;
use crate::project::domain::ProjectId;
use crate::project::domain::authorization::ProjectTypeResource;
use crate::project::ports::project_repository::ProjectRepository;
use crate::user::domain::User;
#[derive(Debug, Clone)]
pub struct RenderingService {
project_repository: Arc<dyn ProjectRepository>,
authorization_engine: Arc<dyn AuthorizationEngine>,
project_assets_route: ParameterizedRoute,
potree_assets_route: WebRoute,
}
impl RenderingService {
pub fn new(
project_datastore: Arc<dyn ProjectRepository>,
authorization_engine: Arc<dyn AuthorizationEngine>,
project_assets_route: ParameterizedRoute,
potree_assets_route: WebRoute,
) -> Self {
Self {
project_repository: project_datastore,
authorization_engine,
project_assets_route,
potree_assets_route,
}
}
pub async fn render_potree(
&self,
user: &Option<User>,
project_id: &ProjectId,
) -> Result<PotreeRender, RenderingServiceError> {
let project = self.project_repository.read(project_id).await?;
let potree_render_resource = PotreeRenderResource {
associated_project: &project,
};
self.authorization_engine
.can_on_instance(user, &Action::Read, &potree_render_resource)?;
Ok(PotreeRender {
project_title: project.name,
potree_static_assets_path: self.potree_assets_route.clone(),
potree_config_path: self.potree_config_route(project_id)?,
})
}
pub async fn project_dashboard(
&self,
user: &Option<User>,
default_project_render_route: &ParameterizedRoute,
) -> Result<ProjectDashboard, RenderingServiceError> {
self.authorization_engine
.can_on_type(user, &Action::Read, &ProjectDashboardResource)?;
let projects = self.list_allowed_projects(user).await?;
Ok(ProjectDashboard::from_domain_projects(
projects,
default_project_render_route,
)?)
}
pub async fn not_found(&self) -> Result<NotFound, RenderingServiceError> {
Ok(NotFound)
}
fn potree_config_route(
&self,
project_id: &ProjectId,
) -> Result<WebRoute, RenderingServiceError> {
let params = serde_json::json!({
"project_id": project_id,
"path": "potree.json5",
});
Ok(self.project_assets_route.to_web_route(¶ms)?)
}
async fn list_allowed_projects(
&self,
user: &Option<User>,
) -> Result<Vec<Project>, RenderingServiceError> {
self.authorization_engine
.can_on_type(user, &Action::List, &ProjectTypeResource)?;
let projects = self.project_repository.list().await?;
let allowed_projects = projects
.into_iter()
.filter(|p| {
self.authorization_engine
.can_on_instance(user, &Action::Read, p)
.is_ok()
})
.collect();
Ok(allowed_projects)
}
}
#[cfg(test)]
mod project_rendering_service_tests {
use std::sync::Arc;
use fake::Fake;
use fake::Faker;
use web_route::ParameterizedRoute;
use web_route::WebRoute;
use super::super::super::application::error::RenderingServiceError;
use super::super::super::application::service::RenderingService;
use crate::authorization::domain::action::Action;
use crate::authorization::domain::error::AuthorizationEngineError;
use crate::authorization::ports::authorization_engine::MockAuthorizationEngine;
use crate::project::ports::project_repository::MockProjectRepository;
mod render_potree {
use super::*;
#[tokio::test]
async fn should_return_the_correct_error_if_user_not_authenticated() {
let mut project_datastore = MockProjectRepository::new();
project_datastore
.expect_read()
.return_const(Ok(Faker.fake()));
let mut authorization_engine = MockAuthorizationEngine::new();
authorization_engine
.expect_can_on_instance()
.return_const(Err(AuthorizationEngineError::NotAuthenticated));
let rendering_service = RenderingService::new(
Arc::new(project_datastore),
Arc::new(authorization_engine),
ParameterizedRoute::new(Faker.fake::<WebRoute>()).join("/{project_id}/{*path}"),
Faker.fake(),
);
let res = rendering_service
.render_potree(&Faker.fake(), &Faker.fake())
.await;
assert!(matches!(res, Err(RenderingServiceError::NotAuthenticated)));
}
#[tokio::test]
async fn should_return_the_correct_error_if_user_not_authorized() {
let mut project_datastore = MockProjectRepository::new();
project_datastore
.expect_read()
.return_const(Ok(Faker.fake()));
let mut authorization_engine = MockAuthorizationEngine::new();
authorization_engine
.expect_can_on_instance()
.return_const(Err(AuthorizationEngineError::NotAuthorized {
user: Faker.fake(),
action: Action::Read,
resource_identifier: Faker.fake(),
resource_type: Faker.fake(),
}));
let rendering_service = RenderingService::new(
Arc::new(project_datastore),
Arc::new(authorization_engine),
ParameterizedRoute::new(Faker.fake::<WebRoute>()).join("/{project_id}/{*path}"),
Faker.fake(),
);
let res = rendering_service
.render_potree(&Faker.fake(), &Faker.fake())
.await;
assert!(matches!(
res,
Err(RenderingServiceError::NotAuthorized { .. })
));
}
}
mod list_allowed_projects {
use std::sync::Mutex;
use super::*;
use crate::project::domain::Project;
#[tokio::test]
async fn should_return_the_projects_that_the_user_is_allowed_to_read() {
let dummy_projects = Faker.fake::<Vec<Project>>();
let mut project_datastore = MockProjectRepository::new();
project_datastore
.expect_list()
.return_const(Ok(dummy_projects.clone()));
let mut authorization_engine = MockAuthorizationEngine::new();
authorization_engine
.expect_can_on_type()
.once()
.return_const(Ok(()));
let authorization_engine_call_count = Arc::new(Mutex::new(0));
let authorization_engine_call_count_clone =
Arc::clone(&authorization_engine_call_count);
authorization_engine
.expect_can_on_instance()
.returning(move |_, _, _| {
let mut count = authorization_engine_call_count_clone.lock().unwrap();
let res = if *count % 2 == 0 {
Err(AuthorizationEngineError::NotAuthorized {
user: Faker.fake(),
action: Action::List,
resource_identifier: Some(Faker.fake()),
resource_type: Faker.fake(),
})
} else {
Ok(())
};
*count += 1;
res
});
let rendering_service = RenderingService::new(
Arc::new(project_datastore),
Arc::new(authorization_engine),
ParameterizedRoute::new(Faker.fake::<WebRoute>()).join("/{project_id}/{*path}"),
Faker.fake(),
);
let allowed_projects = rendering_service
.list_allowed_projects(&Faker.fake())
.await
.unwrap();
assert_eq!(allowed_projects.len(), dummy_projects.len() / 2);
}
#[tokio::test]
async fn should_return_the_correct_error_if_user_not_allowed_to_list_projects() {
let project_datastore = MockProjectRepository::new();
let mut authorization_engine = MockAuthorizationEngine::new();
authorization_engine
.expect_can_on_type()
.once()
.return_const(Err(AuthorizationEngineError::NotAuthorized {
user: Faker.fake(),
action: Action::List,
resource_identifier: None,
resource_type: Faker.fake(),
}));
let rendering_service = RenderingService::new(
Arc::new(project_datastore),
Arc::new(authorization_engine),
ParameterizedRoute::new(Faker.fake::<WebRoute>()).join("/{project_id}/{*path}"),
Faker.fake(),
);
let res = rendering_service.list_allowed_projects(&Faker.fake()).await;
assert!(matches!(
res,
Err(RenderingServiceError::NotAuthorized { .. })
));
}
}
}