Implement API metrics sharing creation functionality

This commit is contained in:
dal 2025-03-19 13:44:40 -06:00
parent f3c902e0c1
commit 6afc57ba5b
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
7 changed files with 262 additions and 4 deletions

View File

@ -0,0 +1,106 @@
use anyhow::{anyhow, Result};
use database::{
enums::{AssetPermissionRole, AssetType, IdentityType},
helpers::metric_files::fetch_metric_file,
};
use sharing::{
check_asset_permission::has_permission,
create_asset_permission::create_share_by_email,
};
use tracing::info;
use uuid::Uuid;
/// Handler to create sharing permissions for a metric
///
/// # Arguments
/// * `metric_id` - The UUID of the metric to create sharing permissions for
/// * `user_id` - The UUID of the user making the request
/// * `emails` - List of emails to share the metric with
/// * `role` - The role to assign to the shared users
///
/// # Returns
/// * `Result<()>` - Success if all sharing permissions were created
pub async fn create_metric_sharing_handler(
metric_id: &Uuid,
user_id: &Uuid,
emails: Vec<String>,
role: AssetPermissionRole,
) -> Result<()> {
info!(
metric_id = %metric_id,
user_id = %user_id,
emails_count = emails.len(),
role = ?role,
"Creating sharing permissions for metric"
);
// 1. Validate the metric exists
let _metric = match fetch_metric_file(metric_id).await? {
Some(metric) => metric,
None => return Err(anyhow!("Metric not found")),
};
// 2. Check if user has permission to share the metric (Owner or FullAccess)
let has_permission = has_permission(
*metric_id,
AssetType::MetricFile,
*user_id,
IdentityType::User,
AssetPermissionRole::FullAccess, // Owner role implicitly has FullAccess permissions
).await?;
if !has_permission {
return Err(anyhow!("User does not have permission to share this metric"));
}
// 3. Process each email and create sharing permissions
for email in emails {
// Validate email format
if !email.contains('@') {
return Err(anyhow!("Invalid email format: {}", email));
}
// Create or update the permission using create_share_by_email
match create_share_by_email(
&email,
*metric_id,
AssetType::MetricFile,
role,
*user_id,
).await {
Ok(_) => {
info!("Created sharing permission for email: {} on metric: {}", email, metric_id);
},
Err(e) => {
return Err(anyhow!("Failed to create sharing for email {}: {}", email, e));
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
#[tokio::test]
async fn test_create_metric_sharing_invalid_email() {
let metric_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
let role = AssetPermissionRole::CanView;
let emails = vec!["invalid-email-format".to_string()];
let result = create_metric_sharing_handler(&metric_id, &user_id, emails, role).await;
assert!(result.is_err());
let error = result.unwrap_err().to_string();
assert!(error.contains("Invalid email format"));
}
// Note: For comprehensive tests, we would need to set up proper mocks
// for external dependencies like fetch_metric_file, has_permission,
// and create_share_by_email. This would typically involve using a
// mocking framework that's compatible with async functions.
}

View File

@ -1,3 +1,5 @@
pub mod list_sharing_handler;
pub mod create_sharing_handler;
pub use list_sharing_handler::*;
pub use list_sharing_handler::*;
pub use create_sharing_handler::*;

View File

@ -1,4 +1,4 @@
# API Metrics Sharing - Create Endpoint PRD
# API Metrics Sharing - Create Endpoint PRD
## Problem Statement
Users need the ability to share metrics with other users via a REST API endpoint.

View File

@ -0,0 +1,48 @@
use axum::{
extract::{Path, Json},
http::StatusCode,
Extension,
};
use database::enums::AssetPermissionRole;
use handlers::metrics::sharing::create_metric_sharing_handler;
use middleware::AuthenticatedUser;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::routes::rest::ApiResponse;
/// Request structure for sharing a metric with users
#[derive(Debug, Deserialize)]
pub struct SharingRequest {
pub emails: Vec<String>,
pub role: AssetPermissionRole,
}
/// REST handler for creating sharing permissions for a metric
pub async fn create_metric_sharing_rest_handler(
Extension(user): Extension<AuthenticatedUser>,
Path(id): Path<Uuid>,
Json(request): Json<SharingRequest>,
) -> Result<ApiResponse<String>, (StatusCode, String)> {
tracing::info!("Processing POST request for metric sharing with ID: {}, user_id: {}", id, user.id);
match create_metric_sharing_handler(&id, &user.id, request.emails, request.role).await {
Ok(_) => Ok(ApiResponse::Success("Sharing permissions created successfully".to_string())),
Err(e) => {
tracing::error!("Error creating sharing permissions: {}", e);
// Map specific errors to appropriate status codes
let error_message = e.to_string();
if error_message.contains("not found") {
return Err((StatusCode::NOT_FOUND, format!("Metric not found: {}", e)));
} else if error_message.contains("permission") {
return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e)));
} else if error_message.contains("Invalid email") {
return Err((StatusCode::BAD_REQUEST, format!("Invalid email: {}", e)));
}
Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create sharing permissions: {}", e)))
}
}
}

View File

@ -1,11 +1,13 @@
use axum::{
routing::get,
routing::{get, post},
Router,
};
mod list_sharing;
mod create_sharing;
pub fn router() -> Router {
Router::new()
.route("/", get(list_sharing::list_metric_sharing_rest_handler))
.route("/", post(create_sharing::create_metric_sharing_rest_handler))
}

View File

@ -0,0 +1,99 @@
use axum::http::StatusCode;
use chrono::Utc;
use database::{
enums::{AssetPermissionRole, AssetType, IdentityType},
models::{AssetPermission},
pool::get_pg_pool,
schema::{users, metric_files, asset_permissions},
};
use diesel::{ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
use serde_json::json;
use uuid::Uuid;
use crate::common::{
http::client::TestClient,
fixtures::{users::create_test_user, metrics::create_test_metric_file},
};
#[tokio::test]
async fn test_create_metric_sharing_success() {
// Setup test database connection
let mut conn = get_pg_pool().get().await.unwrap();
// 1. Create test owner and shared user
let owner = create_test_user("owner@example.com");
let org_id = Uuid::new_v4(); // Need org id for fixture
diesel::insert_into(users::table)
.values(&owner)
.execute(&mut conn)
.await
.unwrap();
let shared_user = create_test_user("shared@example.com");
diesel::insert_into(users::table)
.values(&shared_user)
.execute(&mut conn)
.await
.unwrap();
// 2. Create test metric
let metric = create_test_metric_file(&owner.id, &org_id, None);
diesel::insert_into(metric_files::table)
.values(&metric)
.execute(&mut conn)
.await
.unwrap();
// 3. Create owner permission for test user
let now = Utc::now();
let permission = AssetPermission {
identity_id: owner.id,
identity_type: IdentityType::User,
asset_id: metric.id,
asset_type: AssetType::MetricFile,
role: AssetPermissionRole::Owner,
created_at: now,
updated_at: now,
deleted_at: None,
created_by: owner.id,
updated_by: owner.id,
};
diesel::insert_into(asset_permissions::table)
.values(&permission)
.execute(&mut conn)
.await
.unwrap();
// 4. Create test client
let client = TestClient::new().await;
// 5. Send create sharing request
let response = client
.post(&format!("/metrics/{}/sharing", metric.id))
.with_auth(&owner.id.to_string())
.json(&json!({
"emails": ["shared@example.com"],
"role": "CanView"
}))
.send()
.await;
// 6. Assert response
assert_eq!(response.status(), StatusCode::OK);
// 7. Check that permission was created
let permissions = asset_permissions::table
.filter(asset_permissions::asset_id.eq(metric.id))
.filter(asset_permissions::identity_id.eq(shared_user.id))
.filter(asset_permissions::asset_type.eq(AssetType::MetricFile))
.filter(asset_permissions::identity_type.eq(IdentityType::User))
.filter(asset_permissions::deleted_at.is_null())
.load::<AssetPermission>(&mut conn)
.await
.unwrap();
assert_eq!(permissions.len(), 1);
assert_eq!(permissions[0].role, AssetPermissionRole::CanView);
}

View File

@ -1 +1,2 @@
pub mod list_sharing_test;
pub mod list_sharing_test;
pub mod create_sharing_test;