Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

1. Support and tests for CreateTopic, ListTopics, DeleteTopic, Subscr…

…ibe, Unsubscribe, and Publish

2. Added a few of the missing method calls for the API
3. All SNS methods added
  • Loading branch information...
commit e754c75989b40b1dd5d2dbbb31da9328f1ac5aec 1 parent 8317c98
@rgeyer rgeyer authored konstantin-dzreev committed
View
7 Rakefile
@@ -101,4 +101,11 @@ task :testrds do
require 'test/rds/test_right_rds.rb'
end
+desc "Test just the SNS interface"
+task :testsns do
+ require 'test/test_credentials'
+ TestCredentials.get_credentials
+ require 'test/sns/test_right_sns.rb'
+end
+
# vim: syntax=Ruby
View
1  lib/right_aws.rb
@@ -73,6 +73,7 @@
require 'iam/right_iam_access_keys'
require 'iam/right_iam_mfa_devices'
require 'route_53/right_route_53_interface'
+require 'sns/right_sns_interface'
#-
View
286 lib/sns/right_sns_interface.rb
@@ -0,0 +1,286 @@
+#
+# Copyright (c) 2007-2008 RightScale Inc
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+module RightAws
+ class SnsInterface < RightAwsBase
+ include RightAwsBaseInterface
+
+ DEFAULT_HOST = 'sns.us-east-1.amazonaws.com'
+ DEFAULT_PORT = 443
+ DEFAULT_PROTOCOL = 'https'
+ DEFAULT_SERVICE = '/'
+ REQUEST_TTL = 30
+
+ # Apparently boilerplate stuff
+ @@bench = AwsBenchmarkingBlock.new
+ def self.bench_xml
+ @@bench.xml
+ end
+ def self.bench_service
+ @@bench.service
+ end
+
+ def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
+ if params[:region]
+ server = "sns.#{params[:region]}.amazonaws.com"
+ params.delete(:region)
+ else
+ server = DEFAULT_HOST
+ end
+ init({ :name => 'SNS',
+ :default_host => ENV['SNS_URL'] ? URI.parse(ENV['SNS_URL']).host : server,
+ :default_port => ENV['SNS_URL'] ? URI.parse(ENV['SNS_URL']).port : DEFAULT_PORT,
+ :default_service => ENV['SNS_URL'] ? URI.parse(ENV['SNS_URL']).path : DEFAULT_SERVICE,
+ :default_protocol => ENV['SNS_URL'] ? URI.parse(ENV['SNS_URL']).scheme : DEFAULT_PROTOCOL},
+ aws_access_key_id || ENV['AWS_ACCESS_KEY_ID'],
+ aws_secret_access_key || ENV['AWS_SECRET_ACCESS_KEY'],
+ params)
+ end
+
+ # TODO: RJG - Seems like generate_request and generate_rest_request could be in a sub class?
+ # Generates a request hash for the sns API
+ def generate_request(action, params={}) # :nodoc:
+ # Sometimes we need to use queue uri (delete queue etc)
+ # In that case we will use Symbol key: 'param[:queue_url]'
+ service = params[:sns_url] ? URI(params[:sns_url]).path : '/'
+ # remove unset(=optional) and symbolyc keys
+ params.each{ |key, value| params.delete(key) if (value.nil? || key.is_a?(Symbol)) }
+ # prepare output hash
+ service_hash = { "Action" => action,
+ "Expires" => (Time.now + REQUEST_TTL).utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
+ "AWSAccessKeyId" => @aws_access_key_id }
+ #"Version" => API_VERSION }
+ service_hash.update(params)
+ service_params = signed_service_params(@aws_secret_access_key, service_hash, :get, @params[:server], service)
+ request = Net::HTTP::Get.new("#{AwsUtils::URLencode(service)}?#{service_params}")
+ # prepare output hash
+ { :request => request,
+ :server => @params[:server],
+ :port => @params[:port],
+ :protocol => @params[:protocol] }
+ end
+
+ # Generates a request hash for the REST API
+ def generate_rest_request(method, param) # :nodoc:
+ sns_uri = param[:sns_url] ? URI(param[:sns_url]).path : '/'
+ message = param[:message] # extract message body if nesessary
+ # remove unset(=optional) and symbolyc keys
+ param.each{ |key, value| param.delete(key) if (value.nil? || key.is_a?(Symbol)) }
+ # created request
+ param_to_str = param.to_a.collect{|key,val| key.to_s + "=" + CGI::escape(val.to_s) }.join("&")
+ param_to_str = "?#{param_to_str}" unless param_to_str.right_blank?
+ request = "Net::HTTP::#{method.capitalize}".right_constantize.new("#{sns_uri}#{param_to_str}")
+ request.body = message if message
+ # set main headers
+ request['content-md5'] = ''
+ request['Content-Type'] = 'text/plain'
+ request['Date'] = Time.now.httpdate
+ # generate authorization string
+ auth_string = "#{method.upcase}\n#{request['content-md5']}\n#{request['Content-Type']}\n#{request['Date']}\n#{CGI::unescape(sns_uri)}"
+ signature = AwsUtils::sign(@aws_secret_access_key, auth_string)
+ # set other headers
+ request['Authorization'] = "AWS #{@aws_access_key_id}:#{signature}"
+ #request['AWS-Version'] = API_VERSION
+ # prepare output hash
+ { :request => request,
+ :server => @params[:server],
+ :port => @params[:port],
+ :protocol => @params[:protocol] }
+ end
+
+ # Sends request to Amazon and parses the response
+ # Raises AwsError if any banana happened
+ def request_info(request, parser) # :nodoc:
+ request_info_impl(:sns_connection, @@bench, request, parser)
+ end
+
+ def create_topic(topic_name)
+ req_hash = generate_request('CreateTopic', 'Name' => topic_name)
+ request_info(req_hash, SnsCreateTopicParser.new)
+ end
+
+ def list_topics()
+ req_hash = generate_request('ListTopics')
+ request_info(req_hash, SnsListTopicsParser.new)
+ end
+
+ def delete_topic(topic_arn)
+ req_hash = generate_request('DeleteTopic', 'TopicArn' => topic_arn)
+ request_info(req_hash, RightHttp2xxParser.new)
+ end
+
+ def subscribe(topic_arn, protocol, endpoint)
+ req_hash = generate_request('Subscribe', 'TopicArn' => topic_arn, 'Protocol' => protocol, 'Endpoint' => endpoint)
+ request_info(req_hash, SnsSubscribeParser.new)
+ end
+
+ def unsubscribe(subscription_arn)
+ req_hash = generate_request('Unsubscribe', 'SubscriptionArn' => subscription_arn)
+ request_info(req_hash, RightHttp2xxParser.new)
+ end
+
+ def publish(topic_arn, message, subject)
+ req_hash = generate_request('Publish', 'TopicArn' => topic_arn, 'Message' => message, 'Subject' => subject)
+ request_info(req_hash, SnsPublishParser.new)
+ end
+
+ def set_topic_attribute(topic_arn, attribute_name, attribute_value)
+ if attribute_name != 'Policy' && attribute_name != 'DisplayName'
+ raise(ArgumentError, "The only values accepted for the attribute_name parameter are (Policy, DisplayName)")
+ end
+ req_hash = generate_request('SetTopicAttributes', 'TopicArn' => topic_arn, 'AttributeName' => attribute_name, 'AttributeValue' => attribute_value)
+ request_info(req_hash, RightHttp2xxParser.new)
+ end
+
+ def get_topic_attributes(topic_arn)
+ req_hash = generate_request('GetTopicAttributes', 'TopicArn' => topic_arn)
+ request_info(req_hash, SnsGetTopicAttributesParser.new)
+ end
+
+ # Calls either the ListSubscriptions or ListSubscriptionsByTopic depending on whether or not the topic_arn parameter is provided.
+ def list_subscriptions(topic_arn = nil)
+ req_hash = topic_arn ? generate_request('ListSubscriptionsByTopic', 'TopicArn' => topic_arn) : generate_request('ListSubscriptions')
+ request_info(req_hash, SnsListSubscriptionsParser.new)
+ end
+
+ def confirm_subscription(topic_arn, token, authenticate_on_unsubscribe=false)
+ req_hash = generate_request('ConfirmSubscription', 'AuthenticateOnUnsubscribe' => authenticate_on_unsubscribe.to_s, 'Token' => token, 'TopicArn' => topic_arn)
+ request_info(req_hash, SnsConfirmSubscriptionParser.new)
+ end
+
+ def add_permission(topic_arn, label, acct_action_hash_ary)
+ n_hash = {
+ 'TopicArn' => topic_arn,
+ 'Label' => label
+ }
+
+ acct_action_hash_ary.each_with_index do |hash_val, idx|
+ n_hash["AWSAccountId.member.#{idx+1}"] = hash_val[:aws_account_id]
+ n_hash["ActionName.member.#{idx+1}"] = hash_val[:action]
+ end
+
+ req_hash = generate_request('AddPermission', n_hash)
+ request_info(req_hash, RightHttp2xxParser.new)
+ end
+
+ def remove_permission(topic_arn, label)
+ req_hash = generate_request('RemovePermission', 'TopicArn' => topic_arn, 'Label' => label)
+ request_info(req_hash, RightHttp2xxParser.new)
+ end
+
+ class SnsCreateTopicParser < RightAWSParser # :nodoc:
+ def reset
+ @result = ''
+ @request_id = ''
+ end
+ def tagend(name)
+ case name
+ when 'RequestId' then @result_id = @text
+ when 'TopicArn' then @result = @text
+ end
+ end
+ end
+
+ class SnsListTopicsParser < RightAWSParser # :nodoc:
+ def reset
+ @result = []
+ @request_id = ''
+ end
+ def tagstart(name, attributes)
+ @current_key = {} if name == 'member'
+ end
+ def tagend(name)
+ case name
+ when 'RequestId' then @result_id = @text
+ when 'TopicArn' then @current_key[:arn] = @text
+ when 'member' then @result << @current_key
+ end
+ end
+ end
+
+ class SnsSubscribeParser < RightAWSParser # :nodoc:
+ def reset
+ @result = ''
+ end
+ def tagend(name)
+ case name
+ when 'SubscriptionArn' then @result = @text
+ end
+ end
+ end
+
+ class SnsPublishParser < RightAWSParser # :nodoc:
+ def reset
+ @result = ''
+ end
+ def tagend(name)
+ case name
+ when 'MessageId' then @result = @text
+ end
+ end
+ end
+
+ class SnsGetTopicAttributesParser < RightAWSParser # :nodoc:
+ def reset
+ @result = {}
+ end
+ def tagend(name)
+ case name
+ when 'key' then @current_attr = @text
+ when 'value' then @result[@current_attr] = @text
+ end
+ end
+ end
+
+ class SnsListSubscriptionsParser < RightAWSParser # :nodoc:
+ def reset
+ @result = []
+ end
+ def tagstart(name, attributes)
+ @current_key = {} if name == 'member'
+ end
+ def tagend(name)
+ case name
+ when 'TopicArn' then @current_key[:topic_arn] = @text
+ when 'Protocol' then @current_key[:protocol] = @text
+ when 'SubscriptionArn' then @current_key[:subscription_arn] = @text
+ when 'Owner' then @current_key[:owner] = @text
+ when 'Endpoint' then @current_key[:endpoint] = @text
+ when 'member' then @result << @current_key
+ end
+ end
+ end
+
+ class SnsConfirmSubscriptionParser < RightAWSParser # :nodoc:
+ def reset
+ @result = ''
+ end
+ def tagend(name)
+ case name
+ when 'SubscriptionArn' then @result = @text
+ end
+ end
+ end
+
+ end
+end
View
2  test/sns/test_helper.rb
@@ -0,0 +1,2 @@
+require 'test/unit'
+require File.dirname(__FILE__) + '/../../lib/right_aws'
View
153 test/sns/test_right_sns.rb
@@ -0,0 +1,153 @@
+require File.dirname(__FILE__) + '/test_helper.rb'
+
+class TestSns < Test::Unit::TestCase
+
+ # You can change these things to whatever you like
+ @@subscriber_email = 'foo@bar.baz'
+ @@topic_name = 'RightTestTopic'
+ @@topic_display_name = 'right_aws test notification topic'
+ @@queue_name = "sns_subscribe_queue_#{Time.now.utc.to_i}"
+
+ # These are placeholders for values that get set, and consumed during the course of testing.
+ @@topic_arn = ''
+ @@subscription_arn = ''
+ @@queue_url = ''
+ @@queue_arn = ''
+
+ @@policy_template = <<-EOF
+{
+ "Id": "Policy1300753700208",
+ "Statement": [
+ {
+ "Sid": "Stmt1300753696680",
+ "Action": [
+ "SNS:Publish",
+ "SNS:RemovePermission",
+ "SNS:SetTopicAttributes",
+ "SNS:DeleteTopic",
+ "SNS:ListSubscriptionsByTopic",
+ "SNS:GetTopicAttributes",
+ "SNS:Receive",
+ "SNS:AddPermission",
+ "SNS:Subscribe"
+ ],
+ "Effect": "Allow",
+ "Resource": "@@topic_arn@@",
+ "Principal": {
+ "AWS": [
+ "*"
+ ]
+ }
+ }
+ ]
+}
+ EOF
+
+ def policy_text
+ @@policy_template.gsub('@@topic_arn@@', @@topic_arn)
+ end
+
+ def setup
+ @sns = Rightscale::SnsInterface.new(TestCredentials.aws_access_key_id, TestCredentials.aws_secret_access_key)
+ @sqs = Rightscale::SqsGen2Interface.new(TestCredentials.aws_access_key_id, TestCredentials.aws_secret_access_key)
+ end
+
+ def test_01_create_topic
+ response = @sns.create_topic(@@topic_name)
+ assert_not_nil(response)
+ @@topic_arn = response
+ end
+
+ def test_02_set_topic_attributes
+ response = @sns.set_topic_attribute(@@topic_arn, 'Policy', policy_text())
+ assert_not_nil(response)
+
+ response = @sns.set_topic_attribute(@@topic_arn, 'DisplayName', @@topic_display_name)
+ assert_not_nil(response)
+
+ assert_raise ArgumentError do
+ @sns.set_topic_attribute(@@topic_arn, 'Foo', 'val')
+ end
+ end
+
+ def test_03_get_topic_attributes
+ response = @sns.get_topic_attributes(@@topic_arn)
+ assert_not_nil(response)
+ assert(response['DisplayName'] == @@topic_display_name)
+ assert(response['Policy'] =~ /Policy1300753700208/)
+ end
+
+ def test_04_list_topics
+ sleep(1)
+ assert(@sns.list_topics.collect{|topic| topic[:arn] }.include?(@@topic_arn))
+ end
+
+ def test_05_subscribe_email
+ response = @sns.subscribe(@@topic_arn, 'email', @@subscriber_email)
+ assert_not_nil(response)
+ @@subscription_arn = response
+ end
+
+ def test_06_list_subscriptions
+ sleep(1)
+
+ response = @sns.list_subscriptions()
+ assert_not_nil(response)
+ assert(response.count == 1)
+ assert(response[0][:endpoint] == @@subscriber_email)
+ assert(response[0][:protocol] == 'email')
+ assert(response[0][:subscription_arn] == 'PendingConfirmation')
+ assert(response[0][:topic_arn] == @@topic_arn)
+ end
+
+ def test_07_list_subscriptions_by_topic
+ response = @sns.list_subscriptions(@@topic_arn)
+ assert_not_nil(response)
+ assert(response.count == 1)
+ assert(response[0][:endpoint] == @@subscriber_email)
+ assert(response[0][:protocol] == 'email')
+ assert(response[0][:subscription_arn] == 'PendingConfirmation')
+ assert(response[0][:topic_arn] == @@topic_arn)
+ end
+
+ def test_08_unsubscribe
+ @@queue_url = @sqs.create_queue(@@queue_name)
+ @@queue_arn = "arn:aws:sqs:us-east-1:#{TestCredentials.account_number.gsub('-','')}:#{@@queue_name}"
+ sub_response = @sns.subscribe(@@topic_arn, 'sqs', @@queue_arn)
+ assert_not_nil(sub_response)
+ unsub_response = @sns.unsubscribe(sub_response)
+ @sqs.delete_queue(@@queue_url)
+ end
+
+ def test_09_publish
+ response = @sns.publish(@@topic_arn, 'Message to publish', 'Message Subject')
+ assert_not_nil(response)
+ end
+
+ def test_10_add_and_remove_permission
+ acct_num = TestCredentials.account_number.gsub('-','')
+
+ add_response = @sns.add_permission(@@topic_arn, 'PermissionLbl', [
+ {:aws_account_id => acct_num, :action => "GetTopicAttributes"},
+ {:aws_account_id => acct_num, :action => "Publish"}
+ ])
+ assert_not_nil(add_response)
+
+ remove_response = @sns.remove_permission(@@topic_arn, 'PermissionLbl')
+ assert_not_nil(remove_response)
+ end
+
+# TODO: Cannot easily test confirming subscription because it's only valid for http(s) and email subscriptions.
+# Since we don't want to setup an email box or HTTP server to recive the token, we can't really simulate this
+# def test_10_confirm_subscription
+# response = @sns.confirm_subscription(@@topic_arn, 'SomeToken')
+# assert_not_null(response)
+# end
+
+ def test_30_delete_topic
+ response = @sns.delete_topic(@@topic_arn)
+ assert_not_nil(response)
+ sleep(1)
+ assert(!@sns.list_topics.collect{|topic| topic[:arn] }.include?(@@topic_arn))
+ end
+end
View
1  test/ts_right_aws.rb
@@ -12,3 +12,4 @@
require 'sqs/test_right_sqs_gen2.rb'
require 'sdb/test_right_sdb.rb'
require 'acf/test_right_acf.rb'
+require 'sns/test_right_sns.rb'
Please sign in to comment.
Something went wrong with that request. Please try again.