diff --git a/packages/rs-drive/src/query/contested_resource_votes_given_by_identity_query.rs b/packages/rs-drive/src/query/contested_resource_votes_given_by_identity_query.rs index f0ca0e6d60d..dda54ba32d5 100644 --- a/packages/rs-drive/src/query/contested_resource_votes_given_by_identity_query.rs +++ b/packages/rs-drive/src/query/contested_resource_votes_given_by_identity_query.rs @@ -276,3 +276,196 @@ impl ContestedResourceVotesGivenByIdentityQuery { }) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::drive::votes::paths::{CONTESTED_RESOURCE_TREE_KEY, IDENTITY_VOTES_TREE_KEY}; + use crate::drive::RootTree; + use grovedb::QueryItem; + + fn expected_base_path(identity_id: &[u8; 32]) -> Vec> { + vec![ + vec![RootTree::Votes as u8], + vec![CONTESTED_RESOURCE_TREE_KEY as u8], + vec![IDENTITY_VOTES_TREE_KEY as u8], + identity_id.to_vec(), + ] + } + + // ----------------------------------------------------------------------- + // construct_path_query + // ----------------------------------------------------------------------- + + #[test] + fn construct_path_query_no_start_ascending() { + let identity_id = Identifier::from([0xAA; 32]); + let query = ContestedResourceVotesGivenByIdentityQuery { + identity_id, + offset: None, + limit: Some(10), + start_at: None, + order_ascending: true, + }; + + let pq = query + .construct_path_query() + .expect("should build path query"); + assert_eq!(pq.path, expected_base_path(identity_id.as_bytes())); + assert_eq!(pq.query.limit, Some(10)); + assert_eq!(pq.query.offset, None); + assert!(pq.query.query.left_to_right); + + // No start_at means insert_all -> RangeFull + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + assert!(matches!(&items[0], QueryItem::RangeFull(..))); + } + + #[test] + fn construct_path_query_no_start_descending() { + let identity_id = Identifier::from([0xBB; 32]); + let query = ContestedResourceVotesGivenByIdentityQuery { + identity_id, + offset: None, + limit: None, + start_at: None, + order_ascending: false, + }; + + let pq = query + .construct_path_query() + .expect("should build path query"); + assert!(!pq.query.query.left_to_right); + assert_eq!(pq.query.limit, None); + } + + #[test] + fn construct_path_query_start_at_included_ascending() { + let identity_id = Identifier::from([0xCC; 32]); + let start_key = [0x42u8; 32]; + let query = ContestedResourceVotesGivenByIdentityQuery { + identity_id, + offset: None, + limit: Some(5), + start_at: Some((start_key, true)), + order_ascending: true, + }; + + let pq = query + .construct_path_query() + .expect("should build path query"); + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + assert!( + matches!(&items[0], QueryItem::RangeFrom(r) if r.start == start_key.to_vec()), + "ascending + included = RangeFrom" + ); + } + + #[test] + fn construct_path_query_start_at_excluded_ascending() { + let identity_id = Identifier::from([0xDD; 32]); + let start_key = [0x42u8; 32]; + let query = ContestedResourceVotesGivenByIdentityQuery { + identity_id, + offset: None, + limit: Some(5), + start_at: Some((start_key, false)), + order_ascending: true, + }; + + let pq = query + .construct_path_query() + .expect("should build path query"); + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + assert!( + matches!(&items[0], QueryItem::RangeAfter(r) if r.start == start_key.to_vec()), + "ascending + excluded = RangeAfter" + ); + } + + #[test] + fn construct_path_query_start_at_included_descending() { + let identity_id = Identifier::from([0xEE; 32]); + let start_key = [0x42u8; 32]; + let query = ContestedResourceVotesGivenByIdentityQuery { + identity_id, + offset: None, + limit: Some(5), + start_at: Some((start_key, true)), + order_ascending: false, + }; + + let pq = query + .construct_path_query() + .expect("should build path query"); + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + assert!( + matches!(&items[0], QueryItem::RangeToInclusive(r) if r.end == start_key.to_vec()), + "descending + included = RangeToInclusive" + ); + } + + #[test] + fn construct_path_query_start_at_excluded_descending() { + let identity_id = Identifier::from([0xFF; 32]); + let start_key = [0x42u8; 32]; + let query = ContestedResourceVotesGivenByIdentityQuery { + identity_id, + offset: None, + limit: Some(5), + start_at: Some((start_key, false)), + order_ascending: false, + }; + + let pq = query + .construct_path_query() + .expect("should build path query"); + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + assert!( + matches!(&items[0], QueryItem::RangeTo(r) if r.end == start_key.to_vec()), + "descending + excluded = RangeTo" + ); + } + + #[test] + fn construct_path_query_with_offset_and_limit() { + let identity_id = Identifier::from([0x11; 32]); + let query = ContestedResourceVotesGivenByIdentityQuery { + identity_id, + offset: Some(7), + limit: Some(25), + start_at: None, + order_ascending: true, + }; + + let pq = query + .construct_path_query() + .expect("should build path query"); + assert_eq!(pq.query.limit, Some(25)); + assert_eq!(pq.query.offset, Some(7)); + } + + #[test] + fn construct_path_query_identity_id_appears_in_path() { + let identity_id = Identifier::from([0x99; 32]); + let query = ContestedResourceVotesGivenByIdentityQuery { + identity_id, + offset: None, + limit: None, + start_at: None, + order_ascending: true, + }; + + let pq = query + .construct_path_query() + .expect("should build path query"); + // The 4th path element should be the identity_id + assert_eq!(pq.path.len(), 4); + assert_eq!(pq.path[3], identity_id.as_bytes().to_vec()); + } +} diff --git a/packages/rs-drive/src/query/vote_poll_contestant_votes_query.rs b/packages/rs-drive/src/query/vote_poll_contestant_votes_query.rs index c5aa0f88cec..af508661710 100644 --- a/packages/rs-drive/src/query/vote_poll_contestant_votes_query.rs +++ b/packages/rs-drive/src/query/vote_poll_contestant_votes_query.rs @@ -336,3 +336,241 @@ impl ResolvedContestedDocumentVotePollVotesDriveQuery<'_> { }) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePollWithContractInfoAllowBorrowed; + use crate::util::object_size_info::DataContractResolvedInfo; + use dpp::tests::fixtures::get_dpns_data_contract_fixture; + use dpp::version::PlatformVersion; + use grovedb::QueryItem; + + /// Helper to construct a resolved contestant votes query using the DPNS + /// "domain" contested index. + fn build_resolved_query( + contract: &dpp::data_contract::DataContract, + contestant_id: Identifier, + offset: Option, + limit: Option, + start_at: Option<([u8; 32], bool)>, + order_ascending: bool, + ) -> ResolvedContestedDocumentVotePollVotesDriveQuery<'_> { + let document_type_name = "domain".to_string(); + let index_name = "parentNameAndLabel".to_string(); + + let parent_domain_value = dpp::platform_value::Value::Text("dash".to_string()); + let label_value = dpp::platform_value::Value::Text("test-name".to_string()); + + let index_values = vec![parent_domain_value, label_value]; + + let vote_poll = ContestedDocumentResourceVotePollWithContractInfoAllowBorrowed { + contract: DataContractResolvedInfo::BorrowedDataContract(contract), + document_type_name, + index_name, + index_values, + }; + + ResolvedContestedDocumentVotePollVotesDriveQuery { + vote_poll, + contestant_id, + offset, + limit, + start_at, + order_ascending, + } + } + + // ----------------------------------------------------------------------- + // construct_path_query tests + // ----------------------------------------------------------------------- + + #[test] + fn construct_path_query_no_start_ascending() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let contestant_id = Identifier::from([0xAA; 32]); + let query = build_resolved_query( + &contract, + contestant_id, + None, // offset + Some(10), // limit + None, // start_at + true, // ascending + ); + + let pq = query + .construct_path_query(platform_version) + .expect("should build path query"); + + // Path should end with the contestant identifier and voting storage key + assert!(!pq.path.is_empty()); + assert_eq!(pq.query.limit, Some(10)); + assert_eq!(pq.query.offset, None); + assert!(pq.query.query.left_to_right); + + // No start -> RangeFull + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + assert!(matches!(&items[0], QueryItem::RangeFull(..))); + } + + #[test] + fn construct_path_query_no_start_descending() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let contestant_id = Identifier::from([0xBB; 32]); + let query = build_resolved_query(&contract, contestant_id, None, None, None, false); + + let pq = query + .construct_path_query(platform_version) + .expect("should build path query"); + + assert!(!pq.query.query.left_to_right); + assert_eq!(pq.query.limit, None); + } + + #[test] + fn construct_path_query_start_at_included_ascending() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let contestant_id = Identifier::from([0xCC; 32]); + let start_key = [0x42u8; 32]; + let query = build_resolved_query( + &contract, + contestant_id, + None, + Some(5), + Some((start_key, true)), + true, + ); + + let pq = query + .construct_path_query(platform_version) + .expect("should build path query"); + + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + assert!( + matches!(&items[0], QueryItem::RangeFrom(r) if r.start == start_key.to_vec()), + "ascending + included = RangeFrom" + ); + } + + #[test] + fn construct_path_query_start_at_excluded_ascending() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let contestant_id = Identifier::from([0xDD; 32]); + let start_key = [0x42u8; 32]; + let query = build_resolved_query( + &contract, + contestant_id, + None, + Some(5), + Some((start_key, false)), + true, + ); + + let pq = query + .construct_path_query(platform_version) + .expect("should build path query"); + + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + assert!( + matches!(&items[0], QueryItem::RangeAfter(r) if r.start == start_key.to_vec()), + "ascending + excluded = RangeAfter" + ); + } + + #[test] + fn construct_path_query_start_at_included_descending() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let contestant_id = Identifier::from([0xEE; 32]); + let start_key = [0x42u8; 32]; + let query = build_resolved_query( + &contract, + contestant_id, + None, + Some(5), + Some((start_key, true)), + false, + ); + + let pq = query + .construct_path_query(platform_version) + .expect("should build path query"); + + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + assert!( + matches!(&items[0], QueryItem::RangeToInclusive(r) if r.end == start_key.to_vec()), + "descending + included = RangeToInclusive" + ); + } + + #[test] + fn construct_path_query_start_at_excluded_descending() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let contestant_id = Identifier::from([0xFF; 32]); + let start_key = [0x42u8; 32]; + let query = build_resolved_query( + &contract, + contestant_id, + None, + Some(5), + Some((start_key, false)), + false, + ); + + let pq = query + .construct_path_query(platform_version) + .expect("should build path query"); + + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + assert!( + matches!(&items[0], QueryItem::RangeTo(r) if r.end == start_key.to_vec()), + "descending + excluded = RangeTo" + ); + } + + #[test] + fn construct_path_query_with_offset_and_limit() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let contestant_id = Identifier::from([0x11; 32]); + let query = build_resolved_query( + &contract, + contestant_id, + Some(3), // offset + Some(20), // limit + None, + true, + ); + + let pq = query + .construct_path_query(platform_version) + .expect("should build path query"); + + assert_eq!(pq.query.limit, Some(20)); + assert_eq!(pq.query.offset, Some(3)); + } +} diff --git a/packages/rs-drive/src/query/vote_poll_vote_state_query.rs b/packages/rs-drive/src/query/vote_poll_vote_state_query.rs index 7b616b06c28..f98cc35b463 100644 --- a/packages/rs-drive/src/query/vote_poll_vote_state_query.rs +++ b/packages/rs-drive/src/query/vote_poll_vote_state_query.rs @@ -824,3 +824,614 @@ impl ResolvedContestedDocumentVotePollDriveQuery<'_> { } } } + +#[cfg(test)] +mod tests { + use super::*; + use dpp::identifier::Identifier; + use dpp::tests::fixtures::get_dpns_data_contract_fixture; + use dpp::version::PlatformVersion; + use dpp::voting::contender_structs::ContenderWithSerializedDocumentV0; + + use crate::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePollWithContractInfoAllowBorrowed; + use crate::util::object_size_info::DataContractResolvedInfo; + + /// Helper: build a `ResolvedContestedDocumentVotePollDriveQuery` using + /// the DPNS "domain" document type's contested index (`parentNameAndLabel`). + fn build_resolved_query( + contract: &dpp::data_contract::DataContract, + result_type: ContestedDocumentVotePollDriveQueryResultType, + offset: Option, + limit: Option, + start_at: Option<([u8; 32], bool)>, + allow_include_locked_and_abstaining: bool, + ) -> ResolvedContestedDocumentVotePollDriveQuery<'_> { + // The DPNS "domain" document type has a contested index "parentNameAndLabel" + // with properties: normalizedParentDomainName, normalizedLabel + let document_type_name = "domain".to_string(); + let index_name = "parentNameAndLabel".to_string(); + + let parent_domain_value = dpp::platform_value::Value::Text("dash".to_string()); + let label_value = dpp::platform_value::Value::Text("test-name".to_string()); + + let index_values = vec![parent_domain_value, label_value]; + + let vote_poll = ContestedDocumentResourceVotePollWithContractInfoAllowBorrowed { + contract: DataContractResolvedInfo::BorrowedDataContract(contract), + document_type_name, + index_name, + index_values, + }; + + ResolvedContestedDocumentVotePollDriveQuery { + vote_poll, + result_type, + offset, + limit, + start_at, + allow_include_locked_and_abstaining_vote_tally: allow_include_locked_and_abstaining, + } + } + + // ----------------------------------------------------------------------- + // ContestedDocumentVotePollDriveQueryResultType helper methods + // ----------------------------------------------------------------------- + + #[test] + fn has_vote_tally_returns_correct_values() { + use ContestedDocumentVotePollDriveQueryResultType::*; + assert!(!Documents.has_vote_tally()); + assert!(VoteTally.has_vote_tally()); + assert!(DocumentsAndVoteTally.has_vote_tally()); + assert!(!SingleDocumentByContender(Identifier::default()).has_vote_tally()); + } + + #[test] + fn has_documents_returns_correct_values() { + use ContestedDocumentVotePollDriveQueryResultType::*; + assert!(Documents.has_documents()); + assert!(!VoteTally.has_documents()); + assert!(DocumentsAndVoteTally.has_documents()); + assert!(SingleDocumentByContender(Identifier::default()).has_documents()); + } + + // ----------------------------------------------------------------------- + // TryFrom for ContestedDocumentVotePollDriveQueryResultType + // ----------------------------------------------------------------------- + + #[test] + fn try_from_i32_valid_values() { + let docs = ContestedDocumentVotePollDriveQueryResultType::try_from(0).unwrap(); + assert_eq!( + docs, + ContestedDocumentVotePollDriveQueryResultType::Documents + ); + + let tally = ContestedDocumentVotePollDriveQueryResultType::try_from(1).unwrap(); + assert_eq!( + tally, + ContestedDocumentVotePollDriveQueryResultType::VoteTally + ); + + let both = ContestedDocumentVotePollDriveQueryResultType::try_from(2).unwrap(); + assert_eq!( + both, + ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally + ); + } + + #[test] + fn try_from_i32_value_3_returns_unsupported_error() { + let result = ContestedDocumentVotePollDriveQueryResultType::try_from(3); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + matches!(err, Error::Query(QuerySyntaxError::Unsupported(msg)) if msg.contains("SingleDocumentByContender")) + ); + } + + #[test] + fn try_from_i32_out_of_range_returns_unsupported_error() { + let result = ContestedDocumentVotePollDriveQueryResultType::try_from(99); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + matches!(err, Error::Query(QuerySyntaxError::Unsupported(msg)) if msg.contains("99")) + ); + + let result_neg = ContestedDocumentVotePollDriveQueryResultType::try_from(-1); + assert!(result_neg.is_err()); + } + + // ----------------------------------------------------------------------- + // TryFrom + // for FinalizedContestedDocumentVotePollDriveQueryExecutionResult + // ----------------------------------------------------------------------- + + #[test] + fn finalized_try_from_success_with_complete_data() { + let id = Identifier::from([0xAA; 32]); + let contender = ContenderWithSerializedDocumentV0 { + identity_id: id, + serialized_document: Some(vec![1, 2, 3]), + vote_tally: Some(42), + }; + let result = ContestedDocumentVotePollDriveQueryExecutionResult { + contenders: vec![contender.into()], + locked_vote_tally: Some(10), + abstaining_vote_tally: Some(5), + winner: None, + skipped: 0, + }; + + let finalized: FinalizedContestedDocumentVotePollDriveQueryExecutionResult = + result.try_into().expect("should convert"); + assert_eq!(finalized.contenders.len(), 1); + assert_eq!(finalized.locked_vote_tally, 10); + assert_eq!(finalized.abstaining_vote_tally, 5); + } + + #[test] + fn finalized_try_from_fails_without_locked_tally() { + let result = ContestedDocumentVotePollDriveQueryExecutionResult { + contenders: vec![], + locked_vote_tally: None, + abstaining_vote_tally: Some(5), + winner: None, + skipped: 0, + }; + + let conversion: Result = + result.try_into(); + assert!(conversion.is_err()); + } + + #[test] + fn finalized_try_from_fails_without_abstaining_tally() { + let result = ContestedDocumentVotePollDriveQueryExecutionResult { + contenders: vec![], + locked_vote_tally: Some(10), + abstaining_vote_tally: None, + winner: None, + skipped: 0, + }; + + let conversion: Result = + result.try_into(); + assert!(conversion.is_err()); + } + + #[test] + fn finalized_try_from_fails_when_contender_missing_document() { + let contender = ContenderWithSerializedDocumentV0 { + identity_id: Identifier::from([0xBB; 32]), + serialized_document: None, // missing + vote_tally: Some(10), + }; + let result = ContestedDocumentVotePollDriveQueryExecutionResult { + contenders: vec![contender.into()], + locked_vote_tally: Some(10), + abstaining_vote_tally: Some(5), + winner: None, + skipped: 0, + }; + + let conversion: Result = + result.try_into(); + assert!(conversion.is_err()); + } + + #[test] + fn finalized_try_from_fails_when_contender_missing_vote_tally() { + let contender = ContenderWithSerializedDocumentV0 { + identity_id: Identifier::from([0xCC; 32]), + serialized_document: Some(vec![1]), + vote_tally: None, // missing + }; + let result = ContestedDocumentVotePollDriveQueryExecutionResult { + contenders: vec![contender.into()], + locked_vote_tally: Some(10), + abstaining_vote_tally: Some(5), + winner: None, + skipped: 0, + }; + + let conversion: Result = + result.try_into(); + assert!(conversion.is_err()); + } + + // ----------------------------------------------------------------------- + // construct_path_query on ResolvedContestedDocumentVotePollDriveQuery + // ----------------------------------------------------------------------- + + #[test] + fn construct_path_query_documents_no_start_no_tally() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let query = build_resolved_query( + &contract, + ContestedDocumentVotePollDriveQueryResultType::Documents, + None, // offset + Some(5), // limit + None, // start_at + false, // allow tally + ); + + let path_query = query + .construct_path_query(platform_version) + .expect("should build path query"); + + // Path should have multiple components (voting root + contested + active polls + contract + doc type + index key + index values) + assert!(!path_query.path.is_empty()); + + // Limit should pass through directly for Documents without tally + assert_eq!(path_query.query.limit, Some(5)); + assert_eq!(path_query.query.offset, None); + + // The query items should contain a RangeAfter (after RESOURCE_LOCK_VOTE_TREE_KEY) + let items = &path_query.query.query.items; + assert_eq!( + items.len(), + 1, + "should have exactly 1 query item for Documents without tally" + ); + assert!( + matches!(&items[0], QueryItem::RangeAfter(..)), + "expected RangeAfter, got {:?}", + &items[0] + ); + + // Subquery path should point to document storage [vec![0]] + assert_eq!( + path_query.query.query.default_subquery_branch.subquery_path, + Some(vec![vec![0]]) + ); + } + + #[test] + fn construct_path_query_vote_tally_with_locked_and_abstaining() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let query = build_resolved_query( + &contract, + ContestedDocumentVotePollDriveQueryResultType::VoteTally, + None, // offset + Some(10), // limit + None, // start_at + true, // allow tally (enabled AND result_type has_vote_tally) + ); + + let path_query = query + .construct_path_query(platform_version) + .expect("should build path query"); + + // With allow_include_locked_and_abstaining + VoteTally, query is insert_all() + // and limit is original + 3 + assert_eq!(path_query.query.limit, Some(13)); + + // Query should be RangeFull (insert_all) + let items = &path_query.query.query.items; + assert_eq!(items.len(), 1); + assert!( + matches!(&items[0], QueryItem::RangeFull(..)), + "expected RangeFull, got {:?}", + &items[0] + ); + + // Subquery path should point to vote tally [vec![1]] + assert_eq!( + path_query.query.query.default_subquery_branch.subquery_path, + Some(vec![vec![1]]) + ); + } + + #[test] + fn construct_path_query_documents_and_vote_tally_with_locked_and_abstaining() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let query = build_resolved_query( + &contract, + ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally, + None, // offset + Some(10), // limit + None, // start_at + true, // allow tally + ); + + let path_query = query + .construct_path_query(platform_version) + .expect("should build path query"); + + // With allow_include + DocumentsAndVoteTally: limit = limit * 2 + 3 + assert_eq!(path_query.query.limit, Some(23)); + + // Subquery should be a query with keys [0, 1] (not a path) + assert!(path_query + .query + .query + .default_subquery_branch + .subquery + .is_some()); + assert!(path_query + .query + .query + .default_subquery_branch + .subquery_path + .is_none()); + } + + #[test] + fn construct_path_query_vote_tally_without_locked_and_abstaining() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let query = build_resolved_query( + &contract, + ContestedDocumentVotePollDriveQueryResultType::VoteTally, + None, // offset + Some(10), // limit + None, // start_at + false, // allow_include_locked_and_abstaining = false + ); + + let path_query = query + .construct_path_query(platform_version) + .expect("should build path query"); + + // Without locked/abstaining: VoteTally inserts StoredInfo key + RangeAfter + // limit = limit + 1 + assert_eq!(path_query.query.limit, Some(11)); + + // Should have 2 query items: Key(RESOURCE_STORED_INFO) and RangeAfter + let items = &path_query.query.query.items; + assert_eq!(items.len(), 2); + assert!( + matches!(&items[0], QueryItem::Key(k) if *k == RESOURCE_STORED_INFO_KEY_U8_32.to_vec()) + ); + assert!(matches!(&items[1], QueryItem::RangeAfter(..))); + } + + #[test] + fn construct_path_query_with_start_at_included() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let start_key = [0x42u8; 32]; + let query = build_resolved_query( + &contract, + ContestedDocumentVotePollDriveQueryResultType::Documents, + None, // offset + Some(5), // limit + Some((start_key, true)), // start_at included + false, + ); + + let path_query = query + .construct_path_query(platform_version) + .expect("should build path query"); + + // With start_at included, should be RangeFrom + let items = &path_query.query.query.items; + assert_eq!(items.len(), 1); + assert!( + matches!(&items[0], QueryItem::RangeFrom(r) if r.start == start_key.to_vec()), + "expected RangeFrom starting at start_key" + ); + assert_eq!(path_query.query.limit, Some(5)); + } + + #[test] + fn construct_path_query_with_start_at_excluded() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let start_key = [0x42u8; 32]; + let query = build_resolved_query( + &contract, + ContestedDocumentVotePollDriveQueryResultType::Documents, + None, // offset + Some(5), // limit + Some((start_key, false)), // start_at NOT included + false, + ); + + let path_query = query + .construct_path_query(platform_version) + .expect("should build path query"); + + // With start_at excluded, should be RangeAfter + let items = &path_query.query.query.items; + assert_eq!(items.len(), 1); + assert!( + matches!(&items[0], QueryItem::RangeAfter(r) if r.start == start_key.to_vec()), + "expected RangeAfter starting at start_key" + ); + } + + #[test] + fn construct_path_query_with_offset() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let query = build_resolved_query( + &contract, + ContestedDocumentVotePollDriveQueryResultType::Documents, + Some(3), // offset + Some(10), // limit + None, // start_at + false, + ); + + let path_query = query + .construct_path_query(platform_version) + .expect("should build path query"); + + assert_eq!(path_query.query.offset, Some(3)); + assert_eq!(path_query.query.limit, Some(10)); + } + + #[test] + fn construct_path_query_no_limit() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let query = build_resolved_query( + &contract, + ContestedDocumentVotePollDriveQueryResultType::Documents, + None, // offset + None, // no limit + None, // start_at + false, + ); + + let path_query = query + .construct_path_query(platform_version) + .expect("should build path query"); + + assert_eq!(path_query.query.limit, None); + } + + #[test] + fn construct_path_query_documents_and_vote_tally_with_start_at_doubles_limit() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let start_key = [0x50u8; 32]; + let query = build_resolved_query( + &contract, + ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally, + None, // offset + Some(10), // limit + Some((start_key, true)), // start_at included + false, + ); + + let path_query = query + .construct_path_query(platform_version) + .expect("should build path query"); + + // With start_at + DocumentsAndVoteTally: limit = limit * 2 + assert_eq!(path_query.query.limit, Some(20)); + } + + #[test] + fn construct_path_query_single_document_by_contender() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let contender_id = Identifier::from([0xDD; 32]); + let query = build_resolved_query( + &contract, + ContestedDocumentVotePollDriveQueryResultType::SingleDocumentByContender(contender_id), + None, // offset + Some(1), // limit + None, // start_at + false, + ); + + let path_query = query + .construct_path_query(platform_version) + .expect("should build path query"); + + // Should have a Key query item with the contender_id bytes + let items = &path_query.query.query.items; + assert_eq!(items.len(), 1); + assert!( + matches!(&items[0], QueryItem::Key(k) if k.as_slice() == contender_id.as_bytes()), + "expected Key with contender ID" + ); + assert_eq!(path_query.query.limit, Some(1)); + + // Subquery path for SingleDocumentByContender should be [vec![0]] (document storage) + assert_eq!( + path_query.query.query.default_subquery_branch.subquery_path, + Some(vec![vec![0]]) + ); + } + + #[test] + fn construct_path_query_has_conditional_subquery_for_stored_info() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let query = build_resolved_query( + &contract, + ContestedDocumentVotePollDriveQueryResultType::Documents, + None, + Some(5), + None, + false, + ); + + let path_query = query + .construct_path_query(platform_version) + .expect("should build path query"); + + // Should always have a conditional subquery for RESOURCE_STORED_INFO_KEY + let conditional = path_query + .query + .query + .conditional_subquery_branches + .as_ref() + .expect("should have conditional branches"); + let stored_info_key = QueryItem::Key(RESOURCE_STORED_INFO_KEY_U8_32.to_vec()); + assert!( + conditional.contains_key(&stored_info_key), + "should have conditional subquery for stored info key" + ); + } + + #[test] + fn construct_path_query_with_locked_abstaining_has_conditional_subqueries() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let query = build_resolved_query( + &contract, + ContestedDocumentVotePollDriveQueryResultType::VoteTally, + None, + Some(5), + None, + true, // allow locked and abstaining + ); + + let path_query = query + .construct_path_query(platform_version) + .expect("should build path query"); + + let conditional = path_query + .query + .query + .conditional_subquery_branches + .as_ref() + .expect("should have conditional branches"); + + // Should have conditional subqueries for lock and abstain keys + let lock_key = QueryItem::Key(RESOURCE_LOCK_VOTE_TREE_KEY_U8_32.to_vec()); + let abstain_key = QueryItem::Key(RESOURCE_ABSTAIN_VOTE_TREE_KEY_U8_32.to_vec()); + assert!( + conditional.contains_key(&lock_key), + "should have conditional subquery for lock key" + ); + assert!( + conditional.contains_key(&abstain_key), + "should have conditional subquery for abstain key" + ); + } +} diff --git a/packages/rs-drive/src/query/vote_polls_by_document_type_query.rs b/packages/rs-drive/src/query/vote_polls_by_document_type_query.rs index 7014533eced..8671fa8d823 100644 --- a/packages/rs-drive/src/query/vote_polls_by_document_type_query.rs +++ b/packages/rs-drive/src/query/vote_polls_by_document_type_query.rs @@ -524,3 +524,381 @@ impl<'a> ResolvedVotePollsByDocumentTypeQuery<'a> { } } } + +#[cfg(test)] +mod tests { + use super::*; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::tests::fixtures::get_dpns_data_contract_fixture; + use dpp::version::PlatformVersion; + use grovedb::QueryItem; + + use crate::drive::votes::paths::vote_contested_resource_contract_documents_indexes_path_vec; + + /// Build a `ResolvedVotePollsByDocumentTypeQuery` from a DPNS contract. + /// + /// The DPNS "domain" doc type has the contested index "parentNameAndLabel" + /// with properties: [normalizedParentDomainName, normalizedLabel]. + /// + /// For testing `construct_path_query`, we provide `start_index_values` that + /// fill the first property, leaving `normalizedLabel` as the middle property + /// that the query will range over. + fn build_query( + contract: &dpp::data_contract::DataContract, + start_index_values: Vec, + end_index_values: Vec, + start_at_value: Option<(Value, bool)>, + limit: Option, + order_ascending: bool, + ) -> VotePollsByDocumentTypeQuery { + VotePollsByDocumentTypeQuery { + contract_id: *contract.id_ref(), + document_type_name: "domain".to_string(), + index_name: "parentNameAndLabel".to_string(), + start_index_values, + end_index_values, + start_at_value, + limit, + order_ascending, + } + } + + fn resolve_query<'a>( + query: &'a VotePollsByDocumentTypeQuery, + contract: &'a dpp::data_contract::DataContract, + ) -> ResolvedVotePollsByDocumentTypeQuery<'a> { + query + .resolve_with_provided_borrowed_contract(contract) + .expect("should resolve") + } + + // ----------------------------------------------------------------------- + // resolve_with_provided_borrowed_contract + // ----------------------------------------------------------------------- + + #[test] + fn resolve_with_correct_contract_succeeds() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let query = build_query( + &contract, + vec![Value::Text("dash".to_string())], + vec![], + None, + Some(10), + true, + ); + + let resolved = query.resolve_with_provided_borrowed_contract(&contract); + assert!(resolved.is_ok()); + } + + #[test] + fn resolve_with_wrong_contract_fails() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + // Build query with a different contract_id + let mut query = build_query( + &contract, + vec![Value::Text("dash".to_string())], + vec![], + None, + Some(10), + true, + ); + query.contract_id = Identifier::from([0xFF; 32]); // wrong ID + + let resolved = query.resolve_with_provided_borrowed_contract(&contract); + assert!(resolved.is_err()); + } + + // ----------------------------------------------------------------------- + // construct_path_query_with_known_index + // ----------------------------------------------------------------------- + + #[test] + fn construct_path_query_ascending_no_start_at_no_end_values() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + // Provide 1 start_index_value (normalizedParentDomainName="dash"), + // leaving normalizedLabel as the middle property to range over. + let query = build_query( + &contract, + vec![Value::Text("dash".to_string())], + vec![], + None, // no start_at_value + Some(10), // limit + true, // ascending + ); + let resolved = resolve_query(&query, &contract); + let index = resolved.index().expect("should find contested index"); + let pq = resolved + .construct_path_query_with_known_index(index, platform_version) + .expect("should build path query"); + + // Path should start with the base indexes path and then the serialized + // start_index_values appended. + let base = vote_contested_resource_contract_documents_indexes_path_vec( + contract.id_ref().as_ref(), + "domain", + ); + assert!(pq.path.len() > base.len()); + for (i, component) in base.iter().enumerate() { + assert_eq!(&pq.path[i], component); + } + + assert_eq!(pq.query.limit, Some(10)); + assert!(pq.query.query.left_to_right); + + // No start_at -> insert_all -> RangeFull + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + assert!(matches!(&items[0], QueryItem::RangeFull(..))); + + // No end index values means no subquery_path on default branch + assert!(pq + .query + .query + .default_subquery_branch + .subquery_path + .is_none()); + } + + #[test] + fn construct_path_query_descending() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let query = build_query( + &contract, + vec![Value::Text("dash".to_string())], + vec![], + None, + Some(10), + false, // descending + ); + let resolved = resolve_query(&query, &contract); + let index = resolved.index().expect("should find contested index"); + let pq = resolved + .construct_path_query_with_known_index(index, platform_version) + .expect("should build path query"); + + assert!(!pq.query.query.left_to_right); + } + + #[test] + fn construct_path_query_with_start_at_value_ascending_included() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let query = build_query( + &contract, + vec![Value::Text("dash".to_string())], + vec![], + Some((Value::Text("alice".to_string()), true)), // start_at included + Some(10), + true, + ); + let resolved = resolve_query(&query, &contract); + let index = resolved.index().expect("should find contested index"); + let pq = resolved + .construct_path_query_with_known_index(index, platform_version) + .expect("should build path query"); + + // Ascending + included -> RangeFrom + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + assert!( + matches!(&items[0], QueryItem::RangeFrom(..)), + "expected RangeFrom for ascending + included" + ); + } + + #[test] + fn construct_path_query_with_start_at_value_ascending_excluded() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let query = build_query( + &contract, + vec![Value::Text("dash".to_string())], + vec![], + Some((Value::Text("alice".to_string()), false)), // excluded + Some(10), + true, + ); + let resolved = resolve_query(&query, &contract); + let index = resolved.index().expect("should find contested index"); + let pq = resolved + .construct_path_query_with_known_index(index, platform_version) + .expect("should build path query"); + + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + assert!( + matches!(&items[0], QueryItem::RangeAfter(..)), + "expected RangeAfter for ascending + excluded" + ); + } + + #[test] + fn construct_path_query_with_start_at_value_descending_included() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let query = build_query( + &contract, + vec![Value::Text("dash".to_string())], + vec![], + Some((Value::Text("alice".to_string()), true)), + Some(10), + false, // descending + ); + let resolved = resolve_query(&query, &contract); + let index = resolved.index().expect("should find contested index"); + let pq = resolved + .construct_path_query_with_known_index(index, platform_version) + .expect("should build path query"); + + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + assert!( + matches!(&items[0], QueryItem::RangeToInclusive(..)), + "expected RangeToInclusive for descending + included" + ); + } + + #[test] + fn construct_path_query_with_start_at_value_descending_excluded() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let query = build_query( + &contract, + vec![Value::Text("dash".to_string())], + vec![], + Some((Value::Text("alice".to_string()), false)), + Some(10), + false, + ); + let resolved = resolve_query(&query, &contract); + let index = resolved.index().expect("should find contested index"); + let pq = resolved + .construct_path_query_with_known_index(index, platform_version) + .expect("should build path query"); + + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + assert!( + matches!(&items[0], QueryItem::RangeTo(..)), + "expected RangeTo for descending + excluded" + ); + } + + // ----------------------------------------------------------------------- + // helper methods: result_is_in_key, result_path_index + // ----------------------------------------------------------------------- + + #[test] + fn result_is_in_key_when_end_index_values_empty() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let query_no_end = build_query( + &contract, + vec![Value::Text("dash".to_string())], + vec![], + None, + None, + true, + ); + let resolved = resolve_query(&query_no_end, &contract); + assert!(resolved.result_is_in_key()); + } + + #[test] + fn result_path_index_with_one_start_value() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let query = build_query( + &contract, + vec![Value::Text("dash".to_string())], // 1 start value + vec![], + None, + None, + true, + ); + let resolved = resolve_query(&query, &contract); + + // result_path_index = 6 + start_index_values.len() + assert_eq!(resolved.result_path_index(), 7); + } + + // ----------------------------------------------------------------------- + // indexes_vectors error: too many end index values + // ----------------------------------------------------------------------- + + #[test] + fn too_many_start_index_values_error() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + // The contested index has 2 properties. Providing 2 start values + // leaves no room for a middle property. + let query = build_query( + &contract, + vec![ + Value::Text("dash".to_string()), + Value::Text("extra".to_string()), + ], + vec![], + None, + None, + true, + ); + let resolved = resolve_query(&query, &contract); + let index = resolved.index().expect("should find contested index"); + let result = resolved.construct_path_query_with_known_index(index, platform_version); + assert!(result.is_err()); + } + + // ----------------------------------------------------------------------- + // index() method: wrong index name should fail + // ----------------------------------------------------------------------- + + #[test] + fn index_method_wrong_name_returns_error() { + let platform_version = PlatformVersion::latest(); + let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = dpns.data_contract_owned(); + + let mut query = build_query( + &contract, + vec![Value::Text("dash".to_string())], + vec![], + None, + None, + true, + ); + query.index_name = "nonexistent_index".to_string(); + + let resolved = resolve_query(&query, &contract); + let result = resolved.index(); + assert!(result.is_err()); + } +} diff --git a/packages/rs-drive/src/query/vote_polls_by_end_date_query.rs b/packages/rs-drive/src/query/vote_polls_by_end_date_query.rs index 12aad421eb2..ba138b0fac5 100644 --- a/packages/rs-drive/src/query/vote_polls_by_end_date_query.rs +++ b/packages/rs-drive/src/query/vote_polls_by_end_date_query.rs @@ -506,3 +506,275 @@ impl VotePollsByEndDateDriveQuery { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::drive::votes::paths::END_DATE_QUERIES_TREE_KEY; + use crate::drive::RootTree; + use grovedb::QueryItem; + + fn expected_base_path() -> Vec> { + vec![ + vec![RootTree::Votes as u8], + vec![END_DATE_QUERIES_TREE_KEY as u8], + ] + } + + // ----------------------------------------------------------------------- + // construct_path_query + // ----------------------------------------------------------------------- + + #[test] + fn construct_path_query_no_bounds_ascending() { + let query = VotePollsByEndDateDriveQuery { + start_time: None, + end_time: None, + limit: Some(10), + offset: None, + order_ascending: true, + }; + + let pq = query.construct_path_query(); + assert_eq!(pq.path, expected_base_path()); + assert_eq!(pq.query.limit, Some(10)); + assert_eq!(pq.query.offset, None); + + // Should be RangeFull (insert_all) + assert_eq!(pq.query.query.items.len(), 1); + assert!(matches!(&pq.query.query.items[0], QueryItem::RangeFull(..))); + + // Direction should be ascending + assert!(pq.query.query.left_to_right); + + // Should have a subquery for all items at each timestamp + assert!(pq.query.query.default_subquery_branch.subquery.is_some()); + } + + #[test] + fn construct_path_query_no_bounds_descending() { + let query = VotePollsByEndDateDriveQuery { + start_time: None, + end_time: None, + limit: None, + offset: None, + order_ascending: false, + }; + + let pq = query.construct_path_query(); + assert!(!pq.query.query.left_to_right); + assert_eq!(pq.query.limit, None); + } + + #[test] + fn construct_path_query_start_time_included() { + let query = VotePollsByEndDateDriveQuery { + start_time: Some((1000, true)), + end_time: None, + limit: Some(5), + offset: None, + order_ascending: true, + }; + + let pq = query.construct_path_query(); + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + let encoded_1000 = encode_u64(1000); + assert!( + matches!(&items[0], QueryItem::RangeFrom(r) if r.start == encoded_1000), + "expected RangeFrom for included start time" + ); + } + + #[test] + fn construct_path_query_start_time_excluded() { + let query = VotePollsByEndDateDriveQuery { + start_time: Some((1000, false)), + end_time: None, + limit: Some(5), + offset: None, + order_ascending: true, + }; + + let pq = query.construct_path_query(); + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + let encoded_1000 = encode_u64(1000); + assert!( + matches!(&items[0], QueryItem::RangeAfter(r) if r.start == encoded_1000), + "expected RangeAfter for excluded start time" + ); + } + + #[test] + fn construct_path_query_end_time_included() { + let query = VotePollsByEndDateDriveQuery { + start_time: None, + end_time: Some((2000, true)), + limit: Some(5), + offset: None, + order_ascending: true, + }; + + let pq = query.construct_path_query(); + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + let encoded_2000 = encode_u64(2000); + assert!( + matches!(&items[0], QueryItem::RangeToInclusive(r) if r.end == encoded_2000), + "expected RangeToInclusive for included end time" + ); + } + + #[test] + fn construct_path_query_end_time_excluded() { + let query = VotePollsByEndDateDriveQuery { + start_time: None, + end_time: Some((2000, false)), + limit: Some(5), + offset: None, + order_ascending: true, + }; + + let pq = query.construct_path_query(); + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + let encoded_2000 = encode_u64(2000); + assert!( + matches!(&items[0], QueryItem::RangeTo(r) if r.end == encoded_2000), + "expected RangeTo for excluded end time" + ); + } + + #[test] + fn construct_path_query_both_bounds_included() { + let query = VotePollsByEndDateDriveQuery { + start_time: Some((1000, true)), + end_time: Some((2000, true)), + limit: Some(20), + offset: None, + order_ascending: true, + }; + + let pq = query.construct_path_query(); + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + let encoded_1000 = encode_u64(1000); + let encoded_2000 = encode_u64(2000); + assert!( + matches!(&items[0], QueryItem::RangeInclusive(r) if *r.start() == encoded_1000 && *r.end() == encoded_2000), + "expected RangeInclusive for both bounds included" + ); + } + + #[test] + fn construct_path_query_start_included_end_excluded() { + let query = VotePollsByEndDateDriveQuery { + start_time: Some((1000, true)), + end_time: Some((2000, false)), + limit: None, + offset: None, + order_ascending: true, + }; + + let pq = query.construct_path_query(); + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + let encoded_1000 = encode_u64(1000); + let encoded_2000 = encode_u64(2000); + assert!( + matches!(&items[0], QueryItem::Range(r) if r.start == encoded_1000 && r.end == encoded_2000), + "expected Range (half-open) for start included, end excluded" + ); + } + + #[test] + fn construct_path_query_start_excluded_end_included() { + let query = VotePollsByEndDateDriveQuery { + start_time: Some((1000, false)), + end_time: Some((2000, true)), + limit: None, + offset: None, + order_ascending: true, + }; + + let pq = query.construct_path_query(); + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + let encoded_1000 = encode_u64(1000); + let encoded_2000 = encode_u64(2000); + assert!( + matches!(&items[0], QueryItem::RangeAfterToInclusive(r) if *r.start() == encoded_1000 && *r.end() == encoded_2000), + "expected RangeAfterToInclusive" + ); + } + + #[test] + fn construct_path_query_both_bounds_excluded() { + let query = VotePollsByEndDateDriveQuery { + start_time: Some((1000, false)), + end_time: Some((2000, false)), + limit: None, + offset: None, + order_ascending: true, + }; + + let pq = query.construct_path_query(); + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + let encoded_1000 = encode_u64(1000); + let encoded_2000 = encode_u64(2000); + assert!( + matches!(&items[0], QueryItem::RangeAfterTo(r) if r.start == encoded_1000 && r.end == encoded_2000), + "expected RangeAfterTo for both excluded" + ); + } + + // ----------------------------------------------------------------------- + // path_query_for_end_time_included + // ----------------------------------------------------------------------- + + #[test] + fn path_query_for_end_time_included_builds_correct_query() { + let end_time: u64 = 5000; + let limit: u16 = 50; + + let pq = VotePollsByEndDateDriveQuery::path_query_for_end_time_included(end_time, limit); + assert_eq!(pq.path, expected_base_path()); + assert_eq!(pq.query.limit, Some(limit)); + assert!(pq.query.query.left_to_right); + + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + let encoded_5000 = encode_u64(5000); + assert!( + matches!(&items[0], QueryItem::RangeToInclusive(r) if r.end == encoded_5000), + "expected RangeToInclusive up to end_time" + ); + + // Should have a sub-query for all items + assert!(pq.query.query.default_subquery_branch.subquery.is_some()); + } + + // ----------------------------------------------------------------------- + // path_query_for_single_end_time + // ----------------------------------------------------------------------- + + #[test] + fn path_query_for_single_end_time_builds_key_query() { + let end_time: u64 = 7777; + let limit: u16 = 100; + + let pq = VotePollsByEndDateDriveQuery::path_query_for_single_end_time(end_time, limit); + assert_eq!(pq.path, expected_base_path()); + assert_eq!(pq.query.limit, Some(limit)); + + let items = &pq.query.query.items; + assert_eq!(items.len(), 1); + let encoded_7777 = encode_u64(7777); + assert!( + matches!(&items[0], QueryItem::Key(k) if *k == encoded_7777), + "expected Key query for single end time" + ); + } +}