use super::{AccessDecision, DatabaseObserver, QueryAccessContext, QueryOperationKind};
use crate::collection::CollectionType;
use parking_lot::Mutex;
#[derive(Default)]
struct LegacyObserver {
created: Mutex<Vec<String>>,
upserts: Mutex<Vec<(String, usize)>>,
ddl_should_reject: bool,
}
impl DatabaseObserver for LegacyObserver {
fn on_collection_created(&self, name: &str, _kind: &CollectionType) {
self.created.lock().push(name.to_string());
}
fn on_upsert(&self, collection: &str, point_count: usize) {
self.upserts
.lock()
.push((collection.to_string(), point_count));
}
fn on_ddl_request(&self, operation: &str, collection_name: &str) -> crate::Result<()> {
if self.ddl_should_reject {
return Err(crate::Error::Query(format!(
"rejected {operation} on {collection_name}"
)));
}
Ok(())
}
}
#[test]
fn legacy_observer_query_request_defaults_to_allow() {
let observer = LegacyObserver::default();
let collection = "documents".to_string();
let ctx = QueryAccessContext {
collection: &collection,
operation: QueryOperationKind::Select,
principal: None,
tenant_hint: None,
};
let decision = observer
.on_query_request(&ctx)
.expect("default on_query_request never errors");
assert!(
matches!(decision, AccessDecision::Allow),
"an observer that does not override on_query_request must inherit the \
allow-all default, got {decision:?}"
);
}
#[test]
fn legacy_observer_query_request_allows_every_operation_kind() {
let observer = LegacyObserver::default();
let collection = "vectors".to_string();
for operation in [
QueryOperationKind::VectorSearch,
QueryOperationKind::TextSearch,
QueryOperationKind::HybridSearch,
QueryOperationKind::GraphTraversal,
QueryOperationKind::Select,
] {
let ctx = QueryAccessContext {
collection: &collection,
operation,
principal: None,
tenant_hint: None,
};
let decision = observer
.on_query_request(&ctx)
.expect("default on_query_request never errors");
assert!(
matches!(decision, AccessDecision::Allow),
"operation {operation:?} must be allowed by the default hook"
);
}
}
#[test]
fn legacy_observer_preexisting_hooks_still_behave() {
let observer = LegacyObserver::default();
observer.on_collection_created("docs", &CollectionType::MetadataOnly);
observer.on_upsert("docs", 7);
assert_eq!(observer.created.lock().as_slice(), ["docs"]);
assert_eq!(
observer.upserts.lock().as_slice(),
[("docs".to_string(), 7)]
);
assert!(observer.on_ddl_request("CREATE", "docs").is_ok());
let rejecting = LegacyObserver {
ddl_should_reject: true,
..LegacyObserver::default()
};
assert!(rejecting.on_ddl_request("DROP", "docs").is_err());
}
#[test]
fn legacy_observer_is_usable_as_a_trait_object() {
let observer: &dyn DatabaseObserver = &LegacyObserver::default();
let collection = "graph".to_string();
let ctx = QueryAccessContext {
collection: &collection,
operation: QueryOperationKind::GraphTraversal,
principal: Some("user-1"),
tenant_hint: Some("tenant-a"),
};
let decision = observer
.on_query_request(&ctx)
.expect("default on_query_request never errors");
assert!(matches!(decision, AccessDecision::Allow));
}