fake_dynamo 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,97 @@
1
+ require 'spec_helper'
2
+
3
+ module FakeDynamo
4
+ describe Item do
5
+ subject do
6
+ item = Item.new
7
+ key = Key.new
8
+ key.primary = Attribute.new('id', 'ananth', 'S')
9
+ item.key = key
10
+ item.attributes = {}
11
+ item
12
+ end
13
+
14
+ context "#update" do
15
+ it "should not allow to update primary key" do
16
+ expect { subject.update('id', nil) }.to raise_error(ValidationException, /part of the key/)
17
+ end
18
+
19
+ it "should handle unknown action" do
20
+ expect { subject.update('xyz', {'Action' => 'XYZ'}) }.to raise_error(ValidationException, /unknown action/i)
21
+ end
22
+
23
+ it "should not allow empty value for action other than delete" do
24
+ expect { subject.update('xyz', {'Action' => 'PUT'})}.to raise_error(ValidationException, /only delete/i)
25
+ end
26
+ end
27
+
28
+ context "#delete" do
29
+ it "should not fail when the attribute is not present" do
30
+ subject.delete('friends', nil)
31
+ subject.attributes['friends'].should be_nil
32
+ end
33
+
34
+ it "should delete the attribute" do
35
+ subject.attributes['friends'] = Attribute.new('friends', ["1", "2"], "NS")
36
+ subject.delete('friends', nil)
37
+ subject.attributes['friends'].should be_nil
38
+ end
39
+
40
+ it "should handle value type" do
41
+ subject.attributes['friends'] = Attribute.new('friends', ["1", "2"], "NS")
42
+ expect { subject.delete('friends', { "S" => "XYZ" }) }.to raise_error(ValidationException, /type mismatch/i)
43
+
44
+ subject.attributes['age'] = Attribute.new('age', "5", "N")
45
+ expect { subject.delete('age', { "N" => "10" }) }.to raise_error(ValidationException, /not supported/i)
46
+ end
47
+
48
+ it "should delete values" do
49
+ subject.attributes['friends'] = Attribute.new('friends', ["1", "2"], "NS")
50
+ subject.delete('friends', { "NS" => ["2", "4"]})
51
+ subject.attributes['friends'].value.should == ["1"]
52
+ end
53
+ end
54
+
55
+ context "#put" do
56
+ it "should update the attribute" do
57
+ old_name = Attribute.new('name', 'xxx', 'S')
58
+ subject.attributes['name'] = old_name
59
+ subject.put('name', { 'S' => 'ananth'});
60
+ subject.attributes['name'].should eq(Attribute.new('name', 'ananth', 'S'))
61
+
62
+ subject.attributes['xxx'].should be_nil
63
+ subject.put('xxx', { 'S' => 'new'} )
64
+ subject.attributes['xxx'].should eq(Attribute.new('xxx', 'new', 'S'))
65
+ end
66
+ end
67
+
68
+ context "#add" do
69
+ it "should fail on string type" do
70
+ expect { subject.add('new', { 'S' => 'ananth'}) }.to raise_error(ValidationException, /not supported/)
71
+ end
72
+
73
+ it "should increment numbers" do
74
+ subject.attributes['number'] = Attribute.new('number', '5', 'N')
75
+ subject.add('number', { 'N' => '3'})
76
+ subject.attributes['number'].value.should eq('8')
77
+ end
78
+
79
+ it "should handle sets" do
80
+ subject.attributes['set'] = Attribute.new('set', ['1', '2'], 'SS')
81
+ subject.add('set', { 'SS' => ['3']})
82
+ subject.attributes['set'].value.should eq(['1', '2', '3'])
83
+ end
84
+
85
+ it "should handle type mismatch" do
86
+ subject.attributes['xxx'] = Attribute.new('xxx', ['1', '2'], 'NS')
87
+ expect { subject.add('xxx', {'SS' => ['3']}) }.to raise_error(ValidationException, /type mismatch/i)
88
+ end
89
+
90
+ it "should add the item if attribute is not found" do
91
+ subject.attributes['unknown'].should be_nil
92
+ subject.add('unknown', {'SS' => ['1']})
93
+ subject.attributes['unknown'].should eq(Attribute.new('unknown', ['1'], 'SS'))
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ module FakeDynamo
4
+ describe Server do
5
+ include Rack::Test::Methods
6
+
7
+ def get_server(app)
8
+ s = app.instance_variable_get :@app
9
+ if s.instance_of? Server
10
+ s
11
+ else
12
+ get_server(s)
13
+ end
14
+ end
15
+
16
+ let(:data) do
17
+ {
18
+ "TableName" => "Table1",
19
+ "KeySchema" =>
20
+ {"HashKeyElement" => {"AttributeName" => "AttributeName1","AttributeType" => "S"},
21
+ "RangeKeyElement" => {"AttributeName" => "AttributeName2","AttributeType" => "N"}},
22
+ "ProvisionedThroughput" => {"ReadCapacityUnits" => 5,"WriteCapacityUnits" => 10}
23
+ }
24
+ end
25
+ let(:app) { Server.new }
26
+ let(:server) { get_server(app) }
27
+
28
+ it "should extract_operation" do
29
+ server.extract_operation('HTTP_X_AMZ_TARGET' => 'DynamoDB_20111205.CreateTable').should eq('CreateTable')
30
+ expect {
31
+ server.extract_operation('HTTP_X_AMZ_TARGET' => 'FakeDB_20111205.CreateTable')
32
+ }.to raise_error(UnknownOperationException)
33
+ end
34
+
35
+ it "should send operation to db" do
36
+ post '/', data.to_json, 'HTTP_X_AMZ_TARGET' => 'DynamoDB_20111205.CreateTable'
37
+ last_response.should be_ok
38
+ end
39
+
40
+ it "should handle error properly" do
41
+ post '/', {'x' => 'y'}.to_json, 'HTTP_X_AMZ_TARGET' => 'DynamoDB_20111205.CreateTable'
42
+ last_response.should_not be_ok
43
+ last_response.status.should eq(400)
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,435 @@
1
+ require 'spec_helper'
2
+
3
+ module FakeDynamo
4
+ describe Table do
5
+
6
+ let(:data) do
7
+ {
8
+ "TableName" => "Table1",
9
+ "KeySchema" =>
10
+ {"HashKeyElement" => {"AttributeName" => "AttributeName1","AttributeType" => "S"},
11
+ "RangeKeyElement" => {"AttributeName" => "AttributeName2","AttributeType" => "N"}},
12
+ "ProvisionedThroughput" => {"ReadCapacityUnits" => 5,"WriteCapacityUnits" => 10}
13
+ }
14
+ end
15
+
16
+ let(:item) do
17
+ { 'TableName' => 'Table1',
18
+ 'Item' => {
19
+ 'AttributeName1' => { 'S' => "test" },
20
+ 'AttributeName2' => { 'N' => '11' },
21
+ 'AttributeName3' => { 'S' => "another" }
22
+ }}
23
+ end
24
+
25
+ let(:key) do
26
+ {'TableName' => 'Table1',
27
+ 'Key' => {
28
+ 'HashKeyElement' => { 'S' => 'test' },
29
+ 'RangeKeyElement' => { 'N' => '11' }
30
+ }}
31
+ end
32
+
33
+ let(:consumed_capacity) { { 'ConsumedCapacityUnits' => 1 } }
34
+
35
+ subject { Table.new(data) }
36
+
37
+ its(:status) { should == 'CREATING' }
38
+ its(:creation_date_time) { should_not be_nil }
39
+
40
+ context '#update' do
41
+ subject do
42
+ table = Table.new(data)
43
+ table.update(10, 15)
44
+ table
45
+ end
46
+
47
+ its(:read_capacity_units) { should == 10 }
48
+ its(:write_capacity_units) { should == 15 }
49
+ its(:last_increased_time) { should be_a_kind_of(Fixnum) }
50
+ its(:last_decreased_time) { should be_nil }
51
+ end
52
+
53
+ context '#put_item' do
54
+ it 'should fail if hash key is not present' do
55
+ expect do
56
+ subject.put_item({ 'TableName' => 'Table1',
57
+ 'Item' => {
58
+ 'AttributeName2' => { 'S' => "test" }
59
+ }})
60
+ end.to raise_error(ValidationException, /missing.*item/i)
61
+ end
62
+
63
+ it 'should fail if range key is not present' do
64
+ expect do
65
+ subject.put_item({ 'TableName' => 'Table1',
66
+ 'Item' => {
67
+ 'AttributeName1' => { 'S' => "test" }
68
+ }})
69
+ end.to raise_error(ValidationException, /missing.*item/i)
70
+ end
71
+
72
+ it 'should fail on type mismatch' do
73
+ expect do
74
+ subject.put_item({ 'TableName' => 'Table1',
75
+ 'Item' => {
76
+ 'AttributeName1' => { 'N' => "test" },
77
+ 'AttributeName2' => { 'N' => '11' }
78
+ }})
79
+ end.to raise_error(ValidationException, /mismatch/i)
80
+ end
81
+
82
+ it 'should putitem in the table' do
83
+ subject.put_item(item)
84
+ subject.items.size.should == 1
85
+ end
86
+
87
+ context 'Expected & ReturnValues' do
88
+ subject do
89
+ table = Table.new(data)
90
+ table.put_item(item)
91
+ table
92
+ end
93
+
94
+ it 'should check condition' do
95
+ [[{}, /set to null/],
96
+ [{'Exists' => true}, /set to true/],
97
+ [{'Exists' => false}],
98
+ [{'Value' => { 'S' => 'xxx' } }],
99
+ [{'Value' => { 'S' => 'xxx' }, 'Exists' => true}],
100
+ [{'Value' => { 'S' => 'xxx' }, 'Exists' => false}, /cannot expect/i]].each do |value, message|
101
+
102
+ op = lambda {
103
+ subject.put_item(item.merge({'Expected' => { 'AttributeName3' => value }}))
104
+ }
105
+
106
+ if message
107
+ expect(&op).to raise_error(ValidationException, message)
108
+ else
109
+ expect(&op).to raise_error(ConditionalCheckFailedException)
110
+ end
111
+ end
112
+ end
113
+
114
+ it 'should give default response' do
115
+ item['Item']['AttributeName3'] = { 'S' => "new" }
116
+ subject.put_item(item).should include(consumed_capacity)
117
+ end
118
+
119
+ it 'should send old item' do
120
+ old_item = Utils.deep_copy(item)
121
+ new_item = Utils.deep_copy(item)
122
+ new_item['Item']['AttributeName3'] = { 'S' => "new" }
123
+ new_item.merge!({'ReturnValues' => 'ALL_OLD'})
124
+ subject.put_item(new_item)['Attributes'].should == old_item['Item']
125
+ end
126
+ end
127
+ end
128
+
129
+ context '#get_item' do
130
+ subject do
131
+ table = Table.new(data)
132
+ table.put_item(item)
133
+ table
134
+ end
135
+
136
+ it 'should return empty when the key is not found' do
137
+ response = subject.get_item({'TableName' => 'Table1',
138
+ 'Key' => {
139
+ 'HashKeyElement' => { 'S' => 'xxx' },
140
+ 'RangeKeyElement' => { 'N' => '11' }
141
+ }
142
+ })
143
+ response.should eq(consumed_capacity)
144
+ end
145
+
146
+ it 'should filter attributes' do
147
+ response = subject.get_item({'TableName' => 'Table1',
148
+ 'Key' => {
149
+ 'HashKeyElement' => { 'S' => 'test' },
150
+ 'RangeKeyElement' => { 'N' => '11' }
151
+ },
152
+ 'AttributesToGet' => ['AttributeName3', 'xxx']
153
+ })
154
+ response.should eq({ 'Item' => { 'AttributeName3' => { 'S' => 'another'}},
155
+ 'ConsumedCapacityUnits' => 1})
156
+ end
157
+ end
158
+
159
+ context '#delete_item' do
160
+ subject do
161
+ table = Table.new(data)
162
+ table.put_item(item)
163
+ table
164
+ end
165
+
166
+ it 'should delete item' do
167
+ response = subject.delete_item(key)
168
+ response.should eq(consumed_capacity)
169
+ end
170
+
171
+ it 'should be idempotent' do
172
+ response_1 = subject.delete_item(key)
173
+ response_2 = subject.delete_item(key)
174
+
175
+ response_1.should == response_2
176
+ end
177
+
178
+ it 'should check conditions' do
179
+ expect do
180
+ subject.delete_item(key.merge({'Expected' =>
181
+ {'AttributeName3' => { 'Exists' => false }}}))
182
+ end.to raise_error(ConditionalCheckFailedException)
183
+
184
+ response = subject.delete_item(key.merge({'Expected' =>
185
+ {'AttributeName3' =>
186
+ {'Value' => { 'S' => 'another'}}}}))
187
+ response.should eq(consumed_capacity)
188
+
189
+ expect do
190
+ subject.delete_item(key.merge({'Expected' =>
191
+ {'AttributeName3' =>
192
+ {'Value' => { 'S' => 'another'}}}}))
193
+ end.to raise_error(ConditionalCheckFailedException)
194
+ end
195
+
196
+ it 'should return old value' do
197
+ response = subject.delete_item(key.merge('ReturnValues' => 'ALL_OLD'))
198
+ response.should eq(consumed_capacity.merge({'Attributes' => item['Item']}))
199
+ end
200
+ end
201
+
202
+ context '#update_item' do
203
+ subject do
204
+ table = Table.new(data)
205
+ table.put_item(item)
206
+ table
207
+ end
208
+
209
+ let(:put) do
210
+ {'AttributeUpdates' => {'AttributeName3' => { 'Value' => { 'S' => 'updated' },
211
+ 'Action' => 'PUT'}}}
212
+ end
213
+
214
+ let(:delete) do
215
+ {'AttributeUpdates' => {'AttributeName3' => {'Action' => 'DELETE'}}}
216
+ end
217
+
218
+ it "should check conditions" do
219
+ expect do
220
+ subject.update_item(key.merge({'Expected' =>
221
+ {'AttributeName3' => { 'Exists' => false }}}))
222
+ end.to raise_error(ConditionalCheckFailedException)
223
+ end
224
+
225
+ it "should create new item if the key doesn't exist" do
226
+ key['Key']['HashKeyElement']['S'] = 'new'
227
+ subject.update_item(key.merge(put))
228
+ subject.get_item(key).should include( "Item"=>
229
+ {"AttributeName1"=>{"S"=>"new"},
230
+ "AttributeName2"=>{"N"=>"11"},
231
+ "AttributeName3"=>{"S"=>"updated"}})
232
+ end
233
+
234
+ it "shouldn't create a new item if key doesn't exist and action is delete" do
235
+ key['Key']['HashKeyElement']['S'] = 'new'
236
+ subject.update_item(key.merge(delete))
237
+ subject.get_item(key).should eq(consumed_capacity)
238
+ end
239
+
240
+ it "should handle return values" do
241
+ data = key.merge(put).merge({'ReturnValues' => 'UPDATED_NEW'})
242
+ subject.update_item(data).should include({'Attributes' => { 'AttributeName3' => { 'S' => 'updated'}}})
243
+ end
244
+ end
245
+
246
+ context '#return_values' do
247
+ let(:put) do
248
+ {'AttributeUpdates' => {'AttributeName3' => { 'Value' => { 'S' => 'updated' },
249
+ 'Action' => 'PUT'}}}
250
+ end
251
+
252
+ it "should return values" do
253
+ [['ALL_OLD', {'x' => 'y'}, nil, {"Attributes" => {'x' => 'y'}}],
254
+ ['ALL_NEW', nil, {'x' => 'y'}, {"Attributes" => {'x' => 'y'}}],
255
+ ['NONE', nil, nil, {}]].each do |return_value, old_item, new_item, response|
256
+ data = {'ReturnValues' => return_value }
257
+ subject.return_values(data, old_item, new_item).should eq(response)
258
+ end
259
+ expect { subject.return_values({'ReturnValues' => 'asdf'}, nil, nil) }.to raise_error(/unknown/)
260
+ end
261
+
262
+ it "should return update old value" do
263
+ subject.put_item(item)
264
+ data = key.merge(put).merge({'ReturnValues' => 'UPDATED_OLD'})
265
+ subject.update_item(data).should include({'Attributes' => { 'AttributeName3' => { 'S' => 'another'}}})
266
+ end
267
+
268
+ it "should return update new value" do
269
+ subject.put_item(item)
270
+ data = key.merge(put).merge({'ReturnValues' => 'UPDATED_NEW'})
271
+ subject.update_item(data).should include({'Attributes' => { 'AttributeName3' => { 'S' => 'updated'}}})
272
+ end
273
+ end
274
+
275
+ context '#query' do
276
+ subject do
277
+ t = Table.new(data)
278
+ t.put_item(item)
279
+ (1..3).each do |i|
280
+ (1..10).each do |j|
281
+ item['Item']['AttributeName1']['S'] = "att#{i}"
282
+ item['Item']['AttributeName2']['N'] = j.to_s
283
+ t.put_item(item)
284
+ end
285
+ end
286
+ t
287
+ end
288
+
289
+ let(:query) do
290
+ {
291
+ 'TableName' => 'Table1',
292
+ 'Limit' => 5,
293
+ 'HashKeyValue' => {'S' => 'att1'},
294
+ 'RangeKeyCondition' => {
295
+ 'AttributeValueList' => [{'N' => '1'}],
296
+ 'ComparisonOperator' => 'GT'
297
+ },
298
+ 'ScanIndexForward' => true
299
+ }
300
+ end
301
+
302
+ it 'should not allow count and attributes_to_get simutaneously' do
303
+ expect {
304
+ subject.query({'Count' => 0, 'AttributesToGet' => ['xx']})
305
+ }.to raise_error(ValidationException, /count/i)
306
+ end
307
+
308
+ it 'should not allow to query on a table without rangekey' do
309
+ data['KeySchema'].delete('RangeKeyElement')
310
+ t = Table.new(data)
311
+ expect {
312
+ t.query(query)
313
+ }.to raise_error(ValidationException, /key schema/)
314
+ end
315
+
316
+ it 'should only allow limit greater than zero' do
317
+ expect {
318
+ subject.query(query.merge('Limit' => 0))
319
+ }.to raise_error(ValidationException, /limit/i)
320
+ end
321
+
322
+ it 'should handle basic query' do
323
+ result = subject.query(query)
324
+ result['Count'].should eq(5)
325
+ end
326
+
327
+ it 'should handle scanindexforward' do
328
+ result = subject.query(query)
329
+ result['Items'].first['AttributeName2'].should eq({'N' => '2'})
330
+ result = subject.query(query.merge({'ScanIndexForward' => false}))
331
+ result['Items'].first['AttributeName2'].should eq({'N' => '10'})
332
+ end
333
+
334
+ it 'should return lastevaluated key' do
335
+ result = subject.query(query)
336
+ result['LastEvaluatedKey'].should == {"HashKeyElement"=>{"S"=>"att1"}, "RangeKeyElement"=>{"N"=>"6"}}
337
+ result = subject.query(query.merge('Limit' => 100))
338
+ result['LastEvaluatedKey'].should be_nil
339
+
340
+ query.delete('Limit')
341
+ result = subject.query(query)
342
+ result['LastEvaluatedKey'].should be_nil
343
+ end
344
+
345
+ it 'should handle exclusive start key' do
346
+ result = subject.query(query.merge({'ExclusiveStartKey' => {"HashKeyElement"=>{"S"=>"att1"}, "RangeKeyElement"=>{"N"=>"6"}}}))
347
+ result['Count'].should eq(4)
348
+ result['Items'].first['AttributeName2'].should eq({'N' => '7'})
349
+ result = subject.query(query.merge({'ExclusiveStartKey' => {"HashKeyElement"=>{"S"=>"att1"}, "RangeKeyElement"=>{"N"=>"88"}}}))
350
+ result['Count'].should eq(0)
351
+ result['Items'].should be_empty
352
+ end
353
+
354
+ it 'should return all elements if not rangekeycondition is given' do
355
+ query.delete('RangeKeyCondition')
356
+ result = subject.query(query)
357
+ result['Count'].should eq(5)
358
+ end
359
+
360
+ it 'should handle between operator' do
361
+ query['RangeKeyCondition'] = {
362
+ 'AttributeValueList' => [{'N' => '1'}, {'N' => '4'}],
363
+ 'ComparisonOperator' => 'BETWEEN'
364
+ }
365
+ result = subject.query(query)
366
+ result['Count'].should eq(4)
367
+ end
368
+
369
+ it 'should handle attributes_to_get' do
370
+ query['AttributesToGet'] = ['AttributeName1', "AttributeName2"]
371
+ result = subject.query(query)
372
+ result['Items'].first.should eq('AttributeName1' => { 'S' => 'att1'},
373
+ 'AttributeName2' => { 'N' => '2' })
374
+ end
375
+ end
376
+
377
+ context '#scan' do
378
+ subject do
379
+ t = Table.new(data)
380
+ (1..3).each do |i|
381
+ (1..10).each do |j|
382
+ item['Item']['AttributeName1']['S'] = "att#{i}"
383
+ item['Item']['AttributeName2']['N'] = j.to_s
384
+ t.put_item(item)
385
+ end
386
+ end
387
+ t
388
+ end
389
+
390
+ let(:scan) do
391
+ {
392
+ 'TableName' => 'Table1',
393
+ 'ScanFilter' => {
394
+ 'AttributeName2' => {
395
+ 'AttributeValueList' => [{'N' => '1'}],
396
+ 'ComparisonOperator' => 'GE'
397
+ }
398
+ }
399
+ }
400
+ end
401
+
402
+ it 'should not allow count and attributes_to_get simutaneously' do
403
+ expect {
404
+ subject.scan({'Count' => 0, 'AttributesToGet' => ['xx']})
405
+ }.to raise_error(ValidationException, /count/i)
406
+ end
407
+
408
+ it 'should only allow limit greater than zero' do
409
+ expect {
410
+ subject.scan(scan.merge('Limit' => 0))
411
+ }.to raise_error(ValidationException, /limit/i)
412
+ end
413
+
414
+ it 'should handle basic scan' do
415
+ result = subject.scan(scan)
416
+ result['Count'].should eq(30)
417
+
418
+ scan['ScanFilter']['AttributeName2']['ComparisonOperator'] = 'EQ'
419
+ subject.scan(scan)['Count'].should eq(3)
420
+ end
421
+
422
+ it 'should return lastevaluated key' do
423
+ scan['Limit'] = 5
424
+ result = subject.scan(scan)
425
+ result['LastEvaluatedKey'].should == {"HashKeyElement"=>{"S"=>"att1"}, "RangeKeyElement"=>{"N"=>"5"}}
426
+ result = subject.scan(scan.merge('Limit' => 100))
427
+ result['LastEvaluatedKey'].should be_nil
428
+
429
+ scan.delete('Limit')
430
+ result = subject.scan(scan)
431
+ result['LastEvaluatedKey'].should be_nil
432
+ end
433
+ end
434
+ end
435
+ end