fake_sns 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/.simplecov +4 -0
  5. data/.travis.yml +5 -0
  6. data/Gemfile +2 -0
  7. data/README.md +167 -0
  8. data/Rakefile +24 -0
  9. data/bin/fake_sns +79 -0
  10. data/config.ru +3 -0
  11. data/fake_sns.gemspec +34 -0
  12. data/lib/fake_sns/action.rb +24 -0
  13. data/lib/fake_sns/actions/create_topic.rb +41 -0
  14. data/lib/fake_sns/actions/delete_topic.rb +13 -0
  15. data/lib/fake_sns/actions/get_topic_attributes.rb +32 -0
  16. data/lib/fake_sns/actions/list_subscriptions.rb +24 -0
  17. data/lib/fake_sns/actions/list_subscriptions_by_topic.rb +23 -0
  18. data/lib/fake_sns/actions/list_topics.rb +13 -0
  19. data/lib/fake_sns/actions/publish.rb +37 -0
  20. data/lib/fake_sns/actions/set_topic_attributes.rb +19 -0
  21. data/lib/fake_sns/actions/subscribe.rb +43 -0
  22. data/lib/fake_sns/database.rb +76 -0
  23. data/lib/fake_sns/deliver_message.rb +100 -0
  24. data/lib/fake_sns/error.rb +34 -0
  25. data/lib/fake_sns/error_response.rb +46 -0
  26. data/lib/fake_sns/message.rb +28 -0
  27. data/lib/fake_sns/message_collection.rb +40 -0
  28. data/lib/fake_sns/response.rb +16 -0
  29. data/lib/fake_sns/server.rb +76 -0
  30. data/lib/fake_sns/show_output.rb +20 -0
  31. data/lib/fake_sns/storage.rb +75 -0
  32. data/lib/fake_sns/subscription.rb +17 -0
  33. data/lib/fake_sns/subscription_collection.rb +34 -0
  34. data/lib/fake_sns/test_integration.rb +110 -0
  35. data/lib/fake_sns/topic.rb +13 -0
  36. data/lib/fake_sns/topic_collection.rb +41 -0
  37. data/lib/fake_sns/version.rb +3 -0
  38. data/lib/fake_sns/views/create_topic.xml.erb +8 -0
  39. data/lib/fake_sns/views/delete_topic.xml.erb +5 -0
  40. data/lib/fake_sns/views/error.xml.erb +8 -0
  41. data/lib/fake_sns/views/get_topic_attributes.xml.erb +15 -0
  42. data/lib/fake_sns/views/list_subscriptions.xml.erb +18 -0
  43. data/lib/fake_sns/views/list_subscriptions_by_topic.xml.erb +18 -0
  44. data/lib/fake_sns/views/list_topics.xml.erb +14 -0
  45. data/lib/fake_sns/views/publish.xml.erb +8 -0
  46. data/lib/fake_sns/views/set_topic_attributes.xml.erb +5 -0
  47. data/lib/fake_sns/views/subscribe.xml.erb +8 -0
  48. data/lib/fake_sns.rb +51 -0
  49. data/spec/fake_sns/drain_spec.rb +91 -0
  50. data/spec/fake_sns/publish_spec.rb +28 -0
  51. data/spec/fake_sns/replace_spec.rb +14 -0
  52. data/spec/fake_sns/subscribing_spec.rb +42 -0
  53. data/spec/fake_sns/topics_spec.rb +44 -0
  54. data/spec/spec_helper.rb +54 -0
  55. metadata +271 -0
@@ -0,0 +1,76 @@
1
+ require "etc"
2
+ require "aws-sdk"
3
+
4
+ module FakeSNS
5
+ class Database
6
+
7
+ attr_reader :database_filename
8
+
9
+ def initialize(database_filename)
10
+ @database_filename = database_filename || File.join(Dir.home, ".fake_sns.yml")
11
+ end
12
+
13
+ def perform(action, params)
14
+ action_instance = action_provider(action).new(self, params)
15
+ action_instance.call
16
+ Response.new(action_instance)
17
+ end
18
+
19
+ def topics
20
+ @topics ||= TopicCollection.new(store)
21
+ end
22
+
23
+ def subscriptions
24
+ @subscriptions ||= SubscriptionCollection.new(store)
25
+ end
26
+
27
+ def messages
28
+ @messages ||= MessageCollection.new(store)
29
+ end
30
+
31
+ def reset
32
+ topics.reset
33
+ subscriptions.reset
34
+ messages.reset
35
+ end
36
+
37
+ def transaction
38
+ store.transaction do
39
+ yield
40
+ end
41
+ end
42
+
43
+ def replace(data)
44
+ store.replace(data)
45
+ end
46
+
47
+ def to_yaml
48
+ store.to_yaml
49
+ end
50
+
51
+ def each_deliverable_message
52
+ topics.each do |topic|
53
+ subscriptions.each do |subscription|
54
+ messages.each do |message|
55
+ if message.topic_arn == topic.arn
56
+ yield subscription, message
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def store
66
+ @store ||= Storage.for(database_filename)
67
+ end
68
+
69
+ def action_provider(action)
70
+ Actions.const_get(action)
71
+ rescue NameError
72
+ raise InvalidAction, "not implemented: #{action}"
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,100 @@
1
+ require "forwardable"
2
+ require "faraday"
3
+
4
+ module FakeSNS
5
+ class DeliverMessage
6
+
7
+ extend Forwardable
8
+
9
+ def self.call(options)
10
+ new(options).call
11
+ end
12
+
13
+ attr_reader :subscription, :message, :config, :request
14
+
15
+ def_delegators :subscription, :protocol, :endpoint, :arn
16
+
17
+ def initialize(options)
18
+ @subscription = options.fetch(:subscription)
19
+ @message = options.fetch(:message)
20
+ @request = options.fetch(:request)
21
+ @config = options.fetch(:config)
22
+ end
23
+
24
+ def call
25
+ method_name = protocol.gsub("-", "_")
26
+ if protected_methods.map(&:to_s).include?(method_name)
27
+ send(method_name)
28
+ else
29
+ raise InvalidParameterValue, "Protocol #{protocol} not supported"
30
+ end
31
+ end
32
+
33
+ protected
34
+
35
+ def sqs
36
+ queue_name = endpoint.split(":").last
37
+ sqs = AWS::SQS.new(config["aws_config"] || {})
38
+ queue = sqs.queues.named(queue_name)
39
+ queue.send_message(message_contents)
40
+ end
41
+
42
+ def http
43
+ http_or_https
44
+ end
45
+
46
+ def https
47
+ http_or_https
48
+ end
49
+
50
+ def email
51
+ pending
52
+ end
53
+
54
+ def email_json
55
+ pending
56
+ end
57
+
58
+ def sms
59
+ pending
60
+ end
61
+
62
+ def application
63
+ pending
64
+ end
65
+
66
+ private
67
+
68
+ def message_contents
69
+ message.message_for_protocol protocol
70
+ end
71
+
72
+ def pending
73
+ puts "Not sending to subscription #{arn}, because protocol #{protocol} has no fake implementation. Message: #{message.id} - #{message_contents.inspect}"
74
+ end
75
+
76
+ def http_or_https
77
+ Faraday.new.post(endpoint) do |f|
78
+ f.body = {
79
+ "Type" => "Notification",
80
+ "MessageId" => message.id,
81
+ "TopicArn" => message.topic_arn,
82
+ "Subject" => message.subject,
83
+ "Message" => message_contents,
84
+ "Timestamp" => message.received_at.strftime("%Y-%m-%dT%H:%M:%SZ"),
85
+ "SignatureVersion" => "1",
86
+ "Signature" => "Fake",
87
+ "SigningCertURL" => "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem",
88
+ "UnsubscribeURL" => "", # TODO url to unsubscribe URL on this server
89
+ }.to_json
90
+ f.headers = {
91
+ "x-amz-sns-message-type" => "Notification",
92
+ "x-amz-sns-message-id" => message.id,
93
+ "x-amz-sns-topic-arn" => message.topic_arn,
94
+ "x-amz-sns-subscription-arn" => arn,
95
+ }
96
+ end
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,34 @@
1
+ module FakeSNS
2
+
3
+ class Error < StandardError
4
+ end
5
+
6
+ def self.error_type(status)
7
+ Class.new(Error) do
8
+ define_method :status do
9
+ status
10
+ end
11
+ end
12
+ end
13
+
14
+ # Common Errors, according to AWS docs
15
+ IncompleteSignature = error_type 400
16
+ InternalFailure = error_type 500
17
+ InvalidAction = error_type 400
18
+ InvalidClientTokenId = error_type 403
19
+ InvalidParameterCombination = error_type 400
20
+ InvalidParameterValue = error_type 400
21
+ InvalidQueryParameter = error_type 400
22
+ MalformedQueryString = error_type 400
23
+ MissingAction = error_type 400
24
+ MissingAuthenticationToken = error_type 403
25
+ MissingParameter = error_type 400
26
+ OptInRequired = error_type 403
27
+ RequestExpired = error_type 400
28
+ ServiceUnavailable = error_type 403
29
+ Throttling = error_type 400
30
+
31
+ # Other errors
32
+ NotFound = error_type 404
33
+
34
+ end
@@ -0,0 +1,46 @@
1
+ module FakeSNS
2
+ class ErrorResponse
3
+
4
+ DEFAULT_CODE = "InternalFailure"
5
+ DEFAULT_STATUS = 500
6
+
7
+ attr_reader :error, :parameters
8
+
9
+ def initialize(error, parameters)
10
+ @error = error
11
+ @parameters = parameters
12
+ end
13
+
14
+ def status
15
+ if error.respond_to?(:status)
16
+ error.status
17
+ else
18
+ DEFAULT_STATUS
19
+ end
20
+ end
21
+
22
+ # TODO figure out what this value does
23
+ def type
24
+ "Sender"
25
+ end
26
+
27
+ def code
28
+ if error.respond_to?(:code)
29
+ error.code
30
+ elsif error.is_a?(FakeSNS::Error)
31
+ error.class.to_s.split("::").last
32
+ else
33
+ DEFAULT_CODE
34
+ end
35
+ end
36
+
37
+ def message
38
+ error.message
39
+ end
40
+
41
+ def request_id
42
+ @request_id ||= SecureRandom.uuid
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,28 @@
1
+ require "json"
2
+
3
+ module FakeSNS
4
+ class Message
5
+
6
+ include Virtus.model
7
+
8
+ json = Class.new(Virtus::Attribute) do
9
+ def coerce(value)
10
+ value.is_a?(::Hash) ? value : JSON.parse(value)
11
+ end
12
+ end
13
+
14
+ attribute :id, String
15
+ attribute :subject, String
16
+ attribute :endpoint, String
17
+ attribute :topic_arn, String
18
+ attribute :structure, String
19
+ attribute :target_arn, String
20
+ attribute :received_at, DateTime
21
+ attribute :message, json
22
+
23
+ def message_for_protocol(type)
24
+ message.fetch(type.to_s) { message.fetch("default") }
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,40 @@
1
+ module FakeSNS
2
+ class MessageCollection
3
+
4
+ include Enumerable
5
+
6
+ def initialize(store)
7
+ @store = store
8
+ @store["messages"] ||= []
9
+ end
10
+
11
+ def collection
12
+ @store["messages"]
13
+ end
14
+
15
+ def reset
16
+ @store["messages"] = []
17
+ end
18
+
19
+ def each(*args, &block)
20
+ collection.map { |item| Message.new(item) }.each(*args, &block)
21
+ end
22
+
23
+ def fetch(arn, &default)
24
+ default ||= -> { raise InvalidParameterValue, "Unknown message #{arn}" }
25
+ found = collection.find do |message|
26
+ message["arn"] == arn
27
+ end
28
+ found || default.call
29
+ end
30
+
31
+ def create(attributes)
32
+ collection << attributes
33
+ end
34
+
35
+ def delete(arn)
36
+ collection.delete(fetch(arn))
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,16 @@
1
+ require "securerandom"
2
+ require "delegate"
3
+
4
+ module FakeSNS
5
+ class Response < SimpleDelegator
6
+
7
+ def request_id
8
+ @request_id ||= SecureRandom.uuid
9
+ end
10
+
11
+ def template
12
+ __getobj__.class.name.to_s.split("::").last.gsub(/[A-Z]/){|m| "_#{m[0].downcase}"}.sub(/_/, '')
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,76 @@
1
+ require "fake_sns"
2
+ require "sinatra/base"
3
+
4
+ module FakeSNS
5
+ class Server < Sinatra::Base
6
+
7
+ before do
8
+ content_type :xml
9
+ end
10
+
11
+ def database
12
+ $database ||= FakeSNS::Database.new(settings.database)
13
+ end
14
+
15
+ def action
16
+ params.fetch("Action") { raise MissingAction }
17
+ end
18
+
19
+ get "/" do
20
+ database.transaction do
21
+ database.to_yaml
22
+ end
23
+ end
24
+
25
+ post "/" do
26
+ database.transaction do
27
+ begin
28
+ response = database.perform(action, params)
29
+ status 200
30
+ erb :"#{response.template}.xml", scope: response
31
+ rescue Exception => error
32
+ p error
33
+ puts(*error.backtrace)
34
+ error_response = ErrorResponse.new(error, params)
35
+ status error_response.status
36
+ erb :"error.xml", scope: error_response
37
+ end
38
+ end
39
+ end
40
+
41
+ delete "/" do
42
+ database.transaction do
43
+ database.reset
44
+ 200
45
+ end
46
+ end
47
+
48
+ put "/" do
49
+ database.replace(request.body.read)
50
+ 200
51
+ end
52
+
53
+ post "/drain" do
54
+ database.transaction do
55
+ config = JSON.parse(request.body.read)
56
+ database.each_deliverable_message do |subscription, message|
57
+ DeliverMessage.call(subscription: subscription, message: message, request: request, config: config)
58
+ end
59
+ end
60
+ 200
61
+ end
62
+
63
+ post "/drain/:message_id" do |message_id|
64
+ config = JSON.parse(request.body.read)
65
+ database.transaction do
66
+ database.each_deliverable_message do |subscription, message|
67
+ if message.id == message_id
68
+ DeliverMessage.call(subscription: subscription, message: message, request: request, config: config)
69
+ end
70
+ end
71
+ end
72
+ 200
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,20 @@
1
+ require 'rack/request'
2
+
3
+ module FakeSNS
4
+ class ShowOutput
5
+
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ request = Rack::Request.new(env)
12
+ puts request.params.to_yaml
13
+ result = @app.call(env)
14
+ puts
15
+ puts(*result.last)
16
+ result
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,75 @@
1
+ require "yaml/store"
2
+
3
+ module FakeSNS
4
+
5
+ class Storage
6
+
7
+ def self.for(database_filename)
8
+ if database_filename == ":memory:"
9
+ MemoryStorage.new(database_filename)
10
+ else
11
+ FileStorage.new(database_filename)
12
+ end
13
+ end
14
+
15
+ def initialize(database_filename)
16
+ @database_filename = database_filename
17
+ end
18
+
19
+ def [](key)
20
+ storage[key]
21
+ end
22
+
23
+ def []=(key, value)
24
+ storage[key] = value
25
+ end
26
+
27
+ end
28
+
29
+ class MemoryStorage < Storage
30
+
31
+ def to_yaml
32
+ storage.to_yaml
33
+ end
34
+
35
+
36
+ def storage
37
+ @storage ||= {}
38
+ end
39
+
40
+ def transaction
41
+ yield
42
+ end
43
+
44
+ def replace(data)
45
+ @storage = YAML.load(data)
46
+ end
47
+
48
+ end
49
+
50
+ class FileStorage < Storage
51
+
52
+ def to_yaml
53
+ storage["x"]
54
+ storage.instance_variable_get(:@table).to_yaml
55
+ end
56
+
57
+ def storage
58
+ @storage ||= YAML::Store.new(@database_filename)
59
+ end
60
+
61
+ def transaction
62
+ storage.transaction do
63
+ yield
64
+ end
65
+ end
66
+
67
+ def replace(data)
68
+ File.open(@database_filename, "w:utf-8") do |f|
69
+ f.write(data)
70
+ end
71
+ end
72
+
73
+ end
74
+
75
+ end
@@ -0,0 +1,17 @@
1
+ module FakeSNS
2
+ class Subscription
3
+
4
+ include Virtus.model
5
+
6
+ attribute :arn, String
7
+ attribute :protocol, String
8
+ attribute :endpoint, String
9
+ attribute :topic_arn, String
10
+ attribute :owner, String
11
+
12
+ def sqs?
13
+ protocol == "sqs"
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ module FakeSNS
2
+ class SubscriptionCollection
3
+
4
+ include Enumerable
5
+
6
+ attr_reader :collection
7
+
8
+ def initialize(store)
9
+ @store = store
10
+ @store["subscriptions"] ||= []
11
+ end
12
+
13
+ def collection
14
+ @store["subscriptions"]
15
+ end
16
+
17
+ def reset
18
+ @store["subscriptions"] = []
19
+ end
20
+
21
+ def each(*args, &block)
22
+ collection.map { |item| Subscription.new(item) }.each(*args, &block)
23
+ end
24
+
25
+ def create(attributes)
26
+ collection << attributes
27
+ end
28
+
29
+ def delete(arn)
30
+ collection.delete(fetch(arn))
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,110 @@
1
+ require "faraday"
2
+
3
+ module FakeSNS
4
+ class TestIntegration
5
+
6
+ attr_reader :options
7
+
8
+ def initialize(options = {})
9
+ @options = options
10
+ end
11
+
12
+ def host
13
+ option :sns_endpoint
14
+ end
15
+
16
+ def port
17
+ option :sns_port
18
+ end
19
+
20
+ def start
21
+ start! unless up?
22
+ reset
23
+ end
24
+
25
+ def start!
26
+ @pid = Process.spawn(binfile, "-p", port.to_s, "--database", database, :out => out, :err => out)
27
+ wait_until_up
28
+ end
29
+
30
+ def stop
31
+ if @pid
32
+ Process.kill("INT", @pid)
33
+ Process.waitpid(@pid)
34
+ @pid = nil
35
+ else
36
+ $stderr.puts "FakeSNS is not running"
37
+ end
38
+ end
39
+
40
+ def reset
41
+ connection.delete("/")
42
+ end
43
+
44
+ def url
45
+ "http://#{host}:#{port}"
46
+ end
47
+
48
+ def up?
49
+ @pid && connection.get("/").success?
50
+ rescue Errno::ECONNREFUSED, Faraday::Error::ConnectionFailed
51
+ false
52
+ end
53
+
54
+ def data
55
+ YAML.load(connection.get("/").body)
56
+ end
57
+
58
+ def drain(message_id = nil, options = {})
59
+ path = message_id ? "/drain/#{message_id}" : "/drain"
60
+ default = { aws_config: AWS.config.send(:supplied) }
61
+ body = default.merge(options).to_json
62
+ result = connection.post(path, body)
63
+ if result.success?
64
+ true
65
+ else
66
+ raise "Unable to drain messages: #{result.body}"
67
+ end
68
+ end
69
+
70
+ def connection
71
+ @connection ||= Faraday.new(url)
72
+ end
73
+
74
+ private
75
+
76
+ def database
77
+ options.fetch(:database) { ":memory:" }
78
+ end
79
+
80
+ def option(key)
81
+ options.fetch(key) { AWS.config.public_send(key) }
82
+ end
83
+
84
+
85
+ def wait_until_up(deadline = Time.now + 2)
86
+ fail "FakeSNS didn't start in time" if Time.now > deadline
87
+ unless up?
88
+ sleep 0.1
89
+ wait_until_up(deadline)
90
+ end
91
+ end
92
+
93
+ def binfile
94
+ File.expand_path("../../../bin/fake_sns", __FILE__)
95
+ end
96
+
97
+ def out
98
+ if debug?
99
+ :out
100
+ else
101
+ "/dev/null"
102
+ end
103
+ end
104
+
105
+ def debug?
106
+ ENV["DEBUG"].to_s == "true"
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,13 @@
1
+ module FakeSNS
2
+ class Topic
3
+
4
+ include Virtus.model
5
+
6
+ attribute :arn, String
7
+ attribute :name, String
8
+ attribute :policy, String
9
+ attribute :display_name, String
10
+ attribute :delivery_policy, String
11
+
12
+ end
13
+ end