Skip to content

Commit

Permalink
Initial extraction from old project
Browse files Browse the repository at this point in the history
  • Loading branch information
trotter committed Dec 16, 2010
0 parents commit 1984185
Show file tree
Hide file tree
Showing 27 changed files with 4,581 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
source :rubygems
gemspec
29 changes: 29 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
PATH
remote: .
specs:
hoopla_force_connector (0.0.1)
hoopla-savon

GEM
remote: http://rubygems.org/
specs:
activesupport (3.0.3)
builder (3.0.0)
crack (0.1.8)
hoopla-savon (0.7.9)
builder (>= 2.1.2)
crack (>= 0.1.4)
i18n (0.5.0)
mocha (0.9.10)
rake
rake (0.8.7)

PLATFORMS
ruby

DEPENDENCIES
activesupport
hoopla-savon
hoopla_force_connector!
i18n
mocha
19 changes: 19 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require 'rubygems'
require 'bundler'
Bundler.setup

require 'rake'
require 'rake/gempackagetask'
require 'rake/testtask'
load 'hoopla_force_connector.gemspec'

Rake::GemPackageTask.new($spec) do |t|
t.need_tar = true
end

Rake::TestTask.new do |t|
t.libs << "test"
t.test_files = FileList['test/*_test.rb']
t.verbose = true
end

18 changes: 18 additions & 0 deletions hoopla_force_connector.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
$spec = Gem::Specification.new do |s|
s.name = "hoopla_force_connector"
s.version = '0.0.1'
s.summary = "Ruby interface for the Salesforce API"

s.authors = ['Trotter Cashion', 'Mat Schaffer']
s.email = ['cashion@gmail.com', 'mat@schaffer.me']
s.homepage = 'http://www.openmarket.com'

s.add_development_dependency 'mocha'
s.add_development_dependency 'i18n'
s.add_development_dependency 'activesupport'

s.add_dependency 'hoopla-savon'

s.files = Dir['lib/**']
s.rubyforge_project = 'nowarning'
end
123 changes: 123 additions & 0 deletions lib/hoopla_force_connector.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
require 'savon'
require 'hoopla_force_connector/core_ext/hash'
require 'hoopla_force_connector/sanitizer'

class HooplaForceConnector
attr_reader :session_id, :server_url, :client
attr_accessor :should_sanitize

def self.default_client
Salesforce.new(SalesforceConfig)
end

def self.client_for(session_id, server_url=nil)
Salesforce.new(SalesforceConfig.merge(:session_id => session_id, :server_url => server_url))
end

def initialize(options)
@session_id = options[:session_id]
@server_url = options[:server_url]
@username = options[:username]
@password = options[:password]
@api_key = options[:api_key]
@wsdl = options[:wsdl]
@should_sanitize = true

@client = Savon::Client.new @wsdl

if @session_id && @server_url
@client.wsdl.soap_endpoint = @server_url
else
login
end
end

def login
response = sanitize(@client.login do |soap|
soap.body = { "wsdl:username" => @username, "wsdl:password" => @password + @api_key }
end.to_hash)

@session_id ||= response[:session_id]
@client.wsdl.soap_endpoint = response[:server_url]
end

def query(query)
result = __call__ :query, { "wsdl:queryString" => query }

sanitize(result) + query_more(result)
end

def query_more(previous)
if previous.values.first[:result][:done]
return []
end

locator = previous.values.first[:result][:query_locator]
result = __call__ :query_more, { "wsdl:QueryLocator" => locator}

sanitize(result) + query_more(result)
end

# Note: Salesforce will bomb out if argument ordering is wrong. We only have one call that
# takes multiple arguments, so we'll insert the correct order from the WSDL here
def retrieve(args)
__sanitized_call__ :retrieve, args.merge(:order! => ['wsdl:fieldList', 'wsdl:sObjectType', 'wsdl:ID'])
end

def method_missing(meth, *args, &block)
if @client.respond_to?(meth)
__sanitized_call__ meth, args.last
else
super
end
end

def __call__(meth, params = nil)
result = @client.send(meth) do |soap|
soap.header = { "wsdl:SessionHeader" => { "wsdl:sessionId" => @session_id } }
soap.body = params.map_to_hash { |k, v| [convert_key(k), v] } if params
end.to_hash
end

def __sanitized_call__(meth, params = nil)
sanitize(__call__(meth, params))
end

def convert_key(key)
if key.to_s =~ /^wsdl:/ || key.to_s =~ /!$/
key
else
"wsdl:" + key.to_s.camelize(:lower)
end
end
private :convert_key


def respond_to?(meth)
super || @client.respond_to?(meth)
end

def sanitize(raw)
@should_sanitize ? Sanitizer.sanitize(raw) : raw
end

module ActiveRecordExtensions
def missing_sf_organization_id?
!column_names.include?("sf_organization_id")
rescue Mysql::Error
# do nothing, because we probably don't even have a db
# which means that we're probably running migrations
end

def salesforce_owned_by(owner, opts={})
include Salesforce::Owned
extend Salesforce::OrgScope if owner == :organization && missing_sf_organization_id?

Salesforce::Owned.establish_ownership(self, owner, opts)
end

def owner
@owner.constantize
end
end
end
11 changes: 11 additions & 0 deletions lib/hoopla_force_connector/core_ext/hash.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Some hash helpers for mapping and filtering on keys
class Hash
def map_to_hash(&block)
ret = Hash[*(map(&block).flatten(1))]
ret.without_keys(nil)
end

def without_keys(*keys)
reject { |k, v| keys.include?(k) }
end
end
74 changes: 74 additions & 0 deletions lib/hoopla_force_connector/sanitizer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
class HooplaForceConnector
# Handles sanitization of values that come out of the salesforce WebServices API.
# Basically there's some oddities like extra wrappers on a return value or ids
# coming through as arrays. See the test for examples of what this handles.
class Sanitizer
def self.sanitize(raw)
new(raw).sanitize
end

def initialize(raw)
@raw = raw
raise "Don't know how to handle more than one key: #{@raw.inspect}" unless @raw.keys.size == 1
@result = raw.values.first[:result]
end

def sanitize
send("sanitize_#{@raw.keys.first.to_s}")
end

# These come in from Salesforce messages
def sanitize_notifications
notifications = [@raw[:notifications][:notification]].flatten
notifications.map { |n| n[:s_object].without_keys(:sf) }
end

def sanitize_query_response
records = @result[:records]
records = [records].flatten.compact
dedup_ids(remove_type(records))
end
alias_method :sanitize_query_more_response, :sanitize_query_response

def sanitize_retrieve_response
records = @result
records = [records].flatten.compact
dedup_ids(remove_type(records))
end

def sanitize_get_updated_response
@result[:ids] = [@result[:ids]].flatten.compact
@result
end

def remove_type(records)
records.map { |r| r.without_keys(:type) }
end

def dedup_ids(records)
records.map do |r|
case r[:id]
when nil
r.without_keys(:id)
when Array
r[:id] = r[:id].first
r
else
r
end
end
end

def method_missing(meth, *args, &block)
if meth.to_s =~ /^sanitize_/
@result
else
super
end
end

def respond_to?(meth)
super || meth.to_s =~ /^sanitize_/
end
end
end
105 changes: 105 additions & 0 deletions test/hoopla_force_connector_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
require 'test_helper'

class HooplaForceConnectorTest < ActiveSupport::TestCase
setup do
@query = "SELECT Name FROM Opportunity"
@session_id = "pizza!"
@server_url = "http://pizzahut.com"
unstub(HooplaForceConnector, :new)
@sf = HooplaForceConnector.new(:session_id => @session_id, :server_url => @server_url,
:wsdl => SalesforceConfig[:wsdl])
@soap_mock = mock('soap')
@soap_mock.stubs(:header=)
@soap_mock.stubs(:body=)
end

test "sets the soap endpoint" do
assert_equal URI(@server_url), @sf.client.wsdl.soap_endpoint
end

test "string query with single result" do
sf_api_response = SalesforceResponses.query_single_user
@query = "SELECT Name FROM Opportunity"
@soap_mock.expects(:header=).with({ "wsdl:SessionHeader" => { "wsdl:sessionId" => @session_id }})
@soap_mock.expects(:body=).with({ "wsdl:queryString" => @query })
Savon::Client.any_instance.expects(:query).yields(@soap_mock).returns(sf_api_response)
assert_equal sf_api_response[:query_response][:result][:records][:title], @sf.query(@query)[0][:title]
end

test "query will use queryMore when query isn't done" do
query_response = SalesforceResponses.query_opportunities_not_done
query_more_response = SalesforceResponses.query_more_opportunities

Savon::Client.any_instance.expects(:query).yields(@soap_mock).returns(query_response)
Savon::Client.any_instance.expects(:query_more).yields(@soap_mock).returns(query_more_response)

@sf.query(@query)
end

test "doesn't call login if both session_id and server_url are already available" do
username, password, api_key = "bob", "password", "api_key"
Savon::Client.any_instance.expects(:login).never
@sf = HooplaForceConnector.new(:wsdl => SalesforceConfig[:wsdl],
:username => username,
:password => password,
:api_key => api_key,
:session_id => @session_id,
:server_url => @server_url)
end

test "logs in first when provided with username, password and api key" do
username, password, api_key = "bob", "password", "api_key"
@soap_mock.expects(:body=).with({ "wsdl:username" => username,
"wsdl:password" => password + api_key })
Savon::Client.any_instance.expects(:login).yields(@soap_mock).returns(SalesforceResponses.login)
@sf = HooplaForceConnector.new(:wsdl => SalesforceConfig[:wsdl],
:username => username,
:password => password,
:api_key => api_key)
assert_equal clean_sf(:login)[:session_id], @sf.session_id
assert_equal URI(clean_sf(:login)[:server_url]), @sf.client.wsdl.soap_endpoint
end

test "login with partner credentials but use customer's session id" do
username, password, api_key = "bob", "password", "api_key"
@soap_mock.expects(:body=).with({ "wsdl:username" => username,
"wsdl:password" => password + api_key })
Savon::Client.any_instance.expects(:login).yields(@soap_mock).returns(SalesforceResponses.login)
@sf = HooplaForceConnector.new(:wsdl => SalesforceConfig[:wsdl],
:username => username,
:password => password,
:api_key => api_key,
:session_id => "abc123")
assert_equal "abc123", @sf.session_id
assert_equal URI(clean_sf(:login)[:server_url]), @sf.client.wsdl.soap_endpoint
end

test "runs arbitrary soap operations without body" do
@soap_mock.expects(:header=).with({ "wsdl:SessionHeader" => { "wsdl:sessionId" => @session_id }})
Savon::Client.any_instance.expects(:get_user_info).yields(@soap_mock).returns({ :get_user_info_response => { :result => SalesforceResponses.get_user_info }})
assert_equal SalesforceResponses.get_user_info, @sf.get_user_info
end

test "runs arbitrary soap operations with body and override key" do
@soap_mock.expects(:body=).with({ "wsdl:sObjectType" => "User", "wsdl:SPECIAL" => "special" })
@soap_mock.expects(:header=).with({ "wsdl:SessionHeader" => { "wsdl:sessionId" => @session_id }})
Savon::Client.any_instance.expects(:describe_s_object).yields(@soap_mock).returns({ :describe_s_object_response => { :result => "" }})
assert_equal "", @sf.describe_s_object(:s_object_type => "User", "wsdl:SPECIAL" => "special")
end

test "can toggle whether or not to sanitize savon responses" do
raw = SalesforceResponses.get_user_info
Savon::Client.any_instance.stubs(:get_user_info).yields(@soap_mock).returns(raw)

assert_equal clean_sf(:get_user_info), @sf.get_user_info
@sf.should_sanitize = false
assert_equal raw, @sf.get_user_info
end

test "retrieve enforces argument order" do
args = { 'wsdl:sObjectType' => "Opportunity", 'wsdl:fieldList' => "Id", 'wsdl:ID' => ["AABBCCDD"] }
@soap_mock.expects(:body=).with(args.merge(:order! => ['wsdl:fieldList', 'wsdl:sObjectType', 'wsdl:ID']))
Savon::Client.any_instance.stubs(:retrieve).yields(@soap_mock).returns(SalesforceResponses.retrieve_opportunities)
@sf.retrieve(args)
end
end
Loading

0 comments on commit 1984185

Please sign in to comment.