fake_servicebus 0.0.2

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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +4 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.md +20 -0
  7. data/README.md +41 -0
  8. data/Rakefile +31 -0
  9. data/bin/fake_servicebus +65 -0
  10. data/fake_servicebus.gemspec +33 -0
  11. data/lib/fake_servicebus.rb +90 -0
  12. data/lib/fake_servicebus/actions/create_queue.rb +64 -0
  13. data/lib/fake_servicebus/actions/delete_message.rb +19 -0
  14. data/lib/fake_servicebus/actions/delete_queue.rb +18 -0
  15. data/lib/fake_servicebus/actions/get_queue.rb +20 -0
  16. data/lib/fake_servicebus/actions/list_queues.rb +28 -0
  17. data/lib/fake_servicebus/actions/receive_message.rb +44 -0
  18. data/lib/fake_servicebus/actions/renew_lock_message.rb +19 -0
  19. data/lib/fake_servicebus/actions/send_message.rb +22 -0
  20. data/lib/fake_servicebus/actions/unlock_message.rb +19 -0
  21. data/lib/fake_servicebus/api.rb +71 -0
  22. data/lib/fake_servicebus/catch_errors.rb +19 -0
  23. data/lib/fake_servicebus/collection_view.rb +20 -0
  24. data/lib/fake_servicebus/daemonize.rb +30 -0
  25. data/lib/fake_servicebus/databases/file.rb +129 -0
  26. data/lib/fake_servicebus/databases/memory.rb +30 -0
  27. data/lib/fake_servicebus/error_response.rb +55 -0
  28. data/lib/fake_servicebus/error_responses.yml +32 -0
  29. data/lib/fake_servicebus/message.rb +59 -0
  30. data/lib/fake_servicebus/queue.rb +201 -0
  31. data/lib/fake_servicebus/queue_factory.rb +16 -0
  32. data/lib/fake_servicebus/queues.rb +72 -0
  33. data/lib/fake_servicebus/responder.rb +41 -0
  34. data/lib/fake_servicebus/server.rb +19 -0
  35. data/lib/fake_servicebus/show_output.rb +21 -0
  36. data/lib/fake_servicebus/test_integration.rb +122 -0
  37. data/lib/fake_servicebus/version.rb +3 -0
  38. data/lib/fake_servicebus/web_interface.rb +74 -0
  39. data/spec/acceptance/message_actions_spec.rb +452 -0
  40. data/spec/acceptance/queue_actions_spec.rb +82 -0
  41. data/spec/integration_spec_helper.rb +23 -0
  42. data/spec/spec_helper.rb +3 -0
  43. data/spec/unit/api_spec.rb +76 -0
  44. data/spec/unit/catch_errors_spec.rb +43 -0
  45. data/spec/unit/collection_view_spec.rb +41 -0
  46. data/spec/unit/error_response_spec.rb +65 -0
  47. data/spec/unit/message_spec.rb +76 -0
  48. data/spec/unit/queue_factory_spec.rb +13 -0
  49. data/spec/unit/queue_spec.rb +204 -0
  50. data/spec/unit/queues_spec.rb +102 -0
  51. data/spec/unit/responder_spec.rb +44 -0
  52. data/spec/unit/show_output_spec.rb +22 -0
  53. data/spec/unit/web_interface_spec.rb +15 -0
  54. metadata +266 -0
@@ -0,0 +1,32 @@
1
+ AccessDenied: 403
2
+ AuthFailure: 401
3
+ ConflictingQueryParameter: 400
4
+ InternalError: 500
5
+ InvalidAccessKeyId: 401
6
+ InvalidAction: 400
7
+ InvalidAddress: 404
8
+ InvalidAttributeName: 400
9
+ InvalidHttpRequest: 400
10
+ InvalidMessageContents: 400
11
+ InvalidParameterCombination: 400
12
+ InvalidParameterValue: 400
13
+ InvalidQueryParameter: 400
14
+ InvalidRequest: 400
15
+ InvalidSecurity: 403
16
+ InvalidSecurityToken: 400
17
+ MalformedVersion: 400
18
+ MessageTooLong: 400
19
+ MessageNotInflight: 400
20
+ MissingClientTokenId: 403
21
+ MissingCredentials: 401
22
+ MissingParameter: 400
23
+ NoSuchVersion: 400
24
+ NonExistentQueue: 400
25
+ NotAuthorizedToUseVersion: 401
26
+ QueueDeletedRecently: 400
27
+ ReadCountOutOfRange: 400
28
+ ReceiptHandleIsInvalid: 400
29
+ RequestExpired: 400
30
+ RequestThrottled: 403
31
+ ServiceUnavailable: 503
32
+ X509ParseError: 400
@@ -0,0 +1,59 @@
1
+ require 'securerandom'
2
+ require 'digest/sha1'
3
+
4
+ module FakeServiceBus
5
+ class Message
6
+
7
+ attr_reader :queue_name, :body, :sequence_number, :lock_token, :location, :delay_seconds, :delivery_count,
8
+ :enqueued_timestamp
9
+ attr_accessor :locked_until
10
+
11
+ def initialize(options = {})
12
+ @queue_name = options.fetch("queue_name")
13
+ @body = options.fetch("body")
14
+ @sequence_number = options.fetch("sequence_number") { SecureRandom.random_number(9e5).to_i }
15
+ @lock_token = options.fetch("lock_token") { SecureRandom.uuid }
16
+ @location = "https://fake_servicebus/#{@queue_name}/messages/#{@sequence_number}/#{@lock_token}"
17
+ @delivery_count = 0
18
+ @enqueued_timestamp = Time.now.to_i * 1000
19
+ #@delay_seconds = options.fetch("DelaySeconds", 0).to_i
20
+ end
21
+
22
+ def expire!
23
+ self.locked_until = nil
24
+ end
25
+
26
+ def receive!
27
+ @delivery_count += 1
28
+ end
29
+
30
+ def expired?( limit = Time.now )
31
+ self.locked_until.nil? || self.locked_until < limit
32
+ end
33
+
34
+ def expire_at(seconds)
35
+ self.locked_until = Time.now + seconds
36
+ end
37
+
38
+ def published?
39
+ if self.delay_seconds && self.delay_seconds > 0
40
+ elapsed_seconds = Time.now.to_i - (self.enqueued_timestamp.to_i / 1000)
41
+ elapsed_seconds >= self.delay_seconds
42
+ else
43
+ true
44
+ end
45
+ end
46
+
47
+ def attributes
48
+ {
49
+ "QueueName"=> queue_name,
50
+ "SequenceNumber"=> sequence_number,
51
+ "LockToken"=> lock_token,
52
+ "Location"=> location,
53
+ "DeliveryCount"=> delivery_count,
54
+ "EnqueuedTimestamp"=> enqueued_timestamp
55
+ }
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,201 @@
1
+ require 'duration'
2
+ require 'monitor'
3
+ require 'securerandom'
4
+ require 'fake_servicebus/collection_view'
5
+ require 'json'
6
+
7
+ module FakeServiceBus
8
+
9
+ MessageNotInflight = Class.new(RuntimeError)
10
+ ReadCountOutOfRange = Class.new(RuntimeError)
11
+ ReceiptHandleIsInvalid = Class.new(RuntimeError)
12
+
13
+ class Queue
14
+
15
+ LOCK_DURATION = 60
16
+
17
+ attr_reader :name, :message_factory, :queue_attributes
18
+
19
+ def initialize(options = {})
20
+ @message_factory = options.fetch(:message_factory)
21
+
22
+ @name = options.fetch(:name)
23
+ @queue_attributes = default_attibutes.merge(options.fetch('Attributes'){ {} })
24
+ @lock = Monitor.new
25
+ reset
26
+ end
27
+
28
+ def default_attibutes
29
+ {
30
+ "LockDuration" => "PT1M",
31
+ "MaxSizeInMegabytes" => 1024,
32
+ "RequiresDuplicateDetection" => false,
33
+ "RequiresSession" => false,
34
+ "DefaultMessageTimeToLive" => "P10675199DT2H48M5.4775807S",
35
+ "DeadLetteringOnMessageExpiration" => false,
36
+ "DuplicateDetectionHistoryTimeWindow" => "PT10M",
37
+ "MaxDeliveryCount" => 10,
38
+ "EnableBatchedOperations" => true,
39
+ "SizeInBytes" => 0,
40
+ "MessageCount" => 0,
41
+ "CreatedAt" => Time.now.utc.iso8601,
42
+ "UpdatedAt" => Time.now.utc.iso8601,
43
+ }
44
+ end
45
+
46
+ def to_yaml
47
+ {
48
+ "Attributes" => queue_attributes,
49
+ }
50
+ end
51
+
52
+ def add_queue_attributes(attrs)
53
+ queue_attributes.merge!(attrs)
54
+ end
55
+
56
+ def attributes
57
+ queue_attributes.merge(
58
+ "MessageCount" => @messages.size + @messages_in_flight.size,
59
+ )
60
+ end
61
+
62
+ def send_message(options = {})
63
+ with_lock do
64
+ message = options.fetch(:message){ message_factory.new(options) }
65
+ if message
66
+ @messages[message.lock_token] = message
67
+ end
68
+ message
69
+ end
70
+ end
71
+
72
+ def receive_message(options = {})
73
+ return nil if @messages.empty?
74
+
75
+ result = nil
76
+ with_lock do
77
+ published_messages = @messages.values.select { |m| m.published? }
78
+
79
+ message = published_messages.delete_at(0)
80
+ @messages.delete(message.lock_token)
81
+ unless check_message_for_dlq(message, options)
82
+ message.expire_at(lock_duration)
83
+ message.receive!
84
+ @messages_in_flight[message.lock_token] = message
85
+ result = message
86
+ end
87
+ end
88
+
89
+ result
90
+ end
91
+
92
+ def lock_duration
93
+ if value = attributes['LockDuration']
94
+ Duration.new(value).to_i
95
+ else
96
+ LOCK_DURATION
97
+ end
98
+ end
99
+
100
+ def timeout_messages!
101
+ with_lock do
102
+ expired = @messages_in_flight.inject({}) do |memo,(lock_token,message)|
103
+ if message.expired?
104
+ memo[lock_token] = message
105
+ end
106
+ memo
107
+ end
108
+ expired.each do |lock_token,message|
109
+ message.expire!
110
+ @messages[lock_token] = message
111
+ @messages_in_flight.delete(lock_token)
112
+ end
113
+ end
114
+ end
115
+
116
+ def unlock_message(lock_token)
117
+ with_lock do
118
+ message = @messages_in_flight[lock_token]
119
+ raise MessageNotInflight unless message
120
+
121
+ message.expire!
122
+ @messages[lock_token] = message
123
+ @messages_in_flight.delete(lock_token)
124
+ end
125
+ end
126
+
127
+ def renew_lock_message(lock_token)
128
+
129
+ with_lock do
130
+ message = @messages_in_flight[lock_token]
131
+ raise MessageNotInflight unless message
132
+
133
+ message.expire_at(default_visibility_timeout)
134
+ end
135
+ end
136
+
137
+ def check_message_for_dlq(message, options={})
138
+ if dlq_name = queue_attributes["ForwardDeadLetteredMessagesTo"]
139
+ dlq = options[:queues].list.find{|queue| queue.name == dlq_name}
140
+ if dlq && message.approximate_receive_count >= queue_attributes["MaxDeliveryCount"].to_i
141
+ dlq.send_message(message: message)
142
+ message.expire!
143
+ true
144
+ end
145
+ end
146
+ end
147
+
148
+ def delete_message(lock_token)
149
+ with_lock do
150
+ @messages.delete(lock_token)
151
+ @messages_in_flight.delete(lock_token)
152
+ end
153
+ end
154
+
155
+ def reset
156
+ with_lock do
157
+ @messages = {}
158
+ @messages_view = FakeServiceBus::CollectionView.new(@messages)
159
+ reset_messages_in_flight
160
+ end
161
+ end
162
+
163
+ def expire
164
+ with_lock do
165
+ @messages.merge!(@messages_in_flight)
166
+ @messages_in_flight.clear()
167
+ reset_messages_in_flight
168
+ end
169
+ end
170
+
171
+ def reset_messages_in_flight
172
+ with_lock do
173
+ @messages_in_flight = {}
174
+ @messages_in_flight_view = FakeServiceBus::CollectionView.new(@messages_in_flight)
175
+ end
176
+ end
177
+
178
+ def messages
179
+ @messages_view
180
+ end
181
+
182
+ def messages_in_flight
183
+ @messages_in_flight_view
184
+ end
185
+
186
+ def size
187
+ @messages.size
188
+ end
189
+
190
+ def published_size
191
+ @messages.values.select { |m| m.published? }.size
192
+ end
193
+
194
+ def with_lock
195
+ @lock.synchronize do
196
+ yield
197
+ end
198
+ end
199
+
200
+ end
201
+ end
@@ -0,0 +1,16 @@
1
+ module FakeServiceBus
2
+ class QueueFactory
3
+
4
+ attr_reader :message_factory, :queue
5
+
6
+ def initialize(options = {})
7
+ @message_factory = options.fetch(:message_factory)
8
+ @queue = options.fetch(:queue)
9
+ end
10
+
11
+ def new(options)
12
+ queue.new(options.merge(:message_factory => message_factory))
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,72 @@
1
+ module FakeServiceBus
2
+
3
+ NonExistentQueue = Class.new(RuntimeError)
4
+
5
+ class Queues
6
+
7
+ attr_reader :queue_factory, :database
8
+
9
+ def initialize(options = {})
10
+ @queue_factory = options.fetch(:queue_factory)
11
+ @database = options.fetch(:database)
12
+ @database.load
13
+ end
14
+
15
+ def create(name, options = {})
16
+ return database[name] if database[name]
17
+ queue = queue_factory.new(options.merge(:name=>name))
18
+ database[name] = queue
19
+ end
20
+
21
+ def delete(name, options = {})
22
+ if database[name]
23
+ database.delete(name)
24
+ else
25
+ fail NonExistentQueue, name
26
+ end
27
+ end
28
+
29
+ def list(options = {})
30
+ if (prefix = options["QueueNamePrefix"])
31
+ database.select { |name, queue| name.start_with?(prefix) }.values
32
+ else
33
+ database.values
34
+ end
35
+ end
36
+
37
+ def get(name, options = {})
38
+ if (db = database[name])
39
+ db
40
+ else
41
+ fail NonExistentQueue, name
42
+ end
43
+ end
44
+
45
+ def transaction
46
+ database.transaction do
47
+ yield
48
+ end
49
+ end
50
+
51
+ def save(queue)
52
+ database[queue.name] = queue
53
+ end
54
+
55
+ def reset
56
+ database.reset
57
+ end
58
+
59
+ def timeout_messages!
60
+ transaction do
61
+ database.each { |name,queue| queue.timeout_messages! }
62
+ end
63
+ end
64
+
65
+ def expire
66
+ transaction do
67
+ database.each { |name, queue| queue.expire }
68
+ end
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,41 @@
1
+ require 'builder'
2
+ require 'securerandom'
3
+ require 'time'
4
+
5
+ module FakeServiceBus
6
+ class Responder
7
+
8
+ def queue(xml, queue)
9
+ xml.tag! "entry" do
10
+ xml.id "https://fake_servicebus/#{queue.name}"
11
+ xml.title queue.name, :type=>"text"
12
+ xml.published Time.now.utc.iso8601
13
+ xml.updated Time.now.utc.iso8601
14
+ xml.tag! "author" do
15
+ xml.name "FakeServiceBus"
16
+ end
17
+ xml.link :rel=>"self", :href=>"https://fake_servicebus/#{queue.name}"
18
+ xml.tag! "content" do
19
+ xml.QueueDescription(
20
+ :xmlns=>"http://schemas.microsoft.com/netservices/2010/10/servicebus/connect",
21
+ :'xmlns:i'=>"http://www.w3.org/2001/XMLSchema-instance") do
22
+ xml.LockDuration queue.attributes['LockDuration']
23
+ xml.MaxSizeInMegabytes queue.attributes['MaxSizeInMegabytes']
24
+ xml.RequiresDuplicateDetection queue.attributes['RequiresDuplicateDetection']
25
+ xml.RequiresSession queue.attributes['RequiresSession']
26
+ xml.DefaultMessageTimeToLive queue.attributes['DefaultMessageTimeToLive']
27
+ xml.DeadLetteringOnMessageExpiration queue.attributes['DeadLetteringOnMessageExpiration']
28
+ xml.DuplicateDetectionHistoryTimeWindow queue.attributes['DuplicateDetectionHistoryTimeWindow']
29
+ xml.MaxDeliveryCount queue.attributes['MaxDeliveryCount']
30
+ xml.EnableBatchedOperations queue.attributes['EnableBatchedOperations']
31
+ xml.SizeInBytes queue.attributes['SizeInBytes']
32
+ xml.MessageCount queue.attributes['MessageCount']
33
+ xml.CreatedAt queue.attributes['CreatedAt']
34
+ xml.UpdatedAt queue.attributes['UpdatedAt']
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,19 @@
1
+ module FakeServiceBus
2
+ class Server
3
+
4
+ attr_reader :host, :port
5
+
6
+ def initialize(options)
7
+ @host = options.fetch(:host)
8
+ @port = options.fetch(:port)
9
+ end
10
+
11
+ def url_for(queue_id, options = {})
12
+ host = options[:host] || @host
13
+ port = options[:port] || @port
14
+
15
+ "http://#{host}:#{port}/#{queue_id}"
16
+ end
17
+
18
+ end
19
+ end