# frozen_string_literal: true require 'spec_helper' module Aws module Record describe ItemCollection do let(:model) do Class.new do include(Aws::Record) set_table_name('TestTable') integer_attr(:id, hash_key: true) end end let(:api_requests) { [] } let(:stub_client) do requests = api_requests client = Aws::DynamoDB::Client.new(stub_responses: true) client.handle do |context| requests << context.params @handler.call(context) end client end let(:truncated_resp) do { items: [ { 'id' => 1 }, { 'id' => 2 }, { 'id' => 3 } ], count: 3, last_evaluated_key: { 'id' => { n: '3' } } } end let(:non_truncated_resp) do { items: [ { 'id' => 4 }, { 'id' => 5 } ], count: 2, last_evaluated_key: nil } end describe '#page' do it 'provides an array of items from a single client call' do stub_client.stub_responses(:scan, truncated_resp) c = ItemCollection.new( :scan, { table_name: 'TestTable' }, model, stub_client ) actual = c.page expect(actual.size).to eq(3) actual_ids = actual.map(&:id) expect(actual_ids).to eq([1, 2, 3]) expect(c.last_evaluated_key).to eq('id' => { 'n' => '3' }) end end describe '#new_record' do it 'marks a new record as being new' do record = model.new expect(record.new_record?).to be(true) expect(record.destroyed?).to be(false) end it 'marks records fetched from a client call as not being new' do stub_client.stub_responses(:scan, non_truncated_resp) c = ItemCollection.new( :scan, { table_name: 'TestTable' }, model, stub_client ) c.each do |record| expect(record.new_record?).to be(false) expect(record.destroyed?).to be(false) end end end describe '#new_record with ActiveModel::Model' do it 'marks a new record as being new' do record = model.new expect(record.new_record?).to be(true) expect(record.destroyed?).to be(false) end it 'marks records fetched from a client call as not being new' do stub_client.stub_responses(:scan, non_truncated_resp) c = ItemCollection.new( :scan, { table_name: 'TestTable' }, model, stub_client ) c.each do |record| expect(record.new_record?).to be(false) expect(record.destroyed?).to be(false) expect(record.persisted?).to be(true) end end end describe '#last_evaluated_key' do it 'points you to the client response pagination value if present' do stub_client.stub_responses(:scan, truncated_resp) c = ItemCollection.new( :scan, { table_name: 'TestTable' }, model, stub_client ) c.take(2) # Trigger the "call" expect(c.last_evaluated_key).to eq('id' => { 'n' => '3' }) end it 'provides a nil pagination value if no pages remain' do stub_client.stub_responses(:scan, non_truncated_resp) c = ItemCollection.new( :scan, { table_name: 'TestTable' }, model, stub_client ) c.take(2) # Trigger the "call" expect(c.last_evaluated_key).to be_nil end it 'correctly provides the most recent pagination key' do stub_client.stub_responses(:scan, truncated_resp, non_truncated_resp) c = ItemCollection.new( :scan, { table_name: 'TestTable' }, model, stub_client ) c.take(4) # Trigger the "call" and the second "page" expect(c.last_evaluated_key).to be_nil end it 'gathers evaluation keys from #page as well' do stub_client.stub_responses(:scan, truncated_resp) c = ItemCollection.new( :scan, { table_name: 'TestTable' }, model, stub_client ) c.page expect(c.last_evaluated_key).to eq('id' => { 'n' => '3' }) stub_client.stub_responses(:scan, non_truncated_resp) c = ItemCollection.new( :scan, { table_name: 'TestTable' }, model, stub_client ) c.page expect(c.last_evaluated_key).to be_nil end end describe '#each' do it 'correctly iterates through a paginated response' do stub_client.stub_responses(:scan, truncated_resp, non_truncated_resp) c = ItemCollection.new( :scan, { table_name: 'TestTable' }, model, stub_client ) expected = [1, 2, 3, 4, 5] actual = c.map(&:id) expect(actual).to eq(expected) expect(api_requests.size).to eq(2) end it 'makes the minimum number of required requests' do # This ensures we don't create a query/scan regression where we fully # iterate when we don't need the full item set. stub_client.stub_responses(:scan, truncated_resp, non_truncated_resp) c = ItemCollection.new( :scan, { table_name: 'TestTable' }, model, stub_client ) expected = 1 actual = c.first.id expect(actual).to eq(expected) expect(api_requests.size).to eq(1) end context 'model_filter is set' do let(:model_a) do Class.new do include(Aws::Record) set_table_name('TestTable') integer_attr(:id, hash_key: true) string_attr(:class_name) string_attr(:attr_a) end end let(:model_b) do Class.new do include(Aws::Record) set_table_name('TestTable') integer_attr(:id, hash_key: true) string_attr(:class_name) string_attr(:attr_b) end end let(:resp) do { items: [ { 'id' => 1, 'class_name' => 'A', 'attr_a' => 'a' }, { 'id' => 2, 'class_name' => 'B', 'attr_b' => 'b' }, { 'id' => 3 } ], count: 3 } end let(:model_filter) do proc { |raw_item_attributes| case raw_item_attributes['class_name'] when 'A' then model_a when 'B' then model_b else # rubocop:disable Style/EmptyElse nil end } end before(:each) do stub_client.stub_responses(:scan, resp) end let(:c) do ItemCollection.new( :scan, { table_name: 'TestTable', model_filter: model_filter }, model_a, stub_client ) end it 'uses the model proc to determine the returned model classes' do expected = [model_a, model_b] actual = c.map(&:class) expect(actual).to eq(expected) end it 'maps class specific attributes' do actual = c.page expect(actual[0].attr_a).to eq('a') expect(actual[1].attr_b).to eq('b') end it 'skips items when model_filter returns nil' do actual = c.page expect(actual.size).to eq(2) end end end describe '#empty?' do let(:resp_full) do { items: [ { 'id' => 1 }, { 'id' => 2 }, { 'id' => 3 } ], count: 3 } end let(:resp_empty) do { items: [], count: 0 } end let(:truncated_empty) do { items: [], count: 0, last_evaluated_key: { 'id' => { n: '3' } } } end it 'is not empty' do stub_client.stub_responses(:scan, resp_full) c = ItemCollection.new( :scan, { table_name: 'TestTable' }, model, stub_client ) expect(c.empty?).to be_falsy end it 'is empty' do stub_client.stub_responses(:scan, resp_empty) c = ItemCollection.new( :scan, { table_name: 'TestTable' }, model, stub_client ) expect(c.empty?).to be_truthy end it 'handles initial pages being empty' do # Scans with limit fields may return empty pages, while values still # exist. stub_client.stub_responses(:scan, truncated_empty, resp_full) c = ItemCollection.new( :scan, { table_name: 'TestTable', limit: 3 }, model, stub_client ) expect(c.empty?).to be_falsy end it 'handles final pages being empty' do # LastEvaluatedKey being present does not guarantee additional data is # coming, so make sure we handle a final empty page. stub_client.stub_responses(:scan, truncated_resp, resp_empty) c = ItemCollection.new( :scan, { table_name: 'TestTable' }, model, stub_client ) expect(c.empty?).to be_falsy end end end end end