-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 1984185
Showing
27 changed files
with
4,581 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
source :rubygems | ||
gemspec |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.