use super::*;
#[tokio::test]
async fn entity_list_wrappers_skip_count_but_exact_consumers_retain_it() {
use crate::reference_resolution::{resolve_reference, ReferenceResolution};
use crate::reference_ring::ReferenceRing;
use rusqlite::hooks::{AuthAction, AuthContext, Authorization};
let runtime = rt();
let token = runtime
.authorize_with_visibility(
Namespace::local(),
vec![Namespace::parse("shared").unwrap()],
)
.unwrap();
let store = runtime.entities(&token).unwrap();
for index in 0..14 {
let namespace = match index {
12 => "shared",
13 => "hidden",
_ => "local",
};
let mut row = Entity::new(namespace, "document", "counted-name")
.with_entity_type((index % 2 == 0).then_some("paper"))
.with_properties(serde_json::json!({"type": "paper", "ordinal": index}))
.with_description(format!("row-{index}"))
.with_tags(vec![if index % 2 == 0 { "wanted" } else { "other" }.into()]);
row.created_at = index;
row.updated_at = index;
store.upsert_entity(row).await.unwrap();
}
let filter = EntityFilter {
kinds: vec!["document".into()],
entity_types: vec!["paper".into()],
legacy_entity_type_fallback: true,
namespaces: vec!["local".into(), "shared".into()],
..Default::default()
};
let tagged_filter = EntityFilter {
kinds: vec!["document".into()],
tags_any: vec!["wanted".into()],
namespaces: filter.namespaces.clone(),
..Default::default()
};
let page = PageRequest {
limit: 2,
offset: 1,
};
let expected = store
.query_entities("local", filter.clone(), page.clone())
.await
.unwrap();
assert_eq!(expected.total, Some(13));
assert_eq!(expected.items.len(), 2);
let expected_tagged = store
.query_entities("local", tagged_filter, page.clone())
.await
.unwrap();
assert_eq!(expected_tagged.total, Some(7));
assert_eq!(runtime.backend().pool().max_readers(), 0);
let denied = Arc::new(AtomicUsize::new(0));
{
let attempts = Arc::clone(&denied);
let writer = runtime.backend().pool().writer().unwrap();
writer
.conn()
.authorizer(Some(move |context: AuthContext<'_>| match context.action {
AuthAction::Function { function_name }
if function_name.eq_ignore_ascii_case("count") =>
{
attempts.fetch_add(1, Ordering::SeqCst);
Authorization::Deny
}
_ => Authorization::Allow,
}))
.unwrap();
}
assert!(store
.query_entities("local", filter.clone(), page.clone())
.await
.is_err());
let control_attempts = denied.load(Ordering::SeqCst);
assert!(
control_attempts > 0,
"exact page must encounter the count authorizer"
);
let scalar = runtime
.list_entities(&token, Some("document"), Some("paper"), 2, 1)
.await
.unwrap();
let composed = runtime
.list_entities_filtered(
&token,
EntityFilter {
entity_types_by_kind: [("document".into(), vec!["paper".into()])]
.into_iter()
.collect(),
namespaces: vec!["hidden".into()],
..filter.clone()
},
2,
1,
)
.await
.unwrap();
let tagged = runtime
.list_entities_tagged(&token, Some("document"), Some("wanted"), 2, 1)
.await
.unwrap();
let expected_rows = serde_json::to_value(&expected.items).unwrap();
assert_eq!(serde_json::to_value(scalar).unwrap(), expected_rows);
assert_eq!(serde_json::to_value(composed).unwrap(), expected_rows);
assert_eq!(
serde_json::to_value(tagged).unwrap(),
serde_json::to_value(&expected_tagged.items).unwrap()
);
assert_eq!(denied.load(Ordering::SeqCst), control_attempts);
assert!(runtime
.count_entities_tagged(&token, Some("document"), Some("wanted"))
.await
.is_err());
let count_attempts = denied.load(Ordering::SeqCst);
assert!(count_attempts > control_attempts);
let ring = ReferenceRing::new();
assert!(
resolve_reference(&runtime, &ring, &token, "counted-name", 1, Some("document"))
.await
.is_err()
);
assert!(denied.load(Ordering::SeqCst) > count_attempts);
runtime
.backend()
.pool()
.writer()
.unwrap()
.conn()
.authorizer(None::<fn(AuthContext<'_>) -> Authorization>)
.unwrap();
assert_eq!(
store
.query_entities("local", filter, page)
.await
.unwrap()
.total,
Some(13)
);
assert_eq!(
runtime
.count_entities_tagged(&token, Some("document"), Some("wanted"))
.await
.unwrap(),
7
);
let ReferenceResolution::Ambiguous { candidates } =
resolve_reference(&runtime, &ring, &token, "counted-name", 1, Some("document"))
.await
.unwrap()
else {
panic!("exact-name resolution must retain ambiguity beyond its bounded page");
};
assert_eq!(candidates.len(), 10);
}
#[tokio::test]
async fn list_composed_type_filters_apply_alias_and_disjoint_sets_before_pagination() {
let runtime = rt();
let token = runtime.authorize(Namespace::local()).unwrap();
let store = runtime.entities(&token).unwrap();
let mut expected = Vec::new();
for (kind, column, property, matches) in [
("document", Some("paper"), "ignored", true),
("document", None, "preprint", true),
("document", Some("preprint"), "ignored", true),
("document", Some("report"), "preprint", false),
("concept", None, "preprint", false),
] {
let row = Entity::new("local", kind, "type predicate")
.with_entity_type(column)
.with_properties(serde_json::json!({"type": property}));
if matches {
expected.push(row.id);
}
store.upsert_entity(row).await.unwrap();
}
store
.upsert_entity(
Entity::new("foreign", "document", "foreign alias")
.with_properties(serde_json::json!({"type":"preprint"})),
)
.await
.unwrap();
for (values, should_match) in [
(vec!["paper".to_string(), "preprint".to_string()], true),
(vec!["absent".to_string()], false),
(Vec::new(), false),
] {
let filter = EntityFilter {
entity_types_by_kind: [("document".to_string(), values)].into_iter().collect(),
legacy_entity_type_fallback: true,
namespaces: vec!["foreign".to_string()], ..Default::default()
};
let mut offset_ids = Vec::new();
for offset in 0..=expected.len() {
let page = runtime
.list_entities_filtered(&token, filter.clone(), 1, offset as u32)
.await
.unwrap();
offset_ids.extend(page.into_iter().map(|row| row.id));
}
let mut cursor_ids = Vec::new();
let mut after = None;
for _ in 0..=expected.len() {
let (page, next) = runtime
.list_entities_after_filtered(&token, filter.clone(), after, 1)
.await
.unwrap();
cursor_ids.extend(page.into_iter().map(|row| row.id));
after = next;
if after.is_none() {
break;
}
}
let mut wanted = if should_match {
expected.clone()
} else {
Vec::new()
};
wanted.sort_unstable();
offset_ids.sort_unstable();
cursor_ids.sort_unstable();
assert_eq!(offset_ids, wanted);
assert_eq!(cursor_ids, wanted);
}
assert_eq!(
runtime
.list_entities(&token, None, Some("paper"), 20, 0)
.await
.unwrap()
.len(),
1
);
assert_eq!(
runtime
.list_entities_after(&token, None, Some("paper"), &[], None, 20)
.await
.unwrap()
.0
.len(),
1
);
}