fake_dynamo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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