# frozen_string_literal: true require 'spec_helper' module Aws module Record describe TableMigration do let(:api_requests) { [] } let(:stub_client) do requests = api_requests client = Aws::DynamoDB::Client.new(stub_responses: true) client.handle do |context| requests << context.params @handler.call(context) end client end it 'only accepts Aws::Record models' do expect { TableMigration.new(Class.new) }.to raise_error( Errors::InvalidModel, 'Table models must include Aws::Record' ) end it 'requires that models contain a valid key' do model = Class.new do include(Aws::Record) end expect { TableMigration.new(model) }.to raise_error( Errors::InvalidModel, 'Table models must include a hash key' ) end context 'client' do let(:model_stub_client) { Aws::DynamoDB::Client.new(stub_responses: true) } let(:model) do model = Class.new do include(Aws::Record) set_table_name('TestTable') integer_attr(:id, hash_key: true) end model.configure_client client: model_stub_client model end it 'uses client given as option with the highest priority' do expect(TableMigration.new(model, client: stub_client).client).to eq stub_client end it 'uses client set to model' do expect(TableMigration.new(model).client).to eq model_stub_client end end context 'Migration Operations' do let(:klass) do Class.new do include(Aws::Record) set_table_name('TestTable') integer_attr(:id, hash_key: true) date_attr(:date, range_key: true, database_attribute_name: 'datekey') string_attr(:lsi) string_attr(:gsi_partition) string_attr(:gsi_sort) end end let(:migration) do TableMigration.new(klass, client: stub_client) end context '#create!' do it 'calls #create_table on a client when #create! is called' do create_opts = { provisioned_throughput: { read_capacity_units: 5, write_capacity_units: 2 } } migration.client = stub_client migration.create!(create_opts) expect(api_requests).to eq( [ { table_name: 'TestTable', attribute_definitions: [ { attribute_name: 'id', attribute_type: 'N' }, { attribute_name: 'datekey', attribute_type: 'S' } ], key_schema: [ { attribute_name: 'id', key_type: 'HASH' }, { attribute_name: 'datekey', key_type: 'RANGE' } ], provisioned_throughput: { read_capacity_units: 5, write_capacity_units: 2 } } ] ) end it 'allows specifying on-demand billing instead of provisioned'\ ' througput' do create_opts = { billing_mode: 'PAY_PER_REQUEST' } migration.client = stub_client migration.create!(create_opts) expect(api_requests).to eq( [ { table_name: 'TestTable', attribute_definitions: [ { attribute_name: 'id', attribute_type: 'N' }, { attribute_name: 'datekey', attribute_type: 'S' } ], key_schema: [ { attribute_name: 'id', key_type: 'HASH' }, { attribute_name: 'datekey', key_type: 'RANGE' } ], billing_mode: 'PAY_PER_REQUEST' } ] ) end it 'accepts a value of PROVISIONED for billing_mode if'\ ' provisioned throughput is also specified' do create_opts = { billing_mode: 'PROVISIONED', provisioned_throughput: { read_capacity_units: 5, write_capacity_units: 2 } } migration.client = stub_client migration.create!(create_opts) expect(api_requests).to eq( [ { table_name: 'TestTable', attribute_definitions: [ { attribute_name: 'id', attribute_type: 'N' }, { attribute_name: 'datekey', attribute_type: 'S' } ], key_schema: [ { attribute_name: 'id', key_type: 'HASH' }, { attribute_name: 'datekey', key_type: 'RANGE' } ], billing_mode: 'PROVISIONED', provisioned_throughput: { read_capacity_units: 5, write_capacity_units: 2 } } ] ) end it 'requires billing_mode be PROVISIONED if specified'\ ' and provisioned throughput is provided' do create_opts = { billing_mode: 'INVALID', provisioned_throughput: { read_capacity_units: 5, write_capacity_units: 2 } } migration.client = stub_client expect { migration.create!(create_opts) }.to raise_error( ArgumentError, /billing_mode.*one of.*PAY_PER_REQUEST.*PROVISIONED.*INVALID/ ) expect(api_requests).to eq([]) end it 'requires billing_mode be PAY_PER_REQUEST if specified'\ ' and no provisioned throughput is provided' do create_opts = { billing_mode: 'INVALID' } migration.client = stub_client expect { migration.create!(create_opts) }.to raise_error( ArgumentError, /billing_mode.*one of.*PAY_PER_REQUEST.*PROVISIONED.*INVALID/ ) expect(api_requests).to eq([]) end it 'requires billing_mode be specified and have value PAY_PER_REQUEST'\ ' if no provisioned throughput is provided' do create_opts = {} migration.client = stub_client expect { migration.create!(create_opts) }.to raise_error( ArgumentError, /provisioned_throughput.*not specified.*billing_mode.*PAY_PER_REQUEST/ ) expect(api_requests).to eq([]) end it 'requires only one capacity specification, either provisioned ' \ 'throughput or on-demand' do create_opts = { billing_mode: 'PAY_PER_REQUEST', provisioned_throughput: { read_capacity_units: 5, write_capacity_units: 2 } } migration.client = stub_client expect { migration.create!(create_opts) }.to raise_error( ArgumentError, /provisioned_throughput.*billing_mode.*unspecified.*PROVISIONED/ ) expect(api_requests).to eq([]) end it 'accepts models with a local secondary index' do create_opts = { provisioned_throughput: { read_capacity_units: 5, write_capacity_units: 2 } } klass.local_secondary_index( :test_lsi, range_key: :lsi, projection: { projection_type: 'ALL' } ) migration.client = stub_client migration.create!(create_opts) expect(api_requests).to eq( [ { table_name: 'TestTable', attribute_definitions: [ { attribute_name: 'id', attribute_type: 'N' }, { attribute_name: 'datekey', attribute_type: 'S' }, { attribute_name: 'lsi', attribute_type: 'S' } ], key_schema: [ { attribute_name: 'id', key_type: 'HASH' }, { attribute_name: 'datekey', key_type: 'RANGE' } ], local_secondary_indexes: [{ index_name: 'test_lsi', key_schema: [ { attribute_name: 'id', key_type: 'HASH' }, { attribute_name: 'lsi', key_type: 'RANGE' } ], projection: { projection_type: 'ALL' } }], provisioned_throughput: { read_capacity_units: 5, write_capacity_units: 2 } } ] ) end it 'accepts models with a global secondary index' do create_opts = { provisioned_throughput: { read_capacity_units: 5, write_capacity_units: 2 }, global_secondary_index_throughput: { test_gsi: { read_capacity_units: 3, write_capacity_units: 1 } } } klass.global_secondary_index( :test_gsi, hash_key: :gsi_partition, range_key: :gsi_sort, projection: { projection_type: 'ALL' } ) migration.client = stub_client migration.create!(create_opts) expect(api_requests).to eq( [ { table_name: 'TestTable', attribute_definitions: [ { attribute_name: 'id', attribute_type: 'N' }, { attribute_name: 'datekey', attribute_type: 'S' }, { attribute_name: 'gsi_partition', attribute_type: 'S' }, { attribute_name: 'gsi_sort', attribute_type: 'S' } ], key_schema: [ { attribute_name: 'id', key_type: 'HASH' }, { attribute_name: 'datekey', key_type: 'RANGE' } ], global_secondary_indexes: [{ index_name: 'test_gsi', key_schema: [ { attribute_name: 'gsi_partition', key_type: 'HASH' }, { attribute_name: 'gsi_sort', key_type: 'RANGE' } ], projection: { projection_type: 'ALL' }, provisioned_throughput: { read_capacity_units: 3, write_capacity_units: 1 } }], provisioned_throughput: { read_capacity_units: 5, write_capacity_units: 2 } } ] ) end it 'does not require global secondary index throughput to be ' \ 'provided if the table is configured to use on-demand billing' do create_opts = { billing_mode: 'PAY_PER_REQUEST' } klass.global_secondary_index( :test_gsi, hash_key: :gsi_partition, range_key: :gsi_sort, projection: { projection_type: 'ALL' } ) migration.client = stub_client migration.create!(create_opts) expect(api_requests).to eq( [ { table_name: 'TestTable', attribute_definitions: [ { attribute_name: 'id', attribute_type: 'N' }, { attribute_name: 'datekey', attribute_type: 'S' }, { attribute_name: 'gsi_partition', attribute_type: 'S' }, { attribute_name: 'gsi_sort', attribute_type: 'S' } ], key_schema: [ { attribute_name: 'id', key_type: 'HASH' }, { attribute_name: 'datekey', key_type: 'RANGE' } ], global_secondary_indexes: [{ index_name: 'test_gsi', key_schema: [ { attribute_name: 'gsi_partition', key_type: 'HASH' }, { attribute_name: 'gsi_sort', key_type: 'RANGE' } ], projection: { projection_type: 'ALL' } }], billing_mode: 'PAY_PER_REQUEST' } ] ) end context 'when the table is not configured to use on-demand billing' do let(:throughput_opts) do { provisioned_throughput: { read_capacity_units: 5, write_capacity_units: 2 } } end it 'requires global secondary index throughput to be provided' do create_opts = throughput_opts klass.global_secondary_index( :test_gsi, hash_key: :gsi_partition, range_key: :gsi_sort, projection: { projection_type: 'ALL' } ) migration.client = stub_client expect { migration.create!(create_opts) }.to raise_error( ArgumentError, /define.*:global_secondary_index_throughput/ ) expect(api_requests).to eq([]) end it 'requires global secondary index throughput to be defined for each index' do create_opts = throughput_opts.merge( global_secondary_index_throughput: { test_gsi: { read_capacity_units: 1, write_capacity_units: 1 } } ) klass.global_secondary_index( :test_gsi, hash_key: :gsi_partition, range_key: :gsi_sort, projection: { projection_type: 'ALL' } ) klass.global_secondary_index( :fail_on, hash_key: :gsi_partition, range_key: :gsi_sort, projection: { projection_type: 'ALL' } ) migration.client = stub_client expect { migration.create!(create_opts) }.to raise_error( ArgumentError, /Missing.*throughput.*for the following.*indexes.*fail_on/ ) expect(api_requests).to eq([]) end end end context '#delete!' do it 'calls #delete_table on a client when #delete! is called' do migration.client = stub_client migration.delete! expect(api_requests).to eq( [ { table_name: 'TestTable' } ] ) end it 'throws TableDoesNotExist when table did not exist at call time' do stub_client.stub_responses( :delete_table, 'ResourceNotFoundException' ) migration.client = stub_client expect { migration.delete! }.to raise_error( Errors::TableDoesNotExist ) end end context '#update!' do it 'calles #update_table on a client when #update! is called' do update_opts = { provisioned_throughput: { read_capacity_units: 4, write_capacity_units: 3 } } migration.client = stub_client migration.update!(update_opts) expect(api_requests).to eq( [ { table_name: 'TestTable', provisioned_throughput: { read_capacity_units: 4, write_capacity_units: 3 } } ] ) end it 'throws TableDoesNotExist when table did not exist at call time' do stub_client.stub_responses( :update_table, 'ResourceNotFoundException' ) migration.client = stub_client expect { migration.update!({}) }.to raise_error( Errors::TableDoesNotExist ) end end context '#wait_until_available' do it "can check on the table's availability status" do stub_client.stub_responses( :describe_table, table: { table_status: 'ACTIVE' } ) migration.client = stub_client expect(migration.wait_until_available).not_to eq(nil) end end end end end end