Skip to content

Commit 603cef2

Browse files
committed
Add pagination unit tests
1 parent 896ab83 commit 603cef2

1 file changed

Lines changed: 354 additions & 25 deletions

File tree

crates/jmap/src/api/query.rs

Lines changed: 354 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -130,35 +130,364 @@ impl QueryResponseBuilder {
130130
}
131131

132132
pub fn build(mut self) -> trc::Result<QueryResponse> {
133-
if !self.has_anchor || self.anchor_found {
134-
if !self.has_anchor && self.requested_position >= 0 {
135-
self.response.position = if self.position == 0 {
136-
self.requested_position
137-
} else {
138-
0
139-
};
140-
} else if self.position >= 0 {
141-
self.response.position = self.position;
133+
if self.has_anchor && !self.anchor_found {
134+
return Err(trc::JmapEvent::AnchorNotFound.into_err());
135+
}
136+
137+
if !self.has_anchor && self.requested_position >= 0 {
138+
self.response.position = if self.position == 0 {
139+
self.requested_position
142140
} else {
143-
let position = self.position.unsigned_abs() as usize;
144-
let start_offset = if position < self.response.ids.len() {
145-
self.response.ids.len() - position
146-
} else {
147-
0
148-
};
149-
self.response.position = start_offset as i32;
150-
let end_offset = if self.limit > 0 {
151-
std::cmp::min(start_offset + self.limit, self.response.ids.len())
152-
} else {
153-
self.response.ids.len()
154-
};
141+
0
142+
};
143+
} else if self.position >= 0 {
144+
self.response.position = self.position;
145+
} else {
146+
let position = self.position.unsigned_abs() as usize;
147+
let start_offset = if position < self.response.ids.len() {
148+
self.response.ids.len() - position
149+
} else {
150+
0
151+
};
152+
self.response.position = start_offset as i32;
153+
let end_offset = if self.limit > 0 {
154+
std::cmp::min(start_offset + self.limit, self.response.ids.len())
155+
} else {
156+
self.response.ids.len()
157+
};
158+
159+
self.response.ids = self.response.ids[start_offset..end_offset].to_vec()
160+
}
161+
162+
Ok(self.response)
163+
}
164+
}
165+
166+
#[cfg(test)]
167+
mod tests {
168+
use super::*;
169+
use jmap_proto::types::state::State;
170+
use jmap_tools::Null;
171+
use types::id::Id;
172+
173+
#[derive(Debug, Clone, Copy)]
174+
struct TestJMAPObject;
175+
176+
impl JmapObject for TestJMAPObject {
177+
type Property = Null;
178+
type Element = Null;
179+
type Id = Null;
180+
181+
type Filter = ();
182+
type Comparator = ();
183+
184+
type GetArguments = ();
185+
type SetArguments<'de> = ();
186+
type QueryArguments = ();
187+
type CopyArguments = ();
188+
type ParseArguments = ();
155189

156-
self.response.ids = self.response.ids[start_offset..end_offset].to_vec()
190+
const ID_PROPERTY: Self::Property = Null;
191+
}
192+
193+
#[test]
194+
fn test_pagination_with_zero_position() {
195+
let range: std::ops::Range<u32> = 0..10;
196+
let mut builder = QueryResponseBuilder::new(
197+
range.len(),
198+
100,
199+
State::Initial,
200+
&QueryRequest::<TestJMAPObject> {
201+
position: Some(0),
202+
limit: Some(3),
203+
..Default::default()
204+
},
205+
);
206+
207+
for i in range {
208+
if !builder.add_id(Id::from_parts(0, i)) {
209+
break;
157210
}
211+
}
158212

159-
Ok(self.response)
160-
} else {
161-
Err(trc::JmapEvent::AnchorNotFound.into_err())
213+
let result = builder.build().unwrap();
214+
215+
assert_eq!(result.ids.len(), 3, "Should collect 3 items");
216+
assert_eq!(result.ids[0].document_id(), 0, "First item should be 0");
217+
assert_eq!(result.ids[1].document_id(), 1, "Second item should be 1");
218+
assert_eq!(result.ids[2].document_id(), 2, "Third item should be 2");
219+
assert_eq!(result.position, 0, "Position should be 0");
220+
}
221+
222+
#[test]
223+
fn test_pagination_with_positive_position() {
224+
let range: std::ops::Range<u32> = 0..10;
225+
let mut builder = QueryResponseBuilder::new(
226+
range.len(),
227+
100,
228+
State::Initial,
229+
&QueryRequest::<TestJMAPObject> {
230+
position: Some(5),
231+
limit: Some(3),
232+
..Default::default()
233+
},
234+
);
235+
236+
for i in range {
237+
if !builder.add_id(Id::from_parts(0, i)) {
238+
break;
239+
}
240+
}
241+
242+
let result = builder.build().unwrap();
243+
244+
assert_eq!(result.ids.len(), 3, "Should collect 3 items");
245+
assert_eq!(result.ids[0].document_id(), 5, "First item should be 5");
246+
assert_eq!(result.ids[1].document_id(), 6, "Second item should be 6");
247+
assert_eq!(result.ids[2].document_id(), 7, "Third item should be 7");
248+
assert_eq!(result.position, 5, "Position should be 5");
249+
}
250+
251+
#[test]
252+
fn test_pagination_negative_position() {
253+
let range: std::ops::Range<u32> = 0..30;
254+
let mut builder = QueryResponseBuilder::new(
255+
range.len(),
256+
100,
257+
State::Initial,
258+
&QueryRequest::<TestJMAPObject> {
259+
position: Some(-6),
260+
limit: Some(3),
261+
..Default::default()
262+
},
263+
);
264+
265+
for i in range {
266+
if !builder.add_id(Id::from_parts(0, i)) {
267+
break;
268+
}
269+
}
270+
271+
let result = builder.build().unwrap();
272+
273+
assert_eq!(result.ids.len(), 3, "Should collect 10 items");
274+
assert_eq!(result.ids[0].document_id(), 24, "First item should be 24");
275+
assert_eq!(result.ids[1].document_id(), 25, "Second item should be 25");
276+
assert_eq!(result.ids[2].document_id(), 26, "Third item should be 26");
277+
assert_eq!(result.position, 24);
278+
}
279+
280+
#[test]
281+
fn test_pagination_with_position_partial_results() {
282+
let range: std::ops::Range<u32> = 0..5;
283+
let mut builder = QueryResponseBuilder::new(
284+
range.len(),
285+
100,
286+
State::Initial,
287+
&QueryRequest::<TestJMAPObject> {
288+
position: Some(3),
289+
limit: Some(5),
290+
..Default::default()
291+
},
292+
);
293+
294+
for i in range {
295+
if !builder.add_id(Id::from_parts(0, i)) {
296+
break;
297+
}
298+
}
299+
300+
let result = builder.build().unwrap();
301+
302+
assert_eq!(
303+
result.ids.len(),
304+
2,
305+
"Should collect 2 items (not full limit)"
306+
);
307+
assert_eq!(result.ids[0].document_id(), 3, "First item should be 3");
308+
assert_eq!(result.ids[1].document_id(), 4, "Second item should be 4");
309+
assert_eq!(result.position, 3, " be 3");
310+
}
311+
312+
#[test]
313+
fn test_pagination_with_zero_anchor_offset() {
314+
let range: std::ops::Range<u32> = 0..10;
315+
let mut builder = QueryResponseBuilder::new(
316+
range.len(),
317+
100,
318+
State::Initial,
319+
&QueryRequest::<TestJMAPObject> {
320+
limit: Some(3),
321+
anchor: Some(Id::from_parts(0, 5)),
322+
anchor_offset: Some(0),
323+
..Default::default()
324+
},
325+
);
326+
327+
for i in range {
328+
if !builder.add_id(Id::from_parts(0, i)) {
329+
break;
330+
}
331+
}
332+
333+
let result = builder.build().unwrap();
334+
335+
assert_eq!(result.ids.len(), 3, "Should collect 3 items");
336+
assert_eq!(result.ids[0].document_id(), 5, "First item should be 5");
337+
assert_eq!(result.ids[1].document_id(), 6, "Second item should be 6");
338+
assert_eq!(result.ids[2].document_id(), 7, "Third item should be 7");
339+
assert_eq!(result.position, 5, "Position should be 5");
340+
}
341+
342+
#[test]
343+
fn test_pagination_with_negative_anchor_offset() {
344+
let range: std::ops::Range<u32> = 0..10;
345+
let mut builder = QueryResponseBuilder::new(
346+
range.len(),
347+
100,
348+
State::Initial,
349+
&QueryRequest::<TestJMAPObject> {
350+
limit: Some(3),
351+
anchor: Some(Id::from_parts(0, 5)),
352+
anchor_offset: Some(-2),
353+
..Default::default()
354+
},
355+
);
356+
357+
for i in range {
358+
if !builder.add_id(Id::from_parts(0, i)) {
359+
break;
360+
}
361+
}
362+
363+
let result = builder.build().unwrap();
364+
365+
assert_eq!(result.ids.len(), 3, "Should collect 3 items");
366+
assert_eq!(result.ids[0].document_id(), 3, "First item should be 3");
367+
assert_eq!(result.ids[1].document_id(), 4, "Second item should be 4");
368+
assert_eq!(result.ids[2].document_id(), 5, "Third item should be 5");
369+
assert_eq!(result.position, 3, "Position should be 3");
370+
}
371+
372+
#[test]
373+
fn test_pagination_with_negative_anchor_offset_more_than_limit() {
374+
let range: std::ops::Range<u32> = 0..10;
375+
let mut builder = QueryResponseBuilder::new(
376+
range.len(),
377+
100,
378+
State::Initial,
379+
&QueryRequest::<TestJMAPObject> {
380+
limit: Some(3),
381+
anchor: Some(Id::from_parts(0, 9)),
382+
anchor_offset: Some(-6),
383+
..Default::default()
384+
},
385+
);
386+
387+
for i in range {
388+
if !builder.add_id(Id::from_parts(0, i)) {
389+
break;
390+
}
391+
}
392+
393+
let result = builder.build().unwrap();
394+
395+
assert_eq!(result.ids.len(), 3, "Should collect 3 items");
396+
assert_eq!(result.ids[0].document_id(), 3, "First item should be 3");
397+
assert_eq!(result.ids[1].document_id(), 4, "Second item should be 4");
398+
assert_eq!(result.ids[2].document_id(), 5, "Third item should be 5");
399+
assert_eq!(result.position, 3, "Position should be 3");
400+
}
401+
402+
#[test]
403+
fn test_pagination_with_anchor_offset_1() {
404+
let range: std::ops::Range<u32> = 0..10;
405+
let mut builder = QueryResponseBuilder::new(
406+
range.len(),
407+
100,
408+
State::Initial,
409+
&QueryRequest::<TestJMAPObject> {
410+
limit: Some(3),
411+
anchor: Some(Id::from_parts(0, 3)),
412+
anchor_offset: Some(1),
413+
..Default::default()
414+
},
415+
);
416+
417+
for i in range {
418+
if !builder.add_id(Id::from_parts(0, i)) {
419+
break;
420+
}
421+
}
422+
423+
let result = builder.build().unwrap();
424+
425+
assert_eq!(result.ids.len(), 3, "Should collect 3 items");
426+
assert_eq!(result.ids[0].document_id(), 4, "First item should be 4");
427+
assert_eq!(result.ids[1].document_id(), 5, "Second item should be 5");
428+
assert_eq!(result.ids[2].document_id(), 6, "Third item should be 6");
429+
assert_eq!(result.position, 4, "Position should be 4");
430+
}
431+
432+
#[test]
433+
fn test_pagination_with_anchor_offset_2() {
434+
let range: std::ops::Range<u32> = 0..10;
435+
let mut builder = QueryResponseBuilder::new(
436+
range.len(),
437+
100,
438+
State::Initial,
439+
&QueryRequest::<TestJMAPObject> {
440+
limit: Some(3),
441+
anchor: Some(Id::from_parts(0, 3)),
442+
anchor_offset: Some(2),
443+
..Default::default()
444+
},
445+
);
446+
447+
for i in range {
448+
if !builder.add_id(Id::from_parts(0, i)) {
449+
break;
450+
}
451+
}
452+
453+
let result = builder.build().unwrap();
454+
455+
assert_eq!(result.ids.len(), 3, "Should collect 3 items");
456+
assert_eq!(result.ids[0].document_id(), 5, "First item should be 5");
457+
assert_eq!(result.ids[1].document_id(), 6, "Second item should be 6");
458+
assert_eq!(result.ids[2].document_id(), 7, "Third item should be 7");
459+
assert_eq!(result.position, 5, "Position should be 5");
460+
}
461+
462+
#[test]
463+
fn test_pagination_with_anchor_offset_10() {
464+
let range: std::ops::Range<u32> = 0..30;
465+
let mut builder = QueryResponseBuilder::new(
466+
range.len(),
467+
100,
468+
State::Initial,
469+
&QueryRequest::<TestJMAPObject> {
470+
limit: Some(5),
471+
anchor: Some(Id::from_parts(0, 3)),
472+
anchor_offset: Some(10),
473+
..Default::default()
474+
},
475+
);
476+
477+
for i in range {
478+
if !builder.add_id(Id::from_parts(0, i)) {
479+
break;
480+
}
162481
}
482+
483+
let result = builder.build().unwrap();
484+
485+
assert_eq!(result.ids.len(), 5, "Should collect 5 items");
486+
assert_eq!(result.ids[0].document_id(), 13, "First item should be 13");
487+
assert_eq!(result.ids[1].document_id(), 14, "Second item should be 14");
488+
assert_eq!(result.ids[2].document_id(), 15, "Third item should be 15");
489+
assert_eq!(result.ids[3].document_id(), 16, "Fourth item should be 16");
490+
assert_eq!(result.ids[4].document_id(), 17, "Fifth item should be 17");
491+
assert_eq!(result.position, 13, "Position should be 13");
163492
}
164493
}

0 commit comments

Comments
 (0)