diff --git a/.rubocop.yml b/.rubocop.yml index e986cd04..66530cfa 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,7 +4,6 @@ inherit_from: - .rubocop_performance.yml - .rubocop_rspec.yml - .rubocop_thread_safety.yml - - .rubocop_todo.yml require: - rubocop-packaging @@ -21,6 +20,11 @@ AllCops: DisplayCopNames: true # Display the name of the failing cops NewCops: enable +# Intentionally disabled +# There are several cases with already established method signatires +Style/OptionalArguments: + Enabled: false + # It's a matter of taste Layout/ParameterAlignment: EnforcedStyle: with_fixed_indentation diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml deleted file mode 100644 index e2bc59bc..00000000 --- a/.rubocop_todo.yml +++ /dev/null @@ -1,122 +0,0 @@ -# This configuration was generated by -# `rubocop --auto-gen-config` -# on 2022-12-16 17:45:21 -0700 using RuboCop version 0.81.0. -# The point is for the user to remove these configuration records -# one by one as the offenses are removed from the code base. -# Note that changes in the inspected code, or installation of new -# versions of RuboCop, may require this file to be generated again. - -# Offense count: 1 -# Cop supports --auto-correct. -Lint/OrderedMagicComments: - Exclude: - - 'lib/dynamoid/persistence.rb' - -# Offense count: 3 -# Configuration parameters: AllowComments. -Lint/SuppressedException: - Exclude: - - 'lib/dynamoid/dirty.rb' - - 'lib/dynamoid/persistence/update_fields.rb' - - 'lib/dynamoid/persistence/upsert.rb' - -# Offense count: 1 -# Configuration parameters: EnforcedStyleForLeadingUnderscores. -# SupportedStylesForLeadingUnderscores: disallowed, required, optional -Naming/MemoizedInstanceVariableName: - Exclude: - - 'lib/dynamoid/dirty.rb' - -# Offense count: 13 -RSpec/AnyInstance: - Exclude: - - 'spec/dynamoid/adapter_plugin/aws_sdk_v3_spec.rb' - - 'spec/dynamoid/adapter_spec.rb' - - 'spec/dynamoid/criteria/chain_spec.rb' - - 'spec/dynamoid/persistence_spec.rb' - -# Offense count: 125 -# Configuration parameters: Prefixes. -# Prefixes: when, with, without -RSpec/ContextWording: - Enabled: false - -# Offense count: 2 -RSpec/DescribeClass: - Exclude: - - 'spec/dynamoid/before_type_cast_spec.rb' - - 'spec/dynamoid/type_casting_spec.rb' - -# Offense count: 4 -# Configuration parameters: CustomIncludeMethods. -RSpec/EmptyExampleGroup: - Exclude: - - 'spec/dynamoid/persistence_spec.rb' - - 'spec/dynamoid/type_casting_spec.rb' - -# Offense count: 8 -RSpec/LeakyConstantDeclaration: - Exclude: - - 'spec/dynamoid/criteria/chain_spec.rb' - - 'spec/dynamoid/indexes_spec.rb' - - 'spec/dynamoid/sti_spec.rb' - -# Offense count: 1 -RSpec/LetSetup: - Exclude: - - 'spec/dynamoid/sti_spec.rb' - -# Offense count: 2 -RSpec/RepeatedDescription: - Exclude: - - 'spec/dynamoid/associations/belongs_to_spec.rb' - -# Offense count: 2 -RSpec/RepeatedExample: - Exclude: - - 'spec/dynamoid/associations/has_one_spec.rb' - -# Offense count: 6 -RSpec/RepeatedExampleGroupDescription: - Exclude: - - 'spec/dynamoid/adapter_plugin/aws_sdk_v3_spec.rb' - - 'spec/dynamoid/finders_spec.rb' - -# Offense count: 9 -RSpec/SubjectStub: - Exclude: - - 'spec/dynamoid/adapter_spec.rb' - -# Offense count: 2 -Style/CommentedKeyword: - Exclude: - - 'lib/dynamoid/dirty.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, Autocorrect. -# SupportedStyles: module_function, extend_self, forbidden -Style/ModuleFunction: - Exclude: - - 'lib/dynamoid.rb' - - 'lib/dynamoid/config.rb' - -# Offense count: 3 -Style/OptionalArguments: - Exclude: - - 'lib/dynamoid/persistence.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: AllowAsExpressionSeparator. -Style/Semicolon: - Exclude: - - 'spec/dynamoid/adapter_plugin/aws_sdk_v3_spec.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, AllowedMethods. -# AllowedMethods: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym -Style/TrivialAccessors: - Exclude: - - 'lib/dynamoid/adapter_plugin/aws_sdk_v3.rb' diff --git a/lib/dynamoid.rb b/lib/dynamoid.rb index f21daba2..4e80d2f3 100644 --- a/lib/dynamoid.rb +++ b/lib/dynamoid.rb @@ -44,7 +44,7 @@ require 'dynamoid/railtie' if defined?(Rails) module Dynamoid - extend self + extend self # rubocop:disable Style/ModuleFunction def configure block_given? ? yield(Dynamoid::Config) : Dynamoid::Config diff --git a/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb b/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb index 93a982c4..b3bb8ab6 100644 --- a/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +++ b/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb @@ -156,9 +156,7 @@ def connection_config # Return the client object. # # @since 1.0.0 - def client - @client - end + attr_reader :client # Puts multiple items in one table # diff --git a/lib/dynamoid/config.rb b/lib/dynamoid/config.rb index 139ab2dc..38e45da7 100644 --- a/lib/dynamoid/config.rb +++ b/lib/dynamoid/config.rb @@ -19,7 +19,7 @@ module Config 'dynamoid' end - extend self + extend self # rubocop:disable Style/ModuleFunction extend Options include ActiveModel::Observing if defined?(ActiveModel::Observing) diff --git a/lib/dynamoid/dirty.rb b/lib/dynamoid/dirty.rb index aaee75dc..9ec1a612 100644 --- a/lib/dynamoid/dirty.rb +++ b/lib/dynamoid/dirty.rb @@ -123,7 +123,7 @@ def changes # # @return [ActiveSupport::HashWithIndifferentAccess] def previous_changes - @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new + @previous_changes ||= ActiveSupport::HashWithIndifferentAccess.new end # Returns a hash of the attributes with unsaved changes indicating their original @@ -140,14 +140,14 @@ def changed_attributes # Clear all dirty data: current changes and previous changes. def clear_changes_information - @previously_changed = ActiveSupport::HashWithIndifferentAccess.new + @previous_changes = ActiveSupport::HashWithIndifferentAccess.new @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new @attributes_from_database = HashWithIndifferentAccess.new(DeepDupper.dup_attributes(@attributes, self.class)) end # Clears dirty data and moves +changes+ to +previous_changes+. def changes_applied - @previously_changed = changes + @previous_changes = changes @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new @attributes_from_database = HashWithIndifferentAccess.new(DeepDupper.dup_attributes(@attributes, self.class)) end @@ -248,12 +248,8 @@ def attribute_change(name) def attribute_will_change!(name) return if attribute_changed?(name) - begin - value = read_attribute(name) - value = value.clone if value.duplicable? - rescue TypeError, NoMethodError - end - + value = read_attribute(name) + value = value.clone if value.duplicable? set_attribute_was(name, value) end diff --git a/lib/dynamoid/persistence.rb b/lib/dynamoid/persistence.rb index cf50c79d..be620905 100644 --- a/lib/dynamoid/persistence.rb +++ b/lib/dynamoid/persistence.rb @@ -465,7 +465,7 @@ def upsert(hash_key_value, range_key_value = nil, attrs = {}, conditions = {}) # @return [Model class] self def inc(hash_key_value, range_key_value = nil, counters) # It's similar to Rails' #update_counters. - Inc.call(self, hash_key_value, range_key_value, counters) + Inc.call(self, hash_key_value, range_key_value, **counters) self end diff --git a/lib/dynamoid/persistence/inc.rb b/lib/dynamoid/persistence/inc.rb index 4fd7c50b..ae03c2e2 100644 --- a/lib/dynamoid/persistence/inc.rb +++ b/lib/dynamoid/persistence/inc.rb @@ -6,19 +6,17 @@ module Dynamoid module Persistence # @private class Inc - def self.call(model_class, partition_key, sort_key = nil, counters) # rubocop:disable Style/OptionalArguments - new(model_class, partition_key, sort_key, counters).call + def self.call(model_class, partition_key, sort_key, **counters) + new(model_class, partition_key, sort_key, **counters).call end - # rubocop:disable Style/OptionalArguments - def initialize(model_class, partition_key, sort_key = nil, counters) + def initialize(model_class, partition_key, sort_key, **counters) @model_class = model_class @partition_key = partition_key @sort_key = sort_key @counters = counters @touch = @counters.delete(:touch) end - # rubocop:enable Style/OptionalArguments def call validate_primary_key! diff --git a/lib/dynamoid/persistence/update_fields.rb b/lib/dynamoid/persistence/update_fields.rb index da3e3e7b..847750ad 100644 --- a/lib/dynamoid/persistence/update_fields.rb +++ b/lib/dynamoid/persistence/update_fields.rb @@ -30,7 +30,7 @@ def call raw_attributes = update_item @model_class.new(undump_attributes(raw_attributes)) - rescue Dynamoid::Errors::ConditionalCheckFailedException + rescue Dynamoid::Errors::ConditionalCheckFailedException # rubocop:disable Lint/SuppressedException end private diff --git a/lib/dynamoid/persistence/upsert.rb b/lib/dynamoid/persistence/upsert.rb index 87ad8a67..05bfd33e 100644 --- a/lib/dynamoid/persistence/upsert.rb +++ b/lib/dynamoid/persistence/upsert.rb @@ -28,7 +28,7 @@ def call raw_attributes = update_item @model_class.new(undump_attributes(raw_attributes)) - rescue Dynamoid::Errors::ConditionalCheckFailedException + rescue Dynamoid::Errors::ConditionalCheckFailedException # rubocop:disable Lint/SuppressedException end private diff --git a/lib/dynamoid/transactions/mutation.rb b/lib/dynamoid/transactions/mutation.rb index d501fb02..a91b5f25 100644 --- a/lib/dynamoid/transactions/mutation.rb +++ b/lib/dynamoid/transactions/mutation.rb @@ -443,7 +443,7 @@ def create(model_class, attributes = {}, &block) # @param range_key [Scalar value] range key value (optional) # @param attributes [Hash] # @return [nil] - def upsert(model_class, hash_key, range_key = nil, attributes) # rubocop:disable Style/OptionalArguments + def upsert(model_class, hash_key, range_key = nil, attributes) action = Upsert.new(model_class, hash_key, range_key, attributes) register_action action end diff --git a/spec/dynamoid/adapter_plugin/aws_sdk_v3/create_table_spec.rb b/spec/dynamoid/adapter_plugin/aws_sdk_v3/create_table_spec.rb index ca253a6c..e780593a 100644 --- a/spec/dynamoid/adapter_plugin/aws_sdk_v3/create_table_spec.rb +++ b/spec/dynamoid/adapter_plugin/aws_sdk_v3/create_table_spec.rb @@ -17,7 +17,7 @@ let(:table_description) { double('table_description', table_status: 'ACTIVE') } describe 'call' do - context 'table properties' do + context 'with table properties' do it 'has the correct table name' do expect(client).to receive(:create_table) .with(hash_including(table_name: :dogs)) @@ -42,7 +42,7 @@ described_class.new(client, :dogs, :id, options).call end - context 'on demand' do + context 'when on demand' do let(:options) do { billing_mode: :on_demand } end @@ -65,7 +65,7 @@ end end - context 'key schema' do + context 'with key schema' do it 'defines a simple primary key' do expect(client).to receive(:create_table) .with(hash_including(key_schema: [hash_including(attribute_name: 'id', key_type: 'HASH')])) @@ -110,7 +110,7 @@ end end - context 'local secondary index' do + context 'with local secondary index' do let(:options) do super().merge(local_secondary_indexes: [index]) end @@ -147,7 +147,7 @@ end end - context 'global secondary index' do + context 'with global secondary index' do let(:options) do super().merge(global_secondary_indexes: [index]) end @@ -197,7 +197,7 @@ described_class.new(client, :dogs, :id, options).call end - context 'on demand' do + context 'when on demand' do let(:options) do super().merge(billing_mode: :on_demand) end diff --git a/spec/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status_spec.rb b/spec/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status_spec.rb index 7c0c4dce..82f79681 100644 --- a/spec/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status_spec.rb +++ b/spec/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status_spec.rb @@ -5,7 +5,7 @@ describe Dynamoid::AdapterPlugin::AwsSdkV3::UntilPastTableStatus do describe 'call' do - context 'table creation' do + context 'with table creation' do let(:client) { double('client') } let(:response_creating) { double('response#creating', table: creating_table) } let(:response_active) { double('response#active', table: active_table) } diff --git a/spec/dynamoid/adapter_plugin/aws_sdk_v3_spec.rb b/spec/dynamoid/adapter_plugin/aws_sdk_v3_spec.rb index 66e31354..3dc95d33 100644 --- a/spec/dynamoid/adapter_plugin/aws_sdk_v3_spec.rb +++ b/spec/dynamoid/adapter_plugin/aws_sdk_v3_spec.rb @@ -30,11 +30,11 @@ # requires some inputs. The internal aspects will configure request parameters and # the Dynamoid adapter call correctly. # - # @param [Symbol] request_type the name of the request, either :query or :scan + # @param [Symbol] operation the name of the request, either :Query or :Scan # - shared_examples 'correctly handling limits' do |request_type| + shared_examples 'correctly handling limits' do |operation| before do - @request_type = request_type + @operation = operation end def query_key_conditions @@ -42,14 +42,14 @@ def query_key_conditions end def dynamo_request(table_name, conditions = [], options = {}) - if @request_type == :query + if @operation == :Query Dynamoid.adapter.query(table_name, query_key_conditions, conditions, options).flat_map { |i| i } else Dynamoid.adapter.scan(table_name, conditions, options).flat_map { |i| i } end end - context 'multiple name entities' do + context 'with multiple name entities' do before do (1..4).each do |i| Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Josh', range: i.to_f) @@ -68,13 +68,15 @@ def dynamo_request(table_name, conditions = [], options = {}) it 'returns correct batch' do # Receives 8 times for each item and 1 more for empty page - expect(Dynamoid.adapter.client).to receive(request_type).exactly(9).times.and_call_original - expect(dynamo_request(test_table3, [], { batch_size: 1 }).count).to eq(8) + expect { + expect(dynamo_request(test_table3, [], { batch_size: 1 }).count).to eq(8) + }.to send_request_matching(operation).exactly(9).times end it 'returns correct batch and paginates in batches' do - expect(Dynamoid.adapter.client).to receive(request_type).exactly(3).times.and_call_original - expect(dynamo_request(test_table3, [], { batch_size: 3 }).count).to eq(8) + expect { + expect(dynamo_request(test_table3, [], { batch_size: 3 }).count).to eq(8) + }.to send_request_matching(operation).exactly(3).times end it 'returns correct record limit and batch' do @@ -88,81 +90,87 @@ def dynamo_request(table_name, conditions = [], options = {}) end it 'obeys correct scan limit with filter' do - expect(Dynamoid.adapter.client).to receive(request_type).once.and_call_original - expect( - dynamo_request(test_table3, [{ name: [[:eq, 'Josh']] }], { scan_limit: 2 }).count - ).to eq(2) + expect { + expect( + dynamo_request(test_table3, [{ name: [[:eq, 'Josh']] }], { scan_limit: 2 }).count + ).to eq(2) + }.to send_request_matching(operation).once end it 'obeys correct scan limit over record limit with filter' do - expect(Dynamoid.adapter.client).to receive(request_type).once.and_call_original - expect( - dynamo_request( - test_table3, - [{ name: [[:eq, 'Josh']] }], - { - scan_limit: 2, - record_limit: 10 # Won't be able to return more than 2 due to scan limit - } - ).count - ).to eq(2) + expect { + expect( + dynamo_request( + test_table3, + [{ name: [[:eq, 'Josh']] }], + { + scan_limit: 2, + record_limit: 10 # Won't be able to return more than 2 due to scan limit + } + ).count + ).to eq(2) + }.to send_request_matching(operation).once end it 'obeys correct scan limit with filter with some return' do - expect(Dynamoid.adapter.client).to receive(request_type).once.and_call_original - expect( - dynamo_request(test_table3, [{ name: [[:eq, 'Pascal']] }], { scan_limit: 5 }).count - ).to eq(1) + expect { + expect( + dynamo_request(test_table3, [{ name: [[:eq, 'Pascal']] }], { scan_limit: 5 }).count + ).to eq(1) + }.to send_request_matching(operation).once end it 'obeys correct scan limit and batch size with filter with some return' do - expect(Dynamoid.adapter.client).to receive(request_type).twice.and_call_original - expect( - dynamo_request( - test_table3, - [{ name: [[:eq, 'Josh']] }], - { - scan_limit: 3, - batch_size: 2 # This would force batching of size 2 for potential of 4 results! - } - ).count - ).to eq(3) + expect { + expect( + dynamo_request( + test_table3, + [{ name: [[:eq, 'Josh']] }], + { + scan_limit: 3, + batch_size: 2 # This would force batching of size 2 for potential of 4 results! + } + ).count + ).to eq(3) + }.to send_request_matching(operation).twice end it 'obeys correct scan limit with filter and batching for some return' do - expect(Dynamoid.adapter.client).to receive(request_type).exactly(5).times.and_call_original # We should paginate through 5 responses each of size 1 (batch) and # only scan through 5 records at most which with our given filter # should return 1 result since first 4 are Josh and last is Pascal. - expect( - dynamo_request( - test_table3, - [{ name: [[:eq, 'Pascal']] }], - { - batch_size: 1, - scan_limit: 5, - record_limit: 3 - } - ).count - ).to eq(1) + expect { + expect( + dynamo_request( + test_table3, + [{ name: [[:eq, 'Pascal']] }], + { + batch_size: 1, + scan_limit: 5, + record_limit: 3 + } + ).count + ).to eq(1) + }.to send_request_matching(operation).exactly(5).times end it 'obeys correct record limit with filter, batching, and scan limit' do - expect(Dynamoid.adapter.client).to receive(request_type).exactly(6).times.and_call_original # We should paginate through 6 responses each of size 1 (batch) and # only scan through 6 records at most which with our given filter # should return 2 results, and hit record limit before scan limit. - expect( - dynamo_request( - test_table3, - [{ name: [[:eq, 'Pascal']] }], - { - batch_size: 1, - scan_limit: 10, - record_limit: 2 - } - ).count - ).to eq(2) + expect { + expect( + dynamo_request( + test_table3, + [{ name: [[:eq, 'Pascal']] }], + { + batch_size: 1, + scan_limit: 10, + record_limit: 2 + } + ).count + ).to eq(2) + }.to send_request_matching(operation).exactly(6).times end end @@ -170,7 +178,7 @@ def dynamo_request(table_name, conditions = [], options = {}) # Tests that even with large records we are paginating to pull more data # even if we hit response data size limits # - context 'large records still returns as much data' do + context 'when large records still returns as much data' do before do # 64 of these items will exceed the 1MB result record_limit thus query won't return all results on first loop # We use :age since :range won't work for filtering in queries @@ -193,18 +201,20 @@ def dynamo_request(table_name, conditions = [], options = {}) # Not sure why there is difference but :query will do 1 page and see 100 records and filter out 10 # while :scan will do 2 pages and see 64 records on first page similar to the 1MB return limit # and then look at 36 records and find 10 on the second page. - pages = request_type == :query ? 1 : 2 - expect(Dynamoid.adapter.client).to receive(request_type).exactly(pages).times.and_call_original - expect( - dynamo_request(test_table3, [{ age: [[:gte, 90.0]] }], { scan_limit: 100 }).count - ).to eq(10) + pages = operation == :Query ? 1 : 2 + expect { + expect( + dynamo_request(test_table3, [{ age: [[:gte, 90.0]] }], { scan_limit: 100 }).count + ).to eq(10) + }.to send_request_matching(operation).exactly(pages).times end it 'returns correct for record limit' do - expect(Dynamoid.adapter.client).to receive(request_type).twice.and_call_original - expect( - dynamo_request(test_table3, [{ age: [[:gte, 5.0]] }], { record_limit: 100 }).count - ).to eq(100) + expect { + expect( + dynamo_request(test_table3, [{ age: [[:gte, 5.0]] }], { record_limit: 100 }).count + ).to eq(100) + }.to send_request_matching(operation).twice end it 'returns correct record limit with filtering' do @@ -217,32 +227,35 @@ def dynamo_request(table_name, conditions = [], options = {}) # Since we hit the data size limit 3 times, so we must make 4 requests # which is limitation of DynamoDB and therefore batch limit is # restricted by this limitation as well! - expect(Dynamoid.adapter.client).to receive(request_type).exactly(4).times.and_call_original - expect(dynamo_request(test_table3, [], { batch_size: 100 }).count).to eq(200) + expect { + expect(dynamo_request(test_table3, [], { batch_size: 100 }).count).to eq(200) + }.to send_request_matching(operation).exactly(4).times end it 'returns correct with batching and record limit beyond data size limit' do # Since we hit limit once, we need to make sure the second request only # requests for as many as we have left for our record limit. - expect(Dynamoid.adapter.client).to receive(request_type).twice.and_call_original - expect( - dynamo_request(test_table3, [], { record_limit: 83, batch_size: 100 }).count - ).to eq(83) + expect { + expect( + dynamo_request(test_table3, [], { record_limit: 83, batch_size: 100 }).count + ).to eq(83) + }.to send_request_matching(operation).twice end it 'returns correct with batching and record limit' do - expect(Dynamoid.adapter.client).to receive(request_type).exactly(11).times.and_call_original # Since we do age >= 5.0 we lose the first 5 results so we make 11 paginated requests - expect( - dynamo_request( - test_table3, - [{ age: [[:gte, 5.0]] }], - { - record_limit: 100, - batch_size: 10 - } - ).count - ).to eq(100) + expect { + expect( + dynamo_request( + test_table3, + [{ age: [[:gte, 5.0]] }], + { + record_limit: 100, + batch_size: 10 + } + ).count + ).to eq(100) + }.to send_request_matching(operation).exactly(11).times end end @@ -255,22 +268,23 @@ def dynamo_request(table_name, conditions = [], options = {}) Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Josh', range: i.to_f) end - expect(Dynamoid.adapter.client).to receive(request_type).twice.and_call_original # In faulty code, the record limit would adjust limit to 2 thus on second page # we would get the 5th Josh (range value 6.0) whereas correct implementation would # adjust limit to 1 since can only scan 1 more record therefore would see Pascal # and not go to next valid record. - expect( - dynamo_request( - test_table3, - [{ name: [[:eq, 'Josh']] }], - { - batch_size: 4, - scan_limit: 5, # Scan limit would adjust requested limit to 1 - record_limit: 6 # Record limit would adjust requested limit to 2 - } - ).count - ).to eq(4) + expect { + expect( + dynamo_request( + test_table3, + [{ name: [[:eq, 'Josh']] }], + { + batch_size: 4, + scan_limit: 5, # Scan limit would adjust requested limit to 1 + record_limit: 6 # Record limit would adjust requested limit to 2 + } + ).count + ).to eq(4) + }.to send_request_matching(operation).twice end end @@ -370,10 +384,16 @@ def dynamo_request(table_name, conditions = [], options = {}) end it 'passes options to underlying BatchGet call' do - pending 'at the moment passing the options to underlying batch get is not supported' - - expect_any_instance_of(Aws::DynamoDB::Client).to receive(:batch_get_item).with(request_items: { test_table1 => { keys: [{ 'id' => '1' }, { 'id' => '2' }], consistent_read: true } }).and_call_original - described_class.batch_get_item({ test_table1 => %w[1 2] }, consistent_read: true) + expect { + Dynamoid.adapter.batch_get_item({ table => %w[1 2] }, consistent_read: true) + }.to send_request_matching(:BatchGetItem, { + 'RequestItems' => { + table => { + 'Keys' => [{ 'id' => { 'S' => '1' } }, { 'id' => { 'S' => '2' } }], + 'ConsistentRead' => true + } + } + }) end it 'loads multiple items at once' do @@ -409,8 +429,9 @@ def dynamo_request(table_name, conditions = [], options = {}) end it 'performs BatchGetItem API call' do - expect(Dynamoid.adapter.client).to receive(:batch_get_item).and_call_original - Dynamoid.adapter.batch_get_item(table => ['1']) + expect { + Dynamoid.adapter.batch_get_item(table => ['1']) + }.to send_request_matching(:BatchGetItem) end it 'accepts [] as an ids list' do @@ -534,15 +555,13 @@ def dynamo_request(table_name, conditions = [], options = {}) Dynamoid.adapter.put_item(table, id: id, text: text) end - expect(Dynamoid.adapter.client).to receive(:batch_get_item) - .exactly(3) - .times.and_call_original + expect { + results = Dynamoid.adapter.batch_get_item(table => ids) + items = results[table] - results = Dynamoid.adapter.batch_get_item(table => ids) - items = results[table] - - expect(items.size).to eq 100 - expect(items.map { |h| h[:id] }).to match_array(ids) + expect(items.size).to eq 100 + expect(items.map { |h| h[:id] }).to match_array(ids) + }.to send_request_matching(:BatchGetItem).exactly(3).times end it 'loads unprocessed items for a table with a range key' do @@ -568,15 +587,13 @@ def dynamo_request(table_name, conditions = [], options = {}) Dynamoid.adapter.put_item(table_with_composite_key, id: id, age: age, name: text) end - expect(Dynamoid.adapter.client).to receive(:batch_get_item) - .exactly(3) - .times.and_call_original + expect { + results = Dynamoid.adapter.batch_get_item(table_with_composite_key => ids) + items = results[table_with_composite_key] - results = Dynamoid.adapter.batch_get_item(table_with_composite_key => ids) - items = results[table_with_composite_key] - - expect(items.size).to eq(100) - expect(items.map { |h| [h[:id], h[:age]] }).to match_array(ids) + expect(items.size).to eq(100) + expect(items.map { |h| [h[:id], h[:age]] }).to match_array(ids) + }.to send_request_matching(:BatchGetItem).exactly(3).times end context 'when called with block' do @@ -739,8 +756,9 @@ def dynamo_request(table_name, conditions = [], options = {}) context 'with a preexisting table' do # GetItem, PutItem and DeleteItem it 'passes options to underlying GetItem call' do - expect(Dynamoid.adapter.client).to receive(:get_item).with(hash_including(consistent_read: true)).and_call_original - expect(Dynamoid.adapter.get_item(test_table1, '1', consistent_read: true)).to be_nil + expect { + expect(Dynamoid.adapter.get_item(test_table1, '1', consistent_read: true)).to be_nil + }.to send_request_matching(:GetItem, { ConsistentRead: true }) end it 'performs GetItem for an item that does not exist' do @@ -779,85 +797,86 @@ def dynamo_request(table_name, conditions = [], options = {}) expect(Dynamoid.adapter.get_item(test_table1, '1')).to eq(id: '1', name: 'Josh') end - # BatchDeleteItem - it 'performs BatchDeleteItem with singular keys' do - Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') - Dynamoid.adapter.put_item(test_table2, id: '1', name: 'Justin') - - Dynamoid.adapter.batch_delete_item(test_table1 => ['1'], test_table2 => ['1']) + describe '#batch_delete_item' do + it 'performs BatchDeleteItem with singular keys' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table2, id: '1', name: 'Justin') - results = Dynamoid.adapter.batch_get_item(test_table1 => '1', test_table2 => '1') - expect(results.size).to eq 2 + Dynamoid.adapter.batch_delete_item(test_table1 => ['1'], test_table2 => ['1']) - expect(results[test_table1]).to be_blank - expect(results[test_table2]).to be_blank - end + results = Dynamoid.adapter.batch_get_item(test_table1 => '1', test_table2 => '1') + expect(results.size).to eq 2 - it 'performs BatchDeleteItem with multiple keys' do - Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') - Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Justin') + expect(results[test_table1]).to be_blank + expect(results[test_table2]).to be_blank + end - Dynamoid.adapter.batch_delete_item(test_table1 => %w[1 2]) + it 'performs BatchDeleteItem with multiple keys' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Justin') - results = Dynamoid.adapter.batch_get_item(test_table1 => %w[1 2]) + Dynamoid.adapter.batch_delete_item(test_table1 => %w[1 2]) - expect(results.size).to eq 1 - expect(results[test_table1]).to be_blank - end + results = Dynamoid.adapter.batch_get_item(test_table1 => %w[1 2]) - it 'performs BatchDeleteItem with one ranged key' do - Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Josh', range: 1.0) - Dynamoid.adapter.put_item(test_table3, id: '2', name: 'Justin', range: 2.0) + expect(results.size).to eq 1 + expect(results[test_table1]).to be_blank + end - Dynamoid.adapter.batch_delete_item(test_table3 => [['1', 1.0]]) - results = Dynamoid.adapter.batch_get_item(test_table3 => [['1', 1.0]]) + it 'performs BatchDeleteItem with one ranged key' do + Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Josh', range: 1.0) + Dynamoid.adapter.put_item(test_table3, id: '2', name: 'Justin', range: 2.0) - expect(results.size).to eq 1 - expect(results[test_table3]).to be_blank - end + Dynamoid.adapter.batch_delete_item(test_table3 => [['1', 1.0]]) + results = Dynamoid.adapter.batch_get_item(test_table3 => [['1', 1.0]]) - it 'performs BatchDeleteItem with multiple ranged keys' do - Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Josh', range: 1.0) - Dynamoid.adapter.put_item(test_table3, id: '2', name: 'Justin', range: 2.0) + expect(results.size).to eq 1 + expect(results[test_table3]).to be_blank + end - Dynamoid.adapter.batch_delete_item(test_table3 => [['1', 1.0], ['2', 2.0]]) - results = Dynamoid.adapter.batch_get_item(test_table3 => [['1', 1.0], ['2', 2.0]]) + it 'performs BatchDeleteItem with multiple ranged keys' do + Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Josh', range: 1.0) + Dynamoid.adapter.put_item(test_table3, id: '2', name: 'Justin', range: 2.0) - expect(results.size).to eq 1 - expect(results[test_table3]).to be_blank - end + Dynamoid.adapter.batch_delete_item(test_table3 => [['1', 1.0], ['2', 2.0]]) + results = Dynamoid.adapter.batch_get_item(test_table3 => [['1', 1.0], ['2', 2.0]]) - it 'performs BatchDeleteItem with more than 25 items' do - (25 + 1).times do |i| - Dynamoid.adapter.put_item(test_table1, id: i.to_s) + expect(results.size).to eq 1 + expect(results[test_table3]).to be_blank end - expect(Dynamoid.adapter.client).to receive(:batch_write_item) - .twice.and_call_original - Dynamoid.adapter.batch_delete_item(test_table1 => (0..25).map(&:to_s)) + it 'performs BatchDeleteItem with more than 25 items' do + (25 + 1).times do |i| + Dynamoid.adapter.put_item(test_table1, id: i.to_s) + end - results = Dynamoid.adapter.scan(test_table1).flat_map { |i| i } - expect(results.to_a.size).to eq 0 - end + expect { + Dynamoid.adapter.batch_delete_item(test_table1 => (0..25).map(&:to_s)) + }.to send_request_matching(:BatchWriteItem).twice - it 'performs BatchDeleteItem with more than 25 items and different tables' do - 13.times do |i| - Dynamoid.adapter.put_item(test_table1, id: i.to_s) - Dynamoid.adapter.put_item(test_table2, id: i.to_s) + results = Dynamoid.adapter.scan(test_table1).flat_map { |i| i } + expect(results.to_a.size).to eq 0 end - expect(Dynamoid.adapter.client).to receive(:batch_write_item) - .twice.and_call_original - Dynamoid.adapter.batch_delete_item( - test_table1 => (0..12).map(&:to_s), - test_table2 => (0..12).map(&:to_s) - ) + it 'performs BatchDeleteItem with more than 25 items and different tables' do + 13.times do |i| + Dynamoid.adapter.put_item(test_table1, id: i.to_s) + Dynamoid.adapter.put_item(test_table2, id: i.to_s) + end - results = Dynamoid.adapter.scan(test_table1).flat_map { |i| i } - expect(results.to_a.size).to eq 0 + expect { + Dynamoid.adapter.batch_delete_item( + test_table1 => (0..12).map(&:to_s), + test_table2 => (0..12).map(&:to_s) + ) + }.to send_request_matching(:BatchWriteItem).twice - results = Dynamoid.adapter.scan(test_table2).flat_map { |i| i } - expect(results.to_a.size).to eq 0 + results = Dynamoid.adapter.scan(test_table1).flat_map { |i| i } + expect(results.to_a.size).to eq 0 + + results = Dynamoid.adapter.scan(test_table2).flat_map { |i| i } + expect(results.to_a.size).to eq 0 + end end describe '#batch_write_item' do @@ -882,10 +901,9 @@ def dynamo_request(table_name, conditions = [], options = {}) it 'performs BatchDeleteItem with more than 25 items' do items = (1..26).map { |i| { id: i.to_s } } - expect(Dynamoid.adapter.client).to receive(:batch_write_item) - .twice.and_call_original - - Dynamoid.adapter.batch_write_item(test_table1, items) + expect { + Dynamoid.adapter.batch_write_item(test_table1, items) + }.to send_request_matching(:BatchWriteItem).twice end it 'writes unprocessed items' do @@ -925,7 +943,7 @@ def dynamo_request(table_name, conditions = [], options = {}) ) end - context 'optional block passed' do + context 'when optional block passed' do it 'passes as block arguments flag if there are unprocessed items for each batch' do # dynamodb-local ignores provisioned throughput settings # so we cannot emulate unprocessed items - let's stub @@ -952,197 +970,204 @@ def dynamo_request(table_name, conditions = [], options = {}) end end - # ListTables - it 'performs ListTables' do - # Force creation of the tables - test_table1; test_table2; test_table3; test_table4 + describe '#list_tables' do + it 'performs ListTables' do + # Force creation of the tables + test_table1 + test_table2 + test_table3 + test_table4 - expect(Dynamoid.adapter.list_tables).to include test_table1 - expect(Dynamoid.adapter.list_tables).to include test_table2 - end + expect(Dynamoid.adapter.list_tables).to include test_table1 + expect(Dynamoid.adapter.list_tables).to include test_table2 + end - context 'when calling ListTables with more than 200 tables' do - let!(:count_before) { Dynamoid.adapter.list_tables.size } + context 'when calling ListTables with more than 200 tables' do + let!(:count_before) { Dynamoid.adapter.list_tables.size } - before do - 201.times do |n| - Dynamoid.adapter.create_table("dynamoid_tests_ALotOfTables#{n}", [:id]) + before do + 201.times do |n| + Dynamoid.adapter.create_table("dynamoid_tests_ALotOfTables#{n}", [:id]) + end end - end - after do - 201.times do |n| - Dynamoid.adapter.delete_table("dynamoid_tests_ALotOfTables#{n}") + after do + 201.times do |n| + Dynamoid.adapter.delete_table("dynamoid_tests_ALotOfTables#{n}") + end end - end - it 'automatically pages through all results' do - expect(Dynamoid.adapter.list_tables).to include 'dynamoid_tests_ALotOfTables44' - expect(Dynamoid.adapter.list_tables).to include 'dynamoid_tests_ALotOfTables200' - expect(Dynamoid.adapter.list_tables.size).to eq 201 + count_before + it 'automatically pages through all results' do + expect(Dynamoid.adapter.list_tables).to include 'dynamoid_tests_ALotOfTables44' + expect(Dynamoid.adapter.list_tables).to include 'dynamoid_tests_ALotOfTables200' + expect(Dynamoid.adapter.list_tables.size).to eq 201 + count_before + end end end - # Query - it 'performs query on a table and returns items' do - Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + describe '#query' do + it 'performs query on a table and returns items' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') - expect(Dynamoid.adapter.query(test_table1, { id: [[:eq, '1']] }).first).to eq([[{ id: '1', name: 'Josh' }], { last_evaluated_key: nil }]) - end + expect(Dynamoid.adapter.query(test_table1, { id: [[:eq, '1']] }).first).to eq([[{ id: '1', name: 'Josh' }], { last_evaluated_key: nil }]) + end - it 'performs query on a table and returns items if there are multiple items' do - Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') - Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Justin') + it 'performs query on a table and returns items if there are multiple items' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Justin') - expect(Dynamoid.adapter.query(test_table1, { id: [[:eq, '1']] }).first).to eq([[{ id: '1', name: 'Josh' }], { last_evaluated_key: nil }]) - end + expect(Dynamoid.adapter.query(test_table1, { id: [[:eq, '1']] }).first).to eq([[{ id: '1', name: 'Josh' }], { last_evaluated_key: nil }]) + end - context 'backoff is specified' do - before do - @old_backoff = Dynamoid.config.backoff - @old_backoff_strategies = Dynamoid.config.backoff_strategies.dup + context 'when backoff is specified' do + before do + @old_backoff = Dynamoid.config.backoff + @old_backoff_strategies = Dynamoid.config.backoff_strategies.dup - @counter = 0 - Dynamoid.config.backoff_strategies[:simple] = ->(_) { -> { @counter += 1 } } - Dynamoid.config.backoff = { simple: nil } - end + @counter = 0 + Dynamoid.config.backoff_strategies[:simple] = ->(_) { -> { @counter += 1 } } + Dynamoid.config.backoff = { simple: nil } + end - after do - Dynamoid.config.backoff = @old_backoff - Dynamoid.config.backoff_strategies = @old_backoff_strategies - end + after do + Dynamoid.config.backoff = @old_backoff + Dynamoid.config.backoff_strategies = @old_backoff_strategies + end - it 'uses specified backoff' do - Dynamoid.adapter.put_item(test_table3, id: '1', range: 1) - Dynamoid.adapter.put_item(test_table3, id: '1', range: 2) + it 'uses specified backoff' do + Dynamoid.adapter.put_item(test_table3, id: '1', range: 1) + Dynamoid.adapter.put_item(test_table3, id: '1', range: 2) - expect(Dynamoid.adapter.query(test_table3, { id: [[:eq, '1']] }, [], { batch_size: 1 }).flat_map { |i| i }.count).to eq 2 - expect(@counter).to eq 2 + expect(Dynamoid.adapter.query(test_table3, { id: [[:eq, '1']] }, [], { batch_size: 1 }).flat_map { |i| i }.count).to eq 2 + expect(@counter).to eq 2 + end end - end - it_behaves_like 'range queries' + it_behaves_like 'range queries' - describe 'query' do - it_behaves_like 'correctly handling limits', :query + describe 'query' do + it_behaves_like 'correctly handling limits', :Query + end end - # Scan - it 'performs scan on a table and returns items' do - Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + describe '#scan' do + it 'performs scan on a table and returns items' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') - expect(Dynamoid.adapter.scan(test_table1, [{ name: { eq: 'Josh' } }]).to_a).to eq [[[{ id: '1', name: 'Josh' }], { last_evaluated_key: nil }]] - end + expect(Dynamoid.adapter.scan(test_table1, [{ name: { eq: 'Josh' } }]).to_a).to eq [[[{ id: '1', name: 'Josh' }], { last_evaluated_key: nil }]] + end - it 'performs scan on a table and returns items if there are multiple items but only one match' do - Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') - Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Justin') + it 'performs scan on a table and returns items if there are multiple items but only one match' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Justin') - expect(Dynamoid.adapter.scan(test_table1, [{ name: { eq: 'Josh' } }]).to_a).to eq [[[{ id: '1', name: 'Josh' }], { last_evaluated_key: nil }]] - end + expect(Dynamoid.adapter.scan(test_table1, [{ name: { eq: 'Josh' } }]).to_a).to eq [[[{ id: '1', name: 'Josh' }], { last_evaluated_key: nil }]] + end - it 'performs scan on a table and returns multiple items if there are multiple matches' do - Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') - Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Josh') + it 'performs scan on a table and returns multiple items if there are multiple matches' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Josh') - expect( - Dynamoid.adapter.scan(test_table1, [{ name: { eq: 'Josh' } }]).to_a - ).to match( - [ + expect( + Dynamoid.adapter.scan(test_table1, [{ name: { eq: 'Josh' } }]).to_a + ).to match( [ - contain_exactly({ name: 'Josh', id: '2' }, { name: 'Josh', id: '1' }), - { last_evaluated_key: nil } + [ + contain_exactly({ name: 'Josh', id: '2' }, { name: 'Josh', id: '1' }), + { last_evaluated_key: nil } + ] ] - ] - ) - end - - it 'performs scan on a table and returns all items if no criteria are specified' do - Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') - Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Josh') - - expect(Dynamoid.adapter.scan(test_table1, []).flat_map { |i| i }).to include({ name: 'Josh', id: '2' }, name: 'Josh', id: '1') - end - - it 'performs scan on a table and returns correct limit' do - Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') - Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Josh') - Dynamoid.adapter.put_item(test_table1, id: '3', name: 'Josh') - Dynamoid.adapter.put_item(test_table1, id: '4', name: 'Josh') - - expect(Dynamoid.adapter.scan(test_table1, [], record_limit: 1).flat_map { |i| i }.count).to eq(1) - end - - it 'performs scan on a table and returns correct batch' do - Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') - Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Josh') - Dynamoid.adapter.put_item(test_table1, id: '3', name: 'Josh') - Dynamoid.adapter.put_item(test_table1, id: '4', name: 'Josh') - - expect(Dynamoid.adapter.scan(test_table1, [], batch_size: 1).flat_map { |i| i }.count).to eq(4) - end + ) + end - it 'performs scan on a table and returns correct limit and batch' do - Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') - Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Josh') - Dynamoid.adapter.put_item(test_table1, id: '3', name: 'Josh') - Dynamoid.adapter.put_item(test_table1, id: '4', name: 'Josh') + it 'performs scan on a table and returns all items if no criteria are specified' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Josh') - expect(Dynamoid.adapter.scan(test_table1, [], record_limit: 1, batch_size: 1).flat_map { |i| i }.count).to eq(1) - end + expect(Dynamoid.adapter.scan(test_table1, []).flat_map { |i| i }).to include({ name: 'Josh', id: '2' }, name: 'Josh', id: '1') + end - context 'backoff is specified' do - before do - @old_backoff = Dynamoid.config.backoff - @old_backoff_strategies = Dynamoid.config.backoff_strategies.dup + it 'performs scan on a table and returns correct limit' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '3', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '4', name: 'Josh') - @counter = 0 - Dynamoid.config.backoff_strategies[:simple] = ->(_) { -> { @counter += 1 } } - Dynamoid.config.backoff = { simple: nil } + expect(Dynamoid.adapter.scan(test_table1, [], record_limit: 1).flat_map { |i| i }.count).to eq(1) end - after do - Dynamoid.config.backoff = @old_backoff - Dynamoid.config.backoff_strategies = @old_backoff_strategies + it 'performs scan on a table and returns correct batch' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '3', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '4', name: 'Josh') + + expect(Dynamoid.adapter.scan(test_table1, [], batch_size: 1).flat_map { |i| i }.count).to eq(4) end - it 'uses specified backoff' do + it 'performs scan on a table and returns correct limit and batch' do Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Josh') Dynamoid.adapter.put_item(test_table1, id: '3', name: 'Josh') Dynamoid.adapter.put_item(test_table1, id: '4', name: 'Josh') - expect(Dynamoid.adapter.scan(test_table1, [], batch_size: 1).flat_map { |i| i }.count).to eq 4 - expect(@counter).to eq 4 + expect(Dynamoid.adapter.scan(test_table1, [], record_limit: 1, batch_size: 1).flat_map { |i| i }.count).to eq(1) end - end - describe 'scans' do - it_behaves_like 'correctly handling limits', :scan - end + context 'when backoff is specified' do + before do + @old_backoff = Dynamoid.config.backoff + @old_backoff_strategies = Dynamoid.config.backoff_strategies.dup - # Truncate - it 'performs truncate on an existing table' do - Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') - Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Pascal') + @counter = 0 + Dynamoid.config.backoff_strategies[:simple] = ->(_) { -> { @counter += 1 } } + Dynamoid.config.backoff = { simple: nil } + end - expect(Dynamoid.adapter.get_item(test_table1, '1')).to eq(name: 'Josh', id: '1') - expect(Dynamoid.adapter.get_item(test_table1, '2')).to eq(name: 'Pascal', id: '2') + after do + Dynamoid.config.backoff = @old_backoff + Dynamoid.config.backoff_strategies = @old_backoff_strategies + end - Dynamoid.adapter.truncate(test_table1) + it 'uses specified backoff' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '3', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '4', name: 'Josh') - expect(Dynamoid.adapter.get_item(test_table1, '1')).to be_nil - expect(Dynamoid.adapter.get_item(test_table1, '2')).to be_nil + expect(Dynamoid.adapter.scan(test_table1, [], batch_size: 1).flat_map { |i| i }.count).to eq 4 + expect(@counter).to eq 4 + end + end + + describe 'scans' do + it_behaves_like 'correctly handling limits', :Scan + end end - it 'performs truncate on an existing table with a range key' do - Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Josh', range: 1.0) - Dynamoid.adapter.put_item(test_table3, id: '2', name: 'Justin', range: 2.0) + describe '#truncate' do + it 'performs truncate on an existing table' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Pascal') + + expect(Dynamoid.adapter.get_item(test_table1, '1')).to eq(name: 'Josh', id: '1') + expect(Dynamoid.adapter.get_item(test_table1, '2')).to eq(name: 'Pascal', id: '2') + + Dynamoid.adapter.truncate(test_table1) + + expect(Dynamoid.adapter.get_item(test_table1, '1')).to be_nil + expect(Dynamoid.adapter.get_item(test_table1, '2')).to be_nil + end + + it 'performs truncate on an existing table with a range key' do + Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Josh', range: 1.0) + Dynamoid.adapter.put_item(test_table3, id: '2', name: 'Justin', range: 2.0) - Dynamoid.adapter.truncate(test_table3) + Dynamoid.adapter.truncate(test_table3) - expect(Dynamoid.adapter.get_item(test_table3, '1', range_key: 1.0)).to be_nil - expect(Dynamoid.adapter.get_item(test_table3, '2', range_key: 2.0)).to be_nil + expect(Dynamoid.adapter.get_item(test_table3, '1', range_key: 1.0)).to be_nil + expect(Dynamoid.adapter.get_item(test_table3, '2', range_key: 2.0)).to be_nil + end end it_behaves_like 'correct ordering' @@ -1150,7 +1175,6 @@ def dynamo_request(table_name, conditions = [], options = {}) # DescribeTable - # UpdateItem describe '#update_item' do it 'updates an existing item' do Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') @@ -1172,7 +1196,7 @@ def dynamo_request(table_name, conditions = [], options = {}) expect(Dynamoid.adapter.get_item(test_table1, '1')).to eq(name: 'Justin', id: '1') end - context 'for attribute values' do + context 'when for attribute values' do it 'adds attribute values' do Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') @@ -1223,7 +1247,7 @@ def dynamo_request(table_name, conditions = [], options = {}) end end - context 'updates item conditionally' do + context 'with updates item conditionally' do it 'raises Exception if condition fails' do Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh', age: 17) @@ -1250,8 +1274,6 @@ def dynamo_request(table_name, conditions = [], options = {}) end end - # UpdateTable - describe 'update_time_to_live' do let(:table_name) { "#{Dynamoid::Config.namespace}_table_with_expiration" } @@ -1264,16 +1286,15 @@ def dynamo_request(table_name, conditions = [], options = {}) end it 'calls UpdateTimeToLive' do - allow(Dynamoid.adapter.client).to receive(:update_time_to_live).and_call_original - Dynamoid.adapter.update_time_to_live(table_name, :ttl) - expect(Dynamoid.adapter.client).to have_received(:update_time_to_live) - .with( - table_name: table_name, - time_to_live_specification: { - attribute_name: :ttl, - enabled: true, - } - ) + expect { + Dynamoid.adapter.update_time_to_live(table_name, :ttl) + }.to send_request_matching(:UpdateTimeToLive, { + TableName: table_name, + TimeToLiveSpecification: { + 'AttributeName' => 'ttl', + 'Enabled' => true, + } + }) end it 'updates a table schema' do @@ -1339,17 +1360,13 @@ def dynamo_request(table_name, conditions = [], options = {}) end it 'accepts :consistent_read option' do - expect(Dynamoid.adapter.client).to receive(:execute_statement) - .with(including(consistent_read: true)) - .and_call_original - - Dynamoid.adapter.execute("SELECT * FROM #{test_table1} WHERE id = '1'", [], consistent_read: true) - - expect(Dynamoid.adapter.client).to receive(:execute_statement) - .with(including(consistent_read: false)) - .and_call_original + expect { + Dynamoid.adapter.execute("SELECT * FROM #{test_table1} WHERE id = '1'", [], consistent_read: true) + }.to send_request_matching(:ExecuteStatement, { ConsistentRead: true }) - Dynamoid.adapter.execute("SELECT * FROM #{test_table1} WHERE id = '1'", [], consistent_read: false) + expect { + Dynamoid.adapter.execute("SELECT * FROM #{test_table1} WHERE id = '1'", [], consistent_read: false) + }.to send_request_matching(:ExecuteStatement, { ConsistentRead: false }) end it 'loads lazily all the pages of a paginated result' do @@ -1375,7 +1392,6 @@ def dynamo_request(table_name, conditions = [], options = {}) end end - # connection_config describe '#connectin_config' do subject { described_class.new.connection_config } diff --git a/spec/dynamoid/adapter_spec.rb b/spec/dynamoid/adapter_spec.rb index 263959c1..6b0493cb 100644 --- a/spec/dynamoid/adapter_spec.rb +++ b/spec/dynamoid/adapter_spec.rb @@ -8,8 +8,6 @@ def test_table 'dynamoid_tests_TestTable' end - let(:single_id) { '123' } - let(:many_ids) { %w[1 2] } { 1 => [:id], @@ -24,19 +22,26 @@ def test_table end end + let(:adapter_plugin) { subject.adapter } + describe 'connection management' do + let(:adapter_plugin) { instance_double(described_class.adapter_plugin_class) } + it 'does not auto-establish a connection' do - expect_any_instance_of(described_class.adapter_plugin_class).not_to receive(:connect!) + allow(described_class.adapter_plugin_class).to receive(:new).and_return(adapter_plugin) + expect(adapter_plugin).not_to receive(:connect!) subject end it 'establishes a connection when adapter is requested' do - expect_any_instance_of(described_class.adapter_plugin_class).to receive(:connect!).and_call_original + allow(described_class.adapter_plugin_class).to receive(:new).and_return(adapter_plugin) + expect(adapter_plugin).to receive(:connect!) subject.adapter end it 'reuses a connection' do - expect_any_instance_of(described_class.adapter_plugin_class).to receive(:connect!).once.and_call_original + allow(described_class.adapter_plugin_class).to receive(:new).and_return(adapter_plugin) + expect(adapter_plugin).to receive(:connect!).once subject.adapter subject.adapter end @@ -44,26 +49,31 @@ def test_table describe 'caching tables' do it 'caches list of tables' do - expect(subject).to receive(:list_tables).once.and_call_original - subject.create_table('test_table', 'key') - subject.tables - subject.tables + expect { + subject.create_table('test_table', 'key') + subject.tables + subject.tables + }.to send_request_matching(:ListTables).once end it 'maintains table cache when creating a table' do # cache subject.tables - expect(subject).not_to receive(:list_tables) subject.create_table('test_table', 'key') - expect(subject.tables).to include('test_table') + + expect { + expect(subject.tables).to include('test_table') + }.not_to send_request_matching(:ListTables) end it 'clears cached list via #clear_cache!' do subject.create_table('test_table', 'key') subject.clear_cache! - expect(subject).to receive(:list_tables).and_call_original - subject.tables + + expect { + subject.tables + }.to send_request_matching(:ListTables) end end @@ -72,25 +82,25 @@ def test_table end it 'writes through the adapter' do - expect(subject).to receive(:put_item).with(test_table, { id: single_id }, nil).and_return(true) - subject.write(test_table, id: single_id) + expect(adapter_plugin).to receive(:put_item).with(test_table, { id: '123' }, nil).and_return(true) + subject.write(test_table, id: '123') end describe '#read' do it 'reads through the adapter for one ID' do - expect(subject).to receive(:get_item).with(test_table, single_id, {}).and_return(true) - subject.read(test_table, single_id) + expect(adapter_plugin).to receive(:get_item).with(test_table, '123', {}).and_return(true) + subject.read(test_table, '123') end it 'reads through the adapter for many IDs' do - expect(subject).to receive(:batch_get_item).with({ test_table => many_ids }, {}).and_return(true) - subject.read(test_table, many_ids) + expect(adapter_plugin).to receive(:batch_get_item).with({ test_table => %w[1 2] }, {}).and_return(true) + subject.read(test_table, %w[1 2]) end it 'reads through the adapter for one ID and a range key' do - allow(subject).to receive(:get_item).and_return(true) - subject.read(test_table, single_id, range_key: 'boot') - expect(subject).to have_received(:get_item).with(test_table, single_id, { range_key: 'boot' }) + allow(adapter_plugin).to receive(:get_item).and_return(true) + subject.read(test_table, '123', range_key: 'boot') + expect(adapter_plugin).to have_received(:get_item).with(test_table, '123', { range_key: 'boot' }) end end @@ -102,10 +112,9 @@ def test_table end it 'does not try to create table if it is already in cache' do - expect(Dynamoid.adapter.client).to receive(:create_table).once - .and_call_original - - 3.times { Dynamoid.adapter.create_table(table_name, :id, sync: true) } + expect { + 3.times { Dynamoid.adapter.create_table(table_name, :id, sync: true) } + }.to send_request_matching(:CreateTable).once end it 'returns true if table created' do @@ -169,13 +178,13 @@ def test_table Dynamoid.adapter.put_item(test_table3, id: '2', range: 1.0) Dynamoid.adapter.put_item(test_table3, id: '2', range: 2.0) - expect(subject).to receive(:batch_delete_item).and_call_original - - expect do - subject.delete(test_table3, %w[1 2], range_key: 1.0) - end.to change { - Dynamoid.adapter.scan(test_table3).flat_map { |i| i }.to_a.size - }.from(4).to(2) + expect { + expect { + subject.delete(test_table3, %w[1 2], range_key: 1.0) + }.to change { + Dynamoid.adapter.scan(test_table3).flat_map { |i| i }.to_a.size + }.from(4).to(2) + }.to send_request_matching(:BatchWriteItem) expect(Dynamoid.adapter.get_item(test_table3, '1', range_key: 1.0)).to be_nil expect(Dynamoid.adapter.get_item(test_table3, '2', range_key: 1.0)).to be_nil diff --git a/spec/dynamoid/associations/belongs_to_spec.rb b/spec/dynamoid/associations/belongs_to_spec.rb index 538462a6..d2ca272f 100644 --- a/spec/dynamoid/associations/belongs_to_spec.rb +++ b/spec/dynamoid/associations/belongs_to_spec.rb @@ -6,7 +6,7 @@ require 'spec_helper' describe Dynamoid::Associations::BelongsTo do - context 'has many' do + context 'when has many' do let(:subscription) { Subscription.create } let(:camel_case) { CamelCase.create } let(:magazine) { subscription.magazine.create } @@ -30,7 +30,7 @@ expect(user.books).to include magazine end - context 'proxied behavior' do + context 'with proxied behavior' do let(:proxy) do expect(magazine.subscriptions).to include(subscription) subscription.magazine @@ -50,7 +50,7 @@ end end - context 'has one' do + context 'when has one' do let(:subscription) { Subscription.create } let(:sponsor) { Sponsor.create } let(:magazine) { sponsor.magazine.create } @@ -78,7 +78,7 @@ end describe 'assigning' do - context 'has many' do + context 'when has many' do let(:subscription) { Subscription.create } it 'associates model on this side' do @@ -140,7 +140,7 @@ end end - context 'has one' do + context 'when has one' do let(:sponsor) { Sponsor.create } it 'associates model on this side' do @@ -167,7 +167,7 @@ end.to change { sponsor.magazine.target }.from(magazine_old).to(magazine_new) end - it 're-associates model on this side' do + it 're-associates model on that side' do magazine_old = Magazine.create magazine_new = Magazine.create sponsor.magazine = magazine_old @@ -203,7 +203,7 @@ end end - context 'set to nil' do + context 'when set to nil' do it 'can be set to nil' do subscription = Subscription.new @@ -258,7 +258,7 @@ end.to change { Subscription.find(subscription.id).magazine.target }.from(magazine).to(nil) end - context 'has many' do + context 'when has many' do let(:subscription) { Subscription.create } let!(:magazine) { subscription.magazine.create } @@ -275,7 +275,7 @@ end end - context 'has one' do + context 'when has one' do let(:sponsor) { Sponsor.create } let!(:magazine) { sponsor.magazine.create } diff --git a/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb b/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb index aef0fd89..05014699 100644 --- a/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb +++ b/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb @@ -78,7 +78,7 @@ end.to change { Subscription.find(subscription.id).users.target }.from([user]).to([]) end - context 'has and belongs to many' do + context 'when has and belongs to many' do let(:subscription) { Subscription.create } let!(:user) { subscription.users.create } diff --git a/spec/dynamoid/associations/has_many_spec.rb b/spec/dynamoid/associations/has_many_spec.rb index 6004b9dd..3c48d6de 100644 --- a/spec/dynamoid/associations/has_many_spec.rb +++ b/spec/dynamoid/associations/has_many_spec.rb @@ -129,7 +129,7 @@ end.to change { Magazine.find(magazine.title).subscriptions.target }.from([subscription]).to([]) end - context 'belongs to' do + context 'with belongs to' do let(:magazine) { Magazine.create } let!(:subscription) { magazine.subscriptions.create } diff --git a/spec/dynamoid/associations/has_one_spec.rb b/spec/dynamoid/associations/has_one_spec.rb index ff11771a..9d6cfc7b 100644 --- a/spec/dynamoid/associations/has_one_spec.rb +++ b/spec/dynamoid/associations/has_one_spec.rb @@ -37,12 +37,6 @@ expect(magazine.sponsor).to eq sponsor end - it 'is equal from its target record' do - sponsor = magazine.sponsor.create - - expect(magazine.sponsor).to eq sponsor - end - it 'associates belongs_to automatically' do sponsor = magazine.sponsor.create expect(sponsor.magazine).to eq magazine @@ -53,7 +47,7 @@ end describe 'assigning' do - context 'belongs to' do + context 'with belongs to' do let(:magazine) { Magazine.create } it 'associates model on this side' do @@ -116,7 +110,7 @@ end end - context 'set to nil' do + context 'when set to nil' do it 'can be set to nil' do magazine = Magazine.create! @@ -171,7 +165,7 @@ end.to change { Magazine.find(magazine.title).sponsor.target }.from(sponsor).to(nil) end - context 'belongs to' do + context 'with belongs to' do let(:magazine) { Magazine.create } let!(:sponsor) { magazine.sponsor.create } diff --git a/spec/dynamoid/before_type_cast_spec.rb b/spec/dynamoid/before_type_cast_spec.rb deleted file mode 100644 index f164e2d3..00000000 --- a/spec/dynamoid/before_type_cast_spec.rb +++ /dev/null @@ -1,114 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Before type cast' do - describe '#attributes_before_type_cast', config: { timestamps: false } do - let(:klass) do - new_class do - field :admin, :boolean - end - end - - it 'returns original attributes value' do - obj = klass.new(admin: 0) - - expect(obj.attributes_before_type_cast).to eql( - admin: 0, - ) - end - - it 'returns values for all the attributes even not assigned' do - klass_with_many_fields = new_class do - field :first_name - field :last_name - field :email - end - obj = klass_with_many_fields.new(first_name: 'John') - - expect(obj.attributes_before_type_cast).to eql( - first_name: 'John', - ) - end - - it 'returns original default value if field has default value' do - klass_with_default_value = new_class do - field :activated_on, :date, default: '2018-09-27' - end - obj = klass_with_default_value.new - - expect(obj.attributes_before_type_cast).to eql( - activated_on: '2018-09-27', - ) - end - - it 'returns nil if field does not have default value' do - obj = klass.new - - expect(obj.attributes_before_type_cast).to eql({}) - end - - it 'returns values loaded from the storage before type casting' do - obj = klass.create!(admin: false) - obj2 = klass.find(obj.id) - - expect(obj2.attributes_before_type_cast).to eql( - id: obj.id, - admin: false, - ) - end - end - - describe '#read_attribute_before_type_cast' do - let(:klass) do - new_class do - field :admin, :boolean - end - end - - it 'returns attribute original value' do - obj = klass.new(admin: 1) - - expect(obj.read_attribute_before_type_cast(:admin)).to eql(1) - end - - it 'accepts string as well as symbol argument' do - obj = klass.new(admin: 1) - - expect(obj.read_attribute_before_type_cast('admin')).to eql(1) - end - - it 'returns nil if there is no such attribute' do - obj = klass.new - - expect(obj.read_attribute_before_type_cast(:first_name)).to eql(nil) - end - end - - describe '#_before_type_cast' do - let(:klass) do - new_class do - field :first_name - field :last_name - field :admin, :boolean - end - end - - it 'exists for every model attribute' do - obj = klass.new - - expect(obj).to respond_to(:id) - expect(obj).to respond_to(:first_name_before_type_cast) - expect(obj).to respond_to(:last_name_before_type_cast) - expect(obj).to respond_to(:admin) - expect(obj).to respond_to(:created_at) - expect(obj).to respond_to(:updated_at) - end - - it 'returns attribute original value' do - obj = klass.new(admin: 0) - - expect(obj.admin_before_type_cast).to eql(0) - end - end -end diff --git a/spec/dynamoid/criteria/chain_spec.rb b/spec/dynamoid/criteria/chain_spec.rb index f405afef..030041f4 100644 --- a/spec/dynamoid/criteria/chain_spec.rb +++ b/spec/dynamoid/criteria/chain_spec.rb @@ -52,7 +52,9 @@ end describe 'Limits' do - shared_examples 'correct handling chain limits' do |request_type| + # The operation parameter is a name used in the request header X-AMZ-TARGET + # and is a DynamoDB operation name, e.g. Scan, Query, BatchWriteItems... + shared_examples 'correct handling chain limits' do |operation| let(:model) do new_class do range :age, :integer @@ -61,7 +63,7 @@ end before do - @request_type = request_type + @operation = operation (1..10).each do |i| model.create(id: '1', name: 'Josh', age: i) model.create(id: '1', name: 'Pascal', age: i + 100) @@ -69,7 +71,7 @@ end def request_params - return { id: '1' } if @request_type == :query + return { id: '1' } if @operation == :Query {} end @@ -93,30 +95,32 @@ def request_params # Scanning through 13 means it'll see 10 Josh objects and then # 3 Pascal objects but it'll hit record_limit first with 2 objects # so we'd only see 12 requests due to batching. - expect(Dynamoid.adapter.client).to receive(request_type).exactly(12).times.and_call_original - expect(model.where(request_params.merge(name: 'Pascal')) - .record_limit(2) - .scan_limit(13) - .batch(1).count).to eq(2) + expect { + expect(model.where(request_params.merge(name: 'Pascal')) + .record_limit(2) + .scan_limit(13) + .batch(1).count).to eq(2) + }.to send_request_matching(operation).exactly(12).times end it 'supports combined limits with batch size other than 1' do # Querying in batches of 3 so we'd see: # 3 Josh, 3 Josh, 3 Josh, 1 Josh + 2 Pascal, 3 Pascal, 3 Pascal, 2 Pascal # So total of 7 requests - expect(Dynamoid.adapter.client).to receive(request_type).exactly(7).times.and_call_original - expect(model.where(request_params.merge(name: 'Pascal')) - .record_limit(10) - .batch(3).count).to eq(10) + expect { + expect(model.where(request_params.merge(name: 'Pascal')) + .record_limit(10) + .batch(3).count).to eq(10) + }.to send_request_matching(operation).exactly(7).times end end describe 'Query' do - it_behaves_like 'correct handling chain limits', :query + it_behaves_like 'correct handling chain limits', :Query end describe 'Scan' do - it_behaves_like 'correct handling chain limits', :scan + it_behaves_like 'correct handling chain limits', :Scan end end @@ -132,8 +136,9 @@ def request_params customer2 = model.create(name: 'Bob', age: 30) chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(name: 'Bob', age: 10).all).to contain_exactly(customer1) + expect { + expect(chain.where(name: 'Bob', age: 10).all).to contain_exactly(customer1) + }.to send_request_matching(:Query) expect(chain.key_fields_detector.hash_key).to eq(:name) expect(chain.key_fields_detector.range_key).to eq(:age) expect(chain.key_fields_detector.index_name).to be_nil @@ -150,20 +155,24 @@ def request_params document4 = klass.create(id: '4', config: 42) chain = described_class.new(klass) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(id: '1', config: [1, 2]).all).to contain_exactly(document1) + expect { + expect(chain.where(id: '1', config: [1, 2]).all).to contain_exactly(document1) + }.to send_request_matching(:Query) chain = described_class.new(klass) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(id: '2', config: { a: 1 }).all).to contain_exactly(document2) + expect { + expect(chain.where(id: '2', config: { a: 1 }).all).to contain_exactly(document2) + }.to send_request_matching(:Query) chain = described_class.new(klass) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(id: '3', config: 'foobar').all).to contain_exactly(document3) + expect { + expect(chain.where(id: '3', config: 'foobar').all).to contain_exactly(document3) + }.to send_request_matching(:Query) chain = described_class.new(klass) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(id: '4', config: 42).all).to contain_exactly(document4) + expect { + expect(chain.where(id: '4', config: 42).all).to contain_exactly(document4) + }.to send_request_matching(:Query) end it 'supports lt' do @@ -284,7 +293,7 @@ def request_params end.to raise_error(Dynamoid::Errors::Error, 'Unsupported operator foo in age.foo') end - context 'primary key dumping' do + context 'with primary key dumping' do it 'uses dumped value of partition key to query item' do klass = new_class(partition_key: { name: :published_on, type: :date }) @@ -292,10 +301,10 @@ def request_params obj2 = klass.create(published_on: Date.today + 2) chain = described_class.new(klass) - expect(chain).to receive(:raw_pages_via_query).and_call_original - - objects_found = chain.where(published_on: obj1.published_on).all - expect(objects_found).to contain_exactly(obj1) + expect { + objects_found = chain.where(published_on: obj1.published_on).all + expect(objects_found).to contain_exactly(obj1) + }.to send_request_matching(:Query) end it 'uses dumped value of sort key to query item' do @@ -307,10 +316,10 @@ def request_params obj2 = klass.create(published_on: Date.today + 2) chain = described_class.new(klass) - expect(chain).to receive(:raw_pages_via_query).and_call_original - - objects_found = chain.where(id: obj1.id, published_on: obj1.published_on).all - expect(objects_found).to contain_exactly(obj1) + expect { + objects_found = chain.where(id: obj1.id, published_on: obj1.published_on).all + expect(objects_found).to contain_exactly(obj1) + }.to send_request_matching(:Query) end end end @@ -330,8 +339,9 @@ def request_params customer2 = model.create(name: 'a', last_name: 'b', age: 30) chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(name: 'a', age: 10).all).to contain_exactly(customer1) + expect { + expect(chain.where(name: 'a', age: 10).all).to contain_exactly(customer1) + }.to send_request_matching(:Query) expect(chain.key_fields_detector.hash_key).to eq(:name) expect(chain.key_fields_detector.range_key).to be_nil expect(chain.key_fields_detector.index_name).to be_nil @@ -347,8 +357,9 @@ def request_params document2 = klass.create(id: '1', last_name: 'b', set: [3, 4].to_set) chain = described_class.new(klass) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(id: '1', set: [1, 2].to_set).all).to contain_exactly(document1) + expect { + expect(chain.where(id: '1', set: [1, 2].to_set).all).to contain_exactly(document1) + }.to send_request_matching(:Query) end it 'supports eq for array' do @@ -361,8 +372,9 @@ def request_params document2 = klass.create(id: '1', last_name: 'b', array: [3, 4]) chain = described_class.new(klass) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(id: '1', array: [1, 2]).all).to contain_exactly(document1) + expect { + expect(chain.where(id: '1', array: [1, 2]).all).to contain_exactly(document1) + }.to send_request_matching(:Query) end it 'supports eq for serialized' do @@ -377,20 +389,24 @@ def request_params document4 = klass.create(id: '4', last_name: 'b', config: 42) chain = described_class.new(klass) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(id: '1', config: [1, 2]).all).to contain_exactly(document1) + expect { + expect(chain.where(id: '1', config: [1, 2]).all).to contain_exactly(document1) + }.to send_request_matching(:Query) chain = described_class.new(klass) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(id: '2', config: { a: 1 }).all).to contain_exactly(document2) + expect { + expect(chain.where(id: '2', config: { a: 1 }).all).to contain_exactly(document2) + }.to send_request_matching(:Query) chain = described_class.new(klass) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(id: '3', config: 'foobar').all).to contain_exactly(document3) + expect { + expect(chain.where(id: '3', config: 'foobar').all).to contain_exactly(document3) + }.to send_request_matching(:Query) chain = described_class.new(klass) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(id: '4', config: 42).all).to contain_exactly(document4) + expect { + expect(chain.where(id: '4', config: 42).all).to contain_exactly(document4) + }.to send_request_matching(:Query) end it 'supports ne' do @@ -597,8 +613,9 @@ def request_params customer2 = model.create(age: 30) chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_scan).and_call_original - expect(chain.where(age: 10).all).to contain_exactly(customer1) + expect { + expect(chain.where(age: 10).all).to contain_exactly(customer1) + }.to send_request_matching(:Scan) expect(chain.key_fields_detector.hash_key).to be_nil expect(chain.key_fields_detector.range_key).to be_nil expect(chain.key_fields_detector.index_name).to be_nil @@ -828,8 +845,9 @@ def request_params it 'does load result lazily' do Vehicle.create - expect(Dynamoid.adapter.client).to receive(:scan).exactly(0).times.and_call_original - Vehicle.record_limit(1).all + expect { + Vehicle.record_limit(1).all + }.not_to send_request_matching(:Scan) end end @@ -837,8 +855,9 @@ def request_params it 'does load result lazily' do Vehicle.create - expect(Dynamoid.adapter.client).to receive(:scan).exactly(0).times.and_call_original - Vehicle.record_limit(1).find_by_pages + expect { + Vehicle.record_limit(1).find_by_pages + }.not_to send_request_matching(:Scan) end end end @@ -864,8 +883,9 @@ def request_params it 'supports query on local secondary index but always defaults to table range key' do chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(name: 'Bob', 'range.lt': 3, 'range2.gt': 15).to_a.size).to eq(1) + expect { + expect(chain.where(name: 'Bob', 'range.lt': 3, 'range2.gt': 15).to_a.size).to eq(1) + }.to send_request_matching(:Query) expect(chain.key_fields_detector.hash_key).to eq(:name) expect(chain.key_fields_detector.range_key).to eq(:range) expect(chain.key_fields_detector.index_name).to be_nil @@ -873,15 +893,17 @@ def request_params it 'supports query on local secondary index' do chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(name: 'Bob', 'range2.gt': 15).to_a.size).to eq(2) + expect { + expect(chain.where(name: 'Bob', 'range2.gt': 15).to_a.size).to eq(2) + }.to send_request_matching(:Query) expect(chain.key_fields_detector.hash_key).to eq(:name) expect(chain.key_fields_detector.range_key).to eq(:range2) expect(chain.key_fields_detector.index_name).to eq(:range2index) chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(name: 'Bob', 'range3.lt': 200).to_a.size).to eq(1) + expect { + expect(chain.where(name: 'Bob', 'range3.lt': 200).to_a.size).to eq(1) + }.to send_request_matching(:Query) expect(chain.key_fields_detector.hash_key).to eq(:name) expect(chain.key_fields_detector.range_key).to eq(:range3) expect(chain.key_fields_detector.index_name).to eq(:range3index) @@ -889,15 +911,17 @@ def request_params it 'supports query on local secondary index with start' do chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(name: 'Bob', 'range2.gt': 15).to_a.size).to eq(2) + expect { + expect(chain.where(name: 'Bob', 'range2.gt': 15).to_a.size).to eq(2) + }.to send_request_matching(:Query) expect(chain.key_fields_detector.hash_key).to eq(:name) expect(chain.key_fields_detector.range_key).to eq(:range2) expect(chain.key_fields_detector.index_name).to eq(:range2index) chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(name: 'Bob', 'range2.gt': 15).start(@customer2).all).to contain_exactly(@customer3) + expect { + expect(chain.where(name: 'Bob', 'range2.gt': 15).start(@customer2).all).to contain_exactly(@customer3) + }.to send_request_matching(:Query) expect(chain.key_fields_detector.hash_key).to eq(:name) expect(chain.key_fields_detector.range_key).to eq(:range2) expect(chain.key_fields_detector.index_name).to eq(:range2index) @@ -920,8 +944,9 @@ def request_params customer2 = model.create(name: 'Jeff', city: 'San Francisco', age: 15, gender: 'male', customerid: 2) chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_scan).and_call_original - expect(chain.where(city: 'San Francisco').to_a.size).to eq(2) + expect { + expect(chain.where(city: 'San Francisco').to_a.size).to eq(2) + }.to send_request_matching(:Scan) # Does not use GSI since not projecting all attributes expect(chain.key_fields_detector.hash_key).to be_nil expect(chain.key_fields_detector.range_key).to be_nil @@ -958,8 +983,9 @@ def request_params it 'supports query on global secondary index but always defaults to table hash key' do chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(name: 'Bob').to_a.size).to eq(1) + expect { + expect(chain.where(name: 'Bob').to_a.size).to eq(1) + }.to send_request_matching(:Query) expect(chain.key_fields_detector.hash_key).to eq(:name) expect(chain.key_fields_detector.range_key).to be_nil expect(chain.key_fields_detector.index_name).to be_nil @@ -967,29 +993,33 @@ def request_params it 'supports query on global secondary index' do chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(city: 'San Francisco').to_a.size).to eq(3) + expect { + expect(chain.where(city: 'San Francisco').to_a.size).to eq(3) + }.to send_request_matching(:Query) expect(chain.key_fields_detector.hash_key).to eq(:city) expect(chain.key_fields_detector.range_key).to eq(:age) expect(chain.key_fields_detector.index_name).to eq(:cityage) chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(city: 'San Francisco', 'age.gt': 12).to_a.size).to eq(2) + expect { + expect(chain.where(city: 'San Francisco', 'age.gt': 12).to_a.size).to eq(2) + }.to send_request_matching(:Query) expect(chain.key_fields_detector.hash_key).to eq(:city) expect(chain.key_fields_detector.range_key).to eq(:age) expect(chain.key_fields_detector.index_name).to eq(:cityage) chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(email: 'greg@test.com').to_a.size).to eq(1) + expect { + expect(chain.where(email: 'greg@test.com').to_a.size).to eq(1) + }.to send_request_matching(:Query) expect(chain.key_fields_detector.hash_key).to eq(:email) expect(chain.key_fields_detector.range_key).to eq(:age) expect(chain.key_fields_detector.index_name).to eq(:emailage) chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(email: 'greg@test.com', 'age.gt': 12).to_a.size).to eq(1) + expect { + expect(chain.where(email: 'greg@test.com', 'age.gt': 12).to_a.size).to eq(1) + }.to send_request_matching(:Query) expect(chain.key_fields_detector.hash_key).to eq(:email) expect(chain.key_fields_detector.range_key).to eq(:age) expect(chain.key_fields_detector.index_name).to eq(:emailage) @@ -997,8 +1027,9 @@ def request_params it 'supports scan when no global secondary index available' do chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_scan).and_call_original - expect(chain.where(gender: 'male').to_a.size).to eq(4) + expect { + expect(chain.where(gender: 'male').to_a.size).to eq(4) + }.to send_request_matching(:Scan) expect(chain.key_fields_detector.hash_key).to be_nil expect(chain.key_fields_detector.range_key).to be_nil expect(chain.key_fields_detector.index_name).to be_nil @@ -1006,22 +1037,25 @@ def request_params it 'supports query on global secondary index with start' do chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(city: 'San Francisco').to_a.size).to eq(3) + expect { + expect(chain.where(city: 'San Francisco').to_a.size).to eq(3) + }.to send_request_matching(:Query) expect(chain.key_fields_detector.hash_key).to eq(:city) expect(chain.key_fields_detector.range_key).to eq(:age) expect(chain.key_fields_detector.index_name).to eq(:cityage) # Now query with start at customer2 and we should only see customer3 chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(city: 'San Francisco').start(@customer2).all).to contain_exactly(@customer3) + expect { + expect(chain.where(city: 'San Francisco').start(@customer2).all).to contain_exactly(@customer3) + }.to send_request_matching(:Query) end it "does not use index if a condition for index hash key is other than 'equal'" do chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_scan).and_call_original - expect(chain.where('city.begins_with': 'San').to_a.size).to eq(3) + expect { + expect(chain.where('city.begins_with': 'San').to_a.size).to eq(3) + }.to send_request_matching(:Scan) expect(chain.key_fields_detector.hash_key).to be_nil expect(chain.key_fields_detector.range_key).to be_nil expect(chain.key_fields_detector.index_name).to be_nil @@ -1029,15 +1063,17 @@ def request_params it 'prefers global secondary index with range key used in conditions to index w/o such range key' do chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(city: 'San Francisco', 'age.lte': 15).to_a.size).to eq(2) + expect { + expect(chain.where(city: 'San Francisco', 'age.lte': 15).to_a.size).to eq(2) + }.to send_request_matching(:Query) expect(chain.key_fields_detector.hash_key).to eq(:city) expect(chain.key_fields_detector.range_key).to eq(:age) expect(chain.key_fields_detector.index_name).to eq(:cityage) chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(city: 'San Francisco', gender: 'male').to_a.size).to eq(3) + expect { + expect(chain.where(city: 'San Francisco', gender: 'male').to_a.size).to eq(3) + }.to send_request_matching(:Query) expect(chain.key_fields_detector.hash_key).to eq(:city) expect(chain.key_fields_detector.range_key).to eq(:gender) expect(chain.key_fields_detector.index_name).to eq(:citygender) @@ -1045,8 +1081,9 @@ def request_params it 'uses global secondary index when secondary hash key overlaps with primary hash key and range key matches' do chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(name: 'Bob', age: 10).to_a.size).to eq(1) + expect { + expect(chain.where(name: 'Bob', age: 10).to_a.size).to eq(1) + }.to send_request_matching(:Query) expect(chain.key_fields_detector.hash_key).to eq(:name) expect(chain.key_fields_detector.range_key).to eq(:age) expect(chain.key_fields_detector.index_name).to eq(:nameage) @@ -1065,8 +1102,9 @@ def request_params customer2 = model.create(name: 'Jeff', city: 'San Francisco', age: 15) chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - expect(chain.where(city: 'San Francisco').start(customer1).all).to contain_exactly(customer2) + expect { + expect(chain.where(city: 'San Francisco').start(customer1).all).to contain_exactly(customer2) + }.to send_request_matching(:Query) end end @@ -1144,7 +1182,7 @@ def request_params ).to contain_exactly(customer2) end - context 'Query' do + context 'with Query' do it 'dumps partition key `equal` condition' do model = new_class(partition_key: { name: :registered_on, type: :date }) @@ -1213,7 +1251,7 @@ def request_params end end - context 'Scan' do + context 'with Scan' do it 'dumps field for `equal` condition' do model = new_class do field :birthday, :date @@ -1238,8 +1276,8 @@ def request_params end end - context 'field is not declared in document' do - context 'Query' do + context 'when field is not declared in document' do + context 'with Query' do let(:class_with_not_declared_field) do new_class do field :name @@ -1258,7 +1296,7 @@ def request_params end end - context 'Scan' do + context 'with Scan' do let(:class_with_not_declared_field) do new_class do range :name @@ -1279,7 +1317,7 @@ def request_params end describe '#where' do - context 'passed condition for nonexistent attribute' do + context 'when passed condition for nonexistent attribute' do let(:model) do new_class do field :city @@ -1312,7 +1350,7 @@ def request_params end end - context 'nil check' do + context 'when nil check' do let(:model) do new_class do field :name @@ -1324,7 +1362,7 @@ def request_params @johndoe = model.create(name: nil) end - context 'store_attribute_with_nil_value = true', config: { store_attribute_with_nil_value: true } do + context 'when store_attribute_with_nil_value = true', config: { store_attribute_with_nil_value: true } do it 'supports "eq nil" check' do expect(model.where(name: nil).to_a).to eq [@johndoe] end @@ -1338,7 +1376,7 @@ def request_params end end - context 'store_attribute_with_nil_value = false', config: { store_attribute_with_nil_value: false } do + context 'when store_attribute_with_nil_value = false', config: { store_attribute_with_nil_value: false } do it 'supports "null" check' do expect(model.where('name.null': true).to_a).to eq [@johndoe] expect(model.where('name.null': false).to_a).to eq [@mike] @@ -1367,15 +1405,14 @@ def request_params # https://github.com/Dynamoid/dynamoid/issues/435 context 'when inheritance field (:type by default) is a GSI hash key' do it 'works without exception' do - # rubocop:disable Lint/ConstantDefinitionInBlock - UserWithGSI = new_class class_name: 'UserWithGSI' do + klass = new_class class_name: 'UserWithGSI' do field :type global_secondary_index hash_key: :type, range_key: :created_at, projected_attributes: :all end - # rubocop:enable Lint/ConstantDefinitionInBlock + stub_const 'UserWithGSI', klass obj = UserWithGSI.create @@ -1422,8 +1459,8 @@ def request_params end end - context 'when table arn is specified', remove_constants: [:Payment] do - context 'Query' do + context 'when table arn is specified' do + context 'with Query' do it 'uses given table ARN in requests instead of a table name', config: { create_table_on_save: false } do # Create table manually because CreateTable doesn't accept ARN as a # table name. Add namespace to have this table removed automativally. @@ -1433,12 +1470,13 @@ def request_params table = Dynamoid.adapter.describe_table(table_name) expect(table.arn).to be_present - Payment = Class.new do # rubocop:disable Lint/ConstantDefinitionInBlock + klass = Class.new do include Dynamoid::Document table arn: table.arn field :comment end + stub_const 'Payment', klass obj = Payment.create!(comment: 'foobar') @@ -1451,7 +1489,7 @@ def request_params end end - context 'Scan' do + context 'with Scan' do it 'uses given table ARN in requests instead of a table name', config: { create_table_on_save: false } do # Create table manually because CreateTable doesn't accept ARN as a # table name. Add namespace to have this table removed automativally. @@ -1461,12 +1499,13 @@ def request_params table = Dynamoid.adapter.describe_table(table_name) expect(table.arn).to be_present - Payment = Class.new do # rubocop:disable Lint/ConstantDefinitionInBlock + klass = Class.new do include Dynamoid::Document table arn: table.arn field :comment end + stub_const 'Payment', klass obj = Payment.create!(comment: 'foobar') @@ -1521,7 +1560,7 @@ def request_params expect(klass.where('age < :age', age: 40).where(first_name: 'Alex').all).to contain_exactly(obj3) end - context 'Query' do + context 'with Query' do it 'filters by specified conditions' do obj = klass.create!(first_name: 'Alex', age: 42) @@ -1530,7 +1569,7 @@ def request_params end end - context 'Scan' do + context 'with Scan' do it 'filters by specified conditions' do obj = klass.create!(first_name: 'Alex', age: 42) expect(klass.where('age = :age', age: 42).all.to_a).to eq([obj]) @@ -1539,41 +1578,47 @@ def request_params it 'performs Scan when key attributes are used only in String query' do obj = klass.create!(first_name: 'Alex', age: 42) - expect(Dynamoid.adapter.client).to receive(:scan).and_call_original - expect(klass.where('id = :id', id: obj.id).all.to_a).to eq([obj]) + expect { + expect(klass.where('id = :id', id: obj.id).all.to_a).to eq([obj]) + }.to send_request_matching(:Scan) end end end describe '#find_by_pages' do - let(:model) do - new_class do - self.range_key = :range + it 'yields one page at a time' do + klass = new_class do + range :range field :city field :age, :number field :range, :number field :data end - end - before do 120.times do |i| - model.create( - id: '1', - range: i.to_f, - age: i.to_f, - data: 'A' * 1024 * 16 - ) + klass.create(id: '1', range: i.to_f, age: i.to_f, data: 'A' * 1024 * 16) end - end - it 'yields one page at a time' do - expect { |b| model.where(id: '1').find_by_pages(&b) }.to yield_successive_args( - [all(be_a(model)), { last_evaluated_key: an_instance_of(Hash) }], - [all(be_a(model)), { last_evaluated_key: nil }], + expect { |b| klass.where(id: '1').find_by_pages(&b) }.to yield_successive_args( + [all(be_a(klass)), { last_evaluated_key: an_instance_of(Hash) }], + [all(be_a(klass)), { last_evaluated_key: nil }], ) end + it 'returns :last_evaluated_key which may be used to restart iteration' do + klass = new_class do + field :name + end + + # Creates exactly 2 full pages + 58.times { klass.create!(name: SecureRandom.uuid * 1024) } + + first_page, first_page_meta = klass.find_by_pages.first + second_page, = klass.start(first_page_meta[:last_evaluated_key]).find_by_pages.first + + expect(first_page & second_page).to be_empty + end + describe 'callbacks' do it 'runs after_initialize callback' do klass_with_callback = new_class do @@ -1658,7 +1703,7 @@ def request_params expect(customers + customers_rest).to contain_exactly(customer1, customer2, customer3) end - context 'document with range key' do + context 'with document with range key' do let(:model) do Class.new do include Dynamoid::Document @@ -1680,23 +1725,23 @@ def request_params it 'return query result from the specified item' do chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - customers = chain.where(version: 'v1', 'age.gt': 10).start(@customer2).all.to_a - - expect(customers).to contain_exactly(@customer3, @customer4) + expect { + customers = chain.where(version: 'v1', 'age.gt': 10).start(@customer2).all.to_a + expect(customers).to contain_exactly(@customer3, @customer4) + }.to send_request_matching(:Query) end it 'return scan result from the specified item' do chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_scan).and_call_original - customers = chain.where(gender: 'male').start(@customer1).all.to_a - - expect(customers).to contain_exactly(@customer3) + expect { + customers = chain.where(gender: 'male').start(@customer1).all.to_a + expect(customers).to contain_exactly(@customer3) + }.to send_request_matching(:Scan) end end - context 'document without range key' do + context 'with document without range key' do let(:model) do new_class(partition_key: :name) do field :age, :integer @@ -1713,10 +1758,10 @@ def request_params it 'return scan result from the specified item' do chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_scan).and_call_original - customers = chain.where('age.gt': 10).start(@customer2).all.to_a - - expect(customers).to contain_exactly(@customer3, @customer4) + expect { + customers = chain.where('age.gt': 10).start(@customer2).all.to_a + expect(customers).to contain_exactly(@customer3, @customer4) + }.to send_request_matching(:Scan) end end end @@ -1728,8 +1773,9 @@ def request_params chain = described_class.new(klass) - expect(Dynamoid.adapter.client).to receive(:batch_write_item).and_call_original - chain.delete_all + expect { + chain.delete_all + }.to send_request_matching(:BatchWriteItem) end context 'when some conditions specified' do @@ -1759,8 +1805,9 @@ def request_params chain = described_class.new(klass) chain = chain.where(id: document.id) - expect(Dynamoid.adapter.client).to receive(:query).and_call_original - expect { chain.delete_all }.to change { klass.count }.by(-1) + expect { + expect { chain.delete_all }.to change { klass.count }.by(-1) + }.to send_request_matching(:Query) end it 'loads items with Scan if cannot use Query' do @@ -1774,11 +1821,12 @@ def request_params chain = described_class.new(klass) chain = chain.where(author: 'J. K. Rowling') - expect(Dynamoid.adapter.client).to receive(:scan).and_call_original - expect { chain.delete_all }.to change { klass.count }.by(-1) + expect { + expect { chain.delete_all }.to change { klass.count }.by(-1) + }.to send_request_matching(:Scan) end - context 'Query (partition key specified)' do + context 'when Query (partition key specified)' do it 'works well with composite primary key' do klass = new_class do range :title @@ -1822,7 +1870,7 @@ def request_params end end - context 'Scan (partition key is not specified)' do + context 'when Scan (partition key is not specified)' do it 'works well with composite primary key' do klass = new_class do range :title @@ -1867,7 +1915,7 @@ def request_params end end - context 'there are no conditions' do + context 'when there are no conditions' do it 'deletes all the items' do klass = new_class do field :title @@ -1878,7 +1926,7 @@ def request_params expect { chain.delete_all }.to change { klass.count }.from(3).to(0) end - context 'Scan' do + context 'with Scan' do it 'works well with composite primary key' do klass = new_class do range :title @@ -1899,8 +1947,8 @@ def request_params end end - context 'when table arn is specified', remove_constants: [:Payment] do - context 'Query' do + context 'when table arn is specified' do + context 'with Query' do it 'uses given table ARN in requests instead of a table name', config: { create_table_on_save: false } do # Create table manually because CreateTable doesn't accept ARN as a # table name. Add namespace to have this table removed automativally. @@ -1910,12 +1958,13 @@ def request_params table = Dynamoid.adapter.describe_table(table_name) expect(table.arn).to be_present - Payment = Class.new do # rubocop:disable Lint/ConstantDefinitionInBlock + klass = Class.new do include Dynamoid::Document table arn: table.arn field :comment end + stub_const 'Payment', klass obj = Payment.create!(comment: 'foobar') @@ -1929,7 +1978,7 @@ def request_params end end - context 'Scan' do + context 'with Scan' do it 'uses given table ARN in requests instead of a table name', config: { create_table_on_save: false } do # Create table manually because CreateTable doesn't accept ARN as a # table name. Add namespace to have this table removed automativally. @@ -1939,12 +1988,13 @@ def request_params table = Dynamoid.adapter.describe_table(table_name) expect(table.arn).to be_present - Payment = Class.new do # rubocop:disable Lint/ConstantDefinitionInBlock + klass = Class.new do include Dynamoid::Document table arn: table.arn field :comment end + stub_const 'Payment', klass obj = Payment.create!(comment: 'foobar') @@ -1977,8 +2027,10 @@ def request_params document = model.create(name: 'Bob', age: 5) chain = described_class.new(model) - expect_any_instance_of(described_class).to receive(:scan_limit).with(1).and_call_original - expect(chain.first).to eq(document) + + expect { + expect(chain.first).to eq(document) + }.to send_request_matching(:Scan, { Limit: 1 }) end it 'applies the correct scan limit if no conditions are present' do @@ -1987,16 +2039,18 @@ def request_params document3 = model.create(name: 'Bob', age: 7) chain = described_class.new(model) - expect_any_instance_of(described_class).to receive(:scan_limit).with(2).and_call_original - expect(chain.first(2).to_set).to eq([document1, document2].to_set) + expect { + expect(chain.first(2).to_set).to eq([document1, document2].to_set) + }.to send_request_matching(:Scan, { Limit: 2 }) end it 'applies a record limit if only key conditions are present' do document = model.create(name: 'Bob', age: 5) chain = described_class.new(model) - expect_any_instance_of(described_class).to receive(:record_limit).with(1).and_call_original - expect(chain.where(name: 'Bob', age: 5).first).to eq(document) + expect { + expect(chain.where(name: 'Bob', age: 5).first).to eq(document) + }.to send_request_matching(:Query, { Limit: 1 }) end it 'applies the correct record limit if only key conditions are present' do @@ -2005,26 +2059,36 @@ def request_params document3 = model.create(name: 'Bob', age: 7) chain = described_class.new(model) - expect_any_instance_of(described_class).to receive(:record_limit).with(2).and_call_original - expect(chain.where(name: 'Bob').first(2)).to eq([document1, document2]) + expect { + expect(chain.where(name: 'Bob').first(2)).to eq([document1, document2]) + }.to send_request_matching(:Query, { Limit: 2 }) end it 'does not apply a record limit if the hash key is missing' do document = model.create(name: 'Bob', city: 'New York', age: 5) chain = described_class.new(model) - expect_any_instance_of(described_class).not_to receive(:record_limit) - expect(chain.where(age: 5).first).to eq(document) + expect { + expect(chain.where(age: 5).first).to eq(document) + }.not_to send_request_matching(:Scan, { Limit: anything }) end it 'does not apply a record limit if non-key conditions are present' do document = model.create(name: 'Bob', city: 'New York', age: 5) chain = described_class.new(model) - expect_any_instance_of(described_class).not_to receive(:record_limit) - expect(chain.where(city: 'New York').first).to eq(document) - expect(chain.where(name: 'Bob', city: 'New York').first).to eq(document) - expect(chain.where(name: 'Bob', age: 5, city: 'New York').first).to eq(document) + + expect { + expect(chain.where(city: 'New York').first).to eq(document) + }.not_to send_request_matching(:Scan, { Limit: anything }) + + expect { + expect(chain.where(name: 'Bob', city: 'New York').first).to eq(document) + }.not_to send_request_matching(:Query, { Limit: anything }) + + expect { + expect(chain.where(name: 'Bob', age: 5, city: 'New York').first).to eq(document) + }.not_to send_request_matching(:Query, { Limit: anything }) end it 'does not apply a record limit if non-equality conditions are present' do @@ -2032,8 +2096,9 @@ def request_params document2 = model.create(name: 'Alice', age: 6) chain = described_class.new(model) - expect_any_instance_of(described_class).not_to receive(:record_limit) - expect(chain.where('name.gt': 'Alice').first).to eq(document1) + expect { + expect(chain.where('name.gt': 'Alice').first).to eq(document1) + }.not_to send_request_matching(:Scan, { Limit: anything }) end it 'returns nil if no matching document is present' do @@ -2058,7 +2123,7 @@ def request_params expect(model.where(name: 'Alice').first.age).to eq(6) end - context 'scope is reused' do + context 'when scope is reused' do it 'does not affect other query methods when no key conditions' do klass = new_class do field :name @@ -2174,7 +2239,7 @@ def request_params end end - context 'Query' do + context 'with Query' do let(:model) do Class.new do include Dynamoid::Document @@ -2206,7 +2271,7 @@ def request_params end end - context 'Scan' do + context 'with Scan' do let(:model) do new_class do field :age, :integer @@ -2230,8 +2295,8 @@ def request_params end end - context 'when table arn is specified', remove_constants: [:Payment] do - context 'Query' do + context 'when table arn is specified' do + context 'with Query' do it 'uses given table ARN in requests instead of a table name', config: { create_table_on_save: false } do # Create table manually because CreateTable doesn't accept ARN as a # table name. Add namespace to have this table removed automativally. @@ -2241,12 +2306,13 @@ def request_params table = Dynamoid.adapter.describe_table(table_name) expect(table.arn).to be_present - Payment = Class.new do # rubocop:disable Lint/ConstantDefinitionInBlock + klass = Class.new do include Dynamoid::Document table arn: table.arn field :comment end + stub_const 'Payment', klass obj = Payment.create!(comment: 'foobar') @@ -2259,7 +2325,7 @@ def request_params end end - context 'Scan' do + context 'with Scan' do it 'uses given table ARN in requests instead of a table name', config: { create_table_on_save: false } do # Create table manually because CreateTable doesn't accept ARN as a # table name. Add namespace to have this table removed automativally. @@ -2269,12 +2335,13 @@ def request_params table = Dynamoid.adapter.describe_table(table_name) expect(table.arn).to be_present - Payment = Class.new do # rubocop:disable Lint/ConstantDefinitionInBlock + klass = Class.new do include Dynamoid::Document table arn: table.arn field :comment end + stub_const 'Payment', klass obj = Payment.create!(comment: 'foobar') @@ -2311,20 +2378,20 @@ def request_params model.create(name: 'Alex', age: 21) chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_scan).and_call_original - - obj, = chain.project(:age).to_a - expect(obj.attributes).to eq(age: 21) + expect { + obj, = chain.project(:age).to_a + expect(obj.attributes).to eq(age: 21) + }.to send_request_matching(:Scan) end it 'works with Query' do obj = model.create(name: 'Alex', age: 21) chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - - obj_loaded, = chain.where(id: obj.id).project(:age).to_a - expect(obj_loaded.attributes).to eq(age: 21) + expect { + obj_loaded, = chain.where(id: obj.id).project(:age).to_a + expect(obj_loaded.attributes).to eq(age: 21) + }.to send_request_matching(:Query) end context 'when attribute name is a DynamoDB reserved word' do @@ -2339,20 +2406,20 @@ def request_params model.create(name: 'Alex', bucket: 2) chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_scan).and_call_original - - obj, = chain.project(:bucket).to_a - expect(obj.attributes).to eq(bucket: 2) + expect { + obj, = chain.project(:bucket).to_a + expect(obj.attributes).to eq(bucket: 2) + }.to send_request_matching(:Scan) end it 'works with Query' do object = model.create(name: 'Alex', bucket: 2) chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - - obj, = chain.where(id: object.id).project(:bucket).to_a - expect(obj.attributes).to eq(bucket: 2) + expect { + obj, = chain.where(id: object.id).project(:bucket).to_a + expect(obj.attributes).to eq(bucket: 2) + }.to send_request_matching(:Query) end end @@ -2367,20 +2434,20 @@ def request_params model.create('first:name': 'Alex') chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_scan).and_call_original - - obj, = chain.project(:'first:name').to_a - expect(obj.attributes).to eq('first:name': 'Alex') + expect { + obj, = chain.project(:'first:name').to_a + expect(obj.attributes).to eq('first:name': 'Alex') + }.to send_request_matching(:Scan) end it 'works with Query' do object = model.create('first:name': 'Alex') chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - - obj, = chain.where(id: object.id).project(:'first:name').to_a - expect(obj.attributes).to eq('first:name': 'Alex') + expect { + obj, = chain.where(id: object.id).project(:'first:name').to_a + expect(obj.attributes).to eq('first:name': 'Alex') + }.to send_request_matching(:Query) end end @@ -2395,20 +2462,20 @@ def request_params model.create(_name: 'Alex') chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_scan).and_call_original - - obj, = chain.project(:_name).to_a - expect(obj.attributes).to eq(_name: 'Alex') + expect { + obj, = chain.project(:_name).to_a + expect(obj.attributes).to eq(_name: 'Alex') + }.to send_request_matching(:Scan) end it 'works with Query' do object = model.create(_name: 'Alex') chain = described_class.new(model) - expect(chain).to receive(:raw_pages_via_query).and_call_original - - obj, = chain.where(id: object.id).project(:_name).to_a - expect(obj.attributes).to eq(_name: 'Alex') + expect { + obj, = chain.where(id: object.id).project(:_name).to_a + expect(obj.attributes).to eq(_name: 'Alex') + }.to send_request_matching(:Query) end end end @@ -2456,7 +2523,7 @@ def request_params expect(model.pluck(:created_at)).to eq(['03-04-2020 23:40:00'.to_time]) end - context 'scope is reused' do + context 'when scope is reused' do it 'does not affect other query methods when there is one field to fetch' do klass = new_class do field :name @@ -2833,4 +2900,34 @@ def request_params end end end + + describe '#consistent' do + it 'sends ConsistentRead attribute in Query request' do + klass = new_class do + range :name + end + klass.create_table + + expect { + described_class.new(klass).where(id: '1', name: 'Alex').consistent.all.to_a + }.to send_request_matching(:Query, { ConsistentRead: true }) + + expect { + described_class.new(klass).where(id: '1', name: 'Alex').all.to_a + }.not_to send_request_matching(:Query, { ConsistentRead: true }) + end + + it 'sends ConsistentRead attribute in Scan request' do + klass = new_class do + range :name + end + klass.create_table + + chain = described_class.new(klass) + + expect { + described_class.new(klass).where(name: 'Alex').consistent.all.to_a + }.to send_request_matching(:Scan, { ConsistentRead: true }) + end + end end diff --git a/spec/dynamoid/criteria_new_spec.rb b/spec/dynamoid/criteria_new_spec.rb deleted file mode 100644 index a80beb45..00000000 --- a/spec/dynamoid/criteria_new_spec.rb +++ /dev/null @@ -1,182 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Dynamoid::Criteria do - it 'supports querying with .where method' do - klass = new_class do - field :name - end - - objects = klass.create([{ name: 'Alex' }, { name: 'Bob' }, { name: 'Alex' }]) - expect(klass.where(name: 'Alex')).to contain_exactly(objects[0], objects[2]) - end - - it 'supports querying with .all method' do - klass = new_class do - field :name - end - - objects = klass.create([{ name: 'Alex' }, { name: 'Bob' }]) - expect(klass.all).to match_array(objects) - end - - it 'supports querying with .first method' do - klass = new_class do - range :name - end - - object = klass.create(name: 'Alex') - expect(klass.first).to eq object - end - - it 'supports querying with .last method' do - klass = new_class do - range :name - end - - object = klass.create(name: 'Alex') - expect(klass.last).to eq object - end - - it 'supports querying with .each method' do - klass = new_class do - range :name - end - objects = klass.create([{ name: 'Alex' }, { name: 'Bob' }]) - - result = [] - klass.each { |obj| result << obj } # rubocop:disable Style/MapIntoArray - - expect(result).to match_array(objects) - end - - it 'supports querying with .record_limit method' do - klass = new_class do - field :name - end - - objects = klass.create([{ name: 'Alex' }, { name: 'Bob' }]) - actual = klass.record_limit(1).all.to_a - - expect(actual.size).to eq 1 - expect(actual[0]).to satisfy { |v| %w[Alex Bob].include?(v.name) } - end - - it 'supports querying with .scan_limit method' do - klass = new_class do - field :name - end - - objects = klass.create([{ name: 'Alex' }, { name: 'Bob' }]) - actual = klass.scan_limit(1).all.to_a - - expect(actual.size).to eq 1 - expect(actual[0]).to satisfy { |v| %w[Alex Bob].include?(v.name) } - end - - it 'supports querying with .batch method' do - klass = new_class do - field :name - end - - objects = klass.create([{ name: 'Alex' }, { name: 'Bob' }, { name: 'Alex' }]) - expect(klass.batch(2).all).to match_array(objects) - end - - it 'supports querying with .start method' do - klass = new_class do - table key: :age - range :name - field :age, :integer - end - - objects = klass.create([{ age: 20, name: 'Alex' }, { age: 20, name: 'Bob' }, { age: 20, name: 'Michael' }]) - - actual = klass.start(objects[0]).all.to_a - expect(actual).to eq objects[1..2] - end - - it 'supports querying with .scan_index_forward method' do - klass = new_class do - table key: :age - range :name - field :age, :integer - end - - objects = klass.create([{ age: 20, name: 'Alex' }, { age: 20, name: 'Bob' }, { age: 20, name: 'Michael' }]) - - # force Query with age: 20 partition key condition - actual = klass.scan_index_forward(true).where(age: 20).all.to_a - expect(actual).to eq objects - - # force Query with age: 20 partition key condition - actual = klass.scan_index_forward(false).where(age: 20).all.to_a - expect(actual).to eq objects.reverse - end - - it 'supports querying with .find_by_pages method' do - klass = new_class - objects = klass.create([{}, {}, {}]) - - pages = [] - klass.find_by_pages do |models, _options| - pages << models # actually there is only one page - end - - expect(pages.flatten).to match_array(objects) - end - - it 'supports querying with .project method' do - klass = new_class do - field :age, :integer - field :name, :string - end - klass.create(age: 20, name: 'Alex') - - objects_with_name = klass.project(:name).to_a - expect(objects_with_name.size).to eq 1 - - object_with_name = objects_with_name[0] - expect(object_with_name.name).to eq 'Alex' - expect(object_with_name.age).to eq nil - end - - it 'supports querying with .pluck method' do - klass = new_class do - field :age, :integer - field :name, :string - end - - klass.create([{ age: 20, name: 'Alex' }, { age: 20, name: 'Bob' }]) - expect(klass.pluck(:name)).to contain_exactly('Alex', 'Bob') - end - - it 'supports querying with .consistent method' do - klass = new_class do - field :age, :integer - end - - objects = klass.create([{ age: 20 }, { age: 30 }]) - actual = klass.consistent.all.to_a - expect(actual).to match_array(objects) - end - - it 'supports .delete_all method' do - klass = new_class do - field :age, :integer - end - - objects = klass.create([{ age: 20 }, { age: 30 }]) - expect { klass.delete_all }.to change { klass.all.to_a.size }.from(2).to(0) - end - - it 'supports .destroy_all method' do - klass = new_class do - field :age, :integer - end - - objects = klass.create([{ age: 20 }, { age: 30 }]) - expect { klass.destroy_all }.to change { klass.all.to_a.size }.from(2).to(0) - end -end diff --git a/spec/dynamoid/criteria_spec.rb b/spec/dynamoid/criteria_spec.rb index b52a96d7..ed3d251c 100644 --- a/spec/dynamoid/criteria_spec.rb +++ b/spec/dynamoid/criteria_spec.rb @@ -2,167 +2,263 @@ require 'spec_helper' -# This file contains legacy specs. They should be revised and moved to the -# criteria_new_spec.rb file. - describe Dynamoid::Criteria do - let!(:user1) { User.create(name: 'Josh', email: 'josh@joshsymonds.com', admin: true) } - let!(:user2) { User.create(name: 'Justin', email: 'justin@joshsymonds.com', admin: false) } + it 'supports querying with .where method' do + klass = new_class do + field :name + end - it 'finds first using where' do - expect(User.where(name: 'Josh').first).to eq user1 + objects = klass.create([{ name: 'Alex' }, { name: 'Bob' }, { name: 'Alex' }]) + expect(klass.where(name: 'Alex')).to contain_exactly(objects[0], objects[2]) end - it 'finds last using where' do - expect(User.where(admin: false).last).to eq user2 + it 'supports combining .where and .first methods' do + klass = new_class do + field :name + end + + objects = klass.create([{ name: 'Alex' }, { name: 'Bob' }]) + expect(klass.where(name: 'Alex').first).to eq objects[0] end - it 'finds all using where' do - expect(User.where(name: 'Josh').all.to_a).to eq [user1] + it 'supports combining .where and .last methods' do + klass = new_class do + field :name + end + + objects = klass.create([{ name: 'Alex' }, { name: 'Bob' }]) + expect(klass.where(name: 'Bob').last).to eq objects[1] end - it 'returns all records' do - expect(Set.new(User.all)).to eq Set.new([user1, user2]) - expect(User.all.first.new_record).to be_falsey + it 'supports combining .where and .all methods' do + klass = new_class do + field :name + end + + objects = klass.create([{ name: 'Alex' }, { name: 'Bob' }, { name: 'Alex' }]) + expect(klass.where(name: 'Alex').all.to_a).to contain_exactly(objects[0], objects[2]) end - context 'Magazine table' do - before do - Magazine.create_table + it 'supports querying with .all method' do + klass = new_class do + field :name end - it 'returns empty attributes for where' do - expect(Magazine.where(title: 'Josh').all.to_a).to eq [] + objects = klass.create([{ name: 'Alex' }, { name: 'Bob' }]) + expect(klass.all).to match_array(objects) + end + + it 'supports querying with .first method' do + klass = new_class do + range :name end - it 'returns empty attributes for all' do - expect(Magazine.all.to_a).to eq [] + object = klass.create(name: 'Alex') + expect(klass.first).to eq object + end + + it 'supports querying with .last method' do + klass = new_class do + range :name end + + object = klass.create(name: 'Alex') + expect(klass.last).to eq object end - it 'passes each to all members' do - expect { |b| User.each(&b) }.to yield_successive_args( - be_a(User).and(have_attributes('new_record' => false)), - be_a(User).and(have_attributes('new_record' => false)) - ) + it 'supports querying with .each method' do + klass = new_class do + range :name + end + objects = klass.create([{ name: 'Alex' }, { name: 'Bob' }]) + + result = [] + klass.each { |obj| result << obj } # rubocop:disable Style/MapIntoArray + + expect(result).to match_array(objects) end - it 'passes find_by_pages to all members' do - expect { |b| User.find_by_pages(&b) }.to yield_successive_args( - [all(be_a(User)), { last_evaluated_key: nil }] - ) + it 'supports querying with .record_limit method' do + klass = new_class do + field :name + end + + objects = klass.create([{ name: 'Alex' }, { name: 'Bob' }]) + actual = klass.record_limit(1).all.to_a + + expect(actual.size).to eq 1 + expect(actual[0]).to satisfy { |v| %w[Alex Bob].include?(v.name) } + end + + it 'supports querying with .scan_limit method' do + klass = new_class do + field :name + end + + objects = klass.create([{ name: 'Alex' }, { name: 'Bob' }]) + actual = klass.scan_limit(1).all.to_a + + expect(actual.size).to eq 1 + expect(actual[0]).to satisfy { |v| %w[Alex Bob].include?(v.name) } + end + + it 'supports querying with .batch method' do + klass = new_class do + field :name + end + + objects = klass.create([{ name: 'Alex' }, { name: 'Bob' }, { name: 'Alex' }]) + expect(klass.batch(2).all).to match_array(objects) + end + + it 'supports querying with .start method' do + klass = new_class do + table key: :age + range :name + field :age, :integer + end + + objects = klass.create([{ age: 20, name: 'Alex' }, { age: 20, name: 'Bob' }, { age: 20, name: 'Michael' }]) + + actual = klass.start(objects[0]).all.to_a + expect(actual).to eq objects[1..2] end - it 'returns a last_evaluated_key which may be used to restart iteration' do - # Creates exactly 2 full pages - 58.times { User.create(name: SecureRandom.uuid * 1024) } + it 'supports querying with .scan_index_forward method' do + klass = new_class do + table key: :age + range :name + field :age, :integer + end + + objects = klass.create([{ age: 20, name: 'Alex' }, { age: 20, name: 'Bob' }, { age: 20, name: 'Michael' }]) - first_page, first_page_meta = User.find_by_pages.first - second_page, = User.start(first_page_meta[:last_evaluated_key]).find_by_pages.first + # force Query with age: 20 partition key condition + actual = klass.scan_index_forward(true).where(age: 20).all.to_a + expect(actual).to eq objects - expect(first_page & second_page).to be_empty + # force Query with age: 20 partition key condition + actual = klass.scan_index_forward(false).where(age: 20).all.to_a + expect(actual).to eq objects.reverse end - it 'returns N records' do - 5.times { |i| User.create(name: 'Josh', email: "josh_#{i}@joshsymonds.com") } - expect(User.record_limit(2).all.count).to eq(2) + it 'supports querying with .find_by_pages method' do + klass = new_class + objects = klass.create([{}, {}, {}]) + + pages = [] + klass.find_by_pages do |models, _options| + pages << models # actually there is only one page + end + + expect(pages.flatten).to match_array(objects) end - # TODO: This test is broken using the AWS SDK adapter. - # it 'start with a record' do - # 5.times { |i| User.create(:name => 'Josh', :email => 'josh_#{i}@joshsymonds.com') } - # all = User.all - # User.start(all[3]).all.should eq(all[4..-1]) - # - # all = User.where(:name => 'Josh').all - # User.where(:name => 'Josh').start(all[3]).all.should eq(all[4..-1]) - # end + it 'supports querying with .project method' do + klass = new_class do + field :age, :integer + field :name, :string + end + klass.create(age: 20, name: 'Alex') + + objects_with_name = klass.project(:name).to_a + expect(objects_with_name.size).to eq 1 - it 'send consistent option to adapter' do - pending 'This test is broken as we are overriding the consistent_read option to true inside the adapter' - expect(Dynamoid::Adapter).to receive(:get_item) { |_table_name, _key, options| options[:consistent_read] == true } - User.where(name: 'x').consistent.first + object_with_name = objects_with_name[0] + expect(object_with_name.name).to eq 'Alex' + expect(object_with_name.age).to eq nil + end - expect(Dynamoid::Adapter).to receive(:query) { |_table_name, options| options[:consistent_read] == true }.returns([]) - Tweet.where(tweet_id: 'xx', group: 'two').consistent.all + it 'supports querying with .pluck method' do + klass = new_class do + field :age, :integer + field :name, :string + end - expect(Dynamoid::Adapter).to receive(:query) { |_table_name, options| options[:consistent_read] == false }.returns([]) - Tweet.where(tweet_id: 'xx', group: 'two').all + klass.create([{ age: 20, name: 'Alex' }, { age: 20, name: 'Bob' }]) + expect(klass.pluck(:name)).to contain_exactly('Alex', 'Bob') end - it 'does not raise exception when consistent_read is used with scan' do - expect do - User.where(password: 'password').consistent.first - end.not_to raise_error(Dynamoid::Errors::InvalidQuery) + it 'supports querying with .consistent method' do + klass = new_class do + field :age, :integer + end + + objects = klass.create([{ age: 20 }, { age: 30 }]) + actual = klass.consistent.all.to_a + expect(actual).to match_array(objects) end - context 'when scans using non-indexed fields and warn_on_scan config option is true' do - before do - @warn_on_scan = Dynamoid::Config.warn_on_scan - Dynamoid::Config.warn_on_scan = true + it 'supports .delete_all method' do + klass = new_class do + field :age, :integer end - after do - Dynamoid::Config.warn_on_scan = @warn_on_scan + objects = klass.create([{ age: 20 }, { age: 30 }]) + expect { klass.delete_all }.to change { klass.all.to_a.size }.from(2).to(0) + end + + it 'supports .destroy_all method' do + klass = new_class do + field :age, :integer end - it 'logs warnings' do + objects = klass.create([{ age: 20 }, { age: 30 }]) + expect { klass.destroy_all }.to change { klass.all.to_a.size }.from(2).to(0) + end + + context 'when scans using non-indexed fields and warn_on_scan config option is true' do + it 'logs warnings', config: { warn_on_scan: true } do + klass = new_class(partition_key: :id) do + field :name + field :password + + def self.to_s + 'User' + end + end + klass.create_table + expect(Dynamoid.logger).to receive(:warn).with('Queries without an index are forced to use scan and are generally much slower than indexed queries!') expect(Dynamoid.logger).to receive(:warn).with('You can index this query by adding index declaration to user.rb:') expect(Dynamoid.logger).to receive(:warn).with("* global_secondary_index hash_key: 'some-name', range_key: 'some-another-name'") expect(Dynamoid.logger).to receive(:warn).with("* local_secondary_index range_key: 'some-name'") expect(Dynamoid.logger).to receive(:warn).with('Not indexed attributes: :name, :password') - User.where(name: 'x', password: 'password').all + klass.where(name: 'x', password: 'password').all end end context 'when scans using non-indexed fields and error_on_scan config option is true' do - before do - @error_on_scan = Dynamoid::Config.error_on_scan - Dynamoid::Config.error_on_scan = true - end - - after do - Dynamoid::Config.error_on_scan = @error_on_scan - end + it 'raises an error', config: { error_on_scan: true } do + klass = new_class(partition_key: :id) do + field :name + field :password + end + klass.create_table - it 'raises an error' do expect { - User.where(name: 'x', password: 'password').all + klass.where(name: 'x', password: 'password').all }.to raise_error(Dynamoid::Errors::ScanProhibited) end end context 'when doing intentional, full-table scan (query is empty) and warn_on_scan config option is true' do - before do - @warn_on_scan = Dynamoid::Config.warn_on_scan - Dynamoid::Config.warn_on_scan = true - end - - after do - Dynamoid::Config.warn_on_scan = @warn_on_scan - end - - it 'does not log any warnings' do + it 'does not log any warnings', config: { warn_on_scan: true } do + klass = new_class + klass.create_table expect(Dynamoid.logger).not_to receive(:warn) - User.all + klass.all end end context 'when doing intentional, full-table scan (query is empty) and error_on_scan config option is true' do - before do - @error_on_scan = Dynamoid::Config.error_on_scan - Dynamoid::Config.error_on_scan = true - end - - after do - Dynamoid::Config.error_on_scan = @error_on_scan - end + it 'does not raise an error', config: { error_on_scan: true } do + klass = new_class + klass.create_table - it 'does not raise an error' do - expect { User.all }.not_to raise_error(Dynamoid::Errors::ScanProhibited) + expect(klass.all.to_a).to eq [] end end end diff --git a/spec/dynamoid/dirty_spec.rb b/spec/dynamoid/dirty_spec.rb index 6aa91468..70eb0e3b 100644 --- a/spec/dynamoid/dirty_spec.rb +++ b/spec/dynamoid/dirty_spec.rb @@ -631,7 +631,7 @@ end end - context 'in-place changes' do + context 'with in-place changes' do let(:klass_with_string) do new_class do field :name, :string @@ -692,7 +692,7 @@ end end - context 'string type' do + context 'with string type' do it 'detects in-place modifying a String value' do obj = klass_with_string.create!(name: +'Alex') obj.name.upcase! @@ -701,7 +701,7 @@ end end - context 'set type' do + context 'when set type' do it 'detects adding elements' do obj = klass_with_set.create!(names: ['Alex']) obj.names << 'Michael' @@ -729,7 +729,7 @@ end end - context 'array type' do + context 'with array type' do it 'detects adding elements' do obj = klass_with_array.create!(names: ['Alex']) obj.names << 'Michael' @@ -752,7 +752,7 @@ end end - context 'map type' do + context 'with map type' do it 'detects adding key-value pair' do obj = klass_with_map.create!(config: { 'level' => 'debug' }) obj.config['namespace'] = 'us-west' @@ -775,7 +775,7 @@ end end - context 'raw type' do + context 'with raw type' do it 'detects structure changing' do obj = klass_with_raw.create!(metadata: { 'a' => 1 }) obj.metadata['b'] = [1, 2, 3] @@ -784,7 +784,7 @@ end end - context 'serialized' do + context 'with serialized' do it 'detects structure changing' do obj = klass_with_serialized.create!(metadata: { 'a' => 1 }) obj.metadata['b'] = [1, 2, 3] @@ -793,7 +793,7 @@ end end - context 'binary type' do + context 'with binary type' do it 'detects in-place modifying a String value' do obj = klass_with_binary.create!(image: '012345689'.b) obj.image.sub!('0123', '----') @@ -802,7 +802,7 @@ end end - context 'custom type' do + context 'with custom type' do it 'detects in-place modifying' do obj = klass_with_custom_type.create!(user: DirtySpec::User.new(+'Alex')) obj.user.name.upcase! @@ -859,7 +859,7 @@ # Regression test # See https://github.com/Dynamoid/dynamoid/issues/1000 - context 'field of type :map' do + context 'with field of type :map' do let(:klass_with_map) do new_class do field :config, :map @@ -873,7 +873,7 @@ end end - context 'field of type :raw' do + context 'with field of type :raw' do let(:klass_with_raw) do new_class do field :config, :raw @@ -887,7 +887,7 @@ end end - context 'field of type :array' do + context 'with field of type :array' do let(:klass_with_array) do new_class do field :config, :array @@ -901,7 +901,7 @@ end end - context 'field of type :serialized' do + context 'with field of type :serialized' do let(:klass_with_serialized) do new_class do field :config, :serialized diff --git a/spec/dynamoid/dumping_spec.rb b/spec/dynamoid/dumping_spec.rb index 2b238e7d..6c1f5352 100644 --- a/spec/dynamoid/dumping_spec.rb +++ b/spec/dynamoid/dumping_spec.rb @@ -5,7 +5,7 @@ describe 'Dumping' do describe 'Boolean field' do - context 'string format' do + context 'with string format' do let(:klass) do new_class do field :active, :boolean, store_as_native_boolean: false @@ -34,7 +34,7 @@ end end - context 'boolean format' do + context 'with boolean format' do let(:klass) do new_class do field :active, :boolean, store_as_native_boolean: true @@ -83,7 +83,7 @@ expect(reload(obj).active).to eql(true) end - context 'store_boolean_as_native=true' do + context 'when store_boolean_as_native=true' do it 'is stored as boolean if field option store_as_native_boolean is not set', config: { store_boolean_as_native: true } do klass = new_class do @@ -121,7 +121,7 @@ end end - context 'store_boolean_as_native=false' do + context 'when store_boolean_as_native=false' do it 'is stored as string if field option store_as_native_boolean is not set', config: { store_boolean_as_native: false } do klass = new_class do @@ -162,7 +162,7 @@ end describe 'DateTime field' do - context 'Stored in :number format' do + context 'with Stored in :number format' do let(:klass) do new_class do field :sent_at, :datetime @@ -192,7 +192,7 @@ end end - context 'Stored in :string ISO-8601 format', + context 'with Stored in :string ISO-8601 format', config: { application_timezone: :utc, dynamodb_timezone: :utc } do let(:klass) do new_class do @@ -342,7 +342,7 @@ end describe 'Date field' do - context 'stored in :string format' do + context 'with stored in :string format' do it 'stores in ISO 8601 format' do klass = new_class do field :signed_up_on, :date, store_as_string: true @@ -394,7 +394,7 @@ end end - context 'stored in :number format' do + context 'with stored in :number format' do it 'stores as number of days between dates' do klass = new_class do field :signed_up_on, :date, store_as_string: false @@ -492,7 +492,7 @@ end describe 'typed set' do - context 'set of string' do + context 'when set of string' do let(:class_with_typed_set) do new_class do field :values, :set, of: :string @@ -528,7 +528,7 @@ end end - context 'set of number' do + context 'when set of number' do let(:class_with_typed_set) do new_class do field :values, :set, of: :number @@ -543,7 +543,7 @@ end end - context 'set of integer' do + context 'when set of integer' do let(:class_with_typed_set) do new_class do field :values, :set, of: :integer @@ -558,7 +558,7 @@ end end - context 'set of date' do + context 'when set of date' do let(:class_with_typed_set) do new_class do field :values, :set, of: :date @@ -598,7 +598,7 @@ end end - context 'set of datetime' do + context 'when set of datetime' do let(:class_with_typed_set) do new_class do field :values, :set, of: :datetime @@ -638,7 +638,7 @@ end end - context 'set of serialized' do + context 'when set of serialized' do it 'serializes elements' do class_with_typed_set = new_class do field :values, :set, of: :serialized @@ -664,7 +664,7 @@ end end - context 'set of custom type' do + context 'when set of custom type' do let(:class_with_typed_set) do new_class do field :values, :set, of: DumpingSpecs::User @@ -680,7 +680,7 @@ end end - context 'specified type is not supported' do + context 'when specified type is not supported' do let(:class_with_typed_set) do new_class do field :values, :set, of: :boolean @@ -763,7 +763,7 @@ end describe 'typed array' do - context 'array of string' do + context 'with array of string' do let(:class_with_typed_array) do new_class do field :values, :array, of: :string @@ -799,7 +799,7 @@ end end - context 'array of number' do + context 'with array of number' do let(:class_with_typed_array) do new_class do field :values, :array, of: :number @@ -814,7 +814,7 @@ end end - context 'array of integer' do + context 'with array of integer' do let(:class_with_typed_array) do new_class do field :values, :array, of: :integer @@ -829,7 +829,7 @@ end end - context 'array of date' do + context 'with array of date' do let(:class_with_typed_array) do new_class do field :values, :array, of: :date @@ -869,7 +869,7 @@ end end - context 'array of datetime' do + context 'with array of datetime' do let(:class_with_typed_array) do new_class do field :values, :array, of: :datetime @@ -909,7 +909,7 @@ end end - context 'array of serialized' do + context 'with array of serialized' do it 'serializes elements' do class_with_typed_array = new_class do field :values, :array, of: :serialized @@ -935,7 +935,7 @@ end end - context 'array of custom type' do + context 'with array of custom type' do let(:class_with_typed_array) do new_class do field :values, :array, of: DumpingSpecs::User @@ -951,7 +951,7 @@ end end - context 'specified type is not supported' do + context 'when specified type is not supported' do let(:class_with_typed_array) do new_class do field :values, :array, of: :boolean @@ -1415,9 +1415,9 @@ def self.load(str) end describe 'Custom type field' do - context 'Custom type provided' do + context 'when Custom type provided' do let(:klass) do - new_class do |_options| + new_class do field :user, DumpingSpecs::User end end @@ -1432,7 +1432,7 @@ def self.load(str) end end - context 'Adapter provided' do + context 'when Adapter provided' do let(:klass) do new_class do field :user, DumpingSpecs::UserValueAdapter @@ -1449,7 +1449,7 @@ def self.load(str) end end - context 'Custom type with adapter interface provided' do + context 'when Custom type with adapter interface provided' do let(:klass) do new_class do |_options| field :user, DumpingSpecs::UserWithAdapterInterface @@ -1464,7 +1464,7 @@ def self.load(str) end end - context 'DynamoDB type specified' do + context 'when DynamoDB type specified' do let(:klass) do new_class do field :user, DumpingSpecs::UserValueToArrayAdapter @@ -1486,7 +1486,7 @@ def self.load(str) let(:unfrozen_string) { +"\x00\x88\xFF" } let(:binary_value) { unfrozen_string.force_encoding('ASCII-8BIT') } - context 'default non-native binary' do + context 'with default non-native binary' do let(:klass) do new_class do field :image, :binary @@ -1501,7 +1501,7 @@ def self.load(str) end end - context 'native binary' do + context 'with native binary' do let(:klass) do new_class do field :image, :binary, store_as_native_binary: true @@ -1539,7 +1539,7 @@ def self.load(str) end end - context 'store_binary_as_native config option' do + context 'with store_binary_as_native config option' do it 'is stored as binary if store_binary_as_native config option is true', config: { store_binary_as_native: true } do klass = new_class do diff --git a/spec/dynamoid/type_casting_spec.rb b/spec/dynamoid/fields/type_casting_spec.rb similarity index 89% rename from spec/dynamoid/type_casting_spec.rb rename to spec/dynamoid/fields/type_casting_spec.rb index 8365acc5..d7a3727c 100644 --- a/spec/dynamoid/type_casting_spec.rb +++ b/spec/dynamoid/fields/type_casting_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true require 'spec_helper' +require 'fixtures/dumping' -describe 'Type casting' do +describe Dynamoid::Fields, 'Type casting' do # rubocop:disable RSpec/DescribeMethod describe 'Boolean field' do let(:klass) do new_class do @@ -449,7 +450,25 @@ end end - describe 'Raw field' do # rubocop:disable Lint/EmptyBlock + describe 'Raw field' do + let(:klass) do + new_class do + field :config, :raw + end + end + + it 'does not change scalar values' do + expect(klass.new(config: 1).config).to eq 1 + expect(klass.new(config: 1.0).config).to eq 1.0 + expect(klass.new(config: false).config).to eq false + expect(klass.new(config: 'foo').config).to eq 'foo' + expect(klass.new(config: :foo).config).to eq :foo + end + + it 'does not change collections' do + expect(klass.new(config: [1, 2, 3]).config).to eq [1, 2, 3] + expect(klass.new(config: { a: 'b' }).config).to eq(a: 'b') + end end describe 'Map field' do @@ -693,13 +712,56 @@ def settings.to_hash end end - describe 'Serialized field' do # rubocop:disable Lint/EmptyBlock + describe 'Serialized field' do + let(:klass) do + new_class do + field :config, :serialized + end + end + + it 'does not change scalar values' do + expect(klass.new(config: 1).config).to eq 1 + expect(klass.new(config: 1.0).config).to eq 1.0 + expect(klass.new(config: false).config).to eq false + expect(klass.new(config: 'foo').config).to eq 'foo' + expect(klass.new(config: :foo).config).to eq :foo + end + + it 'does not change collections' do + expect(klass.new(config: [1, 2, 3]).config).to eq [1, 2, 3] + expect(klass.new(config: { a: 'b' }).config).to eq(a: 'b') + end end - describe 'Custom type field' do # rubocop:disable Lint/EmptyBlock + describe 'Custom type field' do + context 'when Custom type provided' do + let(:klass) do + new_class do + field :user, DumpingSpecs::User + end + end + + it 'does not change value' do + user = DumpingSpecs::User.new('John') + expect(klass.new(user: user).user).to eq user + end + end + + context 'when Adapter provided' do + let(:klass) do + new_class do + field :user, DumpingSpecs::UserValueAdapter + end + end + + it 'does not change value' do + user = DumpingSpecs::UserValue.new('John') + expect(klass.new(user: user).user).to eq user + end + end end - context 'there is no such field' do + context 'when there is no such field' do let(:klass) do new_class do attr_accessor :active @@ -712,7 +774,7 @@ def settings.to_hash end end - context 'unknown type' do + context 'with unknown type' do let(:klass) do new_class do field :active, :some_incorrect_type diff --git a/spec/dynamoid/fields_spec.rb b/spec/dynamoid/fields_spec.rb index b14c340f..e67bb5e1 100644 --- a/spec/dynamoid/fields_spec.rb +++ b/spec/dynamoid/fields_spec.rb @@ -241,7 +241,7 @@ def city=(value) end end - context 'default values for fields' do + context 'with default values for fields' do let(:doc_class) do new_class do field :name, :string, default: 'x' @@ -338,7 +338,7 @@ def city=(value) end end - context 'an extention overrides field accessors' do + context 'when an extention overrides field accessors' do let(:klass) do extention = Module.new do def name @@ -627,7 +627,7 @@ def self.load(val) end end - context 'implicitly declared attribute' do + context 'when implicitly declared attribute' do it 'automatically declares id' do expect { address.id }.not_to raise_error end @@ -678,4 +678,113 @@ def self.load(val) end end end + + describe '#attributes_before_type_cast', config: { timestamps: false } do + let(:klass) do + new_class do + field :admin, :boolean + end + end + + it 'returns original attributes value' do + obj = klass.new(admin: 0) + + expect(obj.attributes_before_type_cast).to eql( + admin: 0, + ) + end + + it 'returns values for all the attributes even not assigned' do + klass_with_many_fields = new_class do + field :first_name + field :last_name + field :email + end + obj = klass_with_many_fields.new(first_name: 'John') + + expect(obj.attributes_before_type_cast).to eql( + first_name: 'John', + ) + end + + it 'returns original default value if field has default value' do + klass_with_default_value = new_class do + field :activated_on, :date, default: '2018-09-27' + end + obj = klass_with_default_value.new + + expect(obj.attributes_before_type_cast).to eql( + activated_on: '2018-09-27', + ) + end + + it 'returns nil if field does not have default value' do + obj = klass.new + + expect(obj.attributes_before_type_cast).to eql({}) + end + + it 'returns values loaded from the storage before type casting' do + obj = klass.create!(admin: false) + obj2 = klass.find(obj.id) + + expect(obj2.attributes_before_type_cast).to eql( + id: obj.id, + admin: false, + ) + end + end + + describe '#read_attribute_before_type_cast' do + let(:klass) do + new_class do + field :admin, :boolean + end + end + + it 'returns attribute original value' do + obj = klass.new(admin: 1) + + expect(obj.read_attribute_before_type_cast(:admin)).to eql(1) + end + + it 'accepts string as well as symbol argument' do + obj = klass.new(admin: 1) + + expect(obj.read_attribute_before_type_cast('admin')).to eql(1) + end + + it 'returns nil if there is no such attribute' do + obj = klass.new + + expect(obj.read_attribute_before_type_cast(:first_name)).to eql(nil) + end + end + + describe '#_before_type_cast' do + let(:klass) do + new_class do + field :first_name + field :last_name + field :admin, :boolean + end + end + + it 'exists for every model attribute' do + obj = klass.new + + expect(obj).to respond_to(:id) + expect(obj).to respond_to(:first_name_before_type_cast) + expect(obj).to respond_to(:last_name_before_type_cast) + expect(obj).to respond_to(:admin) + expect(obj).to respond_to(:created_at) + expect(obj).to respond_to(:updated_at) + end + + it 'returns attribute original value' do + obj = klass.new(admin: 0) + + expect(obj.admin_before_type_cast).to eql(0) + end + end end diff --git a/spec/dynamoid/finders_spec.rb b/spec/dynamoid/finders_spec.rb index 39e4c792..73783712 100644 --- a/spec/dynamoid/finders_spec.rb +++ b/spec/dynamoid/finders_spec.rb @@ -14,8 +14,8 @@ end end - context 'a single primary key provided' do - context 'simple primary key' do + context 'when a single primary key provided' do + context 'with simple primary key' do it 'finds a model' do obj = klass.create! expect(klass.find(obj.id)).to eql(obj) @@ -36,7 +36,7 @@ end end - context 'composite primary key' do + context 'with composite primary key' do it 'finds a model' do obj = klass_with_composite_key.create!(age: 12) expect(klass_with_composite_key.find(obj.id, range_key: 12)).to eql(obj) @@ -71,7 +71,7 @@ expect(klass.find(obj.id)).to be_persisted end - context 'field is not declared in document' do + context 'when field is not declared in document' do let(:class_with_not_declared_field) do new_class do field :name @@ -169,8 +169,8 @@ end end - context 'multiple primary keys provided' do - context 'simple primary key' do + context 'when multiple primary keys provided' do + context 'with simple primary key' do it 'finds models with an array of keys' do objects = (1..2).map { klass.create! } obj1, obj2 = objects @@ -221,7 +221,7 @@ end end - context 'composite primary key' do + context 'with composite primary key' do it 'finds with an array of keys' do objects = (1..2).map { |i| klass_with_composite_key.create!(age: i) } obj1, obj2 = objects @@ -358,7 +358,7 @@ expect(objects).to contain_exactly(obj1, obj2) end - context 'field is not declared in document' do + context 'when field is not declared in document' do let(:class_with_not_declared_field) do new_class do field :name @@ -380,7 +380,7 @@ end end - context 'backoff is specified' do + context 'when backoff is specified' do before do @old_backoff = Dynamoid.config.backoff @old_backoff_strategies = Dynamoid.config.backoff_strategies.dup @@ -511,10 +511,9 @@ it 'sends consistent option to the adapter' do address = Address.create!(city: 'Chicago') - expect(Dynamoid.adapter).to receive(:get_item) - .with(anything, anything, hash_including(consistent_read: true)) - .and_call_original - Address.find(address.id, consistent_read: true) + expect { + Address.find(address.id, consistent_read: true) + }.to send_request_matching(:GetItem, { TableName: Address.table_name, ConsistentRead: true }) end context 'with users' do @@ -597,7 +596,7 @@ end end - context 'find_all' do + context 'with find_all' do it 'passes options to the adapter' do pending 'This test is broken as we are overriding the consistent_read option to true inside the adapter' user_ids = [%w[1 red], %w[1 green]] @@ -653,7 +652,7 @@ def time_to_decimal(time) end.to raise_exception(Dynamoid::Errors::MissingIndex) end - context 'local secondary index' do + context 'with local secondary index' do it 'queries the local secondary index' do time = DateTime.now p1 = Post.create!(name: 'p1', post_id: 1, posted_at: time) @@ -672,7 +671,7 @@ def time_to_decimal(time) end end - context 'global secondary index' do + context 'with global secondary index' do it 'can sort' do time = DateTime.now first_visit = Bar.create!(name: 'Drank', visited_at: (time - 1.day).to_i) diff --git a/spec/dynamoid/identity_map_spec.rb b/spec/dynamoid/identity_map_spec.rb index 1bda7d91..ff3f1a89 100644 --- a/spec/dynamoid/identity_map_spec.rb +++ b/spec/dynamoid/identity_map_spec.rb @@ -11,7 +11,7 @@ Dynamoid::Config.identity_map = false end - context 'object identity' do + context 'with object identity' do it 'maintains a single object' do tweet = Tweet.create(tweet_id: 'x', group: 'one') tweet1 = Tweet.where(tweet_id: 'x', group: 'one').first @@ -19,7 +19,7 @@ end end - context 'cache' do + context 'with cache' do it 'uses cache' do tweet = Tweet.create(tweet_id: 'x', group: 'one') expect(Dynamoid::Adapter).not_to receive(:read) @@ -34,7 +34,7 @@ end end - context 'clear' do + context 'with clear' do it 'clears the identiy map' do Tweet.create(tweet_id: 'x', group: 'one') Tweet.create(tweet_id: 'x', group: 'two') diff --git a/spec/dynamoid/indexes_spec.rb b/spec/dynamoid/indexes_spec.rb index d99dee5d..13805f53 100644 --- a/spec/dynamoid/indexes_spec.rb +++ b/spec/dynamoid/indexes_spec.rb @@ -241,7 +241,7 @@ end end - context 'validation' do + context 'with validation' do it 'throws an error when :dynamoid_class is not specified' do expect do Dynamoid::Indexes::Index.new @@ -312,7 +312,7 @@ end end - context 'correct parameters' do + context 'when correct parameters' do context 'with only required params' do let(:defaults_index) do Dynamoid::Indexes::Index.new( diff --git a/spec/dynamoid/persistence/create_spec.rb b/spec/dynamoid/persistence/create_spec.rb index ad7f8c8f..e4563d83 100644 --- a/spec/dynamoid/persistence/create_spec.rb +++ b/spec/dynamoid/persistence/create_spec.rb @@ -282,8 +282,8 @@ def around_save_callback end end - context 'not unique primary key' do - context 'composite key' do + context 'with not unique primary key' do + context 'with composite key' do it 'raises RecordNotUnique error' do klass_with_composite_key.create(id: '10', age: 42) @@ -293,7 +293,7 @@ def around_save_callback end end - context 'simple key' do + context 'with simple key' do it 'raises RecordNotUnique error' do klass.create(id: '10') @@ -305,8 +305,8 @@ def around_save_callback end describe 'primary key validation' do - context 'simple primary key' do - context 'persisted model' do + context 'with simple primary key' do + context 'with persisted model' do it 'allows partition key to be nil so it will be generated' do obj = klass.create(id: nil) expect(obj.id).to be_present @@ -314,8 +314,8 @@ def around_save_callback end end - context 'composite key' do - context 'new model' do + context 'with composite key' do + context 'when new model' do it 'allows partition key to be nil so it will be generated' do obj = klass_with_composite_key.create id: nil, age: 42 expect(obj.id).to be_present @@ -384,7 +384,7 @@ def around_save_callback end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -418,14 +418,14 @@ def around_save_callback end end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil' do obj = klass.create(age: nil) expect(raw_attributes(obj)).to include(age: nil) end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil' do obj = klass.create(age: nil) @@ -434,7 +434,7 @@ def around_save_callback end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil' do obj = klass.create(age: nil) @@ -509,7 +509,7 @@ def around_save_callback end end - context 'validation' do + context 'with validation' do let(:klass_with_validation) do new_class do field :city diff --git a/spec/dynamoid/persistence/create_table_spec.rb b/spec/dynamoid/persistence/create_table_spec.rb index c7e44d32..2177a592 100644 --- a/spec/dynamoid/persistence/create_table_spec.rb +++ b/spec/dynamoid/persistence/create_table_spec.rb @@ -60,7 +60,7 @@ expect(raw_attribute_types(klass.table_name)['id']).to eql('N') end - context 'field option :store_as_string is nil' do + context 'when field option :store_as_string is nil' do it 'maps :datetime to String if :store_datetime_as_string is true', config: { store_datetime_as_string: true } do klass = new_class(partition_key: { name: :id, type: :datetime, options: { store_as_string: nil } }) klass.create_table @@ -94,7 +94,7 @@ expect(raw_attribute_types(klass.table_name)['id']).to eql('N') end - context 'field option :store_as_string is nil' do + context 'when field option :store_as_string is nil' do it 'maps :date to String if :store_date_as_string is true', config: { store_date_as_string: true } do klass = new_class(partition_key: { name: :id, type: :date, options: { store_as_string: nil } }) klass.create_table @@ -214,7 +214,7 @@ expect(raw_attribute_types(klass.table_name)['prop']).to eql('N') end - context 'field option :store_as_string is nil' do + context 'when field option :store_as_string is nil' do it 'maps :datetime to String if :store_datetime_as_string is true', config: { store_datetime_as_string: true } do klass = new_class do range :prop, :datetime, store_as_string: nil @@ -263,7 +263,7 @@ expect(raw_attribute_types(klass.table_name)['prop']).to eql('N') end - context 'field option :store_as_string is nil' do + context 'when field option :store_as_string is nil' do it 'maps :date to String if :store_date_as_string is true', config: { store_date_as_string: true } do klass = new_class do range :prop, :date, store_as_string: nil @@ -363,21 +363,29 @@ end it 'sets up TTL for table' do - expect(Dynamoid.adapter).to receive(:update_time_to_live) - .with(class_with_expiration.table_name, :ttl) - .and_call_original - - class_with_expiration.create_table + expect { + class_with_expiration.create_table + }.to send_request_matching(:UpdateTimeToLive, { + TableName: class_with_expiration.table_name, + TimeToLiveSpecification: { + 'AttributeName' => 'ttl', + 'Enabled' => true + } + }) end it 'sets up TTL for table with specified table_name' do table_name = "#{class_with_expiration.table_name}_alias" - expect(Dynamoid.adapter).to receive(:update_time_to_live) - .with(table_name, :ttl) - .and_call_original - - class_with_expiration.create_table(table_name: table_name) + expect { + class_with_expiration.create_table(table_name: table_name) + }.to send_request_matching(:UpdateTimeToLive, { + TableName: table_name, + TimeToLiveSpecification: { + 'AttributeName' => 'ttl', + 'Enabled' => true + } + }) end end diff --git a/spec/dynamoid/persistence/decrement_spec.rb b/spec/dynamoid/persistence/decrement_spec.rb index 8c8a7c3d..a060a4be 100644 --- a/spec/dynamoid/persistence/decrement_spec.rb +++ b/spec/dynamoid/persistence/decrement_spec.rb @@ -216,14 +216,14 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do obj = klass.new expect { obj.decrement!(:age) }.to raise_error(Dynamoid::Errors::MissingHashKey) end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do obj = klass_with_composite_key.new(name: 'Alex') expect { obj.decrement!(:age) }.to raise_error(Dynamoid::Errors::MissingHashKey) diff --git a/spec/dynamoid/persistence/delete_spec.rb b/spec/dynamoid/persistence/delete_spec.rb index cdcf616b..ccd02093 100644 --- a/spec/dynamoid/persistence/delete_spec.rb +++ b/spec/dynamoid/persistence/delete_spec.rb @@ -100,7 +100,7 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do klass = new_class expect { klass.delete(nil) }.to raise_exception(Dynamoid::Errors::MissingHashKey) @@ -108,7 +108,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do expect { klass_with_composite_key.delete(nil, 1) }.to raise_exception(Dynamoid::Errors::MissingHashKey) end @@ -226,7 +226,7 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do klass = new_class obj = klass.create! @@ -236,7 +236,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do obj = klass_with_composite_key.create!(age: 1) obj.id = nil @@ -253,7 +253,7 @@ end end - context 'optimistic locking' do + context 'with optimistic locking' do let(:klass) do new_class do field :name diff --git a/spec/dynamoid/persistence/destroy_spec.rb b/spec/dynamoid/persistence/destroy_spec.rb index 7ed9ab1c..9519b324 100644 --- a/spec/dynamoid/persistence/destroy_spec.rb +++ b/spec/dynamoid/persistence/destroy_spec.rb @@ -72,7 +72,7 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do obj = klass.create!(name: 'one') obj.id = nil @@ -81,7 +81,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do obj = klass_with_composite_key.create!(name: 'one', age: 1) obj.id = nil @@ -98,7 +98,7 @@ end end - context 'optimistic locking' do + context 'with optimistic locking' do let(:klass) do new_class do field :name diff --git a/spec/dynamoid/persistence/import_spec.rb b/spec/dynamoid/persistence/import_spec.rb index 850dff98..7890c901 100644 --- a/spec/dynamoid/persistence/import_spec.rb +++ b/spec/dynamoid/persistence/import_spec.rb @@ -83,9 +83,10 @@ end it 'makes batch operation' do - expect(Dynamoid.adapter).to receive(:batch_write_item).and_call_original klass.create_table - klass.import([{ city: 'Chicago' }, { city: 'New York' }]) + expect { + klass.import([{ city: 'Chicago' }, { city: 'New York' }]) + }.to send_request_matching(:BatchWriteItem, { RequestItems: { klass.table_name => anything } }) end it 'supports empty containers in serialized fields' do @@ -226,7 +227,7 @@ expect(obj.changed?).to eql false end - context 'backoff is specified' do + context 'when backoff is specified' do let(:backoff_strategy) do ->(_) { -> { @counter += 1 } } end @@ -304,7 +305,7 @@ end end - context ':raw field' do + context 'with :raw field' do let(:klass) do new_class do field :hash, :raw @@ -349,7 +350,7 @@ end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -395,7 +396,7 @@ klass.create_table end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil' do objects = klass.import([{ age: nil }]) obj = objects[0] @@ -404,7 +405,7 @@ end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil' do objects = klass.import([{ age: nil }]) obj = objects[0] @@ -414,7 +415,7 @@ end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil' do objects = klass.import([{ age: nil }]) obj = objects[0] diff --git a/spec/dynamoid/persistence/inc_spec.rb b/spec/dynamoid/persistence/inc_spec.rb index 594dcc31..93457185 100644 --- a/spec/dynamoid/persistence/inc_spec.rb +++ b/spec/dynamoid/persistence/inc_spec.rb @@ -162,7 +162,7 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do expect { document_class.inc(nil, links_count: 1) @@ -170,7 +170,7 @@ end end - context 'composite key' do + context 'with composite key' do let(:klass) do new_class do range :name diff --git a/spec/dynamoid/persistence/increment_spec.rb b/spec/dynamoid/persistence/increment_spec.rb index 1da2fea9..6c57cd23 100644 --- a/spec/dynamoid/persistence/increment_spec.rb +++ b/spec/dynamoid/persistence/increment_spec.rb @@ -216,14 +216,14 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do obj = klass.new expect { obj.increment!(:age) }.to raise_error(Dynamoid::Errors::MissingHashKey) end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do obj = klass_with_composite_key.new(name: 'Alex') expect { obj.increment!(:age) }.to raise_error(Dynamoid::Errors::MissingHashKey) diff --git a/spec/dynamoid/persistence/save_spec.rb b/spec/dynamoid/persistence/save_spec.rb index b5278fa7..df6dc938 100644 --- a/spec/dynamoid/persistence/save_spec.rb +++ b/spec/dynamoid/persistence/save_spec.rb @@ -71,11 +71,11 @@ it 'creates table if it does not exist' do model = klass.new - expect(klass).to receive(:create_table).with(sync: true).and_call_original - - expect { model.save } - .to change { tables_created.include?(klass.table_name) } - .from(false).to(true) + expect { + expect { model.save } + .to change { tables_created.include?(klass.table_name) } + .from(false).to(true) + }.to send_request_matching(:CreateTable, { TableName: klass.table_name }) end it 'dumps attribute values' do @@ -142,18 +142,21 @@ end it 'does not make a request to persist a model if there is no any changed attribute' do - obj = klass.create(name: 'Alex') - - expect(Dynamoid.adapter).to receive(:update_item).and_call_original + obj = klass.create!(name: 'Alex') obj.name = 'Michael' - obj.save - expect(Dynamoid.adapter).not_to receive(:update_item).and_call_original - obj.save + expect { + obj.save + }.to send_request_matching(:UpdateItem, { TableName: klass.table_name }) + + expect { + obj.save + }.not_to send_request_matching(:UpdateItem) - expect(Dynamoid.adapter).not_to receive(:update_item) obj_loaded = klass.find(obj.id) - obj_loaded.save + expect { + obj_loaded.save + }.not_to send_request_matching(:UpdateItem) end it 'returns true if there is no any changed attribute' do @@ -165,8 +168,9 @@ end it 'calls PutItem for a new record' do - expect(Dynamoid.adapter).to receive(:write).and_call_original - klass.create(name: 'Alex') + expect { + klass.create(name: 'Alex') + }.to send_request_matching(:PutItem, { TableName: klass.table_name }) end it 'calls UpdateItem for already persisted record' do @@ -178,8 +182,9 @@ obj = klass.create!(name: 'Alex', age: 21) obj.age = 31 - expect(Dynamoid.adapter).to receive(:update_item).and_call_original - obj.save + expect { + obj.save + }.to send_request_matching(:UpdateItem, { TableName: klass.table_name }) end context 'when a model was concurrently deleted' do @@ -376,8 +381,8 @@ end end - context 'primary key dumping' do - context 'new model' do + context 'with primary key dumping' do + context 'when new model' do it 'uses dumped value of partition key to save item' do klass = new_class(partition_key: { name: :published_on, type: :date }) do field :title @@ -404,7 +409,7 @@ end end - context 'persisted model' do + context 'with persisted model' do it 'uses dumped value of partition key to save item' do klass = new_class(partition_key: { name: :published_on, type: :date }) do field :title @@ -435,7 +440,7 @@ end describe 'callbacks' do - context 'new model' do + context 'when new model' do it 'runs before_create callback' do klass_with_callback = new_class do field :name @@ -573,7 +578,7 @@ def around_save_callback end end - context 'persisted model' do + context 'with persisted model' do it 'runs before_update callback' do klass_with_callback = new_class do field :name @@ -870,8 +875,8 @@ def around_save_callback end end - context 'not unique primary key' do - context 'composite key' do + context 'with not unique primary key' do + context 'with composite key' do it 'raises RecordNotUnique error' do klass_with_composite_key.create(id: '10', age: 42) obj = klass_with_composite_key.new(id: '10', age: 42) @@ -880,7 +885,7 @@ def around_save_callback end end - context 'simple key' do + context 'with simple key' do it 'raises RecordNotUnique error' do klass.create(id: '10') obj = klass.new(id: '10') @@ -890,7 +895,7 @@ def around_save_callback end end - context ':raw field' do + context 'with :raw field' do let(:klass) do new_class do field :hash, :raw @@ -909,8 +914,8 @@ def around_save_callback end describe 'primary key validation' do - context 'simple primary key' do - context 'persisted model' do + context 'with simple primary key' do + context 'with persisted model' do it 'requires partition key to be specified' do obj = klass.create!(name: 'Alex') obj.id = nil @@ -921,8 +926,8 @@ def around_save_callback end end - context 'composite key' do - context 'new model' do + context 'with composite key' do + context 'when new model' do it 'requires sort key to be specified' do obj = klass_with_composite_key.new name: 'Alex', age: nil @@ -930,7 +935,7 @@ def around_save_callback end end - context 'persisted model' do + context 'with persisted model' do it 'requires partition key to be specified' do obj = klass_with_composite_key.create!(name: 'Alex', age: 3) obj.id = nil @@ -957,7 +962,7 @@ def around_save_callback end end - context 'new record' do + context 'when new record' do it 'sets created_at and updated_at if Config.timestamps=true', config: { timestamps: true } do travel 1.hour do time_now = Time.now @@ -988,7 +993,7 @@ def around_save_callback end end - context 'persisted record' do + context 'with persisted record' do it 'does not change created_at if Config.timestamps=true', config: { timestamps: true } do obj = klass.create(title: 'Old title') @@ -1093,7 +1098,7 @@ def around_save_callback end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -1103,7 +1108,7 @@ def around_save_callback end end - context 'new model' do + context 'when new model' do it 'persists successfuly even if a field declared as a GSI primary key is set to nil' do obj = klass_with_gsi.new(name: nil, age: 42) obj.save @@ -1127,7 +1132,7 @@ def around_save_callback end end - context 'persisted model' do + context 'with persisted model' do it 'saves successfuly even if a field declared as a GSI primary key is set to nil' do obj = klass_with_gsi.create!(name: 'Alex', age: 42) obj.name = nil @@ -1151,7 +1156,7 @@ def around_save_callback end end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil when model is not persisted' do obj = klass.new(age: nil) obj.save @@ -1168,7 +1173,7 @@ def around_save_callback end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil when model is not persisted' do obj = klass.new(age: nil) obj.save @@ -1187,7 +1192,7 @@ def around_save_callback end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil when model is not persisted' do obj = klass.new(age: nil) obj.save diff --git a/spec/dynamoid/persistence/touch_spec.rb b/spec/dynamoid/persistence/touch_spec.rb index d8f014f9..ed31199f 100644 --- a/spec/dynamoid/persistence/touch_spec.rb +++ b/spec/dynamoid/persistence/touch_spec.rb @@ -122,7 +122,7 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do obj = klass.create! obj.id = nil @@ -130,7 +130,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do obj = klass_with_composite_key.create!(name: 'Alex') obj.id = nil @@ -180,7 +180,7 @@ end end - context 'concurrent deletion' do + context 'with concurrent deletion' do it 'skips changes for simple primary key' do obj = klass.create! klass.find(obj.id).delete diff --git a/spec/dynamoid/persistence/update_attribute_spec.rb b/spec/dynamoid/persistence/update_attribute_spec.rb index e24683d6..d546d11d 100644 --- a/spec/dynamoid/persistence/update_attribute_spec.rb +++ b/spec/dynamoid/persistence/update_attribute_spec.rb @@ -367,7 +367,7 @@ def around_update_callback end end - context 'concurrent deletion' do + context 'with concurrent deletion' do it 'does not persist changes when simple primary key' do obj = klass.create!(age: 21) klass.find(obj.id).delete @@ -419,7 +419,7 @@ def around_update_callback end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -449,7 +449,7 @@ def around_update_callback end end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil' do obj = klass.create!(age: 42) obj.update_attribute(:age, nil) @@ -458,7 +458,7 @@ def around_update_callback end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil' do obj = klass.create!(age: 42) obj.update_attribute(:age, nil) @@ -468,7 +468,7 @@ def around_update_callback end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil' do obj = klass.create!(age: 42) obj.update_attribute(:age, nil) diff --git a/spec/dynamoid/persistence/update_attributes_spec.rb b/spec/dynamoid/persistence/update_attributes_spec.rb index dd354d89..f6289e4b 100644 --- a/spec/dynamoid/persistence/update_attributes_spec.rb +++ b/spec/dynamoid/persistence/update_attributes_spec.rb @@ -71,7 +71,7 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do obj = klass.create!(name: 'Alex') obj.id = nil @@ -82,7 +82,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do obj = klass_with_composite_key.create!(name: 'Alex', age: 3) obj.id = nil @@ -256,7 +256,7 @@ end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -286,7 +286,7 @@ end end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil' do obj = klass.create!(age: 42) obj.update_attributes(age: nil) @@ -295,7 +295,7 @@ end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil' do obj = klass.create!(age: 42) obj.update_attributes(age: nil) @@ -305,7 +305,7 @@ end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil' do obj = klass.create!(age: 42) obj.update_attributes(age: nil) @@ -464,7 +464,7 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do obj = klass.create!(name: 'Alex') obj.id = nil @@ -475,7 +475,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do obj = klass_with_composite_key.create!(name: 'Alex', age: 3) obj.id = nil @@ -779,7 +779,7 @@ def around_update_callback end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -809,7 +809,7 @@ def around_update_callback end end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil' do obj = klass.create!(age: 42) obj.update_attributes!(age: nil) @@ -818,7 +818,7 @@ def around_update_callback end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil' do obj = klass.create!(age: 42) obj.update_attributes!(age: nil) @@ -828,7 +828,7 @@ def around_update_callback end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil' do obj = klass.create!(age: 42) obj.update_attributes!(age: nil) diff --git a/spec/dynamoid/persistence/update_fields_spec.rb b/spec/dynamoid/persistence/update_fields_spec.rb index 27198d24..865675c0 100644 --- a/spec/dynamoid/persistence/update_fields_spec.rb +++ b/spec/dynamoid/persistence/update_fields_spec.rb @@ -49,7 +49,7 @@ expect(result.title).to eq 'New title' end - context 'condition specified' do + context 'when condition specified' do describe 'if condition' do it 'updates when model matches conditions' do obj = klass.create(title: 'Old title', version: 1) @@ -233,7 +233,7 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do obj = klass.create!(title: 'Alex') @@ -243,7 +243,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do obj = klass_with_composite_key.create!(name: 'Alex', age: 3) @@ -329,7 +329,7 @@ end end - context ':raw field' do + context 'with :raw field' do let(:klass) do new_class do field :hash, :raw @@ -407,7 +407,7 @@ end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -441,7 +441,7 @@ end end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil' do obj = klass.create!(age: 42) klass.update_fields(obj.id, age: nil) @@ -450,7 +450,7 @@ end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil' do obj = klass.create!(age: 42) klass.update_fields(obj.id, age: nil) @@ -460,7 +460,7 @@ end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil' do obj = klass.create!(age: 42) klass.update_fields(obj.id, age: nil) diff --git a/spec/dynamoid/persistence/update_spec.rb b/spec/dynamoid/persistence/update_spec.rb index 463d2344..7167710b 100644 --- a/spec/dynamoid/persistence/update_spec.rb +++ b/spec/dynamoid/persistence/update_spec.rb @@ -831,7 +831,7 @@ def around_update_callback expect(@tweet.user_name).to eq 'Alex' end - context 'condition specified' do + context 'when condition specified' do let(:document_class) do new_class do field :title @@ -1063,7 +1063,7 @@ def around_update_callback end end - context ':raw field' do + context 'with :raw field' do let(:klass) do new_class do field :hash, :raw diff --git a/spec/dynamoid/persistence/upsert_spec.rb b/spec/dynamoid/persistence/upsert_spec.rb index ec76122a..2afd7acc 100644 --- a/spec/dynamoid/persistence/upsert_spec.rb +++ b/spec/dynamoid/persistence/upsert_spec.rb @@ -42,7 +42,7 @@ expect(result.title).to eq 'New title' end - context 'conditions specified' do + context 'when conditions specified' do describe 'if condition' do it 'updates when model matches conditions' do obj = klass.create(title: 'Old title', version: 1) @@ -247,7 +247,7 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do klass.create_table @@ -257,7 +257,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do klass_with_composite_key.create_table @@ -344,7 +344,7 @@ end end - context ':raw field' do + context 'with :raw field' do let(:klass) do new_class do field :hash, :raw @@ -419,7 +419,7 @@ end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -433,7 +433,7 @@ klass_with_gsi.create_table end - context 'new model' do + context 'when new model' do it 'persists successfuly even if a field declared as a GSI primary key is set to nil' do id_new = SecureRandom.uuid expect do @@ -457,7 +457,7 @@ end end - context 'existing model' do + context 'when existing model' do it 'updates successfuly even if a field declared as a GSI primary key is set to nil' do obj = klass_with_gsi.create!(name: 'Alex', age: 42) @@ -487,7 +487,7 @@ klass.create_table end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil when model is not persisted' do id = SecureRandom.uuid klass.upsert(id, age: nil) @@ -504,7 +504,7 @@ end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil when model is not persisted' do id = SecureRandom.uuid klass.upsert(id, age: nil) @@ -523,7 +523,7 @@ end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil when model is not persisted' do id = SecureRandom.uuid klass.upsert(id, age: nil) diff --git a/spec/dynamoid/persistence_spec.rb b/spec/dynamoid/persistence_spec.rb index 13787154..a4b84745 100644 --- a/spec/dynamoid/persistence_spec.rb +++ b/spec/dynamoid/persistence_spec.rb @@ -4,5 +4,285 @@ require 'fixtures/persistence' describe Dynamoid::Persistence do - # TODO: add high level specs for all the persistence methods (see critiria_new_spec.rb) + describe 'class methods' do + it 'supports .table_name' do + klass = new_class + expect(klass.table_name).to be_present + end + + it 'supports .create_table' do + klass = new_class + klass.create_table + expect(Dynamoid.adapter.list_tables).to include(klass.table_name) + end + + it 'supports .delete_table' do + klass = new_class + klass.create_table + expect(Dynamoid.adapter.list_tables).to include(klass.table_name) + klass.delete_table + expect(Dynamoid.adapter.list_tables).not_to include(klass.table_name) + end + + it 'supports .import' do + klass = new_class do + field :name + end + klass.create_table + instances = klass.import([{ name: 'Alex' }, { name: 'Bob' }]) + expect(instances.size).to eq 2 + expect(klass.all.to_a).to match_array(instances) + end + + it 'supports .create' do + klass = new_class do + field :name + end + instance = klass.create(name: 'Alex') + expect(instance.persisted?).to eq true + expect(instance.name).to eq 'Alex' + end + + it 'supports .create!' do + klass = new_class do + field :name + end + instance = klass.create!(name: 'Alex') + expect(instance.persisted?).to eq true + expect(instance.name).to eq 'Alex' + end + + it 'supports .update' do + klass = new_class do + field :name + end + instance = klass.create!(name: 'Alex') + updated = klass.update(instance.id, name: 'Bob') + expect(updated.name).to eq 'Bob' + expect(klass.find(instance.id).name).to eq 'Bob' + end + + it 'supports .update!' do + klass = new_class do + field :name + end + instance = klass.create!(name: 'Alex') + updated = klass.update!(instance.id, name: 'Bob') + expect(updated.name).to eq 'Bob' + end + + it 'supports .update_fields' do + klass = new_class do + field :name + end + instance = klass.create!(name: 'Alex') + updated = klass.update_fields(instance.id, name: 'Bob') + expect(updated.name).to eq 'Bob' + expect(klass.find(instance.id).name).to eq 'Bob' + end + + it 'supports .upsert' do + klass = new_class do + field :name + end + klass.create_table + instance = klass.upsert('123', name: 'Alex') + expect(instance.id).to eq '123' + expect(instance.name).to eq 'Alex' + expect(klass.find('123').name).to eq 'Alex' + end + + it 'supports .inc' do + klass = new_class do + field :age, :integer + end + instance = klass.create!(age: 10) + klass.inc(instance.id, age: 5) + expect(klass.find(instance.id).age).to eq 15 + end + + it 'supports .delete' do + klass = new_class do + field :name + end + instance = klass.create!(name: 'Alex') + expect(klass.find(instance.id)).to be_present + klass.delete(instance.id) + expect { klass.find(instance.id) }.to raise_error(Dynamoid::Errors::RecordNotFound) + end + end + + describe 'instance methods' do + it 'supports #touch' do + klass = new_class do + field :name + end + instance = klass.create!(name: 'Alex') + + old_updated_at = instance.updated_at + sleep 0.1 + instance.touch + expect(instance.updated_at).to be > old_updated_at + expect(klass.find(instance.id).updated_at).to be > old_updated_at + end + + it 'supports #persisted?' do + klass = new_class + instance = klass.new + expect(instance.persisted?).to eq false + instance.save + expect(instance.persisted?).to eq true + instance.delete + expect(instance.persisted?).to eq false + end + + it 'supports #save' do + klass = new_class do + field :name + end + instance = klass.new(name: 'Alex') + expect(instance.save).to be_truthy + expect(klass.find(instance.id).name).to eq 'Alex' + end + + it 'supports #save!' do + klass = new_class do + field :name + validates :name, presence: true + end + instance = klass.new(name: 'Alex') + expect(instance.save!).to eq instance + expect(klass.find(instance.id).name).to eq 'Alex' + end + + it 'supports #update_attributes' do + klass = new_class do + field :name + end + instance = klass.create!(name: 'Alex') + expect(instance.update_attributes(name: 'Bob')).to be_truthy + expect(klass.find(instance.id).name).to eq 'Bob' + end + + it 'supports #update_attributes!' do + klass = new_class do + field :name + end + instance = klass.create!(name: 'Alex') + expect(instance.update_attributes!(name: 'Bob')).to be_truthy + expect(klass.find(instance.id).name).to eq 'Bob' + end + + it 'supports #update_attribute' do + klass = new_class do + field :name + end + instance = klass.create!(name: 'Alex') + instance.update_attribute(:name, 'Bob') + expect(klass.find(instance.id).name).to eq 'Bob' + end + + it 'supports #update_attribute!' do + klass = new_class do + field :name + validates :name, presence: true + end + instance = klass.create!(name: 'Alex') + instance.update_attribute!(:name, 'Bob') + expect(klass.find(instance.id).name).to eq 'Bob' + end + + it 'supports #update!' do + klass = new_class do + field :age, :integer + end + instance = klass.create!(age: 20) + instance.update! do |t| + t.add(age: 5) + end + expect(instance.age).to eq 25 + expect(klass.find(instance.id).age).to eq 25 + end + + it 'supports #update' do + klass = new_class do + field :age, :integer + end + instance = klass.create!(age: 20) + expect(instance.update { |t| t.set(age: 30) }).to eq true + expect(instance.age).to eq 30 + expect(klass.find(instance.id).age).to eq 30 + end + + it 'supports #increment' do + klass = new_class do + field :age, :integer + end + instance = klass.create!(age: 10) + + instance.increment(:age, 3) + expect(instance.age).to eq 13 + expect(klass.find(instance.id).age).to eq 10 + end + + it 'supports #increment!' do + klass = new_class do + field :age, :integer + end + instance = klass.create!(age: 10) + + instance.increment!(:age, 2) + expect(instance.age).to eq 12 + expect(klass.find(instance.id).age).to eq 12 + end + + it 'supports #decrement' do + klass = new_class do + field :age, :integer + end + instance = klass.create!(age: 10) + + instance.decrement(:age, 3) + expect(instance.age).to eq 7 + expect(klass.find(instance.id).age).to eq 10 + end + + it 'supports #decrement!' do + klass = new_class do + field :age, :integer + end + instance = klass.create!(age: 10) + + instance.decrement!(:age, 2) + expect(instance.age).to eq 8 + expect(klass.find(instance.id).age).to eq 8 + end + + it 'supports #destroy' do + klass = new_class do + field :name + end + instance = klass.create!(name: 'Alex') + expect(instance.destroy).to eq instance + expect { klass.find(instance.id) }.to raise_error(Dynamoid::Errors::RecordNotFound) + end + + it 'supports #destroy!' do + klass = new_class do + field :name + end + instance = klass.create!(name: 'Alex') + expect(instance.destroy!).to eq instance + expect { klass.find(instance.id) }.to raise_error(Dynamoid::Errors::RecordNotFound) + end + + it 'supports #delete' do + klass = new_class do + field :name + end + instance = klass.create!(name: 'Alex') + expect(instance.delete).to eq instance + expect { klass.find(instance.id) }.to raise_error(Dynamoid::Errors::RecordNotFound) + end + end end diff --git a/spec/dynamoid/sti_spec.rb b/spec/dynamoid/sti_spec.rb index 4bd04321..7348d94f 100644 --- a/spec/dynamoid/sti_spec.rb +++ b/spec/dynamoid/sti_spec.rb @@ -17,12 +17,6 @@ end end - let!(:class_c) do - Class.new(class_a) do - field :c - end - end - it 'enables only own attributes in a base class' do expect(class_a.attributes.keys).to match_array(%i[id type a created_at updated_at]) end diff --git a/spec/dynamoid/transactions/mutation/commit_spec.rb b/spec/dynamoid/transactions/mutation/commit_spec.rb index a3dcb7e3..80908578 100644 --- a/spec/dynamoid/transactions/mutation/commit_spec.rb +++ b/spec/dynamoid/transactions/mutation/commit_spec.rb @@ -32,7 +32,7 @@ end end - context 'transaction succeeds' do + context 'with transaction succeeds' do it 'runs #after_commit callbacks for each involved model' do klass.create_table @@ -45,7 +45,7 @@ end end - context 'transaction fails' do + context 'with transaction fails' do before do ScratchPad.clear end @@ -66,7 +66,7 @@ end end - context 'transaction interrupted by exception in a callback' do + context 'with transaction interrupted by exception in a callback' do before do ScratchPad.clear end diff --git a/spec/dynamoid/transactions/mutation/create_spec.rb b/spec/dynamoid/transactions/mutation/create_spec.rb index c22bf3ed..2185f100 100644 --- a/spec/dynamoid/transactions/mutation/create_spec.rb +++ b/spec/dynamoid/transactions/mutation/create_spec.rb @@ -125,7 +125,7 @@ def around_save_callback it 'can be called without attributes to modify' describe 'primary key schema' do - context 'simple primary key' do + context 'with simple primary key' do it 'persists a model' do klass.create_table @@ -137,7 +137,7 @@ def around_save_callback end end - context 'composite key' do + context 'with composite key' do it 'persists a model' do klass_with_composite_key.create_table @@ -151,7 +151,7 @@ def around_save_callback end describe 'primary key validation' do - context 'composite key' do + context 'with composite key' do it 'requires sort key to be specified' do klass_with_composite_key.create_table @@ -617,7 +617,7 @@ def around_save_callback end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -667,7 +667,7 @@ def around_save_callback klass.create_table end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil' do obj = nil described_class.execute { |t| obj = t.create klass, age: nil } @@ -675,7 +675,7 @@ def around_save_callback end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil' do obj = nil described_class.execute { |t| obj = t.create klass, age: nil } @@ -683,7 +683,7 @@ def around_save_callback end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil' do obj = nil described_class.execute { |t| obj = t.create klass, age: nil } @@ -817,7 +817,7 @@ def around_save_callback end describe 'primary key schema' do - context 'simple primary key' do + context 'with simple primary key' do it 'persists a model' do klass.create_table @@ -829,7 +829,7 @@ def around_save_callback end end - context 'composite key' do + context 'with composite key' do it 'persists a model' do klass_with_composite_key.create_table @@ -843,7 +843,7 @@ def around_save_callback end describe 'primary key validation' do - context 'composite key' do + context 'with composite key' do it 'requires sort key to be specified' do klass_with_composite_key.create_table @@ -1101,7 +1101,7 @@ def around_save_callback end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -1151,7 +1151,7 @@ def around_save_callback klass.create_table end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil' do obj = nil described_class.execute { |t| obj = t.create! klass, age: nil } @@ -1159,7 +1159,7 @@ def around_save_callback end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil' do obj = nil described_class.execute { |t| obj = t.create! klass, age: nil } @@ -1167,7 +1167,7 @@ def around_save_callback end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil' do obj = nil described_class.execute { |t| obj = t.create! klass, age: nil } diff --git a/spec/dynamoid/transactions/mutation/decrement_spec.rb b/spec/dynamoid/transactions/mutation/decrement_spec.rb index 1014aee0..2a0afd99 100644 --- a/spec/dynamoid/transactions/mutation/decrement_spec.rb +++ b/spec/dynamoid/transactions/mutation/decrement_spec.rb @@ -256,7 +256,7 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do obj = klass.new expect { @@ -267,7 +267,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do obj = klass_with_composite_key.new(name: 'Alex') expect { diff --git a/spec/dynamoid/transactions/mutation/delete_spec.rb b/spec/dynamoid/transactions/mutation/delete_spec.rb index 869319b4..9765aacb 100644 --- a/spec/dynamoid/transactions/mutation/delete_spec.rb +++ b/spec/dynamoid/transactions/mutation/delete_spec.rb @@ -41,7 +41,7 @@ end describe 'primary key schemas' do - context 'simple primary key' do + context 'with simple primary key' do it 'deletes a model' do obj = klass.create!(name: 'one') @@ -53,7 +53,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'deletes a model' do obj = klass_with_composite_key.create!(name: 'one', age: 1) @@ -67,7 +67,7 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do obj = klass.create!(name: 'one') obj.id = nil @@ -80,7 +80,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do obj = klass_with_composite_key.create!(name: 'one', age: 1) obj.id = nil @@ -394,7 +394,7 @@ def around_destroy_callback end describe 'primary key schemas' do - context 'simple primary key' do + context 'with simple primary key' do it 'deletes a model' do obj = klass.create!(name: 'one') @@ -406,7 +406,7 @@ def around_destroy_callback end end - context 'composite key' do + context 'with composite key' do it 'deletes a model' do obj = klass_with_composite_key.create!(name: 'one', age: 1) @@ -420,7 +420,7 @@ def around_destroy_callback end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'raises MissingHashKey if partition key is not specified' do expect { described_class.execute do |txn| @@ -430,7 +430,7 @@ def around_destroy_callback end end - context 'composite key' do + context 'with composite key' do it 'raises MissingHashKey if partition key is not specified' do expect { described_class.execute do |txn| diff --git a/spec/dynamoid/transactions/mutation/destroy_spec.rb b/spec/dynamoid/transactions/mutation/destroy_spec.rb index d719ebc9..45d48676 100644 --- a/spec/dynamoid/transactions/mutation/destroy_spec.rb +++ b/spec/dynamoid/transactions/mutation/destroy_spec.rb @@ -55,7 +55,7 @@ def around_destroy_callback end describe 'primary key schemas' do - context 'simple primary key' do + context 'with simple primary key' do it 'deletes a model' do obj = klass.create!(name: 'one') @@ -67,7 +67,7 @@ def around_destroy_callback end end - context 'composite key' do + context 'with composite key' do it 'deletes a model' do obj = klass_with_composite_key.create!(name: 'one', age: 1) @@ -81,7 +81,7 @@ def around_destroy_callback end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do obj = klass.create!(name: 'one') obj.id = nil @@ -94,7 +94,7 @@ def around_destroy_callback end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do obj = klass_with_composite_key.create!(name: 'one', age: 1) obj.id = nil diff --git a/spec/dynamoid/transactions/mutation/execute_spec.rb b/spec/dynamoid/transactions/mutation/execute_spec.rb index f65a8faa..e195d042 100644 --- a/spec/dynamoid/transactions/mutation/execute_spec.rb +++ b/spec/dynamoid/transactions/mutation/execute_spec.rb @@ -21,7 +21,7 @@ end describe 'callbacks' do - context 'transaction succeeds' do + context 'with transaction succeeds' do before do ScratchPad.clear end @@ -85,7 +85,7 @@ end end - context 'transaction interrupted by user exception' do + context 'with transaction interrupted by user exception' do before do ScratchPad.clear end @@ -175,7 +175,7 @@ end end - context 'transaction interrupted by Dynamoid::Error::Rollback exception' do + context 'with transaction interrupted by Dynamoid::Error::Rollback exception' do before do ScratchPad.clear end diff --git a/spec/dynamoid/transactions/mutation/import_spec.rb b/spec/dynamoid/transactions/mutation/import_spec.rb index 8a45f7bd..7aa0055a 100644 --- a/spec/dynamoid/transactions/mutation/import_spec.rb +++ b/spec/dynamoid/transactions/mutation/import_spec.rb @@ -364,7 +364,7 @@ expect(obj.changed?).to eql false end - context ':raw field' do + context 'with :raw field' do let(:klass) do new_class do field :hash, :raw @@ -394,7 +394,7 @@ end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -444,7 +444,7 @@ klass.create_table end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil' do objects = nil described_class.execute do |t| @@ -456,7 +456,7 @@ end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil' do objects = nil described_class.execute do |t| @@ -469,7 +469,7 @@ end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil' do objects = nil described_class.execute do |t| diff --git a/spec/dynamoid/transactions/mutation/inc_spec.rb b/spec/dynamoid/transactions/mutation/inc_spec.rb index f24829b0..fc852c5a 100644 --- a/spec/dynamoid/transactions/mutation/inc_spec.rb +++ b/spec/dynamoid/transactions/mutation/inc_spec.rb @@ -204,7 +204,7 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do expect { described_class.execute do |t| @@ -214,7 +214,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do expect { described_class.execute do |t| diff --git a/spec/dynamoid/transactions/mutation/increment_spec.rb b/spec/dynamoid/transactions/mutation/increment_spec.rb index 2edea81e..47172286 100644 --- a/spec/dynamoid/transactions/mutation/increment_spec.rb +++ b/spec/dynamoid/transactions/mutation/increment_spec.rb @@ -258,7 +258,7 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do obj = klass.new expect { @@ -269,7 +269,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do obj = klass_with_composite_key.new(name: 'Alex') expect { diff --git a/spec/dynamoid/transactions/mutation/save_spec.rb b/spec/dynamoid/transactions/mutation/save_spec.rb index 59e1987b..04be19e7 100644 --- a/spec/dynamoid/transactions/mutation/save_spec.rb +++ b/spec/dynamoid/transactions/mutation/save_spec.rb @@ -106,7 +106,7 @@ def around_save_callback end describe 'primary key schema' do - context 'simple primary key' do + context 'with simple primary key' do it 'persists a new model' do klass.create_table obj = klass.new @@ -130,7 +130,7 @@ def around_save_callback end end - context 'composite key' do + context 'with composite key' do it 'persists a new model' do klass_with_composite_key.create_table obj = klass_with_composite_key.new(age: 3) @@ -156,8 +156,8 @@ def around_save_callback end describe 'primary key validation' do - context 'simple primary key' do - context 'persisted model' do + context 'with simple primary key' do + context 'with persisted model' do it 'requires partition key to be specified' do obj = klass.create!(name: 'Alex') obj.id = nil @@ -172,8 +172,8 @@ def around_save_callback end end - context 'composite key' do - context 'new model' do + context 'with composite key' do + context 'when new model' do it 'requires sort key to be specified' do obj = klass_with_composite_key.new name: 'Alex', age: nil @@ -185,7 +185,7 @@ def around_save_callback end end - context 'persisted model' do + context 'with persisted model' do it 'requires partition key to be specified' do obj = klass_with_composite_key.create!(name: 'Alex', age: 3) obj.id = nil @@ -324,7 +324,7 @@ def around_save_callback end describe 'timestamps' do - context 'new model' do + context 'when new model' do before do klass.create_table end @@ -376,7 +376,7 @@ def around_save_callback end end - context 'already created model' do + context 'when already created model' do it 'sets updated_at if Config.timestamps=true', config: { timestamps: true } do obj = klass.create! @@ -465,7 +465,7 @@ def around_save_callback end describe 'validation' do - context 'new model' do + context 'when new model' do before do klass_with_validation.create_table end @@ -499,7 +499,7 @@ def around_save_callback expect(obj).to be_changed end - context 'validate: false option' do + context 'when validate: false option' do it 'persists an invalid model' do obj = klass_with_validation.new(name: 'one') expect(obj.valid?).to eql false @@ -560,7 +560,7 @@ def around_save_callback end end - context 'already persisted model' do + context 'when already persisted model' do it 'persists a valid model' do obj = klass_with_validation.create!(name: 'oneone') obj.name = 'twotwo' @@ -592,7 +592,7 @@ def around_save_callback expect(obj).to be_changed end - context 'validate: false option' do + context 'when validate: false option' do it 'persists an invalid model' do obj = klass_with_validation.create!(name: 'oneone') obj.name = 'one' @@ -830,8 +830,8 @@ def around_save_callback expect(obj).to be_changed end - context 'primary key is of non-native DynamoDB type' do - context 'a new model' do + context 'when primary key is of non-native DynamoDB type' do + context 'when a new model' do it 'uses dumped value of partition key to update item' do klass = new_class(partition_key: { name: :published_on, type: :date }) do field :name @@ -864,7 +864,7 @@ def around_save_callback end end - context 'already persisted model' do + context 'when already persisted model' do it 'uses dumped value of partition key to update item' do klass = new_class(partition_key: { name: :published_on, type: :date }) do field :name @@ -901,7 +901,7 @@ def around_save_callback ScratchPad.clear end - context 'new model' do + context 'when new model' do it 'runs before_save callback' do klass_with_callback = new_class do before_save { ScratchPad.record 'run before_save' } @@ -1300,7 +1300,7 @@ def around_save_callback end end - context 'persisted model' do + context 'with persisted model' do it 'runs before_save callback' do klass_with_callback = new_class do field :name @@ -1682,7 +1682,7 @@ def around_save_callback end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -1696,7 +1696,7 @@ def around_save_callback klass_with_gsi.create_table end - context 'new model' do + context 'when new model' do it 'persists successfuly even if a field declared as a GSI primary key is set to nil' do obj = klass_with_gsi.new(name: nil, age: 42) @@ -1726,7 +1726,7 @@ def around_save_callback end end - context 'persisted model' do + context 'with persisted model' do it 'persists successfuly even if a field declared as a GSI primary key is set to nil' do obj = klass_with_gsi.create!(name: 'Alex', age: 42) obj.name = nil @@ -1762,7 +1762,7 @@ def around_save_callback klass.create_table end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil when new model' do obj = klass.new(name: 'Alex', age: nil) described_class.execute { |t| t.save obj } @@ -1777,7 +1777,7 @@ def around_save_callback end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil when new model' do obj = klass.new(name: 'Alex', age: nil) described_class.execute { |t| t.save obj } @@ -1792,7 +1792,7 @@ def around_save_callback end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil when new model' do obj = klass.new(name: 'Alex', age: nil) described_class.execute { |t| t.save obj } @@ -2180,8 +2180,8 @@ def around_save_callback expect(obj).to be_changed end - context 'primary key is of non-native DynamoDB type' do - context 'a new model' do + context 'when primary key is of non-native DynamoDB type' do + context 'when a new model' do it 'uses dumped value of partition key to update item' do klass = new_class(partition_key: { name: :published_on, type: :date }) do field :name @@ -2214,7 +2214,7 @@ def around_save_callback end end - context 'already persisted model' do + context 'when already persisted model' do it 'uses dumped value of partition key to update item' do klass = new_class(partition_key: { name: :published_on, type: :date }) do field :name @@ -2253,7 +2253,7 @@ def around_save_callback end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -2267,7 +2267,7 @@ def around_save_callback klass_with_gsi.create_table end - context 'new model' do + context 'when new model' do it 'persists successfuly even if a field declared as a GSI primary key is set to nil' do obj = klass_with_gsi.new(name: nil, age: 42) @@ -2297,7 +2297,7 @@ def around_save_callback end end - context 'persisted model' do + context 'with persisted model' do it 'persists successfuly even if a field declared as a GSI primary key is set to nil' do obj = klass_with_gsi.create!(name: 'Alex', age: 42) obj.name = nil @@ -2333,7 +2333,7 @@ def around_save_callback klass.create_table end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil when new model' do obj = klass.new(name: 'Alex', age: nil) described_class.execute { |t| t.save! obj } @@ -2348,7 +2348,7 @@ def around_save_callback end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil when new model' do obj = klass.new(name: 'Alex', age: nil) described_class.execute { |t| t.save! obj } @@ -2363,7 +2363,7 @@ def around_save_callback end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil when new model' do obj = klass.new(name: 'Alex', age: nil) described_class.execute { |t| t.save! obj } diff --git a/spec/dynamoid/transactions/mutation/touch_spec.rb b/spec/dynamoid/transactions/mutation/touch_spec.rb index ce6fedc6..cad701e4 100644 --- a/spec/dynamoid/transactions/mutation/touch_spec.rb +++ b/spec/dynamoid/transactions/mutation/touch_spec.rb @@ -138,7 +138,7 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do obj = klass.create! obj.id = nil @@ -148,7 +148,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do obj = klass_with_composite_key.create!(name: 'Alex') obj.id = nil @@ -266,7 +266,7 @@ end end - context 'concurrent deletion' do + context 'with concurrent deletion' do it 'rolls transaction back for simple primary key' do obj = klass.create! klass.find(obj.id).delete diff --git a/spec/dynamoid/transactions/mutation/update_attribute_spec.rb b/spec/dynamoid/transactions/mutation/update_attribute_spec.rb index 3e84ec84..0deaf3c8 100644 --- a/spec/dynamoid/transactions/mutation/update_attribute_spec.rb +++ b/spec/dynamoid/transactions/mutation/update_attribute_spec.rb @@ -453,7 +453,7 @@ def around_update_callback end end - context 'concurrent deletion' do + context 'with concurrent deletion' do it 'recreates the item for simple primary key' do obj = klass.create!(age: 21) klass.delete(obj.id) @@ -498,7 +498,7 @@ def around_update_callback end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -528,7 +528,7 @@ def around_update_callback end end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil' do obj = klass.create!(age: 42) described_class.execute { |t| t.update_attribute(obj, :age, nil) } @@ -537,7 +537,7 @@ def around_update_callback end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil' do obj = klass.create!(age: 42) described_class.execute { |t| t.update_attribute(obj, :age, nil) } @@ -547,7 +547,7 @@ def around_update_callback end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil' do obj = klass.create!(age: 42) described_class.execute { |t| t.update_attribute(obj, :age, nil) } diff --git a/spec/dynamoid/transactions/mutation/update_attributes_spec.rb b/spec/dynamoid/transactions/mutation/update_attributes_spec.rb index c18aac1f..3fa240a9 100644 --- a/spec/dynamoid/transactions/mutation/update_attributes_spec.rb +++ b/spec/dynamoid/transactions/mutation/update_attributes_spec.rb @@ -101,7 +101,7 @@ def around_save_callback it 'can be called without attributes to modify' describe 'primary key schema' do - context 'simple primary key' do + context 'with simple primary key' do it 'persists changes in already persisted model' do obj = klass.create!(name: 'Alex') @@ -113,7 +113,7 @@ def around_save_callback end end - context 'composite key' do + context 'with composite key' do it 'persists changes in already persisted model' do obj = klass_with_composite_key.create!(name: 'Alex', age: 3) @@ -127,7 +127,7 @@ def around_save_callback end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do obj = klass.create!(name: 'Alex') obj.id = nil @@ -140,7 +140,7 @@ def around_save_callback end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do obj = klass_with_composite_key.create!(name: 'Alex', age: 3) obj.id = nil @@ -647,7 +647,7 @@ def around_update_callback end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -689,7 +689,7 @@ def around_update_callback end end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil' do obj = klass.create!(name: 'Alex', age: 42) described_class.execute { |t| t.update_attributes obj, age: nil } @@ -697,7 +697,7 @@ def around_update_callback end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil' do obj = klass.create!(name: 'Alex', age: 42) described_class.execute { |t| t.update_attributes obj, age: nil } @@ -705,7 +705,7 @@ def around_update_callback end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil' do obj = klass.create!(name: 'Alex', age: 42) described_class.execute { |t| t.update_attributes obj, age: nil } @@ -753,7 +753,7 @@ def around_update_callback end describe 'primary key schema' do - context 'simple primary key' do + context 'with simple primary key' do it 'persists changes in already persisted model' do obj = klass.create!(name: 'Alex') @@ -765,7 +765,7 @@ def around_update_callback end end - context 'composite key' do + context 'with composite key' do it 'persists changes in already persisted model' do obj = klass_with_composite_key.create!(name: 'Alex', age: 3) @@ -779,7 +779,7 @@ def around_update_callback end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do obj = klass.create!(name: 'Alex') obj.id = nil @@ -792,7 +792,7 @@ def around_update_callback end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do obj = klass_with_composite_key.create!(name: 'Alex', age: 3) obj.id = nil @@ -1091,7 +1091,7 @@ def around_update_callback end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -1133,7 +1133,7 @@ def around_update_callback end end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil' do obj = klass.create!(name: 'Alex', age: 42) described_class.execute { |t| t.update_attributes! obj, age: nil } @@ -1141,7 +1141,7 @@ def around_update_callback end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil' do obj = klass.create!(name: 'Alex', age: 42) described_class.execute { |t| t.update_attributes! obj, age: nil } @@ -1149,7 +1149,7 @@ def around_update_callback end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil' do obj = klass.create!(name: 'Alex', age: 42) described_class.execute { |t| t.update_attributes! obj, age: nil } diff --git a/spec/dynamoid/transactions/mutation/update_fields_spec.rb b/spec/dynamoid/transactions/mutation/update_fields_spec.rb index b9af1f72..1f0a0047 100644 --- a/spec/dynamoid/transactions/mutation/update_fields_spec.rb +++ b/spec/dynamoid/transactions/mutation/update_fields_spec.rb @@ -56,7 +56,7 @@ end describe 'primary key schema' do - context 'simple primary key' do + context 'with simple primary key' do it 'persists changes in already persisted model' do obj = klass.create!(name: 'Alex') @@ -68,7 +68,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'persists changes in already persisted model' do obj = klass_with_composite_key.create!(name: 'Alex', age: 3) @@ -82,7 +82,7 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do obj = klass.create!(name: 'Alex') @@ -94,7 +94,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do obj = klass_with_composite_key.create!(name: 'Alex', age: 3) @@ -296,7 +296,7 @@ def around_destroy_callback end end - context 'given a block' do + context 'when given a block' do describe 'add' do it 'increments numeric attribute' do klass = new_class do @@ -828,7 +828,7 @@ def around_destroy_callback end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -862,7 +862,7 @@ def around_destroy_callback expect(obj.reload.age).to eql nil end - context 'given a block' do + context 'when given a block' do it 'persists successfuly even if a field declared as a GSI primary key is set to nil' do obj = klass_with_gsi.create!(name: 'Alex', age: 42) @@ -896,7 +896,7 @@ def around_destroy_callback end end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil' do obj = klass.create!(name: 'Alex', age: 42) described_class.execute { |t| t.update_fields klass, obj.id, age: nil } @@ -904,7 +904,7 @@ def around_destroy_callback end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil' do obj = klass.create!(name: 'Alex', age: 42) described_class.execute { |t| t.update_fields klass, obj.id, age: nil } @@ -912,7 +912,7 @@ def around_destroy_callback end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil' do obj = klass.create!(name: 'Alex', age: 42) described_class.execute { |t| t.update_fields klass, obj.id, age: nil } diff --git a/spec/dynamoid/transactions/mutation/upsert_spec.rb b/spec/dynamoid/transactions/mutation/upsert_spec.rb index d996f583..195c4332 100644 --- a/spec/dynamoid/transactions/mutation/upsert_spec.rb +++ b/spec/dynamoid/transactions/mutation/upsert_spec.rb @@ -104,7 +104,7 @@ end describe 'primary key schema' do - context 'simple primary key' do + context 'with simple primary key' do it 'persists a new model' do klass.create_table id_new = SecureRandom.uuid @@ -127,7 +127,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'persists a new model' do klass_with_composite_key.create_table id_new = SecureRandom.uuid @@ -152,7 +152,7 @@ end describe 'primary key validation' do - context 'simple primary key' do + context 'with simple primary key' do it 'requires partition key to be specified' do expect { described_class.execute do |txn| @@ -162,7 +162,7 @@ end end - context 'composite key' do + context 'with composite key' do it 'requires partition key to be specified' do expect { described_class.execute do |txn| @@ -184,7 +184,7 @@ end describe 'timestamps' do - context 'new model' do + context 'when new model' do it 'sets updated_at only if Config.timestamps=true', config: { timestamps: true } do klass.create_table id_new = SecureRandom.uuid @@ -239,7 +239,7 @@ end end - context 'already created model' do + context 'when already created model' do it 'sets updated_at if Config.timestamps=true', config: { timestamps: true } do obj = klass.create! @@ -398,7 +398,7 @@ def around_destroy_callback end # See https://github.com/Dynamoid/dynamoid/issues/885 for details - context 'Global Secondary Index' do + context 'with Global Secondary Index' do let(:klass_with_gsi) do new_class do field :name @@ -412,7 +412,7 @@ def around_destroy_callback klass_with_gsi.create_table end - context 'new model' do + context 'when new model' do it 'persists successfuly even if a field declared as a GSI primary key is set to nil' do id_new = SecureRandom.uuid expect { @@ -440,7 +440,7 @@ def around_destroy_callback end end - context 'existing model' do + context 'when existing model' do it 'persists successfuly even if a field declared as a GSI primary key is set to nil' do obj = klass_with_gsi.create!(name: 'Alex', age: 42) @@ -470,7 +470,7 @@ def around_destroy_callback end end - context 'true', config: { store_attribute_with_nil_value: true } do + context 'when true', config: { store_attribute_with_nil_value: true } do it 'keeps document attribute with nil' do obj = klass.create!(name: 'Alex', age: 42) described_class.execute { |t| t.upsert klass, obj.id, age: nil } @@ -478,7 +478,7 @@ def around_destroy_callback end end - context 'false', config: { store_attribute_with_nil_value: false } do + context 'when false', config: { store_attribute_with_nil_value: false } do it 'does not keep document attribute with nil' do obj = klass.create!(name: 'Alex', age: 42) described_class.execute { |t| t.upsert klass, obj.id, age: nil } @@ -486,7 +486,7 @@ def around_destroy_callback end end - context 'by default', config: { store_attribute_with_nil_value: nil } do + context 'when by default', config: { store_attribute_with_nil_value: nil } do it 'does not keep document attribute with nil' do obj = klass.create!(name: 'Alex', age: 42) described_class.execute { |t| t.upsert klass, obj.id, age: nil } diff --git a/spec/dynamoid/transactions/retrieval/find_spec.rb b/spec/dynamoid/transactions/retrieval/find_spec.rb index 7a6ec61c..0075368b 100644 --- a/spec/dynamoid/transactions/retrieval/find_spec.rb +++ b/spec/dynamoid/transactions/retrieval/find_spec.rb @@ -13,8 +13,8 @@ end end - context 'a single primary key provided' do - context 'simple primary key' do + context 'when a single primary key provided' do + context 'with simple primary key' do it 'finds a model' do obj = klass.create! @@ -62,7 +62,7 @@ end end - context 'composite primary key' do + context 'with composite primary key' do it 'finds a model' do obj = klass_with_composite_key.create!(age: 12) @@ -133,7 +133,7 @@ expect(obj_found).to be_persisted end - context 'field is not declared in document' do + context 'when field is not declared in document' do let(:class_with_not_declared_field) do new_class do field :name @@ -244,8 +244,8 @@ end end - context 'multiple primary keys provided' do - context 'simple primary key' do + context 'when multiple primary keys provided' do + context 'with simple primary key' do it 'finds models by an array of keys' do # rubocop:disable RSpec/RepeatedExample objects = (1..2).map { klass.create! } obj1, obj2 = objects @@ -341,7 +341,7 @@ end end - context 'composite primary key' do + context 'with composite primary key' do it 'finds models by an array of keys' do objects = (1..2).map { |i| klass_with_composite_key.create!(age: i) } obj1, obj2 = objects @@ -537,7 +537,7 @@ expect(objects).to contain_exactly(obj1, obj2) end - context 'field is not declared in document' do + context 'when field is not declared in document' do let(:class_with_not_declared_field) do new_class do field :name diff --git a/spec/support/matchers/send_dynamodb_request_matching.rb b/spec/support/matchers/send_dynamodb_request_matching.rb index c974f83a..96549645 100644 --- a/spec/support/matchers/send_dynamodb_request_matching.rb +++ b/spec/support/matchers/send_dynamodb_request_matching.rb @@ -10,32 +10,52 @@ def matches?(event_proc) PrintHttpBody.enabled = false - PrintHttpBody.logged_requests + matching_requests = PrintHttpBody.logged_requests .select { |r| r[:operation_name] == @operation_name } - .any? { |r| values_match?(@pattern, r[:body]) } + .select { |r| values_match?(@pattern, r[:body]) } + + @actual_count = matching_requests.size + + if @expected_count + @actual_count == @expected_count + else + @actual_count > 0 + end end # @api private - def does_not_match?(_event_proc) - !matches?(given_proc, :negative_expectation) && given_proc.is_a?(Proc) + def does_not_match?(event_proc) + !matches?(event_proc) end # @api private # @return [String] def failure_message - "expected #{PrintHttpBody.logged_requests} to contain #{@operation_name} request matching #{@pattern}" + if @expected_count + "expected #{@operation_name} request matching #{@pattern} to be sent #{@expected_count} times, but it was sent #{@actual_count} times" + else + "expected #{@operation_name} request matching #{@pattern} to be sent, but it was not" + end end # @api private # @return [String] def failure_message_when_negated - "expected #{PrintHttpBody.logged_requests} not to contain #{@operation_name} request matching #{@pattern}" + if @expected_count + "expected #{@operation_name} request matching #{@pattern} not to be sent #{@expected_count} times, but it was" + else + "expected #{@operation_name} request matching #{@pattern} not to be sent, but it was" + end end # @api private # @return [String] def description - "match #{@operation_name} request #{@pattern}" + if @expected_count + "match #{@operation_name} request #{@pattern} exactly #{@expected_count} times" + else + "match #{@operation_name} request #{@pattern}" + end end # @private @@ -48,12 +68,32 @@ def supports_value_expectations? false end + def exactly(count) + @expected_count = count + self + end + + def times + self + end + + def once + @expected_count = 1 + self + end + + def twice + @expected_count = 2 + self + end + private def initialize(operation_name, pattern) super() @operation_name = operation_name.to_s @pattern = pattern.stringify_keys + @expected_count = nil end def values_match?(expected, actual) @@ -105,6 +145,6 @@ def self.hashes_match?(expected_hash, actual_hash) end end -def send_request_matching(operation, pattern) +def send_request_matching(operation, pattern = {}) SendRequestMatching.new(operation, pattern) end