use chrono::Utc;
use tonic::metadata::MetadataValue;
use tonic::{Request, Status};
use crate::ir::{ComparisonOp, LogicalFilter, LogicalValue};
use crate::proto::udb::core::lock::services::v1 as lock_pb;
use crate::proto::udb::core::lock::services::v1::lock_service_server::LockService;
use crate::proto::{ErrorDetail, ErrorKind};
use crate::runtime::executor_utils::ERROR_DETAIL_METADATA_KEY;
use super::LockServiceImpl;
use super::config::{MAX_ACTIVE_LOCKS_PER_TENANT, STATUS_HELD};
use super::errors::{
ensure_fencing_token_fresh, fencing_token_unavailable_status, lock_already_held_status,
lock_capability_status, lock_held_by_different_owner_status, lock_lease_lost_status,
lock_not_held_status,
};
use super::model::lock_dto_from_json;
use super::store::{held_locks_read, lock_filter, lock_inventory_read, lock_model};
use super::workers::expired_locks_claim_sql;
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_policy_detail(status: &Status, operation: &str, policy_decision_id: &str) {
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);
}
#[tokio::test]
async fn acquire_lock_rejects_cross_tenant_body() {
let svc = LockServiceImpl::new(); let mut request = Request::new(lock_pb::AcquireLockRequest {
tenant_id: "tenant-b".to_string(),
lock_name: "orders".to_string(),
owner_id: "worker-1".to_string(),
..Default::default()
});
request
.metadata_mut()
.insert("x-tenant-id", MetadataValue::from_static("tenant-a"));
let err = svc
.acquire_lock(request)
.await
.expect_err("cross-tenant body must be rejected");
assert_eq!(err.code(), tonic::Code::PermissionDenied);
}
#[tokio::test]
async fn acquire_lock_missing_identity_carries_field_violations() {
let svc = LockServiceImpl::new(); let mut request = Request::new(lock_pb::AcquireLockRequest {
tenant_id: "tenant-a".to_string(),
lock_name: " ".to_string(),
owner_id: String::new(),
..Default::default()
});
request
.metadata_mut()
.insert("x-tenant-id", MetadataValue::from_static("tenant-a"));
let err = svc
.acquire_lock(request)
.await
.expect_err("missing lock identity must be rejected before runtime access");
assert_eq!(err.code(), tonic::Code::InvalidArgument);
assert_eq!(err.message(), "lock_name and owner_id are required");
let detail = decode_detail(&err);
assert_eq!(detail.kind, ErrorKind::Validation as i32);
assert_eq!(detail.field_violations.len(), 2);
assert_eq!(detail.field_violations[0].field, "lock_name");
assert_eq!(
detail.field_violations[0].description,
"must be a non-empty lock name"
);
assert_eq!(detail.field_violations[1].field, "owner_id");
assert_eq!(
detail.field_violations[1].description,
"must be a non-empty owner id"
);
}
#[test]
fn stale_fencing_token_is_rejected() {
let err = ensure_fencing_token_fresh(5, 7).expect_err("stale token must be rejected");
assert_eq!(err.code(), tonic::Code::FailedPrecondition);
assert_eq!(
err.message(),
"stale fencing token 5; the lock has been fenced to token 7"
);
assert_policy_detail(&err, "lock_fencing", "stale_fencing_token");
ensure_fencing_token_fresh(7, 7).expect("current token must pass");
ensure_fencing_token_fresh(8, 7).expect("newer token must pass");
}
#[test]
fn lock_lease_lost_carries_policy_detail() {
let err = lock_lease_lost_status();
assert_eq!(err.code(), tonic::Code::FailedPrecondition);
assert_eq!(
err.message(),
"lock lease lost; it is now held by another owner"
);
assert_policy_detail(&err, "renew_lock", "lock_lease_lost");
}
#[test]
fn lock_owner_mismatch_carries_permission_denied_policy_detail() {
for operation in ["renew_lock", "release_lock"] {
let err = lock_held_by_different_owner_status(operation);
assert_eq!(err.code(), tonic::Code::PermissionDenied);
assert_eq!(err.message(), "lock is held by a different owner");
assert_policy_detail(&err, operation, "lock_owner_mismatch");
}
}
#[test]
fn lock_not_held_carries_schema_detail() {
let err = lock_not_held_status("renew_lock");
assert_eq!(err.code(), tonic::Code::NotFound);
assert_eq!(err.message(), "lock not held");
let detail = decode_detail(&err);
assert_eq!(detail.kind, ErrorKind::Schema as i32);
assert_eq!(detail.backend, "lock");
assert_eq!(detail.operation, "renew_lock");
assert_eq!(detail.capability_required, "lock_not_held");
assert!(!detail.retryable);
assert_eq!(detail.retry_after_ms, 0);
}
#[test]
fn lock_already_held_carries_schema_detail() {
let err = lock_already_held_status();
assert_eq!(err.code(), tonic::Code::AlreadyExists);
assert_eq!(err.message(), "lock is already held by another owner");
let detail = decode_detail(&err);
assert_eq!(detail.kind, ErrorKind::Schema as i32);
assert_eq!(detail.backend, "lock");
assert_eq!(detail.operation, "acquire_lock");
assert_eq!(detail.capability_required, "lock_already_held");
assert!(!detail.retryable);
assert_eq!(detail.retry_after_ms, 0);
}
#[test]
fn held_locks_quota_read_excludes_expired_rows() {
let now = Utc::now();
let read = held_locks_read("tenant-a", (MAX_ACTIVE_LOCKS_PER_TENANT as u32) + 1, now);
let expected = LogicalFilter::And(vec![
lock_filter("tenant-a", None, Some(STATUS_HELD)),
LogicalFilter::Comparison {
field: "expires_at".to_string(),
op: ComparisonOp::Gt,
value: LogicalValue::Timestamp(now),
},
]);
assert_eq!(read.filter, Some(expected));
}
#[test]
fn missing_fencing_token_source_returns_typed_refusal_not_a_token() {
let err = fencing_token_unavailable_status();
assert_eq!(err.code(), tonic::Code::FailedPrecondition);
let detail = decode_detail(&err);
assert_eq!(detail.kind, ErrorKind::Capability as i32);
assert_eq!(detail.backend, "lock");
assert_eq!(detail.operation, "lock_fencing");
assert_eq!(
detail.capability_required,
"canonical_store_monotone_counter"
);
assert!(!detail.retryable);
assert_eq!(detail.retry_after_ms, 0);
}
#[test]
fn expired_locks_claim_sql_shape() {
let sql = expired_locks_claim_sql(&lock_model());
assert!(
sql.starts_with("UPDATE"),
"flip and claim in one statement: {sql}"
);
assert!(
sql.contains("FOR UPDATE SKIP LOCKED"),
"no-double-expire: {sql}"
);
assert!(sql.contains("\"status\" = $1"), "flip target bind: {sql}");
assert!(
sql.contains("\"status\" = $2"),
"HELD claim filter bind: {sql}"
);
assert!(
sql.contains("\"expires_at\" < NOW()"),
"lapsed-only filter: {sql}"
);
assert!(sql.contains("LIMIT $3"), "bounded batch: {sql}");
for column in [
"RETURNING",
"lock_id",
"tenant_id",
"lock_name",
"owner_id",
"fencing_token",
] {
assert!(sql.contains(column), "event needs {column}: {sql}");
}
}
#[test]
fn lock_missing_runtime_capability_carries_typed_detail() {
let err = lock_capability_status(
"native_entity_dispatch",
"runtime_native_entity_dispatch",
"lock service requires runtime native-entity dispatch (no runtime configured)",
);
assert_eq!(err.code(), tonic::Code::FailedPrecondition);
assert_eq!(
err.message(),
"lock service requires runtime native-entity dispatch (no runtime configured)"
);
let detail = decode_detail(&err);
assert_eq!(detail.kind, ErrorKind::Capability as i32);
assert_eq!(detail.backend, "lock");
assert_eq!(detail.operation, "native_entity_dispatch");
assert_eq!(detail.capability_required, "runtime_native_entity_dispatch");
assert!(!detail.retryable);
}
#[tokio::test]
async fn get_lock_rejects_cross_tenant_body() {
let svc = LockServiceImpl::new();
let mut request = Request::new(lock_pb::GetLockRequest {
tenant_id: "tenant-b".to_string(),
lock_name: "orders".to_string(),
});
request
.metadata_mut()
.insert("x-tenant-id", MetadataValue::from_static("tenant-a"));
let err = svc
.get_lock(request)
.await
.expect_err("cross-tenant body must be rejected");
assert_eq!(err.code(), tonic::Code::PermissionDenied);
}
#[tokio::test]
async fn list_locks_rejects_cross_tenant_body() {
let svc = LockServiceImpl::new();
let mut request = Request::new(lock_pb::ListLocksRequest {
tenant_id: "tenant-b".to_string(),
..Default::default()
});
request
.metadata_mut()
.insert("x-tenant-id", MetadataValue::from_static("tenant-a"));
let err = svc
.list_locks(request)
.await
.expect_err("cross-tenant body must be rejected");
assert_eq!(err.code(), tonic::Code::PermissionDenied);
}
#[tokio::test]
async fn get_lock_empty_name_is_field_violation() {
let svc = LockServiceImpl::new();
let mut request = Request::new(lock_pb::GetLockRequest {
tenant_id: "tenant-a".to_string(),
lock_name: " ".to_string(),
});
request
.metadata_mut()
.insert("x-tenant-id", MetadataValue::from_static("tenant-a"));
let err = svc
.get_lock(request)
.await
.expect_err("empty lock_name must be rejected");
assert_eq!(err.code(), tonic::Code::InvalidArgument);
let detail = decode_detail(&err);
assert!(
detail
.field_violations
.iter()
.any(|f| f.field == "lock_name"),
"expected a lock_name field violation"
);
}
#[test]
fn lock_dto_from_json_maps_all_fields() {
let row = serde_json::json!({
"lock_id": "11111111-1111-1111-1111-111111111111",
"tenant_id": "tenant-a",
"lock_name": "orders",
"owner_id": "worker-1",
"fencing_token": 42,
"lease_ttl_seconds": 30,
"status": "HELD",
"acquired_at": "2026-07-16T00:00:00Z",
"expires_at": "2026-07-16T00:00:30Z",
"metadata_json": { "region": "eu" },
});
let dto = lock_dto_from_json(&row);
assert_eq!(dto.lock_name, "orders");
assert_eq!(dto.owner_id, "worker-1");
assert_eq!(dto.fencing_token, 42);
assert_eq!(dto.lease_ttl_seconds, 30);
assert_eq!(dto.status, "HELD");
assert_eq!(dto.acquired_at_unix, 1_784_160_000);
assert_eq!(dto.expires_at_unix, 1_784_160_030);
assert_eq!(dto.metadata_json, "{\"region\":\"eu\"}");
}
#[test]
fn lock_inventory_read_applies_status_filter_sort_and_pagination() {
let read = lock_inventory_read("tenant-a", None, Some("HELD"), 100, 50);
assert_eq!(read.sort.len(), 1);
assert_eq!(read.sort[0].field, "acquired_at");
let pg = read.pagination.expect("pagination present");
assert_eq!(pg.limit, Some(50));
assert_eq!(pg.offset, Some(100));
let rendered = format!("{:?}", read.filter);
assert!(
rendered.contains("HELD"),
"status filter must be present: {rendered}"
);
assert!(
rendered.contains("tenant-a"),
"tenant filter must be present"
);
}