fake_servicebus 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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,28 @@
1
+ module FakeServiceBus
2
+ module Actions
3
+ class ListQueues
4
+
5
+ def initialize(options = {})
6
+ @server = options.fetch(:server)
7
+ @queues = options.fetch(:queues)
8
+ @responder = options.fetch(:responder)
9
+ @request = options.fetch(:request)
10
+ end
11
+
12
+ def call(params)
13
+ found = @queues.list(params)
14
+ xml = Builder::XmlMarkup.new()
15
+ xml.tag! "feed", :xmlns=>"http://www.w3.org/2005/Atom" do
16
+ xml.title "Queues", :type=>"text"
17
+ xml.id "https://fake_servicebus/$Resources/Queues"
18
+ xml.updated Time.now.utc.iso8601
19
+ xml.link :rel=>"self", :href=>"fake_servicebus/$Resources/Queues"
20
+ found.each do |queue|
21
+ @responder.queue xml, queue
22
+ end
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,44 @@
1
+ module FakeServiceBus
2
+ module Actions
3
+ class ReceiveMessage
4
+
5
+ MAX_WAIT_TIME_SECONDS = 20
6
+
7
+ def initialize(options = {})
8
+ @server = options.fetch(:server)
9
+ @queues = options.fetch(:queues)
10
+ @start_ts = Time.now.to_f
11
+ @satisfied = false
12
+ end
13
+
14
+ def call(queue_name, params)
15
+ queue = @queues.get(queue_name)
16
+ message = queue.receive_message(params.merge(queues: @queues))
17
+ @satisfied = !message.nil? || expired?(queue, params)
18
+ if !message.nil?
19
+ [201,
20
+ {'location'=>message.location,
21
+ 'BrokerProperties'=>{'SequenceNumber'=>message.sequence_number,
22
+ 'LockToken'=>message.lock_token}.to_json},
23
+ message.body]
24
+ end
25
+ end
26
+
27
+ def satisfied?
28
+ @satisfied
29
+ end
30
+
31
+ protected
32
+ def elapsed
33
+ Time.now.to_f - @start_ts
34
+ end
35
+
36
+ def expired?(queue, params)
37
+ wait_time_seconds = Integer params.fetch("timeout") { 0 }
38
+ wait_time_seconds <= 0 ||
39
+ elapsed >= wait_time_seconds ||
40
+ elapsed >= MAX_WAIT_TIME_SECONDS
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,19 @@
1
+ module FakeServiceBus
2
+ module Actions
3
+ class RenewLockMessage
4
+
5
+ def initialize(options = {})
6
+ @server = options.fetch(:server)
7
+ @queues = options.fetch(:queues)
8
+ end
9
+
10
+ def call(queue_name, lock_token, params)
11
+ queue = @queues.get(queue_name)
12
+
13
+ queue.renew_lock_message(lock_token)
14
+ 200
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ module FakeServiceBus
2
+ module Actions
3
+ class SendMessage
4
+
5
+ def initialize(options = {})
6
+ @server = options.fetch(:server)
7
+ @queues = options.fetch(:queues)
8
+ @request = options.fetch(:request)
9
+ end
10
+
11
+ def call(queue_name, params)
12
+ queue = @queues.get(queue_name)
13
+ message = queue.send_message(params.merge(
14
+ {'queue_name'=>queue_name,
15
+ 'body'=>@request.body.read})
16
+ )
17
+ 201
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ module FakeServiceBus
2
+ module Actions
3
+ class UnlockMessage
4
+
5
+ def initialize(options = {})
6
+ @server = options.fetch(:server)
7
+ @queues = options.fetch(:queues)
8
+ end
9
+
10
+ def call(queue_name, lock_token, params)
11
+ queue = @queues.get(queue_name)
12
+
13
+ queue.unlock_message(lock_token)
14
+ 200
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,71 @@
1
+ require 'fake_servicebus/actions/create_queue'
2
+ require 'fake_servicebus/actions/delete_queue'
3
+ require 'fake_servicebus/actions/list_queues'
4
+ require 'fake_servicebus/actions/get_queue'
5
+ require 'fake_servicebus/actions/send_message'
6
+ require 'fake_servicebus/actions/receive_message'
7
+ require 'fake_servicebus/actions/unlock_message'
8
+ require 'fake_servicebus/actions/renew_lock_message'
9
+ require 'fake_servicebus/actions/delete_message'
10
+
11
+ module FakeServiceBus
12
+
13
+ InvalidAction = Class.new(ArgumentError)
14
+
15
+ class API
16
+
17
+ attr_reader :queues, :options
18
+
19
+ def initialize(options = {})
20
+ @queues = options.fetch(:queues)
21
+ @options = options
22
+ @halt = false
23
+ @timer = Thread.new do
24
+ until @halt
25
+ queues.timeout_messages!
26
+ sleep(0.1)
27
+ end
28
+ end
29
+ end
30
+
31
+ def call(action, request, *args)
32
+ if FakeServiceBus::Actions.const_defined?(action)
33
+ action = FakeServiceBus::Actions.const_get(action).new(options.merge({:request => request}))
34
+ if action.respond_to?(:satisfied?)
35
+ result = nil
36
+ until @halt
37
+ result = attempt_once(action, *args)
38
+ break if action.satisfied?
39
+ sleep(0.1)
40
+ end
41
+ result
42
+ else
43
+ attempt_once(action, *args)
44
+ end
45
+ else
46
+ fail InvalidAction, "Unknown (or not yet implemented) action: #{action}"
47
+ end
48
+ end
49
+
50
+ def attempt_once(action, *args)
51
+ queues.transaction do
52
+ action.call(*args)
53
+ end
54
+ end
55
+
56
+ # Fake actions
57
+
58
+ def reset
59
+ queues.reset
60
+ end
61
+
62
+ def expire
63
+ queues.expire
64
+ end
65
+
66
+ def stop
67
+ @halt = true
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,19 @@
1
+ require 'securerandom'
2
+
3
+ module FakeServiceBus
4
+ class CatchErrors
5
+
6
+ def initialize(app, options = {})
7
+ @app = app
8
+ @response = options.fetch(:response)
9
+ end
10
+
11
+ def call(env)
12
+ @app.call(env)
13
+ rescue => error
14
+ response = @response.new(error)
15
+ [ response.status, {}, [ response.body ] ]
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ require 'forwardable'
2
+
3
+ module FakeServiceBus
4
+ class CollectionView
5
+ include Enumerable
6
+ extend Forwardable
7
+ def_delegators :@original, :[], :each, :empty?, :size, :length
8
+
9
+ UnmodifiableObjectError = Class.new(StandardError)
10
+
11
+ def initialize( original )
12
+ @original = original
13
+ end
14
+
15
+ def []=(key_or_index,value)
16
+ raise UnmodifiableObjectError.new("This is a collection view and can not be modified - #{key_or_index} => #{value}")
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,30 @@
1
+ module FakeServiceBus
2
+ class Daemonize
3
+
4
+ attr_reader :pid
5
+
6
+ def initialize(options)
7
+ @pid = options.fetch(:pid) {
8
+ warn "No PID file specified while daemonizing!"
9
+ exit 1
10
+ }
11
+ end
12
+
13
+ def call
14
+ Process.daemon(true, true)
15
+
16
+ if File.exist?(pid)
17
+ existing_pid = File.open(pid, 'r').read.chomp.to_i
18
+ running = Process.getpgid(existing_pid) rescue false
19
+ if running
20
+ warn "Error, Process #{existing_pid} already running"
21
+ exit 1
22
+ else
23
+ warn "Cleaning up stale pid at #{pid}"
24
+ end
25
+ end
26
+ File.open(pid, 'w') { |f| f.write(Process.pid) }
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,129 @@
1
+ require "yaml/store"
2
+
3
+ module FakeServiceBus
4
+ class FileDatabase
5
+
6
+ attr_reader :filename
7
+
8
+ def initialize(filename)
9
+ @filename = filename
10
+ @queue_objects = {}
11
+ unless thread_safe_store?
12
+ # before ruby 2.4, YAML::Store cannot be declared thread safe
13
+ #
14
+ # without that declaration, attempting to have some thread B enter a
15
+ # store.transaction on the store while another thread A is in one
16
+ # already will raise an error unnecessarily.
17
+ #
18
+ # to prevent this, we'll use our own mutex around store.transaction,
19
+ # so only one thread can even _try_ to enter the transaction at a
20
+ # time.
21
+ @store_mutex = Mutex.new
22
+ @store_mutex_owner = nil
23
+ end
24
+ end
25
+
26
+ def load
27
+ transaction do
28
+ store["queues"] ||= {}
29
+ end
30
+ end
31
+
32
+ def transaction
33
+ if thread_safe_store? || store_mutex_owned?
34
+ # if we already own the store mutex, we can expect the next line to
35
+ # raise (appropriately) when we try to nest transactions in the store.
36
+ # but if we took the other branch, the # @store_mutex.synchronize call
37
+ # would self-deadlock before we could raise the error.
38
+ store.transaction do
39
+ yield
40
+ end
41
+ else
42
+ # we still need to use an inner store.transaction block because it does
43
+ # more than just lock synchronization. it's unfortunately inefficient,
44
+ # but this isn't a production-oriented library.
45
+ @store_mutex.synchronize do
46
+ begin
47
+ # allows us to answer `store_mutex_owned?` above
48
+ @store_mutex_owner = Thread.current
49
+ store.transaction do
50
+ yield
51
+ end
52
+ ensure
53
+ @store_mutex_owner = nil
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def reset
60
+ transaction do
61
+ store["queues"] = {}
62
+ end
63
+ @queue_objects = {}
64
+ end
65
+
66
+ def []=(key, value)
67
+ storage[key] = value.to_yaml
68
+ end
69
+
70
+ def [](key)
71
+ value = storage[key]
72
+ if value
73
+ deserialize(key)
74
+ else
75
+ value
76
+ end
77
+ end
78
+
79
+ def each(&block)
80
+ storage.each do |key, value|
81
+ yield key, deserialize(key)
82
+ end
83
+ end
84
+
85
+ def select(&block)
86
+ new_list = storage.select do |key, value|
87
+ yield key, deserialize(key)
88
+ end
89
+ Hash[new_list.map { |key, value| [key, deserialize(key)] }]
90
+ end
91
+
92
+ def delete(key)
93
+ @queue_objects.delete(key)
94
+ storage.delete(key)
95
+ end
96
+
97
+ def values
98
+ storage.map { |key, value|
99
+ deserialize(key)
100
+ }
101
+ end
102
+
103
+ private
104
+
105
+ def deserialize(key)
106
+ @queue_objects[key] ||= Queue.new(storage[key].merge(name: key, message_factory: Message))
107
+ end
108
+
109
+ def storage
110
+ store["queues"]
111
+ end
112
+
113
+ def thread_safe_store?
114
+ Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.4")
115
+ end
116
+
117
+ def store_mutex_owned?
118
+ # this could be just "@store_mutex && @store_mutex.owned?" in ruby 2.x,
119
+ # but we still support 1.9.3 which doesn't have the "owned?" method
120
+ @store_mutex_owner == Thread.current
121
+ end
122
+
123
+ def store
124
+ @store ||= thread_safe_store? ?
125
+ YAML::Store.new(filename, true) :
126
+ YAML::Store.new(filename)
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,30 @@
1
+ require "forwardable"
2
+ require "thread"
3
+
4
+ module FakeServiceBus
5
+ class MemoryDatabase
6
+ extend Forwardable
7
+
8
+ def_delegators :@queues,
9
+ :[], :[]=, :delete, :each, :select, :values
10
+
11
+ def initialize
12
+ @semaphore = Mutex.new
13
+ end
14
+
15
+ def load
16
+ @queues = {}
17
+ end
18
+
19
+ def transaction
20
+ @semaphore.synchronize do
21
+ yield
22
+ end
23
+ end
24
+
25
+ def reset
26
+ @queues = {}
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,55 @@
1
+ require 'yaml'
2
+ require 'builder'
3
+ require 'securerandom'
4
+
5
+ module FakeServiceBus
6
+ class ErrorResponse
7
+
8
+ attr_reader :error
9
+
10
+ def initialize(error)
11
+ @error = error
12
+ end
13
+
14
+ def status
15
+ @status ||= statuses.fetch(code)
16
+ end
17
+
18
+ def body
19
+ xml = Builder::XmlMarkup.new()
20
+ xml.ErrorResponse do
21
+ xml.Error do
22
+ xml.Type type
23
+ xml.Code code
24
+ xml.Message error.to_s
25
+ xml.Detail
26
+ end
27
+ xml.RequestId SecureRandom.uuid
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def code
34
+ code = error.class.name.sub(/^FakeServiceBus::/, '')
35
+ if statuses.has_key?(code)
36
+ code
37
+ else
38
+ "InternalError"
39
+ end
40
+ end
41
+
42
+ def type
43
+ if status < 500
44
+ "Sender"
45
+ else
46
+ "Receiver"
47
+ end
48
+ end
49
+
50
+ def statuses
51
+ @statuses ||= YAML.load_file(File.expand_path('../error_responses.yml', __FILE__))
52
+ end
53
+
54
+ end
55
+ end