use tonic::metadata::MetadataValue;
use tonic::{Request, Status};
use crate::proto::udb::core::storage::services::v1 as storage_pb;
use crate::proto::udb::core::storage::services::v1::storage_service_server::StorageService;
use crate::proto::{ErrorDetail, ErrorKind};
use crate::runtime::executor_utils::ERROR_DETAIL_METADATA_KEY;
use super::StorageServiceImpl;
use super::config::{
ALREADY_FINALIZED, OBJECT_NOT_PRESENT, UNSUPPORTED_OBJECT_BACKEND, UPLOAD_SIZE_MISMATCH,
};
use super::errors::{
file_object_bytes_missing_status, object_store_bytes_missing_status,
object_stream_requires_store_status, storage_capability_status, storage_file_not_found_status,
storage_internal_status, upload_already_finalized_status, upload_etag_mismatch_status,
upload_size_mismatch_status, uploaded_object_missing_status,
};
use super::model::{file_status_to_db, file_type_to_db, register_is_public_bind};
fn decode_detail(status: &Status) -> ErrorDetail {
let raw = status
.metadata()
.get_bin(ERROR_DETAIL_METADATA_KEY)
.expect("error-detail trailer present")
.to_bytes()
.expect("trailer decodes to bytes");
crate::runtime::executor_utils::decode_error_detail_from_raw(&raw)
}
fn assert_single_field_violation(status: &Status, field: &str, description: &str) {
let detail = decode_detail(status);
assert_eq!(detail.kind, ErrorKind::Validation as i32);
assert_eq!(detail.field_violations.len(), 1);
assert_eq!(detail.field_violations[0].field, field);
assert_eq!(detail.field_violations[0].description, description);
}
fn assert_policy_detail_with_reason(
status: &Status,
operation: &str,
policy_decision_id: &str,
reason: &'static str,
message: &str,
) {
assert_eq!(status.code(), tonic::Code::FailedPrecondition);
assert_eq!(status.message(), message);
assert_eq!(
status
.metadata()
.get("error-reason")
.and_then(|value| value.to_str().ok()),
Some(reason)
);
let detail = decode_detail(status);
assert_eq!(detail.kind, ErrorKind::Policy as i32);
assert_eq!(detail.operation, operation);
assert_eq!(detail.policy_decision_id, policy_decision_id);
assert!(!detail.retryable);
assert_eq!(detail.retry_after_ms, 0);
}
fn assert_capability_detail_with_reason(
status: &Status,
operation: &str,
capability_required: &str,
reason: &'static str,
message: &str,
) {
assert_eq!(status.code(), tonic::Code::FailedPrecondition);
assert_eq!(status.message(), message);
assert_eq!(
status
.metadata()
.get("error-reason")
.and_then(|value| value.to_str().ok()),
Some(reason)
);
let detail = decode_detail(status);
assert_eq!(detail.kind, ErrorKind::Capability as i32);
assert_eq!(detail.backend, "storage");
assert_eq!(detail.operation, operation);
assert_eq!(detail.capability_required, capability_required);
assert!(!detail.retryable);
assert_eq!(detail.retry_after_ms, 0);
}
fn assert_schema_detail(status: &Status, operation: &str, schema_code: &str, message: &str) {
assert_eq!(status.code(), tonic::Code::NotFound);
assert_eq!(status.message(), message);
let detail = decode_detail(status);
assert_eq!(detail.kind, ErrorKind::Schema as i32);
assert_eq!(detail.backend, "storage");
assert_eq!(detail.operation, operation);
assert_eq!(detail.capability_required, schema_code);
assert!(!detail.retryable);
assert_eq!(detail.retry_after_ms, 0);
}
fn assert_internal_detail(status: &Status, operation: &str, message: &str) {
assert_eq!(status.code(), tonic::Code::Internal);
assert_eq!(status.message(), message);
let detail = decode_detail(status);
assert_eq!(detail.kind, ErrorKind::Internal as i32);
assert_eq!(detail.backend, "storage");
assert_eq!(detail.operation, operation);
assert!(detail.capability_required.is_empty());
assert!(detail.policy_decision_id.is_empty());
assert!(detail.field_violations.is_empty());
assert!(!detail.retryable);
assert_eq!(detail.retry_after_ms, 0);
}
fn assert_validation_detail_with_reason(
status: &Status,
field: &str,
description: &str,
reason: &'static str,
message: &str,
) {
assert_eq!(status.code(), tonic::Code::FailedPrecondition);
assert_eq!(status.message(), message);
assert_eq!(
status
.metadata()
.get("error-reason")
.and_then(|value| value.to_str().ok()),
Some(reason)
);
let detail = decode_detail(status);
assert_eq!(detail.kind, ErrorKind::Validation as i32);
assert_eq!(detail.field_violations.len(), 1);
assert_eq!(detail.field_violations[0].field, field);
assert_eq!(detail.field_violations[0].description, description);
assert!(!detail.retryable);
assert_eq!(detail.retry_after_ms, 0);
}
#[tokio::test]
async fn get_file_rejects_cross_tenant_body() {
let svc = StorageServiceImpl::new(); let mut request = Request::new(storage_pb::GetFileRequest {
tenant_id: "tenant-b".to_string(),
file_id: "00000000-0000-0000-0000-000000000001".to_string(),
..Default::default()
});
request
.metadata_mut()
.insert("x-tenant-id", MetadataValue::from_static("tenant-a"));
let err = svc
.get_file(request)
.await
.expect_err("cross-tenant body must be rejected");
assert_eq!(err.code(), tonic::Code::PermissionDenied);
}
#[test]
fn storage_file_not_found_statuses_carry_schema_detail() {
for operation in [
"finalize_upload",
"get_download_url",
"download_file",
"get_file",
"update_file",
"delete_file",
] {
assert_schema_detail(
&storage_file_not_found_status(operation),
operation,
"file_not_found",
"file not found",
);
}
}
#[tokio::test]
async fn register_upload_missing_filename_carries_field_violation() {
let svc = StorageServiceImpl::new(); let mut request = Request::new(storage_pb::RegisterUploadRequest {
tenant_id: "tenant-a".to_string(),
filename: " ".to_string(),
..Default::default()
});
request
.metadata_mut()
.insert("x-tenant-id", MetadataValue::from_static("tenant-a"));
let err = svc
.register_upload(request)
.await
.expect_err("missing filename must be rejected before runtime access");
assert_eq!(err.code(), tonic::Code::InvalidArgument);
assert_eq!(err.message(), "tenant_id and filename are required");
let detail = decode_detail(&err);
assert_eq!(detail.kind, ErrorKind::Validation as i32);
assert_eq!(detail.field_violations.len(), 1);
assert_eq!(detail.field_violations[0].field, "filename");
assert_eq!(
detail.field_violations[0].description,
"must be a non-empty filename"
);
}
#[test]
fn file_type_and_status_validation_carry_field_violations() {
let file_type =
file_type_to_db("directory", "OTHER").expect_err("unknown file type must be rejected");
assert_eq!(file_type.code(), tonic::Code::InvalidArgument);
assert_eq!(file_type.message(), "unknown file type: DIRECTORY");
assert_single_field_violation(
&file_type,
"file_type",
"must be a supported FileType enum value",
);
let status =
file_status_to_db("archived", "PENDING").expect_err("unknown file status must be rejected");
assert_eq!(status.code(), tonic::Code::InvalidArgument);
assert_eq!(status.message(), "unknown file status: ARCHIVED");
assert_single_field_violation(
&status,
"status",
"must be a supported FileStatus enum value",
);
}
#[test]
fn upload_already_finalized_carries_policy_detail_and_reason() {
assert_policy_detail_with_reason(
&upload_already_finalized_status(),
"finalize_upload",
"upload_already_finalized",
ALREADY_FINALIZED,
"upload already finalized",
);
}
#[test]
fn upload_presence_denial_carries_policy_detail_and_reason() {
assert_policy_detail_with_reason(
&uploaded_object_missing_status(),
"finalize_upload",
"uploaded_object_present",
OBJECT_NOT_PRESENT,
"uploaded object is not present in the configured object store",
);
}
#[test]
fn upload_head_mismatches_carry_validation_detail_and_reason() {
assert_validation_detail_with_reason(
&upload_etag_mismatch_status(),
"etag",
"must match the uploaded object's store ETag",
UPLOAD_SIZE_MISMATCH,
"uploaded object etag does not match",
);
assert_validation_detail_with_reason(
&upload_size_mismatch_status(12, 9),
"size_bytes",
"must match the uploaded object's store content length",
UPLOAD_SIZE_MISMATCH,
"uploaded object size 12 does not match declared 9",
);
}
#[test]
fn object_stream_requires_store_carries_capability_detail_and_reason() {
assert_capability_detail_with_reason(
&object_stream_requires_store_status(),
"object_stream",
"object_store",
UNSUPPORTED_OBJECT_BACKEND,
"object byte streaming requires a configured object store",
);
}
#[test]
fn download_object_absence_carries_policy_detail_and_reason() {
assert_policy_detail_with_reason(
&file_object_bytes_missing_status(),
"download_file",
"file_object_bytes_present",
OBJECT_NOT_PRESENT,
"file has no object bytes",
);
assert_policy_detail_with_reason(
&object_store_bytes_missing_status(),
"download_file",
"object_store_bytes_present",
OBJECT_NOT_PRESENT,
"object is not present in the configured object store",
);
}
#[test]
fn storage_missing_runtime_capabilities_carry_typed_detail() {
for (operation, message) in [
(
"native_entity_dispatch",
"storage service requires runtime native entity dispatch",
),
("object_stream", "storage service requires runtime"),
] {
let err = storage_capability_status(operation, "runtime_native_entity_dispatch", message);
assert_eq!(err.code(), tonic::Code::FailedPrecondition);
assert_eq!(err.message(), message);
let detail = decode_detail(&err);
assert_eq!(detail.kind, ErrorKind::Capability as i32);
assert_eq!(detail.backend, "storage");
assert_eq!(detail.operation, operation);
assert_eq!(detail.capability_required, "runtime_native_entity_dispatch");
assert!(!detail.retryable);
}
}
#[test]
fn storage_internal_status_carries_typed_detail() {
assert_internal_detail(
&storage_internal_status(
"tenant_size_sum_aggregate",
"storage usage aggregate failed: database is unavailable",
),
"tenant_size_sum_aggregate",
"storage usage aggregate failed: database is unavailable",
);
}
#[test]
fn register_is_public_defaults_to_private_when_absent() {
assert!(!register_is_public_bind(None));
assert!(register_is_public_bind(Some(true)));
assert!(!register_is_public_bind(Some(false)));
}