fake_sns 0.0.1

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