stripe-ruby/test/stripe/list_object_test.rb
Brandur e3cc91ded2 Support backwards pagination with list's #auto_paging_each (#865)
* Support backwards pagination with list's `#auto_paging_each`

Previously, `#auto_paging_each` would always try to paginate forward,
even if it was clear based on the list's current filters that the user
had been intending to iterate backwards by specifying an `ending_before`
filter exclusively.

Here we implement backwards iteration by detecting this condition,
reversing the current list data, and making new requests for the
previous page (instead of the next one) as needed, which allows the user
to handle elements in reverse logical order.

Reversing the current page's list is intended as a minor user feature,
but may possibly be contentious. For background, when backwards
iterating in the API, results are still returned in "normal" order. So
if I specifying `ending_before=7`, the next page would look like `[4, 5,
6`] instead of `[6, 5, 4]`. In `#auto_paging_each` I reverse it to `[6,
5, 4]` so it feels to the user like they're handling elements in the
order they're iterating, which I think is okay. The reason it might be
contentious though is that it could be a tad confusing to someone who
already understands the normal `ending_before` ordering in the API.

Fixes #864.

* Allow `ending_before` and `starting_after` to remain in hydrated list object
2019-10-10 10:11:12 -07:00

203 lines
7.5 KiB
Ruby

# frozen_string_literal: true
require ::File.expand_path("../test_helper", __dir__)
module Stripe
class ListObjectTest < Test::Unit::TestCase
should "provide .empty_list" do
list = Stripe::ListObject.empty_list
assert list.empty?
end
should "provide #count via enumerable" do
list = Stripe::ListObject.construct_from(data: [{ object: "charge" }])
assert_equal 1, list.count
end
should "provide #each" do
arr = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
]
expected = Util.convert_to_stripe_object(arr, {})
list = Stripe::ListObject.construct_from(data: arr)
assert_equal expected, list.each.to_a
end
should "provide #reverse_each" do
arr = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
]
expected = Util.convert_to_stripe_object(arr.reverse, {})
list = Stripe::ListObject.construct_from(data: arr)
assert_equal expected, list.reverse_each.to_a
end
should "provide #auto_paging_each that supports forward pagination" do
arr = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
{ id: 4 },
{ id: 5 },
{ id: 6 },
]
expected = Util.convert_to_stripe_object(arr, {})
# Initial list object to page on. Notably, its last data element will be
# used as a cursor to fetch the next page.
list = TestListObject.construct_from(data: [{ id: 1 }],
has_more: true,
url: "/things")
list.filters = { limit: 3 }
# The test will start with the synthetic list object above, and use it as
# a starting point to fetch two more pages. The second page indicates
# that there are no more elements by setting `has_more` to `false`, and
# iteration stops.
stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { starting_after: "1", limit: "3" })
.to_return(body: JSON.generate(data: [{ id: 2 }, { id: 3 }, { id: 4 }], has_more: true, url: "/things"))
stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { starting_after: "4", limit: "3" })
.to_return(body: JSON.generate(data: [{ id: 5 }, { id: 6 }], has_more: false, url: "/things"))
assert_equal expected, list.auto_paging_each.to_a
end
should "provide #auto_paging_each that supports backward pagination with `ending_before`" do
arr = [
{ id: 6 },
{ id: 5 },
{ id: 4 },
{ id: 3 },
{ id: 2 },
{ id: 1 },
]
expected = Util.convert_to_stripe_object(arr, {})
# Initial list object to page on. Notably, its first data element will be
# used as a cursor to fetch the next page.
list = TestListObject.construct_from(data: [{ id: 6 }],
has_more: true,
url: "/things")
# We also add an `ending_before` filter on the list to simulate backwards
# pagination.
list.filters = { ending_before: 7, limit: 3 }
# The test will start with the synthetic list object above, and use it as
# a starting point to fetch two more pages. The second page indicates
# that there are no more elements by setting `has_more` to `false`, and
# iteration stops.
stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { ending_before: "6", limit: "3" })
.to_return(body: JSON.generate(data: [{ id: 3 }, { id: 4 }, { id: 5 }], has_more: true, url: "/things"))
stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { ending_before: "3", limit: "3" })
.to_return(body: JSON.generate(data: [{ id: 1 }, { id: 2 }], has_more: false, url: "/things"))
assert_equal expected, list.auto_paging_each.to_a
end
should "provide #auto_paging_each that responds to a block" do
arr = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
]
expected = Util.convert_to_stripe_object(arr, {})
list = TestListObject.construct_from(data: [{ id: 1 }],
has_more: true,
url: "/things")
stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { starting_after: "1" })
.to_return(body: JSON.generate(data: [{ id: 2 }, { id: 3 }], has_more: false))
actual = []
list.auto_paging_each do |obj|
actual << obj
end
assert_equal expected, actual
end
should "provide #empty?" do
list = Stripe::ListObject.construct_from(data: [])
assert list.empty?
list = Stripe::ListObject.construct_from(data: [{}])
refute list.empty?
end
#
# next_page
#
should "fetch a next page through #next_page" do
list = TestListObject.construct_from(data: [{ id: 1 }],
has_more: true,
url: "/things")
stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { starting_after: "1" })
.to_return(body: JSON.generate(data: [{ id: 2 }], has_more: false))
next_list = list.next_page
refute next_list.empty?
end
should "fetch a next page through #next_page and respect limit" do
list = TestListObject.construct_from(data: [{ id: 1 }],
has_more: true,
url: "/things")
list.filters = { expand: ["data.source"], limit: 3 }
stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { "expand[]" => "data.source", "limit" => "3", "starting_after" => "1" })
.to_return(body: JSON.generate(data: [{ id: 2 }], has_more: false))
next_list = list.next_page
assert_equal({ expand: ["data.source"], limit: 3, starting_after: 1 }, next_list.filters)
end
should "fetch an empty page through #next_page" do
list = TestListObject.construct_from(data: [{ id: 1 }],
has_more: false,
url: "/things")
next_list = list.next_page
assert_equal Stripe::ListObject.empty_list, next_list
end
#
# previous_page
#
should "fetch a next page through #previous_page" do
list = TestListObject.construct_from(data: [{ id: 2 }],
has_more: true,
url: "/things")
stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { ending_before: "2" })
.to_return(body: JSON.generate(data: [{ id: 1 }], has_more: false))
next_list = list.previous_page
refute next_list.empty?
end
should "fetch a next page through #previous_page and respect limit" do
list = TestListObject.construct_from(data: [{ id: 2 }],
has_more: true,
url: "/things")
list.filters = { expand: ["data.source"], limit: 3 }
stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { "expand[]" => "data.source", "limit" => "3", "ending_before" => "2" })
.to_return(body: JSON.generate(data: [{ id: 1 }], has_more: false))
next_list = list.previous_page
assert_equal({ ending_before: 2, expand: ["data.source"], limit: 3 }, next_list.filters)
end
end
end
# A helper class with a URL that allows us to try out pagination.
class TestListObject < Stripe::ListObject
end