1use dapi_grpc::platform::v0::{
4 self as proto,
5 get_contested_resource_vote_state_request::{
6 self, get_contested_resource_vote_state_request_v0,
7 },
8 get_contested_resources_request::{
9 self, get_contested_resources_request_v0, GetContestedResourcesRequestV0,
10 },
11 get_vote_polls_by_end_date_request::{self},
12 GetContestedResourceIdentityVotesRequest, GetContestedResourceVoteStateRequest,
13 GetContestedResourceVotersForIdentityRequest, GetContestedResourcesRequest,
14 GetPrefundedSpecializedBalanceRequest, GetVotePollsByEndDateRequest,
15};
16use dpp::{
17 identifier::Identifier, platform_value::Value,
18 voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll,
19};
20use drive::query::{
21 contested_resource_votes_given_by_identity_query::ContestedResourceVotesGivenByIdentityQuery,
22 vote_poll_contestant_votes_query::ContestedDocumentVotePollVotesDriveQuery,
23 vote_poll_vote_state_query::{
24 ContestedDocumentVotePollDriveQuery, ContestedDocumentVotePollDriveQueryResultType,
25 },
26 vote_polls_by_document_type_query::VotePollsByDocumentTypeQuery,
27 VotePollsByEndDateDriveQuery,
28};
29
30use crate::Error;
31
32const BINCODE_CONFIG: dpp::bincode::config::Configuration = dpp::bincode::config::standard();
33
34pub trait TryFromRequest<T>: Sized {
42 fn try_from_request(grpc_request: T) -> Result<Self, Error>;
44
45 fn try_to_request(&self) -> Result<T, Error>;
47}
48
49impl TryFromRequest<get_contested_resource_vote_state_request_v0::ResultType>
50 for ContestedDocumentVotePollDriveQueryResultType
51{
52 fn try_from_request(
53 grpc_request: get_contested_resource_vote_state_request_v0::ResultType,
54 ) -> Result<Self, Error> {
55 use get_contested_resource_vote_state_request_v0::ResultType as GrpcResultType;
56 use ContestedDocumentVotePollDriveQueryResultType as DriveResultType;
57
58 Ok(match grpc_request {
59 GrpcResultType::Documents => DriveResultType::Documents,
60 GrpcResultType::DocumentsAndVoteTally => DriveResultType::DocumentsAndVoteTally,
61 GrpcResultType::VoteTally => DriveResultType::VoteTally,
62 })
63 }
64 fn try_to_request(
65 &self,
66 ) -> Result<get_contested_resource_vote_state_request_v0::ResultType, Error> {
67 use get_contested_resource_vote_state_request_v0::ResultType as GrpcResultType;
68 use ContestedDocumentVotePollDriveQueryResultType as DriveResultType;
69
70 Ok(match self {
71 DriveResultType::Documents => GrpcResultType::Documents,
72 DriveResultType::DocumentsAndVoteTally => GrpcResultType::DocumentsAndVoteTally,
73 DriveResultType::VoteTally => GrpcResultType::VoteTally,
74 DriveResultType::SingleDocumentByContender(_) => {
75 return Err(Error::RequestError {
76 error: "can not perform a single document by contender query remotely"
77 .to_string(),
78 })
79 }
80 })
81 }
82}
83
84impl TryFromRequest<GetContestedResourceVoteStateRequest> for ContestedDocumentVotePollDriveQuery {
85 fn try_from_request(grpc_request: GetContestedResourceVoteStateRequest) -> Result<Self, Error> {
86 let result = match grpc_request.version.ok_or(Error::EmptyVersion)? {
87 get_contested_resource_vote_state_request::Version::V0(v) => {
88 ContestedDocumentVotePollDriveQuery {
89 limit: v.count.map(|v| v as u16),
90 vote_poll: ContestedDocumentResourceVotePoll {
91 contract_id: Identifier::from_bytes(&v.contract_id).map_err(|e| {
92 Error::RequestError {
93 error: format!("cannot decode contract id: {}", e),
94 }
95 })?,
96 document_type_name: v.document_type_name.clone(),
97 index_name: v.index_name.clone(),
98 index_values: bincode_decode_values(v.index_values.iter())?,
99 },
100 result_type: match v.result_type() {
101 get_contested_resource_vote_state_request_v0::ResultType::Documents => {
102 ContestedDocumentVotePollDriveQueryResultType::Documents
103 }
104 get_contested_resource_vote_state_request_v0::ResultType::DocumentsAndVoteTally => {
105 ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally
106 }
107 get_contested_resource_vote_state_request_v0::ResultType::VoteTally => {
108 ContestedDocumentVotePollDriveQueryResultType::VoteTally
109 }
110 },
111 start_at: v
112 .start_at_identifier_info
113 .map(|v| to_bytes32(&v.start_identifier).map(|id| (id, v.start_identifier_included)))
114 .transpose()
115 .map_err(|e| {
116 Error::RequestError {
117 error: format!(
118 "cannot decode start_at: {}",
119 e
120 )}}
121 )?,
122 offset: None, allow_include_locked_and_abstaining_vote_tally: v
124 .allow_include_locked_and_abstaining_vote_tally,
125 }
126 }
127 };
128 Ok(result)
129 }
130
131 fn try_to_request(&self) -> Result<GetContestedResourceVoteStateRequest, Error> {
132 use proto::get_contested_resource_vote_state_request::get_contested_resource_vote_state_request_v0 as request_v0;
133 if self.offset.is_some() {
134 return Err(Error::RequestError{error:"ContestedDocumentVotePollDriveQuery.offset field is internal and must be set to None".into()});
135 }
136
137 let start_at_identifier_info = self.start_at.map(|v| request_v0::StartAtIdentifierInfo {
138 start_identifier: v.0.to_vec(),
139 start_identifier_included: v.1,
140 });
141
142 use proto::get_contested_resource_vote_state_request:: get_contested_resource_vote_state_request_v0::ResultType as GrpcResultType;
143 Ok(proto::get_contested_resource_vote_state_request::GetContestedResourceVoteStateRequestV0 {
144 prove:true,
145 contract_id:self.vote_poll.contract_id.to_vec(),
146 count: self.limit.map(|v| v as u32),
147 document_type_name: self.vote_poll.document_type_name.clone(),
148 index_name: self.vote_poll.index_name.clone(),
149 index_values: self.vote_poll.index_values.iter().map(|v|
150 dpp::bincode::encode_to_vec(v, BINCODE_CONFIG).map_err(|e|Error::RequestError { error: e.to_string() } )).collect::<Result<Vec<_>,_>>()?,
151 result_type:match self.result_type {
152 ContestedDocumentVotePollDriveQueryResultType::Documents => GrpcResultType::Documents.into(),
153 ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally => GrpcResultType::DocumentsAndVoteTally.into(),
154 ContestedDocumentVotePollDriveQueryResultType::VoteTally => GrpcResultType::VoteTally.into(),
155 ContestedDocumentVotePollDriveQueryResultType::SingleDocumentByContender(_) => return Err(Error::RequestError {
156 error: "can not perform a single document by contender query remotely".to_string(),
157 }),
158 },
159 start_at_identifier_info,
160 allow_include_locked_and_abstaining_vote_tally: self.allow_include_locked_and_abstaining_vote_tally,
161 }
162 .into())
163 }
164}
165
166fn to_bytes32(v: &[u8]) -> Result<[u8; 32], Error> {
167 let result: Result<[u8; 32], std::array::TryFromSliceError> = v.try_into();
168 match result {
169 Ok(id) => Ok(id),
170 Err(e) => Err(Error::RequestError {
171 error: format!("cannot decode id: {}", e),
172 }),
173 }
174}
175
176impl TryFromRequest<GetContestedResourceIdentityVotesRequest>
177 for ContestedResourceVotesGivenByIdentityQuery
178{
179 fn try_from_request(
180 grpc_request: GetContestedResourceIdentityVotesRequest,
181 ) -> Result<Self, Error> {
182 let proto::get_contested_resource_identity_votes_request::Version::V0(value) =
183 grpc_request.version.ok_or(Error::EmptyVersion)?;
184 let start_at = value
185 .start_at_vote_poll_id_info
186 .map(|v| {
187 to_bytes32(&v.start_at_poll_identifier)
188 .map(|id| (id, v.start_poll_identifier_included))
189 })
190 .transpose()?;
191
192 Ok(Self {
193 identity_id: Identifier::from_vec(value.identity_id.to_vec()).map_err(|e| {
194 Error::RequestError {
195 error: e.to_string(),
196 }
197 })?,
198 offset: None,
199 limit: value.limit.map(|x| x as u16),
200 start_at,
201 order_ascending: value.order_ascending,
202 })
203 }
204
205 fn try_to_request(&self) -> Result<GetContestedResourceIdentityVotesRequest, Error> {
206 use proto::get_contested_resource_identity_votes_request::get_contested_resource_identity_votes_request_v0 as request_v0;
207 if self.offset.is_some() {
208 return Err(Error::RequestError{error:"ContestedResourceVotesGivenByIdentityQuery.offset field is internal and must be set to None".into()});
209 }
210
211 Ok(proto::get_contested_resource_identity_votes_request::GetContestedResourceIdentityVotesRequestV0 {
212 prove: true,
213 identity_id: self.identity_id.to_vec(),
214 offset: self.offset.map(|x| x as u32),
215 limit: self.limit.map(|x| x as u32),
216 start_at_vote_poll_id_info: self.start_at.map(|(id, included)| {
217 request_v0::StartAtVotePollIdInfo {
218 start_at_poll_identifier: id.to_vec(),
219 start_poll_identifier_included: included,
220 }
221 }),
222 order_ascending: self.order_ascending,
223 }.into()
224 )
225 }
226}
227
228use dapi_grpc::platform::v0::get_contested_resource_voters_for_identity_request;
229
230impl TryFromRequest<GetContestedResourceVotersForIdentityRequest>
231 for ContestedDocumentVotePollVotesDriveQuery
232{
233 fn try_from_request(
234 value: GetContestedResourceVotersForIdentityRequest,
235 ) -> Result<Self, Error> {
236 let result = match value.version.ok_or(Error::EmptyVersion)? {
237 get_contested_resource_voters_for_identity_request::Version::V0(v) => {
238 ContestedDocumentVotePollVotesDriveQuery {
239 vote_poll: ContestedDocumentResourceVotePoll {
240 contract_id: Identifier::from_bytes(&v.contract_id).map_err(|e| {
241 Error::RequestError {
242 error: format!("cannot decode contract id: {}", e),
243 }
244 })?,
245 document_type_name: v.document_type_name.clone(),
246 index_name: v.index_name.clone(),
247 index_values: bincode_decode_values(v.index_values.iter())?,
248 },
249 contestant_id: Identifier::from_bytes(&v.contestant_id).map_err(|e| {
250 Error::RequestError {
251 error: format!("cannot decode contestant_id: {}", e),
252 }
253 })?,
254 limit: v.count.map(|v| v as u16),
255 offset: None,
256 start_at: v
257 .start_at_identifier_info
258 .map(|v| {
259 to_bytes32(&v.start_identifier)
260 .map(|id| (id, v.start_identifier_included))
261 })
262 .transpose()
263 .map_err(|e| Error::RequestError {
264 error: format!("cannot decode start_at value: {}", e),
265 })?,
266 order_ascending: v.order_ascending,
267 }
268 }
269 };
270
271 Ok(result)
272 }
273 fn try_to_request(&self) -> Result<GetContestedResourceVotersForIdentityRequest, Error> {
274 use proto::get_contested_resource_voters_for_identity_request::get_contested_resource_voters_for_identity_request_v0 as request_v0;
275 if self.offset.is_some() {
276 return Err(Error::RequestError{error:"ContestedDocumentVotePollVotesDriveQuery.offset field is internal and must be set to None".into()});
277 }
278
279 Ok(proto::get_contested_resource_voters_for_identity_request::GetContestedResourceVotersForIdentityRequestV0 {
280 prove:true,
281 contract_id: self.vote_poll.contract_id.to_vec(),
282 document_type_name: self.vote_poll.document_type_name.clone(),
283 index_name: self.vote_poll.index_name.clone(),
284 index_values: self.vote_poll.index_values.iter().map(|v|
285 dpp::bincode::encode_to_vec(v, BINCODE_CONFIG).map_err(|e|
286 Error::RequestError { error: e.to_string()})).collect::<Result<Vec<_>,_>>()?,
287 order_ascending: self.order_ascending,
288 count: self.limit.map(|v| v as u32),
289 contestant_id: self.contestant_id.to_vec(),
290 start_at_identifier_info: self.start_at.map(|v| request_v0::StartAtIdentifierInfo{
291 start_identifier: v.0.to_vec(),
292 start_identifier_included: v.1,
293 }),
294 }
295 .into())
296 }
297}
298
299impl TryFromRequest<GetContestedResourcesRequest> for VotePollsByDocumentTypeQuery {
300 fn try_from_request(value: GetContestedResourcesRequest) -> Result<Self, Error> {
301 let result = match value.version.ok_or(Error::EmptyVersion)? {
302 get_contested_resources_request::Version::V0(req) => VotePollsByDocumentTypeQuery {
303 contract_id: Identifier::from_bytes(&req.contract_id).map_err(|e| {
304 Error::RequestError {
305 error: format!("cannot decode contract id: {}", e),
306 }
307 })?,
308 document_type_name: req.document_type_name.clone(),
309 index_name: req.index_name.clone(),
310 start_at_value: req
311 .start_at_value_info
312 .map(|i| {
313 let (value, _): (Value, _) =
314 bincode::decode_from_slice(&i.start_value, BINCODE_CONFIG).map_err(
315 |e| Error::RequestError {
316 error: format!("cannot decode start value: {}", e),
317 },
318 )?;
319 Ok::<_, Error>((value, i.start_value_included))
320 })
321 .transpose()?,
322 start_index_values: bincode_decode_values(req.start_index_values.iter())?,
323 end_index_values: bincode_decode_values(req.end_index_values.iter())?,
324 limit: req.count.map(|v| v as u16),
325 order_ascending: req.order_ascending,
326 },
327 };
328 Ok(result)
329 }
330
331 fn try_to_request(&self) -> Result<GetContestedResourcesRequest, Error> {
332 Ok(GetContestedResourcesRequestV0 {
333 prove: true,
334 contract_id: self.contract_id.to_vec(),
335 count: self.limit.map(|v| v as u32),
336 document_type_name: self.document_type_name.clone(),
337 end_index_values: bincode_encode_values(&self.end_index_values)?,
338 start_index_values: bincode_encode_values(&self.start_index_values)?,
339 index_name: self.index_name.clone(),
340 order_ascending: self.order_ascending,
341 start_at_value_info: self
342 .start_at_value
343 .as_ref()
344 .map(|(start_value, start_value_included)| {
345 Ok::<_, Error>(get_contested_resources_request_v0::StartAtValueInfo {
346 start_value: bincode::encode_to_vec(start_value, BINCODE_CONFIG).map_err(
347 |e| Error::RequestError {
348 error: format!("cannot encode start value: {}", e),
349 },
350 )?,
351 start_value_included: *start_value_included,
352 })
353 })
354 .transpose()?,
355 }
356 .into())
357 }
358}
359
360impl TryFromRequest<GetVotePollsByEndDateRequest> for VotePollsByEndDateDriveQuery {
361 fn try_from_request(value: GetVotePollsByEndDateRequest) -> Result<Self, Error> {
362 let result = match value.version.ok_or(Error::EmptyVersion)? {
363 get_vote_polls_by_end_date_request::Version::V0(v) => VotePollsByEndDateDriveQuery {
364 start_time: v
365 .start_time_info
366 .map(|v| (v.start_time_ms, v.start_time_included)),
367 end_time: v
368 .end_time_info
369 .map(|v| (v.end_time_ms, v.end_time_included)),
370 limit: v.limit.map(|v| v as u16),
371 offset: v.offset.map(|v| v as u16),
372 order_ascending: v.ascending,
373 },
374 };
375 Ok(result)
376 }
377
378 fn try_to_request(&self) -> Result<GetVotePollsByEndDateRequest, Error> {
379 use proto::get_vote_polls_by_end_date_request::get_vote_polls_by_end_date_request_v0 as request_v0;
380 if self.offset.is_some() {
381 return Err(Error::RequestError {
382 error:
383 "VotePollsByEndDateDriveQuery.offset field is internal and must be set to None"
384 .into(),
385 });
386 }
387
388 Ok(
389 proto::get_vote_polls_by_end_date_request::GetVotePollsByEndDateRequestV0 {
390 prove: true,
391 start_time_info: self.start_time.map(|(start_time_ms, start_time_included)| {
392 request_v0::StartAtTimeInfo {
393 start_time_ms,
394 start_time_included,
395 }
396 }),
397 end_time_info: self.end_time.map(|(end_time_ms, end_time_included)| {
398 request_v0::EndAtTimeInfo {
399 end_time_ms,
400 end_time_included,
401 }
402 }),
403 limit: self.limit.map(|v| v as u32),
404 offset: self.offset.map(|v| v as u32),
405 ascending: self.order_ascending,
406 }
407 .into(),
408 )
409 }
410}
411
412impl TryFromRequest<GetPrefundedSpecializedBalanceRequest> for Identifier {
413 fn try_to_request(&self) -> Result<GetPrefundedSpecializedBalanceRequest, Error> {
414 Ok(
415 proto::get_prefunded_specialized_balance_request::GetPrefundedSpecializedBalanceRequestV0 {
416 prove:true,
417 id: self.to_vec(),
418 }.into()
419 )
420 }
421
422 fn try_from_request(
423 grpc_request: GetPrefundedSpecializedBalanceRequest,
424 ) -> Result<Self, Error> {
425 match grpc_request.version.ok_or(Error::EmptyVersion)? {
426 proto::get_prefunded_specialized_balance_request::Version::V0(v) => {
427 Identifier::from_bytes(&v.id).map_err(|e| Error::RequestError {
428 error: format!("cannot decode id: {}", e),
429 })
430 }
431 }
432 }
433}
434
435fn bincode_decode_values<V: AsRef<[u8]>, T: IntoIterator<Item = V>>(
439 values: T,
440) -> Result<Vec<Value>, Error> {
441 values
442 .into_iter()
443 .map(|v| {
444 dpp::bincode::decode_from_slice(v.as_ref(), BINCODE_CONFIG)
445 .map_err(|e| Error::RequestError {
446 error: format!("cannot decode value: {}", e),
447 })
448 .map(|(v, _)| v)
449 })
450 .collect()
451}
452
453fn bincode_encode_values<'a, T: IntoIterator<Item = &'a Value>>(
457 values: T,
458) -> Result<Vec<Vec<u8>>, Error> {
459 values
460 .into_iter()
461 .map(|v| {
462 dpp::bincode::encode_to_vec(v, BINCODE_CONFIG).map_err(|e| Error::RequestError {
463 error: format!("cannot encode value: {}", e),
464 })
465 })
466 .collect::<Result<Vec<_>, _>>()
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use dpp::identifier::Identifier;
473 use dpp::platform_value::Value;
474
475 #[test]
480 fn test_to_bytes32_valid() {
481 let input = [0xABu8; 32];
482 let result = to_bytes32(&input).expect("should convert 32-byte slice");
483 assert_eq!(result, input);
484 }
485
486 #[test]
487 fn test_to_bytes32_invalid_length() {
488 let short = [0u8; 16];
490 assert!(to_bytes32(&short).is_err());
491
492 let long = [0u8; 33];
494 assert!(to_bytes32(&long).is_err());
495
496 assert!(to_bytes32(&[]).is_err());
498 }
499
500 #[test]
505 fn test_bincode_encode_decode_roundtrip() {
506 let values = vec![
507 Value::Text("hello".to_string()),
508 Value::U64(42),
509 Value::Bool(true),
510 ];
511 let encoded = bincode_encode_values(&values).expect("encoding should succeed");
512 assert_eq!(encoded.len(), 3);
513
514 let decoded = bincode_decode_values(encoded.iter()).expect("decoding should succeed");
515 assert_eq!(decoded, values);
516 }
517
518 #[test]
519 fn test_bincode_decode_empty() {
520 let empty: Vec<Vec<u8>> = vec![];
521 let result = bincode_decode_values(empty.iter()).expect("empty input should succeed");
522 assert!(result.is_empty());
523 }
524
525 #[test]
526 fn test_bincode_decode_invalid() {
527 let garbage = vec![vec![0xFF, 0xFE, 0xFD, 0xFC, 0xFB]];
528 let result = bincode_decode_values(garbage.iter());
529 assert!(
530 result.is_err(),
531 "invalid bincode bytes should produce an error"
532 );
533 }
534
535 #[test]
540 fn test_contested_document_vote_poll_result_type_roundtrip() {
541 use get_contested_resource_vote_state_request_v0::ResultType as GrpcResultType;
542
543 let cases = vec![
544 (
545 GrpcResultType::Documents,
546 ContestedDocumentVotePollDriveQueryResultType::Documents,
547 ),
548 (
549 GrpcResultType::VoteTally,
550 ContestedDocumentVotePollDriveQueryResultType::VoteTally,
551 ),
552 (
553 GrpcResultType::DocumentsAndVoteTally,
554 ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally,
555 ),
556 ];
557
558 for (grpc_val, expected_drive) in cases {
559 let drive_val =
561 ContestedDocumentVotePollDriveQueryResultType::try_from_request(grpc_val)
562 .expect("try_from_request should succeed");
563 assert_eq!(drive_val, expected_drive);
564
565 let back = drive_val
567 .try_to_request()
568 .expect("try_to_request should succeed");
569 assert_eq!(back, grpc_val);
570 }
571 }
572
573 #[test]
578 fn test_contested_document_vote_poll_query_roundtrip() {
579 let contract_id = Identifier::from_bytes(&[1u8; 32]).unwrap();
580 let index_values = vec![Value::Text("dash".to_string())];
581
582 let query = ContestedDocumentVotePollDriveQuery {
583 vote_poll: ContestedDocumentResourceVotePoll {
584 contract_id,
585 document_type_name: "domain".to_string(),
586 index_name: "parentNameAndLabel".to_string(),
587 index_values: index_values.clone(),
588 },
589 result_type: ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally,
590 offset: None,
591 limit: Some(10),
592 start_at: None,
593 allow_include_locked_and_abstaining_vote_tally: true,
594 };
595
596 let grpc_request = query
597 .try_to_request()
598 .expect("try_to_request should succeed");
599
600 let roundtripped = ContestedDocumentVotePollDriveQuery::try_from_request(grpc_request)
601 .expect("try_from_request should succeed");
602
603 assert_eq!(
604 roundtripped.vote_poll.contract_id,
605 query.vote_poll.contract_id
606 );
607 assert_eq!(
608 roundtripped.vote_poll.document_type_name,
609 query.vote_poll.document_type_name
610 );
611 assert_eq!(
612 roundtripped.vote_poll.index_name,
613 query.vote_poll.index_name
614 );
615 assert_eq!(
616 roundtripped.vote_poll.index_values,
617 query.vote_poll.index_values
618 );
619 assert_eq!(roundtripped.result_type, query.result_type);
620 assert_eq!(roundtripped.limit, query.limit);
621 assert_eq!(roundtripped.start_at, query.start_at);
622 assert_eq!(
623 roundtripped.allow_include_locked_and_abstaining_vote_tally,
624 query.allow_include_locked_and_abstaining_vote_tally
625 );
626 }
627
628 #[test]
633 fn test_identifier_prefunded_balance_roundtrip() {
634 let id = Identifier::from_bytes(&[7u8; 32]).unwrap();
635
636 let grpc_request: GetPrefundedSpecializedBalanceRequest =
637 id.try_to_request().expect("try_to_request should succeed");
638
639 let roundtripped =
640 Identifier::try_from_request(grpc_request).expect("try_from_request should succeed");
641
642 assert_eq!(roundtripped, id);
643 }
644
645 #[test]
650 fn test_contested_result_type_rejects_single_document_by_contender() {
651 let contender_id = Identifier::from_bytes(&[0xCC; 32]).unwrap();
652 let result_type =
653 ContestedDocumentVotePollDriveQueryResultType::SingleDocumentByContender(contender_id);
654
655 let result = result_type.try_to_request();
656 assert!(
657 result.is_err(),
658 "SingleDocumentByContender should not be convertible to a gRPC request"
659 );
660
661 let err_msg = format!("{}", result.unwrap_err());
662 assert!(
663 err_msg.contains("single document by contender"),
664 "error message should mention 'single document by contender', got: {}",
665 err_msg
666 );
667 }
668
669 #[test]
674 fn test_vote_polls_by_end_date_rejects_offset() {
675 let query = VotePollsByEndDateDriveQuery {
676 start_time: Some((1000, true)),
677 end_time: Some((2000, false)),
678 limit: Some(5),
679 offset: Some(10), order_ascending: true,
681 };
682
683 let result = query.try_to_request();
684 assert!(
685 result.is_err(),
686 "offset must be None for try_to_request to succeed"
687 );
688
689 let err_msg = format!("{}", result.unwrap_err());
690 assert!(
691 err_msg.contains("offset"),
692 "error message should mention 'offset', got: {}",
693 err_msg
694 );
695 }
696}