Skip to content

Commit cc0ece0

Browse files
test(count_offset): adversarial verifier + validator branch coverage
Addresses codecov/patch coverage on PR #669. Adds 18 tests covering rejection branches that the happy-path round-trips don't exercise. ## Merk-level adversarial tests (+8) `merk/src/proofs/query/count_offset/tests.rs`: - `rejects_wrong_inner_range` — verifier called with a different range than the prover; classification shifts trigger shape rejections. - `rejects_wrong_direction` — proof emitted ascending, verified descending (and vice versa); state-machine mismatch. - `rejects_wrong_offset_smaller` / `rejects_wrong_offset_larger` — verifier expects a different number of digest skips than the proof contains. - `rejects_wrong_limit_smaller` — proof emits more value nodes than the verifier's limit window allows. - `rejects_byte_mutated_proof` — single-byte flip in the proof; either the verifier returns Err or it returns Ok with a non-matching root hash (both are acceptable rejections). - `rejects_truncated_proof` — last 10 bytes dropped; decoder or stack check rejects. - `rejects_trailing_garbage` — extra bytes after the encoded ops. ## GroveDB-level validator tests (+10) `grovedb/src/tests/count_offset_paginated_tests.rs`: - One test per branch in `SizedQuery::validate_count_offset_paginated`: no offset / offset=0 / aggregate-count wrapper / aggregate-sum wrapper / default-subquery subquery / default-subquery subquery_path / multi-item query. - `validate_accepts_single_range_variants` — sanity sweep across all 10 allowed `QueryItem` variants. - `path_query_validate_rejects_empty_path` — PathQuery-level empty-path rejection. - `path_query_has_non_zero_offset` — three-case truth table for the helper. Test totals after this commit: - merk count_offset: 17/17 (8 round-trips + 9 adversarial; was 9) - grovedb count_offset: 18/18 (8 end-to-end + 10 validator; was 8) - full merk suite: 510/510 - full grovedb suite: 1731/1731 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0fd0d21 commit cc0ece0

2 files changed

Lines changed: 433 additions & 0 deletions

File tree

grovedb/src/tests/count_offset_paginated_tests.rs

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,205 @@ mod tests {
186186
);
187187
}
188188

189+
// ───────── SizedQuery::validate_count_offset_paginated unit tests ─────────
190+
//
191+
// Each branch in the validator gets its own test so a regression
192+
// (e.g. accidentally accepting a multi-item query) shows up as a
193+
// single failure with a clear message.
194+
195+
use grovedb_merk::proofs::query::QueryItem;
196+
197+
#[test]
198+
fn validate_rejects_no_offset() {
199+
// Calling the count-offset validator on a query that wasn't
200+
// even meant to be paginated is a programming error — surface
201+
// it as `InvalidQuery` instead of silently returning Ok.
202+
let mut q = Query::new();
203+
q.insert_all();
204+
let sized = SizedQuery::new(q, Some(5), None);
205+
let err = sized
206+
.validate_count_offset_paginated()
207+
.expect_err("no offset must reject");
208+
let msg = format!("{:?}", err);
209+
assert!(
210+
msg.contains("non-zero value"),
211+
"error should mention non-zero offset; got {}",
212+
msg
213+
);
214+
}
215+
216+
#[test]
217+
fn validate_rejects_offset_zero() {
218+
let mut q = Query::new();
219+
q.insert_all();
220+
let sized = SizedQuery::new(q, Some(5), Some(0));
221+
let err = sized
222+
.validate_count_offset_paginated()
223+
.expect_err("offset = 0 must reject");
224+
let msg = format!("{:?}", err);
225+
assert!(
226+
msg.contains("non-zero value"),
227+
"error should mention non-zero offset; got {}",
228+
msg
229+
);
230+
}
231+
232+
#[test]
233+
fn validate_rejects_aggregate_count_wrapper() {
234+
// AggregateCountOnRange has its own pagination semantics; we
235+
// reject it from this lane so the two flows don't shadow each
236+
// other.
237+
let mut q = Query::new();
238+
q.insert_item(QueryItem::AggregateCountOnRange(Box::new(
239+
QueryItem::RangeFull(std::ops::RangeFull),
240+
)));
241+
let sized = SizedQuery::new(q, Some(5), Some(2));
242+
let err = sized
243+
.validate_count_offset_paginated()
244+
.expect_err("aggregate count wrapper must reject");
245+
let msg = format!("{:?}", err);
246+
assert!(
247+
msg.contains("AggregateCountOnRange"),
248+
"error should mention AggregateCountOnRange; got {}",
249+
msg
250+
);
251+
}
252+
253+
#[test]
254+
fn validate_rejects_aggregate_sum_wrapper() {
255+
let mut q = Query::new();
256+
q.insert_item(QueryItem::AggregateSumOnRange(Box::new(
257+
QueryItem::RangeFull(std::ops::RangeFull),
258+
)));
259+
let sized = SizedQuery::new(q, Some(5), Some(2));
260+
let err = sized
261+
.validate_count_offset_paginated()
262+
.expect_err("aggregate sum wrapper must reject");
263+
let msg = format!("{:?}", err);
264+
assert!(
265+
msg.contains("AggregateSumOnRange"),
266+
"error should mention AggregateSumOnRange; got {}",
267+
msg
268+
);
269+
}
270+
271+
#[test]
272+
fn validate_rejects_default_subquery() {
273+
let mut q = Query::new();
274+
q.insert_all();
275+
q.default_subquery_branch.subquery = Some(Box::new(Query::new()));
276+
let sized = SizedQuery::new(q, Some(5), Some(2));
277+
let err = sized
278+
.validate_count_offset_paginated()
279+
.expect_err("default subquery must reject");
280+
let msg = format!("{:?}", err);
281+
assert!(
282+
msg.contains("default subquery branch"),
283+
"error should mention default subquery branch; got {}",
284+
msg
285+
);
286+
}
287+
288+
#[test]
289+
fn validate_rejects_default_subquery_path() {
290+
let mut q = Query::new();
291+
q.insert_all();
292+
q.default_subquery_branch.subquery_path = Some(vec![b"x".to_vec()]);
293+
let sized = SizedQuery::new(q, Some(5), Some(2));
294+
let err = sized
295+
.validate_count_offset_paginated()
296+
.expect_err("default subquery_path must reject");
297+
let msg = format!("{:?}", err);
298+
assert!(
299+
msg.contains("default subquery branch"),
300+
"error should mention default subquery branch; got {}",
301+
msg
302+
);
303+
}
304+
305+
#[test]
306+
fn validate_rejects_multi_item_query() {
307+
let mut q = Query::new();
308+
q.insert_key(b"a".to_vec());
309+
q.insert_key(b"b".to_vec());
310+
let sized = SizedQuery::new(q, Some(5), Some(2));
311+
let err = sized
312+
.validate_count_offset_paginated()
313+
.expect_err("multi-item query must reject");
314+
let msg = format!("{:?}", err);
315+
assert!(
316+
msg.contains("exactly one range QueryItem"),
317+
"error should mention single-item requirement; got {}",
318+
msg
319+
);
320+
}
321+
322+
#[test]
323+
fn validate_accepts_single_range_variants() {
324+
// Sanity: every ordinary range / key variant passes.
325+
let variants: Vec<QueryItem> = vec![
326+
QueryItem::Key(b"a".to_vec()),
327+
QueryItem::Range(b"a".to_vec()..b"z".to_vec()),
328+
QueryItem::RangeInclusive(b"a".to_vec()..=b"z".to_vec()),
329+
QueryItem::RangeFrom(b"a".to_vec()..),
330+
QueryItem::RangeFull(std::ops::RangeFull),
331+
QueryItem::RangeTo(..b"z".to_vec()),
332+
QueryItem::RangeToInclusive(..=b"z".to_vec()),
333+
QueryItem::RangeAfter(b"a".to_vec()..),
334+
QueryItem::RangeAfterTo(b"a".to_vec()..b"z".to_vec()),
335+
QueryItem::RangeAfterToInclusive(b"a".to_vec()..=b"z".to_vec()),
336+
];
337+
for item in variants {
338+
let mut q = Query::new();
339+
q.insert_item(item.clone());
340+
let sized = SizedQuery::new(q, Some(5), Some(2));
341+
let result = sized.validate_count_offset_paginated();
342+
assert!(
343+
result.is_ok(),
344+
"variant {:?} should be accepted, got error {:?}",
345+
item,
346+
result.err()
347+
);
348+
}
349+
}
350+
351+
#[test]
352+
fn path_query_validate_rejects_empty_path() {
353+
// PathQuery::validate_count_offset_paginated rejects empty
354+
// paths up-front: a count-offset query against the root
355+
// makes no sense because the root is always a NormalTree.
356+
let mut q = Query::new();
357+
q.insert_all();
358+
let pq = PathQuery::new(vec![], SizedQuery::new(q, Some(5), Some(2)));
359+
let err = pq
360+
.validate_count_offset_paginated()
361+
.expect_err("empty path must reject");
362+
let msg = format!("{:?}", err);
363+
assert!(
364+
msg.contains("root merk"),
365+
"error should mention root merk; got {}",
366+
msg
367+
);
368+
}
369+
370+
#[test]
371+
fn path_query_has_non_zero_offset() {
372+
let mut q = Query::new();
373+
q.insert_all();
374+
// offset = None → false
375+
let pq_none = PathQuery::new(vec![b"x".to_vec()], SizedQuery::new(q.clone(), None, None));
376+
assert!(!pq_none.has_non_zero_offset());
377+
// offset = Some(0) → false
378+
let pq_zero = PathQuery::new(
379+
vec![b"x".to_vec()],
380+
SizedQuery::new(q.clone(), None, Some(0)),
381+
);
382+
assert!(!pq_zero.has_non_zero_offset());
383+
// offset = Some(N) for N > 0 → true
384+
let pq_pos = PathQuery::new(vec![b"x".to_vec()], SizedQuery::new(q, None, Some(7)));
385+
assert!(pq_pos.has_non_zero_offset());
386+
}
387+
189388
#[test]
190389
fn end_to_end_offset_rejects_with_subquery() {
191390
// Sanity: an offset query that fails the syntactic

0 commit comments

Comments
 (0)