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,155 @@
1
+ require 'yaml'
2
+
3
+ module FakeDynamo
4
+ module Validation
5
+
6
+ def validate!(&block)
7
+ @api_errors = []
8
+ yield
9
+ unless @api_errors.empty?
10
+ plural = @api_errors.size == 1 ? '' : 's'
11
+ message = "#{@api_errors.size} error#{plural} detected: #{@api_errors.join('; ')}"
12
+ raise ValidationException, message
13
+ end
14
+ end
15
+
16
+
17
+ def add_errors(message)
18
+ @api_errors << message
19
+ end
20
+
21
+ def validate_payload(operation, data)
22
+ validate! do
23
+ validate_operation(operation)
24
+ validate_input(operation, data)
25
+ end
26
+ end
27
+
28
+ def validate_operation(operation)
29
+ raise UnknownOperationException, "Unknown operation: #{operation}" unless available_operations.include? operation
30
+ end
31
+
32
+ def available_operations
33
+ api_config[:operations].keys
34
+ end
35
+
36
+ def validate_input(operation, data)
37
+ api_input_spec(operation).each do |attribute, spec|
38
+ validate_spec(attribute, data[attribute], spec, [])
39
+ end
40
+ end
41
+
42
+ def validate_spec(attribute, data, spec, parents)
43
+ if not data
44
+ if spec.include?(:required)
45
+ add_errors("value null at '#{param(attribute, parents)}' failed to satisfy the constraint: Member must not be null")
46
+ end
47
+ return
48
+ end
49
+
50
+ spec.each do |constrain|
51
+ case constrain
52
+ when :string
53
+ add_errors("The parameter '#{param(attribute, parents)}' must be a string") unless data.kind_of? String
54
+ when :long
55
+ add_errors("The parameter '#{param(attribute, parents)}' must be a long") unless data.kind_of? Fixnum
56
+ when :integer
57
+ add_errors("The parameter '#{param(attribute, parents)}' must be a integer") unless data.kind_of? Fixnum
58
+ when :boolean
59
+ add_errors("The parameter '#{param(attribute, parents)}' must be a boolean") unless (data.kind_of? TrueClass or data.kind_of? FalseClass)
60
+ when Hash
61
+ new_parents = parents + [attribute]
62
+ case constrain.keys.first
63
+ when :pattern
64
+ pattern = constrain[:pattern]
65
+ unless data =~ pattern
66
+ add_errors("The parameter '#{param(attribute, parents)}' should match the pattern #{pattern}")
67
+ end
68
+ when :within
69
+ range = constrain[:within]
70
+ unless range.include? data.size
71
+ add_errors("The parameter '#{param(attribute, parents)}' value '#{data}' should be within #{range} characters")
72
+ end
73
+ when :enum
74
+ enum = constrain[:enum]
75
+ unless enum.include? data
76
+ add_errors("Value '#{data}' at '#{param(attribute, parents)}' failed to satisfy the constraint: Member must satisfy enum values set: #{enum}")
77
+ end
78
+ when :structure
79
+ structure = constrain[:structure]
80
+ structure.each do |attribute, spec|
81
+ validate_spec(attribute, data[attribute], spec, new_parents)
82
+ end
83
+ when :map
84
+ map = constrain[:map]
85
+ raise "#{param(attribute, parents)} must be a Hash" unless data.kind_of? Hash
86
+ data.each do |key, value|
87
+ validate_spec(key, key, map[:key], new_parents)
88
+ validate_spec(key, value, map[:value], new_parents)
89
+ end
90
+ when :list
91
+ raise "#{param(attribute, parents)} must be a Array" unless data.kind_of? Array
92
+ data.each do |element|
93
+ validate_spec(element, element, constrain[:list], new_parents)
94
+ end
95
+ else
96
+ raise "Unhandled constraint #{constrain}"
97
+ end
98
+ when :required
99
+ # handled earlier
100
+ else
101
+ raise "Unhandled constraint #{constrain}"
102
+ end
103
+ end
104
+ end
105
+
106
+ def param(attribute, parents)
107
+ (parents + [attribute]).join('.')
108
+ end
109
+
110
+ def api_input_spec(operation)
111
+ api_config[:operations][operation][:input]
112
+ end
113
+
114
+ def api_config
115
+ @api_config ||= YAML.load_file(api_config_path)
116
+ end
117
+
118
+ def api_config_path
119
+ File.join File.expand_path(File.dirname(__FILE__)), 'api.yml'
120
+ end
121
+
122
+ def validate_type(value, attribute)
123
+ if attribute.kind_of?(Attribute)
124
+ expected_type = value.keys.first
125
+ if expected_type != attribute.type
126
+ raise ValidationException, "Type mismatch for key #{attribute.name}"
127
+ end
128
+ else
129
+ raise 'Unknown attribute'
130
+ end
131
+ end
132
+
133
+ def validate_key_schema(data, key_schema)
134
+ key = data[key_schema.hash_key.name] or raise ValidationException, "Missing the key #{key_schema.hash_key.name} in the item"
135
+ validate_type(key, key_schema.hash_key)
136
+
137
+ if key_schema.range_key
138
+ range_key = data[key_schema.range_key.name] or raise ValidationException, "Missing the key #{key_schema.range_key.name} in the item"
139
+ validate_type(range_key, key_schema.range_key)
140
+ end
141
+ end
142
+
143
+ def validate_key_data(data, key_schema)
144
+ validate_type(data['HashKeyElement'], key_schema.hash_key)
145
+
146
+ if key_schema.range_key
147
+ range_key = data['RangeKeyElement'] or raise ValidationException, "Missing the key RangeKeyElement in the Key"
148
+ validate_type(range_key, key_schema.range_key)
149
+ elsif data['RangeKeyElement']
150
+ raise ValidationException, "RangeKeyElement is not present in the schema"
151
+ end
152
+ end
153
+
154
+ end
155
+ end
@@ -0,0 +1,3 @@
1
+ module FakeDynamo
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,257 @@
1
+ require 'spec_helper'
2
+
3
+ module FakeDynamo
4
+ describe DB do
5
+ let(:data) do
6
+ {
7
+ "TableName" => "Table1",
8
+ "KeySchema" =>
9
+ {"HashKeyElement" => {"AttributeName" => "AttributeName1","AttributeType" => "S"},
10
+ "RangeKeyElement" => {"AttributeName" => "AttributeName2","AttributeType" => "N"}},
11
+ "ProvisionedThroughput" => {"ReadCapacityUnits" => 5,"WriteCapacityUnits" => 10}
12
+ }
13
+ end
14
+
15
+ let(:user_table) do
16
+ {"TableName" => "User",
17
+ "KeySchema" =>
18
+ {"HashKeyElement" => {"AttributeName" => "id","AttributeType" => "S"}},
19
+ "ProvisionedThroughput" => {"ReadCapacityUnits" => 5,"WriteCapacityUnits" => 10}
20
+ }
21
+ end
22
+
23
+ it 'should not allow to create duplicate tables' do
24
+ subject.create_table(data)
25
+ expect { subject.create_table(data) }.to raise_error(ResourceInUseException, /duplicate/i)
26
+ end
27
+
28
+ it 'should fail on unknown operation' do
29
+ expect { subject.process('unknown', data) }.to raise_error(UnknownOperationException, /unknown/i)
30
+ end
31
+
32
+ context 'DescribeTable' do
33
+ it 'should describe table' do
34
+ table = subject.create_table(data)
35
+ description = subject.describe_table({'TableName' => 'Table1'})
36
+ description.should include({
37
+ "ItemCount"=>0,
38
+ "TableSizeBytes"=>0})
39
+ end
40
+
41
+ it 'should fail on unavailable table' do
42
+ expect { subject.describe_table({'TableName' => 'Table1'}) }.to raise_error(ResourceNotFoundException, /table1 not found/i)
43
+ end
44
+
45
+ it 'should fail on invalid payload' do
46
+ expect { subject.process('DescribeTable', {}) }.to raise_error(ValidationException, /null/)
47
+ end
48
+ end
49
+
50
+ context 'DeleteTable' do
51
+ it "should delete table" do
52
+ subject.create_table(data)
53
+ response = subject.delete_table(data)
54
+ subject.tables.should be_empty
55
+ response['TableDescription']['TableStatus'].should == 'DELETING'
56
+ end
57
+
58
+ it "should not allow to delete the same table twice" do
59
+ subject.create_table(data)
60
+ subject.delete_table(data)
61
+ expect { subject.delete_table(data) }.to raise_error(ResourceNotFoundException, /table1 not found/i)
62
+ end
63
+ end
64
+
65
+ context 'ListTable' do
66
+ before :each do
67
+ (1..5).each do |i|
68
+ data['TableName'] = "Table#{i}"
69
+ subject.create_table(data)
70
+ end
71
+ end
72
+
73
+ it "should list all table" do
74
+ result = subject.list_tables({})
75
+ result.should eq({"TableNames"=>["Table1", "Table2", "Table3", "Table4", "Table5"]})
76
+ end
77
+
78
+ it 'should handle limit and exclusive_start_table_name' do
79
+ result = subject.list_tables({'Limit' => 3,
80
+ 'ExclusiveStartTableName' => 'Table1'})
81
+ result.should eq({'TableNames'=>["Table2", "Table3", "Table4"],
82
+ 'LastEvaluatedTableName' => "Table4"})
83
+
84
+ result = subject.list_tables({'Limit' => 3,
85
+ 'ExclusiveStartTableName' => 'Table2'})
86
+ result.should eq({'TableNames' => ['Table3', 'Table4', 'Table5']})
87
+
88
+ result = subject.list_tables({'ExclusiveStartTableName' => 'blah'})
89
+ result.should eq({"TableNames"=>["Table1", "Table2", "Table3", "Table4", "Table5"]})
90
+ end
91
+
92
+ it 'should validate payload' do
93
+ expect { subject.process('ListTables', {'Limit' => 's'}) }.to raise_error(ValidationException)
94
+ end
95
+ end
96
+
97
+ context 'UpdateTable' do
98
+
99
+ it 'should update throughput' do
100
+ subject.create_table(data)
101
+ response = subject.update_table({'TableName' => 'Table1',
102
+ 'ProvisionedThroughput' => {
103
+ 'ReadCapacityUnits' => 7,
104
+ 'WriteCapacityUnits' => 15
105
+ }})
106
+
107
+ response['TableDescription'].should include({'TableStatus' => 'UPDATING'})
108
+ end
109
+
110
+ it 'should handle validation' do
111
+ subject.create_table(data)
112
+ expect { subject.process('UpdateTable', {'TableName' => 'Table1'}) }.to raise_error(ValidationException, /null/)
113
+ end
114
+ end
115
+
116
+ context 'delegate to table' do
117
+ subject do
118
+ db = DB.new
119
+ db.create_table(data)
120
+ db
121
+ end
122
+
123
+ let(:item) do
124
+ { 'TableName' => 'Table1',
125
+ 'Item' => {
126
+ 'AttributeName1' => { 'S' => "test" },
127
+ 'AttributeName2' => { 'N' => '11' },
128
+ 'AttributeName3' => { 'S' => "another" }
129
+ }}
130
+ end
131
+
132
+ it 'should delegate to table' do
133
+ subject.process('PutItem', item)
134
+ subject.process('GetItem', {
135
+ 'TableName' => 'Table1',
136
+ 'Key' => {
137
+ 'HashKeyElement' => { 'S' => 'test' },
138
+ 'RangeKeyElement' => { 'N' => '11' }
139
+ },
140
+ 'AttributesToGet' => ['AttributeName3']
141
+ })
142
+ subject.process('DeleteItem', {
143
+ 'TableName' => 'Table1',
144
+ 'Key' => {
145
+ 'HashKeyElement' => { 'S' => 'test' },
146
+ 'RangeKeyElement' => { 'N' => '11' }
147
+ }})
148
+ subject.process('UpdateItem', {
149
+ 'TableName' => 'Table1',
150
+ 'Key' => {
151
+ 'HashKeyElement' => { 'S' => 'test' },
152
+ 'RangeKeyElement' => { 'N' => '11' }
153
+ },
154
+ 'AttributeUpdates' =>
155
+ {'AttributeName3' =>
156
+ {'Value' => {'S' => 'AttributeValue3_New'},
157
+ 'Action' => 'PUT'}
158
+ },
159
+ 'ReturnValues' => 'ALL_NEW'
160
+ })
161
+
162
+ subject.process('Query', {
163
+ 'TableName' => 'Table1',
164
+ 'Limit' => 5,
165
+ 'Count' => true,
166
+ 'HashKeyValue' => {'S' => 'att1'},
167
+ 'RangeKeyCondition' => {
168
+ 'AttributeValueList' => [{'N' => '1'}],
169
+ 'ComparisonOperator' => 'GT'
170
+ },
171
+ 'ScanIndexForward' => true
172
+ })
173
+ end
174
+ end
175
+
176
+ context 'batch get item' do
177
+ subject do
178
+ db = DB.new
179
+ db.create_table(data)
180
+ db.create_table(user_table)
181
+
182
+ db.put_item({ 'TableName' => 'Table1',
183
+ 'Item' => {
184
+ 'AttributeName1' => { 'S' => "test" },
185
+ 'AttributeName2' => { 'N' => '11' },
186
+ 'AttributeName3' => { 'S' => "another" }
187
+ }})
188
+
189
+ db.put_item({'TableName' => 'User',
190
+ 'Item' => { 'id' => { 'S' => '1' }}
191
+ })
192
+ db.put_item({'TableName' => 'User',
193
+ 'Item' => { 'id' => { 'S' => '2' }}
194
+ })
195
+ db
196
+ end
197
+
198
+ it 'should validate payload' do
199
+ expect {
200
+ subject.process('BatchGetItem', {})
201
+ }.to raise_error(FakeDynamo::ValidationException)
202
+ end
203
+
204
+ it 'should return items' do
205
+ response = subject.process('BatchGetItem', { 'RequestItems' =>
206
+ {
207
+ 'User' => {
208
+ 'Keys' => [{ 'HashKeyElement' => { 'S' => '1' }},
209
+ { 'HashKeyElement' => { 'S' => '2' }}]
210
+ },
211
+ 'Table1' => {
212
+ 'Keys' => [{'HashKeyElement' => { 'S' => 'test' },
213
+ 'RangeKeyElement' => { 'N' => '11' }}],
214
+ 'AttributesToGet' => ['AttributeName1', 'AttributeName2']
215
+ }
216
+ }})
217
+
218
+ response.should eq({"Responses"=>
219
+ {"User"=>
220
+ {"ConsumedCapacityUnits"=>1,
221
+ "Items"=>[{"id"=>{"S"=>"1"}}, {"id"=>{"S"=>"2"}}]},
222
+ "Table1"=>
223
+ {"ConsumedCapacityUnits"=>1,
224
+ "Items"=>
225
+ [{"AttributeName1"=>{"S"=>"test"},
226
+ "AttributeName2"=>{"N"=>"11"}}]}},
227
+ "UnprocessedKeys"=>{}})
228
+ end
229
+
230
+ it 'should handle missing items' do
231
+ response = subject.process('BatchGetItem', { 'RequestItems' =>
232
+ {
233
+ 'User' => {
234
+ 'Keys' => [{ 'HashKeyElement' => { 'S' => '1' }},
235
+ { 'HashKeyElement' => { 'S' => 'asd' }}]
236
+ }
237
+ }})
238
+ response.should eq({"Responses"=>
239
+ {"User"=>
240
+ {"ConsumedCapacityUnits"=>1,
241
+ "Items"=>[{"id"=>{"S"=>"1"}}]}},
242
+ "UnprocessedKeys"=>{}})
243
+ end
244
+
245
+ it 'should fail if table not found' do
246
+ expect {
247
+ subject.process('BatchGetItem', { 'RequestItems' =>
248
+ {
249
+ 'xxx' => {
250
+ 'Keys' => [{ 'HashKeyElement' => { 'S' => '1' }},
251
+ { 'HashKeyElement' => { 'S' => 'asd' }}]}
252
+ }})
253
+ }.to raise_error(FakeDynamo::ResourceNotFoundException)
254
+ end
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,122 @@
1
+ require 'spec_helper'
2
+
3
+ module FakeDynamo
4
+
5
+ class FilterTest
6
+ include Filter
7
+ end
8
+
9
+ describe Filter do
10
+ subject { FilterTest.new }
11
+
12
+ let(:s_attr) { Attribute.new('test', 'bcd', 'S')}
13
+ let(:ss_attr) { Attribute.new('test', ['ab', 'cd'], 'SS') }
14
+ let(:n_attr) { Attribute.new('test', '10', 'N')}
15
+ let(:ns_attr) { Attribute.new('test', ['1', '2', '3', '4'], 'NS')}
16
+
17
+
18
+ it 'tests eq' do
19
+ subject.eq_filter([{'S' => 'bcd'}], s_attr, false).should be_true
20
+ subject.eq_filter([{'S' => '10'}], n_attr, false).should be_false
21
+ expect { subject.eq_filter([{'S' => '10'}], n_attr, true) }.to raise_error(ValidationException, /mismatch/)
22
+ end
23
+
24
+ it 'tests le' do
25
+ subject.le_filter([{'S' => 'c'}], s_attr, false).should be_true
26
+ subject.le_filter([{'S' => 'bcd'}], s_attr, false).should be_true
27
+ subject.le_filter([{'N' => 'bcd'}], s_attr, false).should be_false
28
+ subject.le_filter([{'S' => 'a'}], s_attr, false).should be_false
29
+ subject.le_filter([{'N' => '10'}], n_attr, false).should be_true
30
+ subject.le_filter([{'N' => '11'}], n_attr, false).should be_true
31
+ subject.le_filter([{'N' => '1'}], n_attr, false).should be_false
32
+ end
33
+
34
+ it 'tests lt' do
35
+ subject.lt_filter([{'S' => 'c'}], s_attr, false).should be_true
36
+ subject.lt_filter([{'S' => 'bcd'}], s_attr, false).should be_false
37
+ subject.lt_filter([{'N' => 'bcd'}], s_attr, false).should be_false
38
+ subject.lt_filter([{'S' => 'a'}], s_attr, false).should be_false
39
+ subject.lt_filter([{'N' => '10'}], n_attr, false).should be_false
40
+ subject.lt_filter([{'N' => '11'}], n_attr, false).should be_true
41
+ subject.lt_filter([{'N' => '1'}], n_attr, false).should be_false
42
+ end
43
+
44
+ it 'test ge' do
45
+ subject.ge_filter([{'S' => 'c'}], s_attr, false).should be_false
46
+ subject.ge_filter([{'S' => 'bcd'}], s_attr, false).should be_true
47
+ subject.ge_filter([{'N' => 'bcd'}], s_attr, false).should be_false
48
+ subject.ge_filter([{'S' => 'a'}], s_attr, false).should be_true
49
+ subject.ge_filter([{'N' => '10'}], n_attr, false).should be_true
50
+ subject.ge_filter([{'N' => '11'}], n_attr, false).should be_false
51
+ subject.ge_filter([{'N' => '1'}], n_attr, false).should be_true
52
+ end
53
+
54
+ it 'test gt' do
55
+ subject.gt_filter([{'S' => 'c'}], s_attr, false).should be_false
56
+ subject.gt_filter([{'S' => 'bcd'}], s_attr, false).should be_false
57
+ subject.gt_filter([{'N' => 'bcd'}], s_attr, false).should be_false
58
+ subject.gt_filter([{'S' => 'a'}], s_attr, false).should be_true
59
+ subject.gt_filter([{'N' => '10'}], n_attr, false).should be_false
60
+ subject.gt_filter([{'N' => '11'}], n_attr, false).should be_false
61
+ subject.gt_filter([{'N' => '1'}], n_attr, false).should be_true
62
+ end
63
+
64
+ it 'test begins_with' do
65
+ subject.begins_with_filter([{'S' => 'bc'}], s_attr, false).should be_true
66
+ subject.begins_with_filter([{'S' => 'cd'}], s_attr, false).should be_false
67
+ expect {
68
+ subject.begins_with_filter([{'N' => '10'}], n_attr, false)
69
+ }.to raise_error(ValidationException, /not supported/)
70
+ end
71
+
72
+ it 'test between' do
73
+ expect {
74
+ subject.between_filter([{'S' => 'bc'}], s_attr, false)
75
+ }.to raise_error(ValidationException, /argument count/)
76
+ subject.between_filter([{'S' => 'a'},{'S' => 'c'}], s_attr, false).should be_true
77
+ subject.between_filter([{'S' => 'bcd'},{'S' => 'bcd'}], s_attr, false).should be_true
78
+ subject.between_filter([{'N' => '9'},{'N' => '11'}], n_attr, false).should be_true
79
+ subject.between_filter([{'S' => '9'},{'S' => '11'}], n_attr, false).should be_false
80
+ end
81
+
82
+ it 'test ne' do
83
+ subject.ne_filter([{'S' => 'bcd'}], s_attr, false).should be_false
84
+ subject.ne_filter([{'S' => '10'}], n_attr, false).should be_false
85
+ subject.ne_filter([{'S' => 'xx'}], s_attr, false).should be_true
86
+ subject.ne_filter([{'S' => '10'}], n_attr, false).should be_false
87
+ end
88
+
89
+ it 'test not null' do
90
+ subject.not_null_filter(nil, nil, false).should be_false
91
+ subject.not_null_filter(nil, s_attr, false).should be_true
92
+ end
93
+
94
+ it 'test null' do
95
+ subject.null_filter(nil, nil, false).should be_true
96
+ subject.null_filter(nil, s_attr, false).should be_false
97
+ end
98
+
99
+ it 'test contains' do
100
+ subject.contains_filter([{'S' => 'cd'}], s_attr, false).should be_true
101
+ subject.contains_filter([{'S' => 'cd'}], ss_attr, false).should be_true
102
+ subject.contains_filter([{'N' => '2'}], ns_attr, false).should be_true
103
+ subject.contains_filter([{'N' => '10'}], n_attr, false).should be_false
104
+ end
105
+
106
+ it 'test not contains' do
107
+ subject.not_contains_filter([{'S' => 'xx'}], s_attr, false).should be_true
108
+ subject.not_contains_filter([{'S' => 'cd'}], s_attr, false).should be_false
109
+ subject.not_contains_filter([{'S' => 'cd'}], ss_attr, false).should be_false
110
+ subject.not_contains_filter([{'N' => '2'}], ns_attr, false).should be_false
111
+ subject.not_contains_filter([{'N' => '12'}], ns_attr, false).should be_true
112
+ subject.not_contains_filter([{'N' => '10'}], n_attr, false).should be_false
113
+ end
114
+
115
+ it 'test in' do
116
+ subject.in_filter([{'S' => 'bcd'}], s_attr, false).should be_true
117
+ subject.in_filter([{'S' => 'bcd'}, {'N' => '10'}], n_attr, true).should be_true
118
+ subject.in_filter([{'N' => '1'}], ns_attr, true).should be_false
119
+ subject.in_filter([{'S' => 'xx'}], s_attr, false).should be_false
120
+ end
121
+ end
122
+ end