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,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