spec/aws-record/record/transactions_spec.rb (539 lines of code) (raw):
# frozen_string_literal: true
require 'spec_helper'
module Aws
module Record
describe Transactions do
let(:stub_client) do
Aws::DynamoDB::Client.new(stub_responses: true)
end
let(:table_one) do
Class.new do
include(Aws::Record)
set_table_name('TableOne')
integer_attr(:id, hash_key: true)
string_attr(:range, range_key: true)
string_attr(:body)
string_attr(:has_default, default_value: 'Lorem ipsum.')
end
end
let(:table_two) do
Class.new do
include(Aws::Record)
set_table_name('TableTwo')
string_attr(:uuid, hash_key: true)
string_attr(:body)
string_attr(:has_default, default_value: 'Lorem ipsum.')
end
end
describe '#transact_find' do
it 'uses tfind_opts to construct a request and returns modeled items' do
stub_client.stub_responses(
:transact_get_items,
responses: [
{
item: { 'id' => 1, 'range' => 'a', 'body' => 'One' }
},
{
item: { 'uuid' => 'foo', 'body' => 'Two' }
},
{
item: { 'id' => 2, 'range' => 'b', 'body' => 'Three' }
}
]
)
Aws::Record::Transactions.configure_client(client: stub_client)
items = Aws::Record::Transactions.transact_find(
transact_items: [
table_one.tfind_opts(key: { id: 1, range: 'a' }),
table_two.tfind_opts(key: { uuid: 'foo' }),
table_one.tfind_opts(key: { id: 2, range: 'b' })
]
)
expect(items.responses.size).to eq(3)
expect(items.responses[0].class).to eq(table_one)
expect(items.responses[1].class).to eq(table_two)
expect(items.responses[2].class).to eq(table_one)
expect(items.responses[0].dirty?).to be_falsey
expect(items.responses[1].dirty?).to be_falsey
expect(items.responses[2].dirty?).to be_falsey
expect(items.responses[0].body).to eq('One')
expect(items.responses[1].body).to eq('Two')
expect(items.responses[2].body).to eq('Three')
expect(items.missing_items.size).to eq(0)
end
it 'handles and reports missing keys' do
stub_client.stub_responses(
:transact_get_items,
responses: [
{
item: { 'id' => 1, 'range' => 'a', 'body' => 'One' }
},
{ item: nil },
{
item: { 'id' => 2, 'range' => 'b', 'body' => 'Three' }
}
]
)
Aws::Record::Transactions.configure_client(client: stub_client)
items = Aws::Record::Transactions.transact_find(
transact_items: [
table_one.tfind_opts(key: { id: 1, range: 'a' }),
table_two.tfind_opts(key: { uuid: 'foo' }),
table_one.tfind_opts(key: { id: 2, range: 'b' })
]
)
expect(items.responses.size).to eq(3)
expect(items.responses[1]).to be_nil
expect(items.responses[0].class).to eq(table_one)
expect(items.responses[2].class).to eq(table_one)
expect(items.responses[0].body).to eq('One')
expect(items.responses[2].body).to eq('Three')
expect(items.missing_items.size).to eq(1)
expect(items.missing_items[0]).to eq(
model_class: table_two,
key: { 'uuid' => 'foo' }
)
end
it 'raises when tfind_opts is missing a key' do
expect {
Aws::Record::Transactions.transact_find(
transact_items: [
table_one.tfind_opts(key: { range: 'a' }),
table_two.tfind_opts(key: { uuid: 'foo' }),
table_one.tfind_opts(key: { id: 2, range: 'b' })
]
)
}.to raise_error(Aws::Record::Errors::KeyMissing)
end
end
describe '#transact_write' do
it 'supports the basic update transaction types' do
Aws::Record::Transactions.configure_client(client: stub_client)
put_item = table_one.new(id: 1, range: 'a')
update_item = table_two.new(uuid: 'foo')
update_item.clean! # like we got it from #find
update_item.body = 'Content'
delete_item = table_one.new(id: 2, range: 'b')
delete_item.clean! # like we got it from #find
expect(put_item.dirty?).to be_truthy
expect(update_item.dirty?).to be_truthy
expect(delete_item.destroyed?).to be_falsey
Aws::Record::Transactions.transact_write(
transact_items: [
{ put: put_item },
{ update: update_item },
{ delete: delete_item }
]
)
expect(put_item.dirty?).to be_falsey
expect(update_item.dirty?).to be_falsey
expect(delete_item.destroyed?).to be_truthy
expect(stub_client.api_requests.size).to eq(1)
request_params = stub_client.api_requests.first[:params]
expect(request_params[:transact_items]).to eq(
[
{
put: {
table_name: 'TableOne',
item: {
'has_default' => { s: 'Lorem ipsum.' },
'id' => { n: '1' },
'range' => { s: 'a' }
}
}
},
{
update: {
table_name: 'TableTwo',
key: { 'uuid' => { s: 'foo' } },
update_expression: 'SET #UE_A = :ue_a',
expression_attribute_names: { '#UE_A' => 'body' },
expression_attribute_values: { ':ue_a' => { s: 'Content' } }
}
},
{
delete: {
table_name: 'TableOne',
key: {
'id' => { n: '2' },
'range' => { s: 'b' }
}
}
}
]
)
end
it 'supports manually defined check operations' do
Aws::Record::Transactions.configure_client(client: stub_client)
check_exp = table_one.transact_check_expression(
key: { id: 10, range: 'z' },
condition_expression: 'size(#T) <= :v',
expression_attribute_names: {
'#T' => 'body'
},
expression_attribute_values: {
':v' => 1024
}
)
put_item = table_one.new(id: 1, range: 'a')
Aws::Record::Transactions.transact_write(
transact_items: [
{ check: check_exp },
{ put: put_item }
]
)
expect(stub_client.api_requests.size).to eq(1)
request_params = stub_client.api_requests.first[:params]
expect(request_params[:transact_items]).to eq(
[
{
condition_check: {
key: {
'id' => { n: '10' },
'range' => { s: 'z' }
},
table_name: 'TableOne',
condition_expression: 'size(#T) <= :v',
expression_attribute_names: {
'#T' => 'body'
},
expression_attribute_values: {
':v' => { n: '1024' }
}
}
},
{
put: {
table_name: 'TableOne',
item: {
'has_default' => { s: 'Lorem ipsum.' },
'id' => { n: '1' },
'range' => { s: 'a' }
}
}
}
]
)
end
it 'supports transactional save as an update or safe put' do
Aws::Record::Transactions.configure_client(client: stub_client)
put_item = table_one.new(id: 1, range: 'a')
update_item = table_two.new(uuid: 'foo')
update_item.clean! # like we got it from #find
update_item.body = 'Content'
expect(put_item.dirty?).to be_truthy
expect(update_item.dirty?).to be_truthy
Aws::Record::Transactions.transact_write(
transact_items: [
{ save: put_item },
{ save: update_item }
]
)
expect(put_item.dirty?).to be_falsey
expect(update_item.dirty?).to be_falsey
expect(stub_client.api_requests.size).to eq(1)
request_params = stub_client.api_requests.first[:params]
expect(request_params[:transact_items]).to eq(
[
{
put: {
table_name: 'TableOne',
item: {
'has_default' => { s: 'Lorem ipsum.' },
'id' => { n: '1' },
'range' => { s: 'a' }
},
condition_expression: 'attribute_not_exists(#H) and attribute_not_exists(#R)',
expression_attribute_names: {
'#H' => 'id',
'#R' => 'range'
}
}
},
{
update: {
table_name: 'TableTwo',
key: { 'uuid' => { s: 'foo' } },
update_expression: 'SET #UE_A = :ue_a',
expression_attribute_names: { '#UE_A' => 'body' },
expression_attribute_values: { ':ue_a' => { s: 'Content' } }
}
}
]
)
end
it 'supports additional options per transaction' do
Aws::Record::Transactions.configure_client(client: stub_client)
put_item = table_one.new(id: 1, range: 'a')
update_item = table_two.new(uuid: 'foo')
update_item.clean! # like we got it from #find
update_item.body = 'Content'
delete_item = table_one.new(id: 2, range: 'b')
delete_item.clean! # like we got it from #find
save_item = table_one.new(id: 3, range: 'c')
Aws::Record::Transactions.transact_write(
transact_items: [
{
put: put_item,
return_values_on_condition_check_failure: 'ALL_OLD'
},
{
update: update_item,
return_values_on_condition_check_failure: 'ALL_OLD'
},
{
delete: delete_item,
return_values_on_condition_check_failure: 'ALL_OLD'
},
{
save: save_item,
return_values_on_condition_check_failure: 'ALL_OLD'
}
]
)
expect(stub_client.api_requests.size).to eq(1)
request_params = stub_client.api_requests.first[:params]
expect(request_params[:transact_items]).to eq(
[
{
put: {
table_name: 'TableOne',
item: {
'has_default' => { s: 'Lorem ipsum.' },
'id' => { n: '1' },
'range' => { s: 'a' }
},
return_values_on_condition_check_failure: 'ALL_OLD'
}
},
{
update: {
table_name: 'TableTwo',
key: { 'uuid' => { s: 'foo' } },
update_expression: 'SET #UE_A = :ue_a',
expression_attribute_names: { '#UE_A' => 'body' },
expression_attribute_values: { ':ue_a' => { s: 'Content' } },
return_values_on_condition_check_failure: 'ALL_OLD'
}
},
{
delete: {
table_name: 'TableOne',
key: {
'id' => { n: '2' },
'range' => { s: 'b' }
},
return_values_on_condition_check_failure: 'ALL_OLD'
}
},
{
put: {
table_name: 'TableOne',
item: {
'has_default' => { s: 'Lorem ipsum.' },
'id' => { n: '3' },
'range' => { s: 'c' }
},
condition_expression: 'attribute_not_exists(#H) and attribute_not_exists(#R)',
expression_attribute_names: {
'#H' => 'id',
'#R' => 'range'
},
return_values_on_condition_check_failure: 'ALL_OLD'
}
}
]
)
end
it 'can combine expression attributes for update' do
Aws::Record::Transactions.configure_client(client: stub_client)
update_item = table_two.new(uuid: 'foo')
update_item.clean! # like we got it from #find
update_item.body = 'Content'
save_item = table_two.new(uuid: 'bar')
save_item.clean! # like we got it from #find
save_item.body = 'Content'
Aws::Record::Transactions.transact_write(
transact_items: [
{
update: update_item,
condition_expression: 'size(#T) <= :v',
expression_attribute_names: {
'#T' => 'body'
},
expression_attribute_values: {
':v' => 1024
},
return_values_on_condition_check_failure: 'ALL_OLD'
},
{
save: save_item,
condition_expression: 'size(#T) <= :v',
expression_attribute_names: {
'#T' => 'body'
},
expression_attribute_values: {
':v' => 1024
},
return_values_on_condition_check_failure: 'ALL_OLD'
}
]
)
expect(stub_client.api_requests.size).to eq(1)
request_params = stub_client.api_requests.first[:params]
expect(request_params[:transact_items]).to eq(
[
{
update: {
table_name: 'TableTwo',
key: { 'uuid' => { s: 'foo' } },
update_expression: 'SET #UE_A = :ue_a',
condition_expression: 'size(#T) <= :v',
expression_attribute_names: {
'#UE_A' => 'body',
'#T' => 'body'
},
expression_attribute_values: {
':ue_a' => { s: 'Content' },
':v' => { n: '1024' }
},
return_values_on_condition_check_failure: 'ALL_OLD'
}
},
{
update: {
table_name: 'TableTwo',
key: { 'uuid' => { s: 'bar' } },
update_expression: 'SET #UE_A = :ue_a',
condition_expression: 'size(#T) <= :v',
expression_attribute_names: {
'#UE_A' => 'body',
'#T' => 'body'
},
expression_attribute_values: {
':ue_a' => { s: 'Content' },
':v' => { n: '1024' }
},
return_values_on_condition_check_failure: 'ALL_OLD'
}
}
]
)
end
it 'supports custom update expressions' do
Aws::Record::Transactions.configure_client(client: stub_client)
update_item = table_two.new(uuid: 'foo')
update_item.clean! # like we got it from #find
Aws::Record::Transactions.transact_write(
transact_items: [
{
update: update_item,
update_expression: 'SET #S = if_not_exists(#S, :s)',
expression_attribute_names: {
'#S' => 'body'
},
expression_attribute_values: {
':s' => 'Content'
}
}
]
)
expect(stub_client.api_requests.size).to eq(1)
request_params = stub_client.api_requests.first[:params]
expect(request_params[:transact_items]).to eq(
[
{
update: {
table_name: 'TableTwo',
key: { 'uuid' => { s: 'foo' } },
update_expression: 'SET #S = if_not_exists(#S, :s)',
expression_attribute_names: {
'#S' => 'body'
},
expression_attribute_values: {
':s' => { s: 'Content' }
}
}
}
]
)
end
it 'raises a validation exception when safe put collides with a condition expression' do
Aws::Record::Transactions.configure_client(client: stub_client)
save_item = table_two.new(uuid: 'bar', body: 'Content')
expect(save_item.dirty?).to be_truthy
expect {
Aws::Record::Transactions.transact_write(
transact_items: [
{
save: save_item,
condition_expression: 'size(#T) <= :v',
expression_attribute_names: {
'#T' => 'body'
},
expression_attribute_values: {
':v' => 1024
}
}
]
)
}.to raise_error(
Aws::Record::Errors::TransactionalSaveConditionCollision
)
expect(save_item.dirty?).to be_truthy
end
it 'raises an exception when attribute updates collide with an update expression' do
Aws::Record::Transactions.configure_client(client: stub_client)
update_item = table_two.new(uuid: 'foo')
update_item.clean! # like we got it from #find
update_item.body = 'Content'
expect {
Aws::Record::Transactions.transact_write(
transact_items: [
{
update: update_item,
update_expression: 'SET #H = :v',
expression_attribute_names: {
'#H' => 'has_default'
},
expression_attribute_values: {
':v' => 'other'
}
}
]
)
}.to raise_error(
Aws::Record::Errors::UpdateExpressionCollision
)
end
it 'does not clean items when the transaction fails' do
stub_client.stub_responses(
:transact_write_items,
'TransactionCanceledException'
)
Aws::Record::Transactions.configure_client(client: stub_client)
put_item = table_one.new(id: 1, range: 'a')
update_item = table_two.new(uuid: 'foo')
update_item.clean! # like we got it from #find
update_item.body = 'Content'
delete_item = table_one.new(id: 2, range: 'b')
delete_item.clean! # life we got it from #find
expect(put_item.dirty?).to be_truthy
expect(update_item.dirty?).to be_truthy
expect(delete_item.destroyed?).to be_falsey
expect {
Aws::Record::Transactions.transact_write(
transact_items: [
{ save: put_item },
{ save: update_item },
{ delete: delete_item }
]
)
}.to raise_error(Aws::DynamoDB::Errors::TransactionCanceledException)
expect(put_item.dirty?).to be_truthy
expect(update_item.dirty?).to be_truthy
expect(delete_item.destroyed?).to be_falsey
end
end
end
end
end