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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +4 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.md +20 -0
- data/README.md +41 -0
- data/Rakefile +31 -0
- data/bin/fake_servicebus +65 -0
- data/fake_servicebus.gemspec +33 -0
- data/lib/fake_servicebus.rb +90 -0
- data/lib/fake_servicebus/actions/create_queue.rb +64 -0
- data/lib/fake_servicebus/actions/delete_message.rb +19 -0
- data/lib/fake_servicebus/actions/delete_queue.rb +18 -0
- data/lib/fake_servicebus/actions/get_queue.rb +20 -0
- data/lib/fake_servicebus/actions/list_queues.rb +28 -0
- data/lib/fake_servicebus/actions/receive_message.rb +44 -0
- data/lib/fake_servicebus/actions/renew_lock_message.rb +19 -0
- data/lib/fake_servicebus/actions/send_message.rb +22 -0
- data/lib/fake_servicebus/actions/unlock_message.rb +19 -0
- data/lib/fake_servicebus/api.rb +71 -0
- data/lib/fake_servicebus/catch_errors.rb +19 -0
- data/lib/fake_servicebus/collection_view.rb +20 -0
- data/lib/fake_servicebus/daemonize.rb +30 -0
- data/lib/fake_servicebus/databases/file.rb +129 -0
- data/lib/fake_servicebus/databases/memory.rb +30 -0
- data/lib/fake_servicebus/error_response.rb +55 -0
- data/lib/fake_servicebus/error_responses.yml +32 -0
- data/lib/fake_servicebus/message.rb +59 -0
- data/lib/fake_servicebus/queue.rb +201 -0
- data/lib/fake_servicebus/queue_factory.rb +16 -0
- data/lib/fake_servicebus/queues.rb +72 -0
- data/lib/fake_servicebus/responder.rb +41 -0
- data/lib/fake_servicebus/server.rb +19 -0
- data/lib/fake_servicebus/show_output.rb +21 -0
- data/lib/fake_servicebus/test_integration.rb +122 -0
- data/lib/fake_servicebus/version.rb +3 -0
- data/lib/fake_servicebus/web_interface.rb +74 -0
- data/spec/acceptance/message_actions_spec.rb +452 -0
- data/spec/acceptance/queue_actions_spec.rb +82 -0
- data/spec/integration_spec_helper.rb +23 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/unit/api_spec.rb +76 -0
- data/spec/unit/catch_errors_spec.rb +43 -0
- data/spec/unit/collection_view_spec.rb +41 -0
- data/spec/unit/error_response_spec.rb +65 -0
- data/spec/unit/message_spec.rb +76 -0
- data/spec/unit/queue_factory_spec.rb +13 -0
- data/spec/unit/queue_spec.rb +204 -0
- data/spec/unit/queues_spec.rb +102 -0
- data/spec/unit/responder_spec.rb +44 -0
- data/spec/unit/show_output_spec.rb +22 -0
- data/spec/unit/web_interface_spec.rb +15 -0
- 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
|