├── bin ├── setup └── console ├── .rspec ├── Gemfile ├── lib ├── bing_ads_ruby_sdk │ ├── version.rb │ ├── services │ │ ├── ad_insight.rb │ │ ├── reporting.rb │ │ ├── customer_billing.rb │ │ ├── bulk.rb │ │ ├── customer_management.rb │ │ ├── base.rb │ │ └── campaign_management.rb │ ├── string_utils.rb │ ├── configuration.rb │ ├── postprocessors │ │ ├── cast_long_arrays.rb │ │ └── snakize.rb │ ├── oauth2 │ │ ├── fs_store.rb │ │ └── authorization_handler.rb │ ├── preprocessors │ │ ├── camelize.rb │ │ └── order.rb │ ├── wsdl_operation_wrapper.rb │ ├── log_message.rb │ ├── header.rb │ ├── http_client.rb │ ├── errors │ │ ├── error_handler.rb │ │ └── errors.rb │ ├── wsdl │ │ └── wsdl_source.txt │ ├── api.rb │ ├── augmented_parser.rb │ └── soap_client.rb └── bing_ads_ruby_sdk.rb ├── spec ├── bing_ads_ruby_sdk_spec.rb ├── bing_ads_ruby_sdk │ ├── errors │ │ ├── errors_spec.rb │ │ └── error_handler_spec.rb │ ├── oauth2 │ │ └── fs_store_spec.rb │ ├── postprocessors │ │ ├── cast_long_arrays_spec.rb │ │ └── snakize_spec.rb │ ├── api_spec.rb │ ├── preprocessors │ │ ├── camelize_spec.rb │ │ └── order_spec.rb │ ├── header_spec.rb │ ├── http_client_spec.rb │ └── services │ │ ├── bulk_spec.rb │ │ ├── customer_management_spec.rb │ │ └── campaign_management_spec.rb ├── fixtures │ ├── customer_management │ │ ├── update_account │ │ │ ├── standard_response.xml │ │ │ └── standard.xml │ │ ├── signup_customer │ │ │ ├── standard_response.xml │ │ │ └── standard.xml │ │ ├── get_account │ │ │ ├── standard.xml │ │ │ └── standard_response.xml │ │ └── find_accounts_or_customers_info │ │ │ ├── standard_response.xml │ │ │ └── standard.xml │ ├── campaign_management │ │ ├── update_uet_tags │ │ │ ├── standard_response.xml │ │ │ └── standard.xml │ │ ├── update_conversion_goals │ │ │ ├── standard_response.xml │ │ │ └── standard.xml │ │ ├── set_ad_extensions_associations │ │ │ ├── standard_response.xml │ │ │ └── standard.xml │ │ ├── get_ad_extension_ids_by_account_id │ │ │ ├── standard_response.xml │ │ │ └── standard.xml │ │ ├── add_shared_entity │ │ │ ├── standard_response.xml │ │ │ └── standard.xml │ │ ├── add_conversion_goals │ │ │ ├── standard_response.xml │ │ │ └── standard.xml │ │ ├── add_ad_extensions │ │ │ ├── standard_response.xml │ │ │ └── standard.xml │ │ ├── get_budgets_by_ids │ │ │ ├── standard_response.xml │ │ │ └── standard.xml │ │ ├── get_shared_entity_associations_by_entity_ids │ │ │ ├── standard_response.xml │ │ │ └── standard.xml │ │ ├── get_shared_entities_by_account_id │ │ │ ├── standard_response.xml │ │ │ └── standard.xml │ │ ├── get_campaigns_by_account_id │ │ │ ├── standard.xml │ │ │ └── standard_response.xml │ │ ├── get_uet_tags_by_ids │ │ │ ├── standard.xml │ │ │ └── standard_response.xml │ │ ├── add_uet_tags │ │ │ ├── standard.xml │ │ │ └── standard_response.xml │ │ ├── get_conversion_goals_by_ids │ │ │ ├── standard.xml │ │ │ └── standard_response.xml │ │ └── get_ad_extensions_associations │ │ │ ├── standard.xml │ │ │ └── standard_response.xml │ └── bulk │ │ ├── download_campaigns_by_account_ids │ │ ├── standard_response.xml │ │ └── standard.xml │ │ └── get_bulk_download_status │ │ ├── standard_response.xml │ │ └── standard.xml ├── examples │ ├── 2_with_customer │ │ ├── campaigns_spec.rb │ │ ├── uet_tags_spec.rb │ │ ├── budget_spec.rb │ │ └── customer_management_spec.rb │ ├── 5_with_campaign │ │ ├── ad_group_spec.rb │ │ ├── campaign_criterions_spec.rb │ │ ├── campaign_spec.rb │ │ └── ad_extension_spec.rb │ ├── 1_customer_creation │ │ └── customer_spec.rb │ ├── 3_with_uet_tag │ │ ├── conversion_goal_spec.rb │ │ └── uet_tags_spec.rb │ ├── examples.rb │ ├── 6_with_ad_group │ │ ├── ad_group_spec.rb │ │ ├── keywords_spec.rb │ │ └── ads_spec.rb │ └── 4_with_conversion_goal │ │ └── conversion_goals_spec.rb ├── spec_helper.rb └── support │ └── spec_helpers.rb ├── .github ├── delete-merged-branch-config.yml └── stale.yml ├── Rakefile ├── .gitignore ├── changelog.md ├── tasks └── bing_ads_ruby_sdk.rake ├── LICENSE.txt ├── .circleci └── config.yml ├── bing_ads_ruby_sdk.gemspec └── README.md /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format progress 2 | --color 3 | --exclude-pattern '**/examples/**/*_spec.rb' 4 | --require spec_helper 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in bing_ads_ruby_sdk.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BingAdsRubySdk 4 | VERSION = '1.3.0'.freeze 5 | DEFAULT_SDK_VERSION = :v13 6 | end 7 | -------------------------------------------------------------------------------- /spec/bing_ads_ruby_sdk_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe BingAdsRubySdk do 2 | it "has a version number" do 3 | expect(BingAdsRubySdk::VERSION).not_to be nil 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.github/delete-merged-branch-config.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/SvanBoxel/delete-merged-branch 2 | exclude: 3 | - development 4 | - production 5 | - master 6 | # - BranchNameToExclude*WildcardsSupported -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | require './lib/bing_ads_ruby_sdk' 4 | import 'tasks/bing_ads_ruby_sdk.rake' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/services/ad_insight.rb: -------------------------------------------------------------------------------- 1 | module BingAdsRubySdk 2 | module Services 3 | class AdInsight < Base 4 | 5 | def self.service 6 | :ad_insight 7 | end 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/services/reporting.rb: -------------------------------------------------------------------------------- 1 | module BingAdsRubySdk 2 | module Services 3 | class Reporting < Base 4 | 5 | def self.service 6 | :reporting 7 | end 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/services/customer_billing.rb: -------------------------------------------------------------------------------- 1 | module BingAdsRubySdk 2 | module Services 3 | class CustomerBilling < Base 4 | 5 | def self.service 6 | :customer_billing 7 | end 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | /log/* 11 | vendor/bundle/ 12 | 13 | # rspec failure tracking 14 | .rspec_status 15 | .token_* 16 | 17 | .byebug_history 18 | .env 19 | custom_setup.rb 20 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "bing_ads_ruby_sdk" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /spec/bing_ads_ruby_sdk/errors/errors_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe BingAdsRubySdk::Errors::ApplicationFault do 4 | describe '#fault_hash' do 5 | context 'when creating an instance' do 6 | subject(:create_instance) { described_class.new({ details: nil }) } 7 | 8 | it 'instantiates without raising an exception' do 9 | expect { create_instance }.not_to raise_error 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/fixtures/customer_management/update_account/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | a19fe953-1cb6-4012-93c3-7c4d442bb020 5 | 6 | 7 | 8 | 2019-01-18T13:16:38.827 9 | 10 | 11 | -------------------------------------------------------------------------------- /spec/bing_ads_ruby_sdk/oauth2/fs_store_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | RSpec.describe BingAdsRubySdk::OAuth2::FsStore do 3 | after do 4 | File.unlink('./.abc') if File.file?('./.abc') 5 | end 6 | let(:store) { described_class.new('.abc') } 7 | 8 | context "when not empty" do 9 | before { store.write(a: 1, b: "2") } 10 | it "writes and read properly" do 11 | expect(store.read).to eq("a" => 1, "b" => "2") 12 | end 13 | end 14 | 15 | context "when empty" do 16 | it "reads properly" do 17 | expect(store.read).to be nil 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/update_uet_tags/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | f73bfe6d-3e85-42ae-8912-93c684de36be 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /spec/fixtures/bulk/download_campaigns_by_account_ids/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2f391723-4844-443e-9dfc-f6768716887d 5 | 6 | 7 | 8 | 618504973441 9 | 10 | 11 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/update_conversion_goals/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10179ec3-7c67-4ee5-87fe-863d2044c32c 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/set_ad_extensions_associations/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 29921349-7ed0-43e9-b9d8-15246bee3651 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/services/bulk.rb: -------------------------------------------------------------------------------- 1 | module BingAdsRubySdk 2 | module Services 3 | class Bulk < Base 4 | 5 | def download_campaigns_by_account_ids(message) 6 | call(__method__, message) 7 | end 8 | 9 | def get_bulk_download_status(message) 10 | call(__method__, message) 11 | end 12 | 13 | def get_bulk_upload_url(message) 14 | call(__method__, message) 15 | end 16 | 17 | def get_bulk_upload_status(message) 18 | call(__method__, message) 19 | end 20 | 21 | def self.service 22 | :bulk 23 | end 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/string_utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BingAdsRubySdk 4 | module StringUtils 5 | 6 | def self.camelize(string) 7 | string.split(UNDERSCORE).collect!{ |w| w.capitalize }.join 8 | end 9 | 10 | def self.snakize(string) 11 | string.gsub(MULTIPLE_CAPSREGEX, MATCHING_PATTERN) 12 | .gsub(SPLIT_REGEX, MATCHING_PATTERN) 13 | .tr('-', '_') 14 | .downcase 15 | .to_sym 16 | end 17 | 18 | UNDERSCORE = '_' 19 | MULTIPLE_CAPSREGEX = /([A-Z]+)([A-Z][a-z])/ 20 | SPLIT_REGEX = /([a-z\d])([A-Z])/ 21 | MATCHING_PATTERN = '\1_\2' 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BingAdsRubySdk 4 | class Configuration 5 | attr_accessor :pretty_print_xml, :filters, :log, :instrumentor 6 | attr_writer :logger 7 | 8 | def initialize 9 | @log = false 10 | @pretty_print_xml = false 11 | @filters = [] 12 | @instrumentor = nil 13 | end 14 | 15 | def logger 16 | @logger ||= default_logger 17 | end 18 | 19 | private 20 | 21 | def default_logger 22 | Logger.new(File.join(BingAdsRubySdk::ROOT_PATH, "log", "bing-sdk.log")).tap do |l| 23 | l.level = Logger::INFO 24 | end 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /spec/bing_ads_ruby_sdk/postprocessors/cast_long_arrays_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe BingAdsRubySdk::Postprocessors::CastLongArrays do 4 | 5 | def action(params) 6 | described_class.new(params).call 7 | end 8 | 9 | it "casts and simplifies long arrays" do 10 | expect(action({ 11 | long: "foo", 12 | bar_bar: { 13 | long: ['1', '2'] 14 | }, 15 | foos: [ 16 | { 17 | bar: { 18 | long: ['3', '4'] 19 | } 20 | } 21 | ] 22 | })).to eq({ 23 | long: "foo", 24 | bar_bar: [1, 2], 25 | foos: [ 26 | { bar: [3, 4] } 27 | ] 28 | }) 29 | end 30 | end -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/services/customer_management.rb: -------------------------------------------------------------------------------- 1 | module BingAdsRubySdk 2 | module Services 3 | class CustomerManagement < Base 4 | 5 | def get_account(message) 6 | call(__method__, message) 7 | end 8 | 9 | def update_account(message) 10 | call(__method__, message) 11 | end 12 | 13 | def find_accounts_or_customers_info(message) 14 | call_wrapper(__method__, message, :account_info_with_customer_data, :account_info_with_customer_data) 15 | end 16 | 17 | def signup_customer(message) 18 | call(__method__, message) 19 | end 20 | 21 | def self.service 22 | :customer_management 23 | end 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /spec/fixtures/customer_management/signup_customer/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8b29b0c2-3168-4eb2-acf4-bcfa1d744cad 5 | 6 | 7 | 8 | 1234 9 | G11800KYC4 10 | 5678 11 | F118HLSM 12 | 2019-01-18T12:53:01.883 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/get_ad_extension_ids_by_account_id/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 43bc4fce-dd6b-4c3b-8fed-7a84779aaf33 5 | 6 | 7 | 8 | 9 | 8177660966625 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/add_shared_entity/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5452498b-22a7-451a-b9a2-294ecbf10f24 5 | 6 | 7 | 8 | 9 | 10 | 229798145242911 11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'time' 3 | require 'lolsoap' 4 | 5 | require 'bing_ads_ruby_sdk/version' 6 | require 'bing_ads_ruby_sdk/configuration' 7 | require 'bing_ads_ruby_sdk/api' 8 | require 'bing_ads_ruby_sdk/string_utils' 9 | 10 | module BingAdsRubySdk 11 | def self.config 12 | @configuration ||= BingAdsRubySdk::Configuration.new 13 | end 14 | 15 | def self.configure 16 | yield(config) 17 | end 18 | 19 | def self.log(level, *args, &block) 20 | return unless config.log 21 | config.logger.send(level, *args, &block) 22 | end 23 | 24 | def self.root_path 25 | ROOT_PATH 26 | end 27 | 28 | def self.type_key 29 | TYPE_KEY 30 | end 31 | 32 | TYPE_KEY = '@type' 33 | ROOT_PATH = File.join(__dir__,'..') 34 | end -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/add_conversion_goals/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 27c5447b-da43-4f03-b704-8ea00de874f4 5 | 6 | 7 | 8 | 9 | 46068449 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/postprocessors/cast_long_arrays.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BingAdsRubySdk 4 | module Postprocessors 5 | class CastLongArrays 6 | 7 | def initialize(params) 8 | @params = params 9 | end 10 | 11 | def call 12 | process(@params) 13 | end 14 | 15 | private 16 | 17 | def process(obj) 18 | return unless obj.is_a?(Hash) 19 | 20 | obj.each do |k, v| 21 | case v 22 | when Hash 23 | if v[:long].is_a?(Array) 24 | obj[k] = v[:long].map(&:to_i) 25 | else 26 | process(v) 27 | end 28 | when Array 29 | v.each {|elt| process(elt) } 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # https://probot.github.io/apps/stale/ 2 | 3 | # Number of days of inactivity before an issue becomes stale 4 | daysUntilStale: 30 5 | # Number of days of inactivity before a stale issue is closed 6 | daysUntilClose: 15 7 | # Issues with these labels will never be considered stale 8 | exemptLabels: 9 | - keep 10 | # Label to use when marking an issue as stale 11 | staleLabel: autotag_Stale 12 | # Comment to post when marking an issue as stale. Set to `false` to disable 13 | markComment: > 14 | This issue has been automatically marked as stale because it has not had 15 | recent activity. It will be closed shortly if no further activity occurs. Thank you 16 | for your contributions. 17 | # Comment to post when closing a stale issue. Set to `false` to disable 18 | closeComment: > 19 | Closed automatically due to inactivity. -------------------------------------------------------------------------------- /spec/bing_ads_ruby_sdk/api_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe BingAdsRubySdk::Api do 2 | subject do 3 | described_class.new( 4 | environment: :test, 5 | oauth_store: SpecHelpers.default_store, 6 | client_id: 'client_id', 7 | developer_token: 'developer_token' 8 | ) 9 | end 10 | 11 | it { expect(subject.ad_insight).to be_a(BingAdsRubySdk::Services::AdInsight) } 12 | it { expect(subject.bulk).to be_a(BingAdsRubySdk::Services::Bulk) } 13 | it { expect(subject.campaign_management).to be_a(BingAdsRubySdk::Services::CampaignManagement) } 14 | it { expect(subject.customer_billing).to be_a(BingAdsRubySdk::Services::CustomerBilling) } 15 | it { expect(subject.customer_management).to be_a(BingAdsRubySdk::Services::CustomerManagement) } 16 | it { expect(subject.reporting).to be_a(BingAdsRubySdk::Services::Reporting) } 17 | end -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/add_ad_extensions/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ac8ec6a3-9844-430c-9038-956cffb9ba68 5 | 6 | 7 | 8 | 9 | 10 | 8177660966625 11 | 1 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /spec/bing_ads_ruby_sdk/postprocessors/snakize_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe BingAdsRubySdk::Postprocessors::Snakize do 4 | 5 | def action(params) 6 | described_class.new(params).call 7 | end 8 | 9 | it "changes keys to snake version" do 10 | expect(action({ 11 | "Foo" => "foo", 12 | "BarBar" => { 13 | "BazBaz" => "baz" 14 | }, 15 | "Coucou" => [ 16 | { 17 | "Bisou" => 1 18 | } 19 | ] 20 | })).to eq({ 21 | foo: "foo", 22 | bar_bar: { 23 | baz_baz: "baz" 24 | }, 25 | coucou: [ 26 | { 27 | bisou: 1 28 | } 29 | ] 30 | }) 31 | end 32 | 33 | it "handles properly 'long' tag name" do 34 | expect(action({ 35 | "long" => "1" 36 | })).to eq({ 37 | long: "1" 38 | }) 39 | end 40 | end -------------------------------------------------------------------------------- /spec/bing_ads_ruby_sdk/preprocessors/camelize_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe BingAdsRubySdk::Preprocessors::Camelize do 4 | 5 | def action(params) 6 | described_class.new(params).call 7 | end 8 | 9 | it "changes keys to camelize version" do 10 | expect(action({ 11 | foo: "foo", 12 | bar_bar: { 13 | baz_baz: "baz" 14 | }, 15 | coucou: [ 16 | { 17 | bisou: 1 18 | } 19 | ] 20 | })).to eq({ 21 | "Foo" => "foo", 22 | "BarBar" => { 23 | "BazBaz" => "baz" 24 | }, 25 | "Coucou" => [ 26 | { 27 | "Bisou" => 1 28 | } 29 | ] 30 | }) 31 | end 32 | 33 | it "doesnt camelize 'long' tag name" do 34 | expect(action({ 35 | long: "1" 36 | })).to eq({ 37 | "long" => "1" 38 | }) 39 | end 40 | end -------------------------------------------------------------------------------- /spec/fixtures/bulk/get_bulk_download_status/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0f780b92-8feb-42be-aebb-49e2f17b98d5 5 | 6 | 7 | 8 | 9 | 10 | 100 11 | Completed 12 | cool_url 13 | 14 | 15 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## V1.3.0 Release 2 | Allow instrumentation of HTTP requests via ActiveSupport::Notifications 3 | 4 | ## V1.2.0 Release 5 | Replaced Live connect auth with Microsoft Identity as it is now the default from Bing. 6 | 7 | ## V1.1.1 Release 8 | 9 | - fix broken 1.1.0 which didnt bundle lib folder in gem release 10 | 11 | ## V1.1.0 Release 12 | 13 | - Use bing api v13 14 | 15 | - Bulk api v6 16 | 17 | 18 | ## V1.0.0 Release 19 | The main reasons of the refactoring were to: 20 | 21 | - add convenient methods returning structured data 22 | 23 | - remove metaprogramming 24 | 25 | - remove the dependency on an unmerged and unmaintained branch of the lolsoap gem 26 | 27 | 28 | Alongside these key points, we now have: 29 | 30 | - filtered logs 31 | 32 | - split concerns 33 | 34 | - strong specs suite 35 | 36 | - a customizable configuration 37 | 38 | - Use bing api v12 39 | 40 | - Bulk api v6 41 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/postprocessors/snakize.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BingAdsRubySdk 4 | module Postprocessors 5 | class Snakize 6 | 7 | def initialize(params) 8 | @params = params 9 | end 10 | 11 | def call 12 | process(@params) 13 | end 14 | 15 | private 16 | 17 | # NOTE: there is a potential for high memory usage here as we're using recursive method calling 18 | def process(obj) 19 | return obj unless obj.is_a?(Hash) 20 | 21 | obj.each_with_object({}) do |(k, v), h| 22 | case v 23 | when Hash then v = process(v) 24 | when Array then v = v.map {|elt| process(elt) } 25 | end 26 | h[snakize(k)] = v 27 | end 28 | end 29 | 30 | def snakize(string) 31 | BingAdsRubySdk::StringUtils.snakize(string) 32 | end 33 | end 34 | end 35 | end -------------------------------------------------------------------------------- /spec/examples/2_with_customer/campaigns_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../examples' 2 | 3 | RSpec.describe 'CampaignManagement service' do 4 | include_context 'use api' 5 | 6 | describe 'Campaign methods' do 7 | it 'returns campaign ids' do 8 | campaigns = api.campaign_management.call(:add_campaigns, 9 | account_id: Examples.account_id, 10 | campaigns: { 11 | campaign: 12 | { 13 | name: "Acceptance Test Campaign #{random}", 14 | daily_budget: 10, 15 | budget_type: 'DailyBudgetStandard', 16 | time_zone: 'BrusselsCopenhagenMadridParis' 17 | } 18 | } 19 | ) 20 | expect(campaigns).to include( 21 | partial_errors: '', 22 | campaign_ids: [a_kind_of(Integer)] 23 | ) 24 | puts "You can now fill in examples.rb with campaign_id: #{campaigns[:campaign_ids].first}" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/get_budgets_by_ids/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 63914b99-7c12-4f79-9e45-ca11c01777ce 5 | 6 | 7 | 8 | 9 | 10 | 0.05 11 | 3 12 | DailyBudgetAccelerated 13 | 8177617799488 14 | budget_DEFAULT 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/get_budgets_by_ids/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tasks/bing_ads_ruby_sdk.rake: -------------------------------------------------------------------------------- 1 | require 'dotenv/load' 2 | 3 | namespace :bing_token do 4 | desc "Gets and stores Bing OAuth token in file" 5 | task :get, [:filename, :bing_developer_token, :bing_client_id] do |task, args| 6 | 7 | filename = args[:filename] || ENV.fetch('BING_STORE_FILENAME') 8 | developer_token = args[:bing_developer_token] || ENV.fetch('BING_DEVELOPER_TOKEN') 9 | bing_client_id = args[:bing_client_id] || ENV.fetch('BING_CLIENT_ID') 10 | 11 | store = ::BingAdsRubySdk::OAuth2::FsStore.new(filename) 12 | auth = BingAdsRubySdk::OAuth2::AuthorizationHandler.new( 13 | developer_token: developer_token, 14 | client_id: bing_client_id, 15 | store: store 16 | ) 17 | puts "Go to #{auth.code_url}", 18 | "You will be redirected to a URL at the end. Paste it here in the console and press enter" 19 | 20 | full_url = STDIN.gets.chomp 21 | auth.fetch_from_url(full_url) 22 | 23 | puts "Written to store" 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/fixtures/bulk/get_bulk_download_status/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 618504973441 12 | 13 | 14 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'dotenv/load' 3 | require 'byebug' 4 | 5 | SimpleCov.start do 6 | add_filter '/spec/' 7 | end 8 | 9 | require 'bing_ads_ruby_sdk' 10 | 11 | Dir[File.join(BingAdsRubySdk.root_path, "spec", "support", "**", "*.rb")].each { |f| require f } 12 | Dir[File.join(BingAdsRubySdk.root_path, "log", "*.log")].each do |log_file| 13 | File.open(log_file, 'w') { |f| f.truncate(0) } 14 | end 15 | 16 | BingAdsRubySdk.configure do |conf| 17 | conf.log = true 18 | conf.logger.level = Logger::DEBUG 19 | conf.pretty_print_xml = true 20 | conf.filters = ["AuthenticationToken", "DeveloperToken", "CustomerId", "CustomerAccountId"] 21 | end 22 | 23 | RSpec.configure do |config| 24 | # Enable flags like --only-failures and --next-failure 25 | config.example_status_persistence_file_path = '.rspec_status' 26 | 27 | # Disable RSpec exposing methods globally on `Module` and `main` 28 | config.disable_monkey_patching! 29 | 30 | config.expect_with :rspec do |c| 31 | c.syntax = :expect 32 | end 33 | end -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/get_shared_entity_associations_by_entity_ids/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0b9ca013-9254-4f32-a88b-fa6c83eb0699 5 | 6 | 7 | 8 | 9 | 10 | 349704435 11 | Campaign 12 | 223200992903993 13 | NegativeKeywordList 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/get_shared_entities_by_account_id/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 37faa802-0f8b-4478-9c5b-ca045a64677c 5 | 6 | 7 | 8 | 9 | 10 | 0 11 | 12 | 229798145242911 13 | sdk list 14 | NegativeKeywordList 15 | 0 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /spec/examples/5_with_campaign/ad_group_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../examples' 2 | 3 | RSpec.describe 'AdGroup methods' do 4 | include_context 'use api' 5 | 6 | describe '#add_ad_groups' do 7 | it 'returns created AdGroup ids' do 8 | ad_groups = api.campaign_management.call(:add_ad_groups, 9 | campaign_id: Examples.campaign_id, 10 | ad_groups: { 11 | ad_group: { 12 | name: "AcceptanceTestAdGroup - #{random}", 13 | language: 'French', 14 | start_date: { 15 | day: '1', 16 | month: '1', 17 | year: '2049', 18 | }, 19 | end_date: { 20 | day: '1', 21 | month: '2', 22 | year: '2049', 23 | } 24 | } 25 | } 26 | ) 27 | expect(ad_groups).to include( 28 | ad_group_ids: [a_kind_of(Integer)], 29 | partial_errors: '' 30 | ) 31 | 32 | puts "Please fill in examples.rb with ad_group_id: #{ad_groups[:ad_group_ids].first}" 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/oauth2/fs_store.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module BingAdsRubySdk 4 | module OAuth2 5 | # Oauth2 token default non-encrypted File System store 6 | class FsStore 7 | # @param filename [String] the uniq filename to identify filename storing data. 8 | def initialize(filename) 9 | @filename = filename 10 | end 11 | 12 | # Writes the token to file 13 | # @return [File] if the file was written (doesn't mean the token is). 14 | # @return [self] if the filename don't exist. 15 | def write(value) 16 | return nil unless filename 17 | File.open(filename, 'w') { |f| JSON.dump(value, f) } 18 | self 19 | end 20 | 21 | # Reads the token from file 22 | # @return [Hash] if the token information that was stored. 23 | # @return [nil] if the file doesn't exist. 24 | def read 25 | return nil unless File.file?("./#{filename}") 26 | JSON.parse(IO.read(filename)) 27 | end 28 | 29 | private 30 | 31 | attr_reader :filename 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/get_campaigns_by_account_id/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 150168726 12 | 13 | 14 | -------------------------------------------------------------------------------- /spec/fixtures/customer_management/get_account/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 5678 12 | 13 | 14 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/get_uet_tags_by_ids/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 12 | 96031109 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/get_shared_entities_by_account_id/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | NegativeKeywordList 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/preprocessors/camelize.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BingAdsRubySdk 4 | module Preprocessors 5 | class Camelize 6 | 7 | def initialize(params) 8 | @params = params 9 | end 10 | 11 | def call 12 | process(@params) 13 | end 14 | 15 | private 16 | 17 | # NOTE: there is a potential for high memory usage here as we're using recursive method calling 18 | def process(obj) 19 | return obj unless obj.is_a?(Hash) 20 | 21 | obj.each_with_object({}) do |(k, v), h| 22 | case v 23 | when Hash then v = process(v) 24 | when Array then v = v.map {|elt| process(elt) } 25 | end 26 | h[transform_key(k.to_s)] = v 27 | end 28 | end 29 | 30 | def transform_key(key) 31 | if BLACKLIST.include?(key) 32 | key 33 | else 34 | camelize(key) 35 | end 36 | end 37 | 38 | def camelize(string) 39 | BingAdsRubySdk::StringUtils.camelize(string) 40 | end 41 | 42 | BLACKLIST = %w(long string) 43 | end 44 | end 45 | end -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Effilab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/add_shared_entity/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 12 | sdk list 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/add_uet_tags/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 12 | 13 | 14 | SDK-test 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /spec/fixtures/customer_management/find_accounts_or_customers_info/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 37d18ec7-de78-4000-8a5d-ba4efadf1de0 5 | 6 | 7 | 8 | 9 | 10 | 1234 11 | SDKTEST updated 12 | 5678 13 | SDK account 14 | F118HLSM 15 | Active 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /spec/fixtures/customer_management/find_accounts_or_customers_info/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | SDKTEST 12 | 1 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/get_ad_extension_ids_by_account_id/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 150168726 12 | CallAdExtension SitelinkAdExtension CalloutAdExtension 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/examples/1_customer_creation/customer_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../examples' 2 | 3 | RSpec.describe 'CustomerManagement service' do 4 | include_context 'use api' 5 | 6 | it "creates customer" do 7 | created_customer = api.customer_management.signup_customer( 8 | customer: { 9 | customer_address: { 10 | city: 'Paris', 11 | postal_code: 75_001, 12 | line1: '1 rue de Rivoli', 13 | country_code: 'FR', 14 | }, 15 | industry: 'Entertainment', 16 | name: "Test Customer #{random}", 17 | }, 18 | account: { 19 | '@type' => 'AdvertiserAccount', 20 | name: "Test Account #{random}", 21 | currency_code: 'USD', 22 | }, 23 | parent_customer_id: Examples.parent_customer_id 24 | ) 25 | expect(created_customer).to include( 26 | customer_id: a_kind_of(String), 27 | customer_number: a_kind_of(String), 28 | account_id: a_kind_of(String), 29 | account_number: a_kind_of(String), 30 | create_time: a_kind_of(String) 31 | ) 32 | 33 | puts "You can now fill in examples.rb with customer_id: #{created_customer[:customer_id]} and account_id: #{created_customer[:account_id]}" 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/update_uet_tags/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 12 | 13 | 14 | 96031109 15 | updated SDK-test 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /spec/bing_ads_ruby_sdk/header_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe BingAdsRubySdk::Header do 2 | let(:oauth_store) { double(:oauth_store) } 3 | let(:subject) { described_class.new(developer_token: '123abc', client_id: '1a-2b-3c', store: oauth_store) } 4 | let(:auth_handler) do 5 | double(:auth_handler, fetch_or_refresh: 'yes/we/can') 6 | end 7 | 8 | before do 9 | expect(::BingAdsRubySdk::OAuth2::AuthorizationHandler).to receive(:new).with( 10 | developer_token: '123abc', 11 | client_id: '1a-2b-3c', 12 | store: oauth_store 13 | ).and_return auth_handler 14 | end 15 | 16 | describe '.content' do 17 | it do 18 | expect(subject.content).to eq( 19 | "AuthenticationToken" => 'yes/we/can', 20 | "DeveloperToken" => '123abc', 21 | "CustomerId" => nil, 22 | "CustomerAccountId" => nil 23 | ) 24 | end 25 | 26 | it 'sets customer' do 27 | subject.set_customer(customer_id: 777, account_id: 666 ) 28 | 29 | expect(subject.content).to eq( 30 | "AuthenticationToken" => 'yes/we/can', 31 | "DeveloperToken" => '123abc', 32 | "CustomerId" => 777, 33 | "CustomerAccountId" => 666 34 | ) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/get_conversion_goals_by_ids/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 12 | 46068449 13 | 46068448 14 | 15 | Event 16 | 17 | 18 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/get_shared_entity_associations_by_entity_ids/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 12 | 349704435 13 | 14 | Campaign 15 | NegativeKeywordList 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/wsdl_operation_wrapper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BingAdsRubySdk 4 | class WsdlOperationWrapper 5 | 6 | attr_reader :request_namespace_type 7 | 8 | def initialize(parser, operation_name) 9 | @parser = parser 10 | @request_namespace_type = parser.operations.fetch(operation_name).fetch(:input).fetch(:body).first 11 | end 12 | 13 | def ordered_fields_hash(namespace_type) 14 | # we check types first as its the main source of data, except for the Request type which lives in elements 15 | if parser.types.fetch(namespace_type, nil) 16 | parser.types.fetch(namespace_type).fetch(:elements) 17 | else 18 | parser.elements.fetch(namespace_type).fetch(:type).fetch(:elements) 19 | end 20 | end 21 | 22 | def namespace_and_type_from_name(all_attributes, type_name) 23 | all_attributes.fetch(type_name).fetch(:type) 24 | end 25 | 26 | def base_type_name(elements, type_name) 27 | return nil if type_name == BingAdsRubySdk.type_key 28 | elements.fetch(type_name).fetch(:base_type_name, type_name) 29 | end 30 | 31 | def self.prefix_and_name(wsdl, type_name) 32 | wsdl.types.fetch(type_name).prefix_and_name 33 | end 34 | 35 | private 36 | 37 | attr_reader :parser 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/get_ad_extensions_associations/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 150168726 12 | CalloutAdExtension 13 | Campaign 14 | 15 | 349704437 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /spec/fixtures/customer_management/update_account/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 12 | EUR 13 | 5678 14 | SDKTEST updated 15 | AAAAAE496a4= 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Ruby CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-ruby/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | - image: circleci/ruby:2.4.1 10 | 11 | working_directory: ~/repo 12 | 13 | steps: 14 | - checkout 15 | 16 | # We can't use restore_cache because we don't include a Gemfile.lock 17 | # in the Gem repo 18 | 19 | - run: 20 | name: install dependencies 21 | command: | 22 | bundle install --jobs=4 --retry=3 --path vendor/bundle 23 | 24 | # run tests! 25 | - run: 26 | name: run tests 27 | command: | 28 | mkdir log/ 29 | mkdir /tmp/test-results 30 | TEST_FILES="$(circleci tests glob "spec/bing_ads_ruby_sdk/**/*_spec.rb" | \ 31 | circleci tests split --split-by=timings)" 32 | 33 | bundle exec rspec \ 34 | --format progress \ 35 | --format RspecJunitFormatter \ 36 | --out /tmp/test-results/rspec.xml \ 37 | --format progress \ 38 | $TEST_FILES 39 | 40 | # collect reports 41 | - store_test_results: 42 | path: /tmp/test-results 43 | - store_artifacts: 44 | path: /tmp/test-results 45 | destination: test-results -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/add_ad_extensions/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 150168726 12 | 13 | 14 | 15 | NZ 16 | 0123456699 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /spec/bing_ads_ruby_sdk/http_client_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe BingAdsRubySdk::HttpClient do 2 | 3 | describe ".post" do 4 | let(:request) do 5 | double(:request, 6 | url: "http://bing_url.com/foo", 7 | content: "body", 8 | headers: "headers" 9 | ) 10 | end 11 | let(:excon) { double(:excon) } 12 | 13 | before do 14 | expect(::Excon).to receive(:new).and_return(excon) 15 | expect(excon).to receive(:post).with( 16 | path: "/foo", 17 | body: "body", 18 | headers: "headers" 19 | ).and_return(response) 20 | end 21 | 22 | context "successful request" do 23 | let(:response) { double(:response, body: "soap xml") } 24 | it "returns response's body" do 25 | expect(described_class.post(request)).to eq("soap xml") 26 | end 27 | end 28 | end 29 | 30 | describe ".close_http_connections" do 31 | let(:connection1) { double("connection1") } 32 | let(:connection2) { double("connection2") } 33 | it "closes existing connections" do 34 | expect(described_class).to receive(:http_connections).and_return({ 35 | "url1" => connection1, 36 | "url2" => connection2, 37 | }) 38 | expect(connection1).to receive :reset 39 | expect(connection2).to receive :reset 40 | 41 | described_class.close_http_connections 42 | end 43 | end 44 | end -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/log_message.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BingAdsRubySdk 4 | class LogMessage 5 | 6 | def initialize(message) 7 | @message = message 8 | end 9 | 10 | def to_s 11 | return message unless message_is_xml 12 | return message unless filters.any? || pretty_print 13 | 14 | document = Nokogiri::XML(message) 15 | document = apply_filter(document) if filters.any? 16 | document.to_xml(nokogiri_options) 17 | end 18 | 19 | FILTERED = "***FILTERED***" 20 | 21 | private 22 | 23 | attr_reader :message 24 | 25 | def message_is_xml 26 | message =~ /^ auth_handler.fetch_or_refresh, 20 | "DeveloperToken" => developer_token, 21 | "CustomerId" => customer[:customer_id], 22 | "CustomerAccountId" => customer[:account_id] 23 | } 24 | end 25 | 26 | def set_customer(account_id:, customer_id:) 27 | customer[:account_id] = account_id 28 | customer[:customer_id] = customer_id 29 | self 30 | end 31 | 32 | private 33 | 34 | attr_reader :oauth_store, :developer_token, :client_id, :customer 35 | 36 | def auth_handler 37 | @auth_handler ||= ::BingAdsRubySdk::OAuth2::AuthorizationHandler.new( 38 | developer_token: developer_token, 39 | client_id: client_id, 40 | store: oauth_store 41 | ) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/examples/3_with_uet_tag/conversion_goal_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../examples' 2 | 3 | RSpec.describe 'Conversion goals methods' do 4 | include_context 'use api' 5 | 6 | it 'returns a list of Conversion goal Ids' do 7 | conversion_goals = api.campaign_management.add_conversion_goals( 8 | conversion_goals: { 9 | event_goal: { 10 | conversion_window_in_minutes: 43_200, # 30days 11 | count_type: 'All', 12 | name: "Acceptance Test Conversion goal #{random}", 13 | revenue: { 14 | currency_code: 'EUR', 15 | type: 'FixedValue', 16 | value: 5.20, 17 | }, 18 | scope: 'Account', 19 | status: 'Active', 20 | tag_id: Examples.uet_tag_id, 21 | action_operator: 'Equals', 22 | action_expression: 'display_phone', 23 | category_operator: 'Equals', 24 | category_expression: 'contact_form', 25 | label_operator: 'Equals', 26 | label_expression: 'lower_button', 27 | value_operator: 'Equals', 28 | value: '1' 29 | } 30 | } 31 | ) 32 | 33 | expect(conversion_goals).to include( 34 | conversion_goal_ids: [a_kind_of(Integer)], 35 | partial_errors: '' 36 | ) 37 | 38 | puts "Please fill in examples.rb with conversion_goal_id: #{conversion_goals[:conversion_goal_ids].first}" 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/fixtures/bulk/download_campaigns_by_account_ids/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 12 | 150168726 13 | 14 | Zip 15 | EntityData 16 | 17 | Campaigns 18 | 19 | Csv 20 | 6.0 21 | 22 | 23 | -------------------------------------------------------------------------------- /spec/examples/2_with_customer/uet_tags_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../examples' 2 | 3 | RSpec.describe 'CampaignManagement service' do 4 | include_context 'use api' 5 | 6 | describe 'UET methods' do 7 | subject(:add_uet_tags) do 8 | api.campaign_management.add_uet_tags( 9 | uet_tags: [ 10 | uet_tag: { 11 | description: 'UET Tag Description', 12 | name: "Acceptance Test UET Tag #{random}", 13 | } 14 | ] 15 | ) 16 | end 17 | 18 | describe '#add_uet_tags' do 19 | it 'returns a list of newly created UET tags' do 20 | uet_tags = add_uet_tags 21 | expect(uet_tags).to include( 22 | uet_tags: { 23 | uet_tag: [ 24 | { 25 | description: 'UET Tag Description', 26 | id: a_kind_of(String), 27 | name: a_string_starting_with('Acceptance Test UET Tag'), 28 | tracking_no_script: a_string_starting_with("(function(w,d,t,r,u)'), 30 | tracking_status: 'Unverified', 31 | customer_share: nil 32 | } 33 | ] 34 | }, 35 | partial_errors: '' 36 | ) 37 | 38 | puts "Please fill in examples.rb with uet_tag_id: #{uet_tags[:uet_tags][:uet_tag].first[:id]}" 39 | end 40 | end 41 | end 42 | end -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/set_ad_extensions_associations/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 150168726 12 | 13 | 14 | 8177660966942 15 | 349704437 16 | 17 | 18 | Campaign 19 | 20 | 21 | -------------------------------------------------------------------------------- /spec/examples/2_with_customer/budget_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../examples' 2 | 3 | RSpec.describe 'Budget methods' do 4 | include_context 'use api' 5 | 6 | let(:budget_id) { add_budget[:budget_ids].first } 7 | 8 | let(:add_budget) do 9 | api.campaign_management.call(:add_budgets, 10 | budgets: [ 11 | budget: { 12 | amount: '10', 13 | budget_type: 'DailyBudgetStandard', 14 | name: "test_budget #{random}", 15 | } 16 | ] 17 | ) 18 | end 19 | 20 | describe '#add_budget' do 21 | it 'returns budget ids for created Budgets' do 22 | expect(add_budget).to include( 23 | budget_ids: [a_kind_of(Integer)], 24 | partial_errors: '' 25 | ) 26 | end 27 | end 28 | 29 | describe '#get_budgets_by_ids' do 30 | before { add_budget } 31 | 32 | it 'returns a list of budgets' do 33 | expect(api.campaign_management.get_budgets_by_ids( 34 | budget_ids: [long: budget_id] 35 | )).to include({ 36 | amount: '10.00', 37 | association_count: '0', 38 | budget_type: 'DailyBudgetStandard', 39 | id: a_kind_of(String), 40 | name: a_string_starting_with('test_budget'), 41 | }) 42 | end 43 | end 44 | 45 | describe '#delete_budgets' do 46 | before { add_budget } 47 | 48 | it 'returns no errors' do 49 | expect(api.campaign_management.call(:delete_budgets, 50 | budget_ids: [ { long: budget_id } ] 51 | )).to eq(partial_errors: '') 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /bing_ads_ruby_sdk.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'bing_ads_ruby_sdk/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'bing_ads_ruby_sdk' 8 | spec.required_ruby_version = '>= 2.0' 9 | 10 | spec.version = BingAdsRubySdk::VERSION 11 | spec.authors = %w[Effilab developers] 12 | spec.email = %w[developers@effilab-local.com] 13 | 14 | spec.summary = 'Bing Ads Ruby SDK' 15 | spec.description = 'Bing Ads Api Wrapper' 16 | spec.homepage = 'https://github.com/Effilab/bing_ads_ruby_sdk' 17 | spec.license = 'MIT' 18 | 19 | spec.files = `git ls-files`.split($/) 20 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 21 | spec.test_files = spec.files.grep(%r{^(spec)/}) 22 | spec.require_paths = ["lib"] 23 | 24 | spec.add_runtime_dependency 'signet', '~> 0.8.1' 25 | spec.add_runtime_dependency 'excon', '>= 0.62.0' 26 | spec.add_runtime_dependency 'lolsoap', '>=0.9.0' 27 | 28 | spec.add_development_dependency 'bundler' 29 | spec.add_development_dependency 'dotenv' 30 | spec.add_development_dependency 'rake' 31 | spec.add_development_dependency 'yard' 32 | spec.add_development_dependency 'rspec' 33 | spec.add_development_dependency 'simplecov' 34 | spec.add_development_dependency 'byebug' 35 | spec.add_development_dependency 'awesome_print' 36 | spec.add_development_dependency 'rspec_junit_formatter', '~> 0.4.1' 37 | end 38 | -------------------------------------------------------------------------------- /spec/support/spec_helpers.rb: -------------------------------------------------------------------------------- 1 | module SpecHelpers 2 | def self.request_xml_for(service, action, filename) 3 | Nokogiri::XML(File.read(xml_path_for(service, action, filename))) 4 | end 5 | 6 | def self.response_xml_for(service, action, filename) 7 | File.read(xml_path_for(service, action, filename, false)) 8 | end 9 | 10 | def self.fake_header 11 | OpenStruct.new( 12 | content: { 13 | "AuthenticationToken" => BingAdsRubySdk::LogMessage::FILTERED, 14 | "DeveloperToken" => BingAdsRubySdk::LogMessage::FILTERED, 15 | "CustomerId" => BingAdsRubySdk::LogMessage::FILTERED, 16 | "CustomerAccountId" => BingAdsRubySdk::LogMessage::FILTERED 17 | }) 18 | end 19 | 20 | def self.soap_client(service, header = fake_header) 21 | BingAdsRubySdk::SoapClient.new( 22 | service_name: service, 23 | version: BingAdsRubySdk::DEFAULT_SDK_VERSION, 24 | environment: 'test', 25 | header: header 26 | ) 27 | end 28 | 29 | def self.wrapper(service, action_name) 30 | soap_client(service).wsdl_wrapper(action_name) 31 | end 32 | 33 | def self.default_store 34 | ::BingAdsRubySdk::OAuth2::FsStore.new(ENV['BING_STORE_FILENAME']) 35 | end 36 | 37 | # default fixture for now is standard.xml but door is open to get more use cases 38 | def self.xml_path_for(service, action, filename, request = true) 39 | if request 40 | File.join(BingAdsRubySdk.root_path, 'spec', 'fixtures', service.to_s, action, "#{filename}.xml") 41 | else 42 | File.join(BingAdsRubySdk.root_path, 'spec', 'fixtures', service.to_s, action, "#{filename}_response.xml") 43 | end 44 | end 45 | end -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/add_conversion_goals/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 12 | 13 | 43200 14 | Unique 15 | sdk test 16 | 17 | NoValue 18 | 19 | 96031109 20 | Event 21 | contact_form 22 | Equals 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/add_uet_tags/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | e7b8fc9b-934f-4851-b33b-414cdf777023 5 | 6 | 7 | 8 | 9 | 10 | 11 | 96031109 12 | SDK-test 13 | <img src="//bat.bing.com/action/0?ti=96031109&Ver=2" height="0" width="0" style="display:none; visibility: hidden;" /> 14 | <script>(function(w,d,t,r,u){var f,n,i;w[u]=w[u]||[],f=function(){var o={ti:"96031109"};o.q=w[u],w[u]=new UET(o),w[u].push("pageLoad")},n=d.createElement(t),n.src=r,n.async=1,n.onload=n.onreadystatechange=function(){var s=this.readyState;s&&s!=="loaded"&&s!=="complete"||(f(),n.onload=n.onreadystatechange=null)},i=d.getElementsByTagName(t)[0],i.parentNode.insertBefore(n,i)})(window,document,"script","//bat.bing.com/bat.js","uetq");</script><noscript><img src="//bat.bing.com/action/0?ti=96031109&Ver=2" height="0" width="0" style="display:none; visibility: hidden;" /></noscript> 15 | Unverified 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/update_conversion_goals/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 12 | 13 | 43200 14 | Unique 15 | 46068449 16 | updated sdk test 17 | 18 | NoValue 19 | 20 | 96031109 21 | contact_form 22 | Equals 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/get_uet_tags_by_ids/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2566fd81-b382-4b58-b62b-bd2d5184e15e 5 | 6 | 7 | 8 | 9 | 10 | 11 | 96031109 12 | updated SDK-test 13 | <img src="//bat.bing.com/action/0?ti=96031109&Ver=2" height="0" width="0" style="display:none; visibility: hidden;" /> 14 | <script>(function(w,d,t,r,u){var f,n,i;w[u]=w[u]||[],f=function(){var o={ti:"96031109"};o.q=w[u],w[u]=new UET(o),w[u].push("pageLoad")},n=d.createElement(t),n.src=r,n.async=1,n.onload=n.onreadystatechange=function(){var s=this.readyState;s&&s!=="loaded"&&s!=="complete"||(f(),n.onload=n.onreadystatechange=null)},i=d.getElementsByTagName(t)[0],i.parentNode.insertBefore(n,i)})(window,document,"script","//bat.bing.com/bat.js","uetq");</script><noscript><img src="//bat.bing.com/action/0?ti=96031109&Ver=2" height="0" width="0" style="display:none; visibility: hidden;" /></noscript> 15 | Unverified 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /spec/bing_ads_ruby_sdk/services/bulk_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe BingAdsRubySdk::Services::Bulk do 2 | 3 | let(:service_name) { described_class.service } 4 | let(:soap_client) { SpecHelpers.soap_client(service_name) } 5 | let(:expected_xml) { SpecHelpers.request_xml_for(service_name, action, filename) } 6 | let(:mocked_response) { SpecHelpers.response_xml_for(service_name, action, filename) } 7 | 8 | let(:service) { described_class.new(soap_client) } 9 | 10 | before do 11 | expect(BingAdsRubySdk::HttpClient).to receive(:post) do |req| 12 | expect(Nokogiri::XML(req.content).to_xml).to eq expected_xml.to_xml 13 | mocked_response 14 | end 15 | end 16 | 17 | describe "download_campaigns_by_account_ids" do 18 | let(:action) { 'download_campaigns_by_account_ids' } 19 | let(:filename) { 'standard' } 20 | 21 | it "returns expected result" do 22 | expect( 23 | service.download_campaigns_by_account_ids( 24 | account_ids: [{ long: 150168726 }], 25 | data_scope: "EntityData", 26 | download_file_type: "Csv", 27 | compression_type: "Zip", 28 | download_entities: [ 29 | { download_entity: "Campaigns" } 30 | ], 31 | format_version: "6.0" 32 | ) 33 | ).to eq({ 34 | download_request_id: "618504973441" 35 | }) 36 | end 37 | end 38 | 39 | describe "get_bulk_download_status" do 40 | let(:action) { 'get_bulk_download_status' } 41 | let(:filename) { 'standard' } 42 | 43 | it "returns expected result" do 44 | expect( 45 | service.get_bulk_download_status(request_id: 618504973441) 46 | ).to include( 47 | request_status: 'Completed', 48 | result_file_url: "cool_url" 49 | ) 50 | end 51 | end 52 | end 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /spec/fixtures/customer_management/signup_customer/standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ***FILTERED*** 5 | ***FILTERED*** 6 | ***FILTERED*** 7 | ***FILTERED*** 8 | 9 | 10 | 11 | 12 | NA 13 | FR 14 | French 15 | sdk customer 16 | 17 | Nice 18 | FR 19 | 127 bd risso 20 | 06000 21 | 22 | 23 | 24 | EUR 25 | SDK account 26 | 27 | 9876 28 | 29 | 30 | -------------------------------------------------------------------------------- /spec/examples/3_with_uet_tag/uet_tags_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../examples' 2 | 3 | RSpec.describe 'CampaignManagement service' do 4 | include_context 'use api' 5 | 6 | describe 'UET methods' do 7 | let(:get_uet_tags_by_ids) do 8 | api.campaign_management.get_uet_tags_by_ids( 9 | tag_ids: [ 10 | { long: Examples.uet_tag_id } 11 | ] 12 | ) 13 | end 14 | 15 | describe '#get_uet_tags_by_ids' do 16 | it 'returns a list of UET tags' do 17 | expect(get_uet_tags_by_ids).to contain_exactly( 18 | { 19 | description: a_kind_of(String), 20 | id: a_kind_of(String), 21 | name: a_string_starting_with('Acceptance Test UET Tag'), 22 | tracking_no_script: a_string_starting_with("(function(w,d,t,r,u)'), 24 | tracking_status: 'Unverified', 25 | customer_share: nil 26 | } 27 | ) 28 | end 29 | end 30 | 31 | describe '#update_uet_tags' do 32 | subject do 33 | api.campaign_management.update_uet_tags({ 34 | uet_tags: [ 35 | { 36 | uet_tag: { 37 | name: "Acceptance Test UET Tag - #{random}", 38 | id: Examples.uet_tag_id, 39 | description: "UET Tag Description - #{random}", 40 | } 41 | } 42 | ] 43 | }) 44 | end 45 | 46 | it 'updates the UET tag fields' do 47 | is_expected.to eq(partial_errors: '') 48 | 49 | expect(get_uet_tags_by_ids.first).to include( 50 | name: "Acceptance Test UET Tag - #{random}", 51 | description: "UET Tag Description - #{random}" 52 | ) 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/preprocessors/order.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BingAdsRubySdk 4 | module Preprocessors 5 | class Order 6 | 7 | def initialize(wsdl_wrapper, params) 8 | @wrapper = wsdl_wrapper 9 | @params = params 10 | end 11 | 12 | def call 13 | process(params, wrapper.request_namespace_type) 14 | end 15 | 16 | private 17 | 18 | attr_reader :wrapper, :params 19 | 20 | # NOTE: there is a potential for high memory usage here as we're using recursive method calling 21 | def process(obj, namespace_type) 22 | return obj unless obj.is_a?(Hash) 23 | 24 | allowed_attributes = wrapper.ordered_fields_hash(namespace_type) 25 | 26 | order(obj, allowed_attributes).tap do |ordered_hash| 27 | ordered_hash.each do |type_name, value| 28 | ordered_hash[type_name] = ordered_value(allowed_attributes, type_name, value) 29 | end 30 | end 31 | end 32 | 33 | def ordered_value(allowed_attributes, type_name, value) 34 | case value 35 | when Hash 36 | namespace_type = wrapper.namespace_and_type_from_name(allowed_attributes, type_name) 37 | process(value, namespace_type) 38 | when Array 39 | value.map do |elt| 40 | namespace_type = wrapper.namespace_and_type_from_name(allowed_attributes, type_name) 41 | process(elt, namespace_type) 42 | end 43 | else value 44 | end 45 | end 46 | 47 | def ordered_params(namespace_type) 48 | wrapper.ordered_fields_hash(namespace_type) 49 | end 50 | 51 | def order(hash, allowed_attributes) 52 | array = allowed_attributes.keys 53 | # basically order by index in reference array 54 | Hash[ hash.sort_by { |k, _| array.index(wrapper.base_type_name(allowed_attributes, k)) || k.ord } ] 55 | end 56 | end 57 | end 58 | end -------------------------------------------------------------------------------- /spec/examples/examples.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'securerandom' 4 | 5 | module Examples 6 | class << self 7 | 8 | def random 9 | SecureRandom.hex 10 | end 11 | 12 | def build_api 13 | BingAdsRubySdk::Api.new( 14 | developer_token: developer_token, 15 | client_id: client_id, 16 | oauth_store: store 17 | ).tap do |api| 18 | if account_id && customer_id 19 | api.set_customer( 20 | customer_id: customer_id, 21 | account_id: account_id 22 | ) 23 | end 24 | end 25 | end 26 | 27 | def client_id 28 | # you have to fill this in with data from bing 29 | end 30 | 31 | def developer_token 32 | # you have to fill this in with data from bing 33 | end 34 | 35 | def parent_customer_id 36 | # you have to fill this in with data from bing 37 | end 38 | 39 | def customer_id 40 | # you have to fill this in with data you get after running 1_customer folder 41 | end 42 | 43 | def account_id 44 | # you have to fill this in with data you get after running 1_customer folder 45 | end 46 | 47 | def uet_tag_id 48 | # you have to fill this in with data you get after running 2_with_customer folder 49 | end 50 | 51 | def campaign_id 52 | # you have to fill this in with data you get after running 2_with_customer folder 53 | end 54 | 55 | def conversion_goal_id 56 | # you have to fill this in with data you get after running 3_with_uet_tag folder 57 | end 58 | 59 | def ad_group_id 60 | # you have to fill this in with data you get after running 5_with_campaign folder 61 | end 62 | 63 | def store 64 | ::BingAdsRubySdk::OAuth2::FsStore.new(store_filename) 65 | end 66 | 67 | def store_filename 68 | ENV.fetch('BING_STORE_FILENAME') 69 | end 70 | end 71 | end 72 | 73 | RSpec.shared_context 'use api' do 74 | let(:random) { Examples.random } 75 | let(:api) { Examples.build_api } 76 | end 77 | -------------------------------------------------------------------------------- /spec/examples/6_with_ad_group/ad_group_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../examples' 2 | 3 | RSpec.describe 'AdGroup methods' do 4 | include_context 'use api' 5 | 6 | let(:ad_group_record) do 7 | a_hash_including( 8 | ad_rotation: nil, 9 | bidding_scheme: a_kind_of(Hash), 10 | cpc_bid: a_kind_of(Hash), 11 | id: a_kind_of(String), 12 | language: a_kind_of(String), 13 | name: a_kind_of(String), 14 | network: a_kind_of(String), 15 | settings: nil, 16 | start_date: { 17 | day: '1', 18 | month: '1', 19 | year: '2049', 20 | }, 21 | end_date: { 22 | day: '1', 23 | month: '2', 24 | year: '2049', 25 | }, 26 | status: a_kind_of(String), 27 | tracking_url_template: nil, 28 | url_custom_parameters: nil, 29 | ) 30 | end 31 | 32 | describe '#get_ad_groups_by_ids' do 33 | it 'returns AdGroups' do 34 | expect(api.campaign_management.get_ad_groups_by_ids( 35 | campaign_id: Examples.campaign_id, 36 | ad_group_ids: [ { long: Examples.ad_group_id } ] 37 | )).to include(ad_group_record) 38 | end 39 | end 40 | 41 | describe '#get_ad_groups_by_campaign_id' do 42 | it 'returns AdGroups' do 43 | expect(api.campaign_management.get_ad_groups_by_campaign_id( 44 | campaign_id: Examples.campaign_id 45 | )).to include(ad_group_record) 46 | end 47 | end 48 | 49 | describe '#update_ad_groups' do 50 | it 'updates the ad' do 51 | expect(api.campaign_management.call(:update_ad_groups, 52 | campaign_id: Examples.campaign_id, 53 | ad_groups: { 54 | ad_group: [{ 55 | id: Examples.ad_group_id, 56 | name: "AcceptanceTestAdGroup - #{random}" 57 | }] 58 | } 59 | )).to eq(partial_errors: '', inherited_bid_strategy_types: nil) 60 | 61 | ad_group = api.campaign_management.get_ad_groups_by_ids( 62 | campaign_id: Examples.campaign_id, 63 | ad_group_ids: [ { long: Examples.ad_group_id } ] 64 | ).first 65 | 66 | expect(ad_group).to include( 67 | name: "AcceptanceTestAdGroup - #{random}" 68 | ) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/services/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bing_ads_ruby_sdk/preprocessors/camelize' 4 | require 'bing_ads_ruby_sdk/preprocessors/order' 5 | require 'bing_ads_ruby_sdk/postprocessors/snakize' 6 | require 'bing_ads_ruby_sdk/postprocessors/cast_long_arrays' 7 | 8 | 9 | module BingAdsRubySdk 10 | module Services 11 | class Base 12 | 13 | def initialize(soap_client) 14 | @soap_client = soap_client 15 | end 16 | 17 | def call(operation_name, message = {}) 18 | camelized_name = BingAdsRubySdk::StringUtils.camelize(operation_name.to_s) 19 | response = soap_client.call( 20 | camelized_name, 21 | preprocess(message, camelized_name), 22 | ) 23 | postprocess(response) 24 | end 25 | 26 | def self.service 27 | raise 'implement me' 28 | end 29 | 30 | private 31 | 32 | attr_reader :soap_client 33 | 34 | def call_wrapper(action, message, *response_nesting) 35 | response = call(action, message) 36 | wrap_array(dig_response(response, response_nesting)) 37 | end 38 | 39 | def preprocess(message, operation_name) 40 | order( 41 | soap_client.wsdl_wrapper(operation_name), 42 | camelize(message) 43 | ) 44 | end 45 | 46 | def postprocess(message) 47 | cast_long_arrays( 48 | snakize(message) 49 | ) 50 | end 51 | 52 | def order(wrapper, hash) 53 | ::BingAdsRubySdk::Preprocessors::Order.new(wrapper, hash).call 54 | end 55 | 56 | def camelize(hash) 57 | ::BingAdsRubySdk::Preprocessors::Camelize.new(hash).call 58 | end 59 | 60 | def snakize(hash) 61 | ::BingAdsRubySdk::Postprocessors::Snakize.new(hash).call 62 | end 63 | 64 | def cast_long_arrays(hash) 65 | ::BingAdsRubySdk::Postprocessors::CastLongArrays.new(hash).call 66 | end 67 | 68 | def dig_response(response, keys) 69 | response.dig(*keys) 70 | rescue StandardError => e 71 | nil 72 | end 73 | 74 | def wrap_array(arg) 75 | case arg 76 | when Array then arg 77 | when nil, "" then [] 78 | else [arg] 79 | end 80 | end 81 | end 82 | end 83 | end -------------------------------------------------------------------------------- /spec/fixtures/customer_management/get_account/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17caa13f-9073-43f9-83fd-347bc3f44a26 5 | 6 | 7 | 8 | 9 | 168403565 10 | EUR 11 | ClearFinancialStatus 12 | 5678 13 | English 14 | 131003009 15 | 2019-01-16T14:39:45.443 16 | SDKTEST 17 | F1194A36 18 | 9876 19 | 131001762 20 | 21 | 131003009 22 | Active 23 | AAAAAE496a4= 24 | PacificTimeUSCanadaTijuana 25 | 26 | 27 | 28 | 29 | 1234 30 | updated name 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Inactive 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/http_client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/http" 4 | require "excon" 5 | 6 | module BingAdsRubySdk 7 | class HttpClient 8 | @http_connections = {} 9 | HTTP_OPEN_TIMEOUT = 10 10 | HTTP_READ_TIMEOUT = 20 11 | HTTP_RETRY_COUNT_ON_TIMEOUT = 2 12 | HTTP_INTERVAL_RETRY_COUNT_ON_TIMEOUT = 1 13 | HTTP_ERRORS = [ Net::HTTPServerError, Net::HTTPClientError ] 14 | CONNECTION_SETTINGS = { 15 | persistent: true, 16 | tcp_nodelay: true, 17 | retry_limit: HTTP_RETRY_COUNT_ON_TIMEOUT, 18 | idempotent: true, 19 | retry_interval: HTTP_INTERVAL_RETRY_COUNT_ON_TIMEOUT, 20 | connect_timeout: HTTP_OPEN_TIMEOUT, 21 | read_timeout: HTTP_READ_TIMEOUT, 22 | ssl_version: :TLSv1_2, 23 | ciphers: "TLSv1.2:!aNULL:!eNULL", 24 | } 25 | 26 | class << self 27 | def post(request) 28 | uri = URI(request.url) 29 | conn = self.connection(request.url) 30 | raw_response = conn.post( 31 | path: uri.path, 32 | body: request.content, 33 | headers: request.headers, 34 | ) 35 | 36 | if contains_error?(raw_response) 37 | BingAdsRubySdk.log(:warn) { BingAdsRubySdk::LogMessage.new(raw_response.body).to_s } 38 | raise BingAdsRubySdk::Errors::ServerError, raw_response.body 39 | else 40 | BingAdsRubySdk.log(:debug) { BingAdsRubySdk::LogMessage.new(raw_response.body).to_s } 41 | end 42 | 43 | raw_response.body 44 | end 45 | 46 | def close_http_connections 47 | self.http_connections.values.each do |connection| 48 | connection.reset 49 | end 50 | end 51 | 52 | protected 53 | 54 | attr_reader :http_connections 55 | 56 | def contains_error?(response) 57 | HTTP_ERRORS.any? { |http_error_class| response.class <= http_error_class } 58 | end 59 | 60 | def connection_settings 61 | CONNECTION_SETTINGS.tap do |args| 62 | instrumentor = BingAdsRubySdk.config.instrumentor 63 | args[:instrumentor] = instrumentor if instrumentor 64 | end 65 | end 66 | 67 | def connection(host) 68 | self.http_connections[host] ||= Excon.new( 69 | host, 70 | connection_settings 71 | ) 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/errors/error_handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BingAdsRubySdk 4 | module Errors 5 | # Parses the response from the API to raise errors if they are returned 6 | class ErrorHandler 7 | def initialize(response) 8 | @response = response 9 | end 10 | 11 | def call 12 | # Some operations don't return a response, for example: 13 | # https://msdn.microsoft.com/en-us/library/bing-ads-customer-management-deleteaccount.aspx 14 | return unless response.is_a? Hash 15 | raise fault_class.new(response) if contains_error? 16 | end 17 | 18 | private 19 | 20 | attr_reader :response 21 | 22 | def contains_error? 23 | partial_error_keys.any? || contains_fault? 24 | end 25 | 26 | def contains_fault? 27 | (ERROR_KEYS & response.keys).any? 28 | end 29 | 30 | def fault_class 31 | ERRORS_MAPPING.fetch(hash_with_error.keys.first, BASE_FAULT) 32 | end 33 | 34 | def hash_with_error 35 | response[:detail] || partial_errors || {} 36 | end 37 | 38 | def partial_errors 39 | response.select {|key| partial_error_keys.include?(key)} 40 | end 41 | 42 | # Gets populated partial error keys from the response 43 | # @return [Array] array of symbols for keys in the response 44 | # that are populated with errors 45 | def partial_error_keys 46 | @partial_error_keys ||= (PARTIAL_ERROR_KEYS & response.keys).reject do |key| 47 | response[key].nil? || response[key].is_a?(String) 48 | end 49 | end 50 | 51 | BASE_FAULT = BingAdsRubySdk::Errors::GeneralError 52 | PARTIAL_ERROR_KEYS = %i[partial_errors nested_partial_errors].freeze 53 | ERROR_KEYS = %i[faultcode error_code] 54 | ERRORS_MAPPING = { 55 | api_fault_detail: BingAdsRubySdk::Errors::ApiFaultDetail, 56 | ad_api_fault_detail: BingAdsRubySdk::Errors::AdApiFaultDetail, 57 | editorial_api_fault_detail: BingAdsRubySdk::Errors::EditorialApiFaultDetail, 58 | api_batch_fault: BingAdsRubySdk::Errors::ApiBatchFault, 59 | api_fault: BingAdsRubySdk::Errors::ApiFault, 60 | nested_partial_errors: BingAdsRubySdk::Errors::NestedPartialError, 61 | partial_errors: BingAdsRubySdk::Errors::PartialError 62 | } 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/examples/5_with_campaign/campaign_criterions_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../examples' 2 | 3 | RSpec.describe 'CampaignCriterion methods' do 4 | include_context 'use api' 5 | 6 | def add_campaign_criterions(location_id) 7 | api.campaign_management.call(:add_campaign_criterions, 8 | campaign_criterions: [ 9 | { 10 | negative_campaign_criterion: { 11 | campaign_id: Examples.campaign_id, 12 | location_criterion: { 13 | location_id: location_id 14 | } 15 | } 16 | } 17 | ], 18 | criterion_type: 'Targets' 19 | ) 20 | end 21 | 22 | describe '#add_campaign_criterions' do 23 | it 'returns CampaignCriterion ids' do 24 | expect(add_campaign_criterions(190)).to include( 25 | campaign_criterion_ids: [a_kind_of(Integer)], 26 | nested_partial_errors: '' 27 | ) 28 | end 29 | end 30 | 31 | describe '#delete_campaign_criterions' do 32 | it 'returns no errors' do 33 | response = add_campaign_criterions(191) 34 | 35 | expect(api.campaign_management.call(:delete_campaign_criterions, 36 | campaign_criterion_ids: [ 37 | { long: response[:campaign_criterion_ids].first } 38 | ], 39 | campaign_id: Examples.campaign_id, 40 | criterion_type: 'Targets' 41 | )).to eq( 42 | partial_errors: '' 43 | ) 44 | end 45 | end 46 | 47 | describe '#get_campaign_criterions_by_ids' do 48 | it 'returns CampaignCriterions' do 49 | response = add_campaign_criterions(193) 50 | criterion_id = response[:campaign_criterion_ids].first.to_s 51 | 52 | criterions = api.campaign_management.get_campaign_criterions_by_ids( 53 | campaign_criterion_ids: [{ long: criterion_id }], 54 | campaign_id: Examples.campaign_id, 55 | criterion_type: 'Age DayTime Device Gender Location LocationIntent Radius' 56 | ) 57 | 58 | expect(criterions).to include( 59 | campaign_id: Examples.campaign_id.to_s, 60 | criterion: { 61 | type: 'LocationCriterion', 62 | display_name: a_kind_of(String), 63 | enclosed_location_ids: nil, 64 | location_id: '193', 65 | location_type: a_kind_of(String), 66 | }, 67 | forward_compatibility_map: nil, 68 | id: criterion_id, 69 | status: a_kind_of(String), 70 | type: 'NegativeCampaignCriterion' 71 | ) 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/examples/5_with_campaign/campaign_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../examples' 2 | 3 | RSpec.describe 'CampaignManagement service' do 4 | include_context 'use api' 5 | 6 | describe 'Campaign methods' do 7 | let(:a_campaign_hash) do 8 | a_hash_including( 9 | audience_ads_bid_adjustment: a_kind_of(String), 10 | bidding_scheme: a_kind_of(Hash), 11 | budget_type: a_kind_of(String), 12 | daily_budget: a_kind_of(String), 13 | forward_compatibility_map: '', 14 | id: Examples.campaign_id.to_s, 15 | name: a_string_starting_with('Acceptance Test Campaign'), 16 | status: a_kind_of(String), 17 | time_zone: a_kind_of(String), 18 | tracking_url_template: nil, 19 | url_custom_parameters: nil, 20 | campaign_type: a_kind_of(String), 21 | settings: nil, 22 | budget_id: nil, 23 | languages: nil, 24 | experiment_id: nil, 25 | final_url_suffix: nil, 26 | sub_type: nil, 27 | ) 28 | end 29 | 30 | describe '#get_campaigns_by_account_id' do 31 | it 'returns a list of campaigns' do 32 | expect(api.campaign_management.get_campaigns_by_account_id( 33 | account_id: Examples.account_id 34 | )).to include(a_campaign_hash) 35 | end 36 | end 37 | 38 | describe '#get_campaigns_by_ids' do 39 | it 'returns a list of campaigns' do 40 | expect(api.campaign_management.get_campaigns_by_ids( 41 | account_id: Examples.account_id, 42 | campaign_ids: [{ long: Examples.campaign_id }] 43 | )).to include(a_campaign_hash) 44 | end 45 | end 46 | 47 | describe '#update_campaigns' do 48 | subject do 49 | api.campaign_management.call(:update_campaigns, 50 | account_id: Examples.account_id, 51 | campaigns: { 52 | campaign: [ 53 | id: Examples.campaign_id, 54 | name: "Acceptance Test Campaign - #{random}" 55 | ] 56 | } 57 | ) 58 | end 59 | 60 | it 'returns no errors' do 61 | is_expected.to eq(partial_errors: '') 62 | updated_campaign = api.campaign_management.get_campaigns_by_ids( 63 | account_id: Examples.account_id, 64 | campaign_ids: [{ long: Examples.campaign_id }] 65 | ).first 66 | 67 | expect(updated_campaign).to include(name: "Acceptance Test Campaign - #{random}") 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/get_ad_extensions_associations/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | d82a1bef-f6ca-408d-92a3-24b310561cb1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 8177650858590 16 | 17 | Active 18 | CalloutAdExtension 19 | 1 20 | Informations Et Contact 21 | 22 | Campaign 23 | Active 24 | 349704437 25 | 26 | 27 | 28 | 29 | 30 | 8177660966942 31 | 32 | Active 33 | CalloutAdExtension 34 | 1 35 | CalloutText 36 | 37 | Campaign 38 | Active 39 | 349704437 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/wsdl/wsdl_source.txt: -------------------------------------------------------------------------------- 1 | For the records and future maintenance, here are the urls where we got the wsdl files: 2 | 3 | PRODUCTION: 4 | ad_insight: https://adinsight.api.bingads.microsoft.com/Api/Advertiser/AdInsight/v13/AdInsightService.svc?singleWsdl 5 | bulk: https://bulk.api.bingads.microsoft.com/Api/Advertiser/CampaignManagement/v13/BulkService.svc?singleWsdl 6 | campaign_management: https://campaign.api.bingads.microsoft.com/Api/Advertiser/CampaignManagement/v13/CampaignManagementService.svc?singleWsdl 7 | customer_billing: https://clientcenter.api.bingads.microsoft.com/Api/Billing/v13/CustomerBillingService.svc?singleWsdl 8 | customer_management: https://clientcenter.api.bingads.microsoft.com/Api/CustomerManagement/v13/CustomerManagementService.svc?singleWsdl 9 | reporting: https://reporting.api.bingads.microsoft.com/Api/Advertiser/Reporting/v13/ReportingService.svc?singleWsdl 10 | 11 | SANDBOX: 12 | ad_insight: https://adinsight.api.sandbox.bingads.microsoft.com/Api/Advertiser/AdInsight/v13/AdInsightService.svc?singleWsdl 13 | bulk: https://bulk.api.sandbox.bingads.microsoft.com/Api/Advertiser/CampaignManagement/v13/BulkService.svc?singleWsdl 14 | campaign_management: https://campaign.api.sandbox.bingads.microsoft.com/Api/Advertiser/CampaignManagement/v13/CampaignManagementService.svc?singleWsdl 15 | customer_billing: https://clientcenter.api.sandbox.bingads.microsoft.com/Api/Billing/v13/CustomerBillingService.svc?singleWsdl 16 | customer_management: https://clientcenter.api.sandbox.bingads.microsoft.com/Api/CustomerManagement/v13/CustomerManagementService.svc?singleWsdl 17 | reporting: https://reporting.api.sandbox.bingads.microsoft.com/Api/Advertiser/Reporting/v13/ReportingService.svc?singleWsdl 18 | 19 | TEST: 20 | ad_insight: https://adinsight.api.bingads.microsoft.com/Api/Advertiser/AdInsight/v13/AdInsightService.svc?singleWsdl 21 | bulk: https://bulk.api.bingads.microsoft.com/Api/Advertiser/CampaignManagement/v13/BulkService.svc?singleWsdl 22 | campaign_management: https://campaign.api.bingads.microsoft.com/Api/Advertiser/CampaignManagement/v13/CampaignManagementService.svc?singleWsdl 23 | customer_billing: https://clientcenter.api.bingads.microsoft.com/Api/Billing/v13/CustomerBillingService.svc?singleWsdl 24 | customer_management: https://clientcenter.api.bingads.microsoft.com/Api/CustomerManagement/v13/CustomerManagementService.svc?singleWsdl 25 | reporting: https://reporting.api.bingads.microsoft.com/Api/Advertiser/Reporting/v13/ReportingService.svc?singleWsdl 26 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bing_ads_ruby_sdk/header" 4 | require "bing_ads_ruby_sdk/soap_client" 5 | require "bing_ads_ruby_sdk/services/base" 6 | require "bing_ads_ruby_sdk/services/ad_insight" 7 | require "bing_ads_ruby_sdk/services/bulk" 8 | require "bing_ads_ruby_sdk/services/campaign_management" 9 | require "bing_ads_ruby_sdk/services/customer_billing" 10 | require "bing_ads_ruby_sdk/services/customer_management" 11 | require "bing_ads_ruby_sdk/services/reporting" 12 | require "bing_ads_ruby_sdk/oauth2/authorization_handler" 13 | require "bing_ads_ruby_sdk/errors/errors" 14 | require "bing_ads_ruby_sdk/errors/error_handler" 15 | 16 | module BingAdsRubySdk 17 | class Api 18 | attr_reader :header 19 | 20 | # @param version [Symbol] API version, used to choose WSDL configuration version 21 | # @param environment [Symbol] 22 | # @option environment [Symbol] :production Use the production WSDL configuration 23 | # @option environment [Symbol] :sandbox Use the sandbox WSDL configuration 24 | # @param developer_token 25 | # @param client_id 26 | def initialize(version: DEFAULT_SDK_VERSION, 27 | environment: :production, 28 | developer_token:, 29 | client_id:, 30 | oauth_store:) 31 | @version = version 32 | @environment = environment 33 | @header = Header.new( 34 | developer_token: developer_token, 35 | client_id: client_id, 36 | store: oauth_store 37 | ) 38 | end 39 | 40 | def ad_insight 41 | build_service(BingAdsRubySdk::Services::AdInsight) 42 | end 43 | 44 | def bulk 45 | build_service(BingAdsRubySdk::Services::Bulk) 46 | end 47 | 48 | def campaign_management 49 | build_service(BingAdsRubySdk::Services::CampaignManagement) 50 | end 51 | 52 | def customer_billing 53 | build_service(BingAdsRubySdk::Services::CustomerBilling) 54 | end 55 | 56 | def customer_management 57 | build_service(BingAdsRubySdk::Services::CustomerManagement) 58 | end 59 | 60 | def reporting 61 | build_service(BingAdsRubySdk::Services::Reporting) 62 | end 63 | 64 | def set_customer(account_id:, customer_id:) 65 | header.set_customer(account_id: account_id, customer_id: customer_id) 66 | end 67 | 68 | private 69 | 70 | def build_service(klass) 71 | klass.new( 72 | BingAdsRubySdk::SoapClient.new( 73 | version: @version, 74 | environment: @environment, 75 | header: header, 76 | service_name: klass.service 77 | ) 78 | ) 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/examples/4_with_conversion_goal/conversion_goals_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../examples' 2 | 3 | RSpec.describe 'Conversion goals methods' do 4 | include_context 'use api' 5 | 6 | let(:a_conversion_goal) do 7 | { 8 | conversion_window_in_minutes: a_kind_of(String), 9 | count_type: a_kind_of(String), 10 | id: a_kind_of(String), 11 | name: a_string_starting_with('Acceptance Test Conversion goal'), 12 | revenue: a_kind_of(Hash), 13 | scope: a_kind_of(String), 14 | status: a_kind_of(String), 15 | tag_id: Examples.uet_tag_id.to_s, 16 | tracking_status: a_kind_of(String), 17 | type: 'Event', 18 | action_expression: a_kind_of(String), 19 | action_operator: a_kind_of(String), 20 | category_expression: a_kind_of(String), 21 | category_operator: a_kind_of(String), 22 | label_expression: a_kind_of(String), 23 | label_operator: a_kind_of(String), 24 | value: a_kind_of(String), 25 | value_operator: a_kind_of(String), 26 | exclude_from_bidding: nil 27 | } 28 | end 29 | 30 | describe '#get_conversion_goals_by_ids' do 31 | it 'returns a list of conversion goals' do 32 | expect(api.campaign_management.get_conversion_goals_by_ids( 33 | conversion_goal_types: 'Event', 34 | conversion_goal_ids: [{ long: Examples.conversion_goal_id }] 35 | )).to contain_exactly(a_conversion_goal) 36 | end 37 | end 38 | 39 | describe '#get_conversion_goals_by_tag_ids' do 40 | it 'returns a list of conversion_goals' do 41 | expect(api.campaign_management.call(:get_conversion_goals_by_tag_ids, { 42 | conversion_goal_types: 'Event', 43 | tag_ids: [long: Examples.uet_tag_id] 44 | })).to include( 45 | conversion_goals: { 46 | conversion_goal: a_collection_including(a_conversion_goal) 47 | }, 48 | partial_errors: "" 49 | ) 50 | end 51 | end 52 | 53 | describe '#update_conversion_goals' do 54 | it 'updates the conversion goals' do 55 | expect( 56 | api.campaign_management.update_conversion_goals( 57 | conversion_goals: { 58 | event_goal: { 59 | id: Examples.conversion_goal_id, 60 | name: "Acceptance Test Conversion goal #{random}", 61 | } 62 | }) 63 | ).to eq(partial_errors: '') 64 | 65 | updated_conversion = api.campaign_management.get_conversion_goals_by_ids( 66 | conversion_goal_types: 'Event', 67 | conversion_goal_ids: [{ long: Examples.conversion_goal_id }] 68 | ).first 69 | 70 | expect(updated_conversion).to include( 71 | name:"Acceptance Test Conversion goal #{random}" 72 | ) 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/get_conversion_goals_by_ids/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 791f173b-cb9b-4b9e-9a1d-c08c60ccf3b5 5 | 6 | 7 | 8 | 9 | 10 | 43200 11 | Unique 12 | 46068449 13 | updated sdk test 14 | 15 | 16 | NoValue 17 | 18 | 19 | Customer 20 | Active 21 | 96031109 22 | TagUnverified 23 | Event 24 | contact_form 25 | Equals 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 43200 35 | Unique 36 | 46068448 37 | random 38 | 39 | 40 | NoValue 41 | 42 | 43 | Customer 44 | Active 45 | 96031109 46 | TagUnverified 47 | Event 48 | contact_form 49 | Equals 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/augmented_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BingAdsRubySdk 4 | class AugmentedParser 5 | 6 | def initialize(wsdl_file_path) 7 | @lolsoap_parser = LolSoap::WSDLParser.parse(File.read(wsdl_file_path)) 8 | @concrete_abstract_mapping = {} 9 | end 10 | 11 | def call 12 | add_subtypes_to_definitions 13 | 14 | [lolsoap_parser, concrete_abstract_mapping] 15 | end 16 | 17 | private 18 | 19 | attr_reader :lolsoap_parser, :concrete_abstract_mapping 20 | 21 | # adds subtypes to existing definitions. 22 | # for instance, the wsdl specifies AdExtensionAssociation are accepted for AddAdExtension 23 | # but there is no way to specify the type we want 24 | # the goal is to: 25 | # - validate properly the attributes 26 | # - ensure the attributes are properly formatted when xml is created 27 | # - ensure we inject proper type to the xml 28 | def add_subtypes_to_definitions 29 | # to augment all types definitions 30 | lolsoap_parser.types.each_value do |content| 31 | add_subtypes(content[:elements]) 32 | end 33 | # we have to augment operations because some Requests are abstract, for instance: 34 | # ReportRequest which can be AccountPerformanceReportRequest etc... 35 | lolsoap_parser.operations.each_value do |content| 36 | content[:input][:body].each do |full_name| 37 | add_subtypes(lolsoap_parser.elements[full_name][:type][:elements]) 38 | end 39 | end 40 | @grouped_subtypes = nil # we can reset this as its not needed anymore 41 | end 42 | 43 | def add_subtypes(content) 44 | content.keys.each do |base| 45 | grouped_subtypes.fetch(base, []).each do |sub_type| 46 | elem = lolsoap_parser.elements[sub_type.id] 47 | elem[:base_type_name] = base 48 | content[sub_type.name] = elem 49 | end 50 | end 51 | end 52 | 53 | def grouped_subtypes 54 | @grouped_subtypes ||= begin 55 | grouped_types = {} 56 | # types are defined there: https://github.com/loco2/lolsoap/blob/master/lib/lolsoap/wsdl_parser.rb#L305 57 | lolsoap_parser.each_node('xs:complexType[not(@abstract="true")]') do |node, schema| 58 | type = ::LolSoap::WSDLParser::Type.new(lolsoap_parser, schema, node) 59 | if type.base_type # it has a base_type, its a subtype 60 | base_type = extract_base_type(type.base_type) 61 | concrete_abstract_mapping[type.name] = base_type.name 62 | grouped_types[base_type.name] ||= [] 63 | grouped_types[base_type.name].push(type) 64 | end 65 | end 66 | grouped_types 67 | end 68 | end 69 | 70 | # we want the real base: sometimes there are many layers of inheritance 71 | def extract_base_type(type) 72 | if type.base_type 73 | extract_base_type(type.base_type) 74 | else 75 | type 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/bing_ads_ruby_sdk/services/customer_management_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe BingAdsRubySdk::Services::CustomerManagement do 2 | 3 | let(:service_name) { described_class.service } 4 | let(:soap_client) { SpecHelpers.soap_client(service_name) } 5 | let(:expected_xml) { SpecHelpers.request_xml_for(service_name, action, filename) } 6 | let(:mocked_response) { SpecHelpers.response_xml_for(service_name, action, filename) } 7 | 8 | let(:service) { described_class.new(soap_client) } 9 | 10 | before do 11 | expect(BingAdsRubySdk::HttpClient).to receive(:post) do |req| 12 | expect(Nokogiri::XML(req.content).to_xml).to eq expected_xml.to_xml 13 | mocked_response 14 | end 15 | end 16 | 17 | describe "signup_customer" do 18 | let(:action) { 'signup_customer' } 19 | let(:filename) { 'standard' } 20 | 21 | it "returns expected result" do 22 | expect( 23 | service.signup_customer( 24 | parent_customer_id: 9876, 25 | customer: { 26 | industry: "NA", 27 | market_country: 'FR', 28 | market_language: 'French', 29 | name: 'sdk customer', 30 | customer_address: { 31 | city: "Nice", 32 | country_code: "FR", 33 | line1: "127 bd risso", 34 | postal_code: "06000" 35 | } 36 | }, 37 | # Note that the structure of this type is slightly different to other types, in accord with the Bing WSDL 38 | account: { 39 | '@type' => 'AdvertiserAccount', 40 | currency_code: "EUR", 41 | name: 'SDK account' 42 | } 43 | ) 44 | ).to include( 45 | customer_id: "1234", 46 | account_id: "5678" 47 | ) 48 | end 49 | end 50 | 51 | describe "get_account" do 52 | let(:action) { 'get_account' } 53 | let(:filename) { 'standard' } 54 | 55 | it "returns expected result" do 56 | expect( 57 | service.get_account(account_id: 5678) 58 | ).to include( 59 | account: a_hash_including( id: "5678", name: "SDKTEST") 60 | ) 61 | end 62 | end 63 | 64 | describe "update_account" do 65 | let(:action) { 'update_account' } 66 | let(:filename) { 'standard' } 67 | 68 | it "returns expected result" do 69 | expect( 70 | service.update_account( 71 | account: { 72 | '@type' => 'AdvertiserAccount', 73 | id: 5678, 74 | time_stamp: "AAAAAE496a4=", 75 | currency_code: "EUR", 76 | name: "SDKTEST updated" 77 | } 78 | ) 79 | ).to eq( 80 | last_modified_time: "2019-01-18T13:16:38.827" 81 | ) 82 | end 83 | end 84 | 85 | describe "find_accounts_or_customers_info" do 86 | let(:action) { 'find_accounts_or_customers_info' } 87 | let(:filename) { 'standard' } 88 | 89 | it "returns expected result" do 90 | expect( 91 | service.find_accounts_or_customers_info(filter: 'SDKTEST', top_n: 1) 92 | ).to contain_exactly( 93 | a_hash_including(customer_name: "SDKTEST updated", account_id: "5678") 94 | ) 95 | end 96 | end 97 | end -------------------------------------------------------------------------------- /spec/bing_ads_ruby_sdk/preprocessors/order_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe BingAdsRubySdk::Preprocessors::Order do 4 | 5 | def action 6 | new_params = described_class.new(wrapper, unordered_params).call 7 | expect(new_params.to_json).to eq(ordered_params.to_json) 8 | end 9 | 10 | context "nested hashes" do 11 | let(:wrapper) do 12 | SpecHelpers.wrapper(:customer_management, "SignupCustomer") 13 | end 14 | 15 | let(:unordered_params) {{ 16 | "Account" => { 17 | "Name" => "test account", 18 | "CurrencyCode" => "EUR", 19 | "ParentCustomerId" => "1234" 20 | }, 21 | "Customer" => { 22 | "CustomerAddress" => "address", 23 | "Industry" => "industry", 24 | "MarketCountry" => "country" 25 | }, 26 | "ParentCustomerId" => "1234" 27 | }} 28 | 29 | let(:ordered_params) {{ 30 | "Customer" => { 31 | "Industry" => "industry", 32 | "MarketCountry" => "country", 33 | "CustomerAddress" => "address" 34 | }, 35 | "Account" => { 36 | "CurrencyCode" => "EUR", 37 | "Name" => "test account", 38 | "ParentCustomerId" => "1234" 39 | }, 40 | "ParentCustomerId" => "1234" 41 | }} 42 | 43 | it("orders") { action } 44 | end 45 | 46 | context "arrays" do 47 | let(:wrapper) do 48 | SpecHelpers.wrapper(:campaign_management, "UpdateUetTags") 49 | end 50 | 51 | let(:unordered_params) {{ 52 | "UetTags" => [ 53 | { 54 | "UetTag" => { 55 | "Name" => 'mofo2', 56 | "Description" => nil, 57 | "Id" => '26034398' 58 | } 59 | } 60 | ] 61 | }} 62 | 63 | let(:ordered_params) {{ 64 | "UetTags" => [ 65 | { 66 | "UetTag" => { 67 | "Description" => nil, 68 | "Id" => '26034398', 69 | "Name" => 'mofo2' 70 | } 71 | } 72 | ] 73 | }} 74 | 75 | it("orders") { action } 76 | end 77 | 78 | context "abstract types" do 79 | let(:wrapper) do 80 | SpecHelpers.wrapper(:campaign_management, "AddConversionGoals") 81 | end 82 | 83 | let(:unordered_params) {{ 84 | "ConversionGoals" => [ 85 | { 86 | "EventGoal" => { 87 | "ActionExpression" => 'contact_form', 88 | "ActionOperator" => 'Equals', 89 | "ConversionWindowInMinutes" => 43200, 90 | "CountType" => "Unique", 91 | "Name" => "contact_form", 92 | "Revenue" => { "Type" => "NoValue" }, 93 | "Type" => "Event", 94 | "TagId" => 26003317 95 | } 96 | } 97 | ] 98 | }} 99 | 100 | let(:ordered_params) {{ 101 | "ConversionGoals" => [ 102 | { 103 | "EventGoal" => { 104 | "ConversionWindowInMinutes" => 43200, 105 | "CountType" => "Unique", 106 | "Name" => "contact_form", 107 | "Revenue" => { "Type" => "NoValue" }, 108 | "TagId" => 26003317, 109 | "Type" => "Event", 110 | "ActionExpression" => 'contact_form', 111 | "ActionOperator" => 'Equals', 112 | } 113 | } 114 | ] 115 | }} 116 | 117 | it("orders") { action } 118 | end 119 | end -------------------------------------------------------------------------------- /spec/examples/6_with_ad_group/keywords_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../examples' 2 | 3 | RSpec.describe 'Keyword methods' do 4 | include_context 'use api' 5 | 6 | describe 'Keyword methods' do 7 | let(:a_keyword) do 8 | { 9 | bid: a_kind_of(Hash), 10 | bidding_scheme: a_kind_of(Hash), 11 | destination_url: a_kind_of(String), 12 | editorial_status: a_kind_of(String), 13 | final_app_urls: nil, 14 | final_mobile_urls: nil, 15 | final_url_suffix: nil, 16 | final_urls: nil, 17 | forward_compatibility_map: a_kind_of(String), 18 | id: a_kind_of(String), 19 | match_type: a_kind_of(String), 20 | param1: a_kind_of(String), 21 | param2: a_kind_of(String), 22 | param3: a_kind_of(String), 23 | status: a_kind_of(String), 24 | text: a_kind_of(String), 25 | tracking_url_template: nil, 26 | url_custom_parameters: nil, 27 | } 28 | end 29 | 30 | let(:keyword_id) { add_keywords[:keyword_ids].first } 31 | let(:add_keywords) do 32 | api.campaign_management.call(:add_keywords, 33 | ad_group_id: Examples.ad_group_id, 34 | keywords: { keyword: { 35 | bid: { amount: 0.05 }, 36 | match_type: 'Exact', 37 | text: "AcceptanceTestKeyword - #{random}", 38 | } } 39 | ) 40 | end 41 | 42 | describe '#add_keywords' do 43 | it 'returns created Keyword ids' do 44 | expect(add_keywords).to include( 45 | keyword_ids: [a_kind_of(Integer)], 46 | partial_errors: '' 47 | ) 48 | end 49 | end 50 | 51 | describe '#get_keywords_by_ad_group_id' do 52 | before { add_keywords } 53 | 54 | it 'returns a list of keywords' do 55 | expect(api.campaign_management.get_keywords_by_ad_group_id( 56 | ad_group_id: Examples.ad_group_id 57 | )).to include(a_keyword) 58 | end 59 | end 60 | 61 | describe '#get_keywords_by_editorial_status' do 62 | before { add_keywords } 63 | 64 | it 'returns a list of Keywords' do 65 | expect(api.campaign_management.get_keywords_by_editorial_status( 66 | ad_group_id: Examples.ad_group_id, 67 | editorial_status: 'Active' 68 | )).to include(a_keyword) 69 | end 70 | end 71 | 72 | describe '#get_keywords_by_ids' do 73 | before { add_keywords } 74 | 75 | it 'returns a list of Keywords' do 76 | expect(api.campaign_management.get_keywords_by_ids( 77 | ad_group_id: Examples.ad_group_id, 78 | keyword_ids: [{ long: keyword_id }] 79 | )).to include(a_keyword) 80 | end 81 | end 82 | 83 | describe '#update_keywords' do 84 | before { add_keywords } 85 | 86 | it 'updates the keyword' do 87 | expect(api.campaign_management.call(:update_keywords, 88 | ad_group_id: Examples.ad_group_id, 89 | keywords: { keyword: [ 90 | id: keyword_id, 91 | bid: { amount: 0.50 }, 92 | ] } 93 | )).to include(partial_errors: '') 94 | end 95 | end 96 | 97 | describe '#delete_keywords' do 98 | before { add_keywords } 99 | 100 | it 'returns no errors' do 101 | expect(api.campaign_management.call(:delete_keywords, 102 | ad_group_id: Examples.ad_group_id, 103 | keyword_ids: [{ long: keyword_id }] 104 | )).to eq(partial_errors: '') 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /spec/examples/6_with_ad_group/ads_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../examples' 2 | 3 | RSpec.describe 'Ad methods' do 4 | include_context 'use api' 5 | 6 | def add_ads 7 | api.campaign_management.call(:add_ads, 8 | ad_group_id: Examples.ad_group_id, 9 | ads: [ 10 | { 11 | expanded_text_ad: { 12 | ad_format_preference: 'All', 13 | domain: 'https://www.example.com/', 14 | final_urls: [string: 'http://www.contoso.com/'], 15 | path_1: 'subdirectory1', 16 | path_2: 'su§bdirectory2', 17 | text: 'Ad text goes here', 18 | title_part_1: 'Title goes here', 19 | title_part_2: 'Title 2 goes here', 20 | status: 'Paused', 21 | tracking_url_template: '{lpurl}', 22 | }, 23 | }, 24 | ] 25 | ) 26 | end 27 | 28 | def get_ads 29 | api.campaign_management.get_ads_by_ad_group_id( 30 | ad_group_id: Examples.ad_group_id, 31 | ad_types: [ 32 | { ad_type: 'Text' }, 33 | { ad_type: 'Image' }, 34 | { ad_type: 'Product' }, 35 | { ad_type: 'AppInstall' }, 36 | { ad_type: 'ExpandedText' }, 37 | { ad_type: 'DynamicSearch' }, 38 | ] 39 | ) 40 | end 41 | 42 | describe '#add_ads' do 43 | it 'returns created Ad ids' do 44 | expect(add_ads).to include( 45 | ad_ids: [a_kind_of(Integer)], 46 | partial_errors: '' 47 | ) 48 | end 49 | end 50 | 51 | describe '#get_ads_by_ad_group_id' do 52 | before { add_ads } 53 | 54 | it 'returns a list of ads' do 55 | expect(get_ads).to include( 56 | { 57 | ad_format_preference: a_kind_of(String), 58 | device_preference: a_kind_of(String), 59 | editorial_status: a_kind_of(String), 60 | final_app_urls: nil, 61 | final_mobile_urls: nil, 62 | final_urls: a_kind_of(Hash), 63 | final_url_suffix: nil, 64 | forward_compatibility_map: "", 65 | id: a_kind_of(String), 66 | status: a_kind_of(String), 67 | tracking_url_template: a_kind_of(String), 68 | type: "ExpandedText", 69 | url_custom_parameters: nil, 70 | domain: a_kind_of(String), 71 | path1: a_kind_of(String), 72 | path2: a_kind_of(String), 73 | text: a_kind_of(String), 74 | text_part2: '', 75 | title_part1: a_kind_of(String), 76 | title_part2: a_kind_of(String), 77 | title_part3: '' 78 | } 79 | ) 80 | end 81 | end 82 | 83 | describe '#update_ads' do 84 | before { add_ads } 85 | 86 | it 'updates the Ad' do 87 | expect(api.campaign_management.call(:update_ads, 88 | ad_group_id: Examples.ad_group_id, 89 | ads: { 90 | expanded_text_ad: [{ 91 | id: get_ads.first[:id], 92 | text: "Ad text goes here - #{random}" 93 | }], 94 | } 95 | )).to eq(partial_errors: '') 96 | 97 | expect(get_ads.first).to include( text: "Ad text goes here - #{random}") 98 | end 99 | end 100 | 101 | describe 'test_delete_ads' do 102 | let(:ad_id) { add_ads[:ad_ids].first } 103 | 104 | it 'returns no errors' do 105 | expect(api.campaign_management.call(:delete_ads, 106 | ad_group_id: Examples.ad_group_id, 107 | ad_ids: [long: ad_id] 108 | )).to eq(partial_errors: '') 109 | 110 | expect(get_ads.map{|h| h[:id]}).not_to include ad_id.to_s 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/oauth2/authorization_handler.rb: -------------------------------------------------------------------------------- 1 | require 'signet/oauth_2/client' 2 | require 'bing_ads_ruby_sdk/oauth2/fs_store' 3 | 4 | module BingAdsRubySdk 5 | module OAuth2 6 | # Adds some useful methods to Signet::OAuth2::Client 7 | class AuthorizationHandler 8 | 9 | # @param developer_token 10 | # @param client_id 11 | # @param store [Store] 12 | def initialize(developer_token:, client_id:, store:) 13 | @client = build_client(developer_token, client_id) 14 | @store = store 15 | refresh_from_store 16 | end 17 | 18 | # @return [String] unless client.client_id url is nil interpolated url. 19 | # @return [nil] if client.client_id is nil. 20 | def code_url 21 | return nil if client.client_id.nil? 22 | "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=#{client.client_id}&"\ 23 | "scope=offline_access+https://ads.microsoft.com/ads.manage&response_type=code&"\ 24 | "redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient" 25 | end 26 | 27 | # Once you have completed the oauth process in your browser using the code_url 28 | # copy the url your browser has been redirected to and use it as argument here 29 | def fetch_from_url(url) 30 | codes = extract_codes(url) 31 | 32 | return false if codes.none? 33 | fetch_from_code(codes.last) 34 | rescue Signet::AuthorizationError, URI::InvalidURIError 35 | false 36 | end 37 | 38 | # Get or fetch an access token. 39 | # @return [String] The access token. 40 | def fetch_or_refresh 41 | if client.expired? 42 | client.refresh! 43 | store.write(token_data) 44 | end 45 | client.access_token 46 | end 47 | 48 | private 49 | 50 | attr_reader :client, :store 51 | 52 | # Refresh existing authorization token 53 | # @return [Signet::OAuth2::Client] if everything went well. 54 | # @return [nil] if the token can't be read from the store. 55 | def refresh_from_store 56 | ext_token = store.read 57 | client.update_token!(ext_token) if ext_token 58 | end 59 | 60 | # Request the Api to exchange the code for the access token. 61 | # Save the access token through the store. 62 | # @param [String] code authorization code from bing's ads. 63 | # @return [#store.write] store's write output. 64 | def fetch_from_code(code) 65 | client.code = code 66 | client.fetch_access_token! 67 | store.write(token_data) 68 | end 69 | 70 | def extract_codes(url) 71 | url = URI.parse(url) 72 | query_params = URI.decode_www_form(url.query) 73 | query_params.find { |arg| arg.first.casecmp("CODE").zero? } 74 | end 75 | 76 | def build_client(developer_token, client_id) 77 | Signet::OAuth2::Client.new({ 78 | authorization_uri: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', 79 | token_credential_uri: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', 80 | redirect_uri: 'https://login.microsoftonline.com/common/oauth2/nativeclient', 81 | developer_token: developer_token, 82 | client_id: client_id, 83 | scope: 'offline_access' 84 | }) 85 | end 86 | 87 | def token_data 88 | { 89 | access_token: client.access_token, 90 | refresh_token: client.refresh_token, 91 | issued_at: client.issued_at, 92 | expires_in: client.expires_in 93 | } 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/services/campaign_management.rb: -------------------------------------------------------------------------------- 1 | module BingAdsRubySdk 2 | module Services 3 | class CampaignManagement < Base 4 | 5 | 6 | def add_ad_extensions(message) 7 | call(__method__, message) 8 | end 9 | 10 | def add_conversion_goals(message) 11 | call(__method__, message) 12 | end 13 | 14 | def add_shared_entity(message) 15 | call(__method__, message) 16 | end 17 | 18 | def add_uet_tags(message) 19 | call(__method__, message) 20 | end 21 | 22 | def set_ad_extensions_associations(message) 23 | call(__method__, message) 24 | end 25 | 26 | 27 | def update_conversion_goals(message) 28 | call(__method__, message) 29 | end 30 | 31 | def update_uet_tags(message) 32 | call(__method__, message) 33 | end 34 | 35 | 36 | def get_ad_extensions_associations(message) 37 | wrap_array( 38 | call(__method__, message) 39 | .dig(:ad_extension_association_collection, :ad_extension_association_collection) 40 | .first 41 | .dig(:ad_extension_associations, :ad_extension_association) 42 | ) 43 | rescue 44 | [] 45 | end 46 | 47 | def get_ad_extension_ids_by_account_id(message) 48 | call_wrapper(__method__, message, :ad_extension_ids) 49 | end 50 | 51 | def get_ad_extensions_by_ids(message) 52 | call_wrapper(__method__, message, :ad_extensions, :ad_extension) 53 | end 54 | 55 | def get_ad_groups_by_ids(message) 56 | call_wrapper(__method__, message, :ad_groups, :ad_group) 57 | end 58 | 59 | def get_ad_groups_by_campaign_id(message) 60 | call_wrapper(__method__, message, :ad_groups, :ad_group) 61 | end 62 | 63 | def get_ads_by_ad_group_id(message) 64 | call_wrapper(__method__, message, :ads, :ad) 65 | end 66 | 67 | def get_budgets_by_ids(message= {}) 68 | call_wrapper(__method__, message, :budgets, :budget) 69 | end 70 | 71 | def get_campaigns_by_account_id(message) 72 | call_wrapper(__method__, message, :campaigns, :campaign) 73 | end 74 | 75 | def get_campaigns_by_ids(message) 76 | call_wrapper(__method__, message, :campaigns, :campaign) 77 | end 78 | 79 | def get_campaign_criterions_by_ids(message) 80 | call_wrapper(__method__, message, :campaign_criterions, :campaign_criterion) 81 | end 82 | 83 | def get_conversion_goals_by_ids(message) 84 | call_wrapper(__method__, message, :conversion_goals, :conversion_goal) 85 | end 86 | 87 | def get_keywords_by_ad_group_id(message) 88 | call_wrapper(__method__, message, :keywords, :keyword) 89 | end 90 | 91 | def get_keywords_by_editorial_status(message) 92 | call_wrapper(__method__, message, :keywords, :keyword) 93 | end 94 | 95 | def get_keywords_by_ids(message) 96 | call_wrapper(__method__, message, :keywords, :keyword) 97 | end 98 | 99 | def get_shared_entities_by_account_id(message) 100 | call_wrapper(__method__, message, :shared_entities, :shared_entity) 101 | end 102 | 103 | def get_uet_tags_by_ids(message = {}) 104 | call_wrapper(__method__, message, :uet_tags, :uet_tag) 105 | end 106 | 107 | def get_shared_entity_associations_by_entity_ids(message) 108 | call_wrapper(__method__, message, :associations, :shared_entity_association) 109 | end 110 | 111 | def self.service 112 | :campaign_management 113 | end 114 | end 115 | end 116 | end -------------------------------------------------------------------------------- /spec/fixtures/campaign_management/get_campaigns_by_account_id/standard_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | a5709565-28f6-4322-9d9b-08ae6bad962c 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | MaxClicks 13 | 14 | 15 | 16 | 17 | DailyBudgetAccelerated 18 | 0.05 19 | 20200015 - 20200015 - SN - B - Activité - Stations_Service - Geoloc - ETA 20 | 21 | 349704435 22 | 20200015 - 20200015 - SN - B - Activité - Stations_Service - Geoloc - ETA 23 | Paused 24 | 25 | BrusselsCopenhagenMadridParis 26 | 27 | 28 | Search 29 | 30 | 8177617799488 31 | 32 | 33 | 34 | 35 | 36 | MaxClicks 37 | 38 | 39 | 40 | 41 | DailyBudgetAccelerated 42 | 0.05 43 | 20200015 - 20200015 - SN - E - Produits - Stations_Service - Geoloc - ETA 44 | 45 | 349704436 46 | 20200015 - 20200015 - SN - E - Produits - Stations_Service - Geoloc - ETA 47 | Paused 48 | 49 | BrusselsCopenhagenMadridParis 50 | 51 | 52 | Search 53 | 54 | 8177617799488 55 | 56 | 57 | 58 | 59 | 60 | MaxClicks 61 | 62 | 63 | 64 | 65 | DailyBudgetAccelerated 66 | 0.05 67 | 20200015 - SN - X - Station Service #1 - Geozone_custom - 5KW - V3 - ETA 68 | 69 | 349704437 70 | 20200015 - SN - X - Station Service #1 - Geozone_custom - 5KW - V3 - ETA 71 | Paused 72 | 73 | BrusselsCopenhagenMadridParis 74 | 75 | 76 | Search 77 | 78 | 8177617799488 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BingAdsRubySdk 2 | 3 | ## Installation 4 | 5 | Add the following to your application's Gemfile: 6 | 7 | ```ruby 8 | gem 'bing_ads_ruby_sdk' 9 | ``` 10 | 11 | And then execute: 12 | 13 | $ bundle 14 | 15 | Or install it yourself as: 16 | 17 | $ gem install bing_ads_ruby_sdk 18 | 19 | ## Getting Started 20 | 21 | In order to use Bing's api you need to get your api credentials from bing. From there gem handles the oauth token generation. 22 | 23 | By default, there is only one store in the gem to store the oauth token. It's a file system based store. You can create one yourself to store credentials in a database or wherever you desire. The store class must implement `read` and `write(data)` instance methods. 24 | 25 | To get your token, run: 26 | ```ruby 27 | rake bing_token:get[my_token.json,your_dev_token,your_bing_client_id] 28 | 29 | ``` 30 | 31 | 32 | Then to use the api: 33 | ```ruby 34 | store = ::BingAdsRubySdk::OAuth2::FsStore.new('my_token.json') 35 | api = BingAdsRubySdk::Api.new( 36 | oauth_store: store, 37 | developer_token: 'your_dev_token', 38 | client_id: 'your_bing_client_id' 39 | ) 40 | api.customer_management.signup_customer(params) 41 | filter: 'name', 42 | top_n: 1 43 | ) 44 | 45 | # once you have your bing customer and account ids: 46 | api.set_customer(customer_id: customer_id, account_id: account_id ) 47 | 48 | api.campaign_management.get_campaigns_by_account_id(account_id: account_id) 49 | ``` 50 | 51 | You'll see services like `customer_management` implement some methods, but not all the ones available in the API. 52 | 53 | The methods implemented contain additional code to ease data manipulation but any endpoint can be reached using `call` on a service. 54 | 55 | ```ruby 56 | @cm.call(:find_accounts_or_customers_info, filter: 'name', top_n: 1) 57 | # => { account_info_with_customer_data: { account_info_with_customer_data: [{ customer_id: "250364751", : 58 | 59 | # VS method dedicated to extract data 60 | 61 | @cm.find_accounts_or_customers_info(filter: 'name', top_n: 1) 62 | # => [{ customer_id: "250364731" ... 63 | 64 | ``` 65 | 66 | 67 | ## Configure the gem 68 | ```ruby 69 | BingAdsRubySdk.configure do |conf| 70 | conf.log = true 71 | conf.logger.level = Logger::DEBUG 72 | conf.pretty_print_xml = true 73 | # to filter sensitive data before logging 74 | conf.filters = ["AuthenticationToken", "DeveloperToken"] 75 | 76 | # Optionally allow ActiveSupport::Notifications to be emitted by Excon. 77 | # These notifications can then be sent on to your profiling system 78 | # conf.instrumentor = ActiveSupport::Notifications 79 | end 80 | ``` 81 | 82 | ## Development 83 | You can run `bin/console` for an interactive prompt that will allow you to experiment. 84 | 85 | To release a new version, update the version number in `version.rb`, and then run 86 | `bundle exec rake release`, which will create a git tag for the version, push git 87 | commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 88 | 89 | ### Updating to a new Bing API version 90 | Bing regularly releases new versions of the API and removes support for old versions. 91 | When you want to support a new version of the API, here are some of the things that 92 | need to be changed: 93 | * Go to https://docs.microsoft.com/en-us/bingads/guides/migration-guide to see what has changed 94 | * Set the default SDK version in lib/bing_ads_ruby_sdk/version.rb 95 | 96 | ### Specs 97 | After checking out the repo, run `bin/setup` to install dependencies. Then, run 98 | `rake spec` to run unit tests. 99 | 100 | If you want to run the integration tests they are in the `spec/examples/` 101 | folders. Remember that these will create real accounts and entities in Microsoft 102 | Advertising so take care to check your account spending settings. 103 | 104 | Here's how to run the tests: 105 | * Make sure you have the token as described above 106 | * Put your Client ID, Developer Token, and Parent Customer ID in the methods 107 | with the same names in `spec/examples/examples.rb` 108 | * Run the specs in order, for example: 109 | * `bundle exec rspec spec/examples/1_...`, at the end of the spec there will be 110 | a message at the end about copying an ID into `spec/examples/examples.rb` 111 | * `bundle exec rspec spec/examples/2_...` 112 | * keep repeating until you have run all the specs in `spec/examples` 113 | 114 | ## Contributing 115 | 116 | Bug reports and pull requests are welcome on GitHub at https://github.com/Effilab/bing_ads_ruby_sdk. 117 | 118 | ## License 119 | 120 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 121 | -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/soap_client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'bing_ads_ruby_sdk/wsdl_operation_wrapper' 3 | require 'bing_ads_ruby_sdk/augmented_parser' 4 | require "bing_ads_ruby_sdk/http_client" 5 | require "bing_ads_ruby_sdk/log_message" 6 | 7 | module BingAdsRubySdk 8 | class SoapClient 9 | 10 | def initialize(service_name:, version:, environment:, header:) 11 | @header = header 12 | @lolsoap_parser, @concrete_abstract_mapping = cache(service_name) do 13 | ::BingAdsRubySdk::AugmentedParser.new( 14 | path_to_wsdl(version, environment, service_name) 15 | ).call 16 | end 17 | end 18 | 19 | def call(operation_name, message = {}) 20 | request = lolsoap_client.request(operation_name) 21 | 22 | request.header do |h| 23 | header.content.each do |k, v| 24 | h.__send__(k, v) 25 | end 26 | end 27 | request.body do |node| 28 | insert_args(message, node) 29 | end 30 | 31 | BingAdsRubySdk.log(:debug) { format_xml(request.content) } 32 | 33 | response_body = BingAdsRubySdk::HttpClient.post(request) 34 | 35 | parse_response(request, response_body) 36 | end 37 | 38 | def wsdl_wrapper(operation_name) 39 | WsdlOperationWrapper.new(lolsoap_parser, operation_name) 40 | end 41 | 42 | private 43 | 44 | attr_reader :client, :header, :concrete_abstract_mapping, :lolsoap_parser 45 | 46 | def insert_args(args, node) 47 | # if ever the current node is a subtype 48 | if base_type_name = concrete_abstract_mapping[node.__type__.name] 49 | # and add an attribute to specify the real type we want 50 | node.__attribute__( 51 | type_attribute_name, 52 | "#{node.__type__.prefix}:#{node.__node__.name}" 53 | ) 54 | # we have to change the node name to the base type 55 | node.__node__.name = base_type_name 56 | end 57 | 58 | args.each do |arg_name, arg_value| 59 | case arg_value 60 | when Hash 61 | node.__send__(arg_name) do |subnode| 62 | insert_args(arg_value, subnode) 63 | end 64 | when Array 65 | node.__send__(arg_name) do |subnode| 66 | # arrays can only contain hashes 67 | arg_value.each do |elt| 68 | insert_args(elt, subnode) 69 | end 70 | end 71 | else 72 | if arg_name == BingAdsRubySdk.type_key 73 | # this is for now only useful for Account. Indeed, for some unknown reason 74 | # Account is abstract, AdvertiserAccount is the only expect subtype 75 | # yet the wsdl doesnt declare it as an actual subtype 76 | node.__attribute__( 77 | type_attribute_name, 78 | prefixed_type_name(arg_value) 79 | ) 80 | else 81 | node.__send__(arg_name, arg_value) 82 | end 83 | end 84 | end 85 | end 86 | 87 | def parse_response(req, response_body) 88 | lolsoap_client.response(req, response_body).body_hash.tap do |b_h| 89 | BingAdsRubySdk.log(:debug) { b_h } 90 | BingAdsRubySdk::Errors::ErrorHandler.new(b_h).call 91 | end 92 | rescue BingAdsRubySdk::Errors::GeneralError => e 93 | BingAdsRubySdk.log(:warn) { format_xml(response_body) } 94 | raise e 95 | end 96 | 97 | def lolsoap_client 98 | @lolsoap ||= LolSoap::Client.new(lolsoap_wsdl).tap do |c| 99 | c.wsdl.namespaces[XSI_NAMESPACE_KEY] = XSI_NAMESPACE 100 | end 101 | end 102 | 103 | def lolsoap_wsdl 104 | @lolsoap_wsdl ||= LolSoap::WSDL.new(lolsoap_parser) 105 | end 106 | 107 | def format_xml(string) 108 | BingAdsRubySdk::LogMessage.new(string).to_s 109 | end 110 | 111 | def path_to_wsdl(version, environment, service_name) 112 | File.join( 113 | BingAdsRubySdk.root_path, 114 | 'lib', 115 | 'bing_ads_ruby_sdk', 116 | 'wsdl', 117 | version.to_s, 118 | environment.to_s, 119 | "#{service_name}.xml" 120 | ) 121 | end 122 | 123 | def prefixed_type_name(typename) 124 | WsdlOperationWrapper.prefix_and_name(lolsoap_wsdl, typename) 125 | end 126 | 127 | def type_attribute_name 128 | "#{XSI_NAMESPACE_KEY}:type" 129 | end 130 | 131 | def cache(name) 132 | self.class.cached_parsers[name] ||= yield 133 | end 134 | 135 | @cached_parsers = {} 136 | def self.cached_parsers 137 | @cached_parsers 138 | end 139 | 140 | XSI_NAMESPACE_KEY = "xsi" 141 | XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance" 142 | end 143 | end -------------------------------------------------------------------------------- /lib/bing_ads_ruby_sdk/errors/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BingAdsRubySdk 4 | module Errors 5 | # Base exception class for reporting API errors 6 | class GeneralError < ::StandardError 7 | attr_accessor :raw_response, :message 8 | 9 | def initialize(response) 10 | @raw_response = response 11 | 12 | code = response[:error_code] || 'Bing Ads API error' 13 | 14 | message = response[:message] || 15 | response[:faultstring] || 16 | 'See exception details for more information.' 17 | 18 | @message = format_message(code, message) 19 | end 20 | 21 | private 22 | 23 | # Format the message separated by hyphen if 24 | # there is a code and a message 25 | def format_message(code, message) 26 | [code, message].compact.join(' - ') 27 | end 28 | end 29 | 30 | class ServerError < GeneralError 31 | def initialize(server_error) 32 | super "Server raised error #{server_error}" 33 | end 34 | end 35 | 36 | # Base exception class for handling errors where the detail is supplied 37 | class ApplicationFault < GeneralError 38 | def initialize(response) 39 | super 40 | 41 | populate_error_lists 42 | end 43 | 44 | def message 45 | error_list = all_errors 46 | return @message if error_list.empty? 47 | 48 | first_message = first_error_message(error_list) 49 | if error_list.count > 1 50 | "API raised #{ error_list.count } errors, including: #{first_message}" 51 | else 52 | first_message 53 | end 54 | end 55 | 56 | private 57 | 58 | def populate_error_lists 59 | self.class.error_lists.each do |key| 60 | instance_variable_set("@#{key}", array_wrap(fault_hash[key])) 61 | end 62 | end 63 | 64 | def all_errors 65 | self.class.error_lists.flat_map do |list_name| 66 | list = send(list_name) 67 | 68 | # Call sometimes returns an empty string instead of 69 | # nil for empty lists 70 | list.nil? || list.empty? ? nil : list 71 | end.compact 72 | end 73 | 74 | # The fault hash from the API response detail element 75 | # @return [Hash] containing the fault information if provided 76 | # @return [Hash] empty hash if no fault information 77 | def fault_hash 78 | raw_response[:detail][fault_key] || {} 79 | end 80 | 81 | # The fault key that corresponds to the inherited class 82 | # @return [Symbol] the fault key 83 | def fault_key 84 | class_name = self.class.name.split('::').last 85 | BingAdsRubySdk::StringUtils.snakize(class_name).to_sym 86 | end 87 | 88 | def first_error_message(error_list) 89 | error = error_list.first.values.first 90 | format_message(error[:error_code], error[:message]) 91 | end 92 | 93 | def array_wrap(value) 94 | case value 95 | when Array then value 96 | when nil, "" then [] 97 | else 98 | [value] 99 | end 100 | end 101 | 102 | class << self 103 | def error_lists=(value) 104 | @error_lists = value 105 | end 106 | 107 | def error_lists 108 | @error_lists ||= [] 109 | end 110 | 111 | def define_error_lists(*error_list_array) 112 | self.error_lists += error_list_array 113 | 114 | error_list_array.each { |attr| attr_accessor attr } 115 | end 116 | end 117 | end 118 | 119 | # Base class for handling partial errors 120 | class PartialErrorBase < ApplicationFault 121 | 122 | private 123 | 124 | # The parent hash for this type of error is the root of the response 125 | def fault_hash 126 | raw_response[fault_key] || {} 127 | end 128 | 129 | # Gets the first error message in the list. This is 130 | # overridden because partial errors are structured differently 131 | # to application faults 132 | # @return [Hash] containing the details of the error 133 | def first_error_message(error_list) 134 | error = error_list.first 135 | format_message(error[:error_code], error[:message]) 136 | end 137 | end 138 | 139 | class PartialError < PartialErrorBase 140 | define_error_lists :batch_error 141 | 142 | private 143 | def fault_key 144 | :partial_errors 145 | end 146 | end 147 | 148 | class NestedPartialError < PartialErrorBase 149 | define_error_lists :batch_error_collection 150 | 151 | private 152 | def fault_key 153 | :nested_partial_errors 154 | end 155 | end 156 | 157 | # For handling API errors of the same name. 158 | # Documentation: 159 | # https://msdn.microsoft.com/en-gb/library/bing-ads-overview-adapifaultdetail.aspx 160 | class AdApiFaultDetail < ApplicationFault 161 | define_error_lists :errors 162 | end 163 | 164 | # For handling API errors of the same name. 165 | # Documentation: 166 | # https://msdn.microsoft.com/en-gb/library/bing-ads-overview-apifaultdetail.aspx 167 | class ApiFaultDetail < ApplicationFault 168 | define_error_lists :batch_errors, :operation_errors 169 | end 170 | 171 | # For handling API errors of the same name. 172 | # Documentation: 173 | # https://msdn.microsoft.com/en-gb/library/bing-ads-overview-editorialapifaultdetail.aspx 174 | class EditorialApiFaultDetail < ApplicationFault 175 | define_error_lists :batch_errors, :editorial_errors, :operation_errors 176 | end 177 | 178 | # For handling API errors of the same name. 179 | # Documentation: 180 | # https://msdn.microsoft.com/en-gb/library/bing-ads-apibatchfault-customer-billing.aspx 181 | class ApiBatchFault < ApplicationFault 182 | define_error_lists :batch_errors, :operation_errors 183 | end 184 | 185 | # For handling API errors of the same name. 186 | # Documentation: 187 | # https://msdn.microsoft.com/en-gb/library/bing-ads-apifault-customer-billing.aspx 188 | class ApiFault < ApplicationFault 189 | define_error_lists :operation_errors 190 | end 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /spec/examples/2_with_customer/customer_management_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../examples' 2 | 3 | RSpec.describe 'CustomerManagement service' do 4 | include_context 'use api' 5 | 6 | let(:get_customer) do 7 | api.customer_management.call(:get_customer, customer_id: Examples.customer_id) 8 | end 9 | 10 | let(:get_account) do 11 | api.customer_management.get_account(account_id: Examples.account_id) 12 | end 13 | 14 | describe 'Account methods' do 15 | describe '#find_accounts' do 16 | subject do 17 | api.customer_management.call(:find_accounts, 18 | account_filter: '', 19 | customer_id: Examples.customer_id, 20 | top_n: 1 21 | ) 22 | end 23 | 24 | it 'returns a list of basic account information' do 25 | is_expected.to include( 26 | accounts_info: { 27 | account_info: [ 28 | { 29 | id: a_kind_of(String), 30 | name: a_kind_of(String), 31 | number: a_kind_of(String), 32 | account_life_cycle_status: a_kind_of(String), 33 | pause_reason: nil, 34 | }, 35 | ], 36 | } 37 | ) 38 | end 39 | end 40 | 41 | describe '#find_accounts_or_customers_info' do 42 | subject do 43 | api.customer_management.find_accounts_or_customers_info( 44 | filter: '', 45 | top_n: 1 46 | ) 47 | end 48 | 49 | it 'returns a list of records containing account / customer pairs' do 50 | is_expected.to contain_exactly( 51 | { 52 | customer_id: a_kind_of(String), 53 | customer_name: a_kind_of(String), 54 | account_id: a_kind_of(String), 55 | account_name: a_kind_of(String), 56 | account_number: a_kind_of(String), 57 | account_life_cycle_status: a_kind_of(String), # e.g. 'Active' 58 | pause_reason: nil, 59 | } 60 | ) 61 | end 62 | end 63 | 64 | describe '#get_account' do 65 | it 'returns information about the current account' do 66 | expect(get_account).to include( 67 | account: { 68 | bill_to_customer_id: a_kind_of(String), 69 | currency_code: "USD", 70 | account_financial_status: "ClearFinancialStatus", 71 | id: a_kind_of(String), 72 | language: "English", 73 | last_modified_by_user_id: a_kind_of(String), 74 | last_modified_time: a_kind_of(String), 75 | name: a_string_starting_with("Test Account"), 76 | number: a_kind_of(String), 77 | parent_customer_id: a_kind_of(String), 78 | payment_method_id: a_kind_of(String), 79 | payment_method_type: nil, 80 | primary_user_id: a_kind_of(String), 81 | account_life_cycle_status: "Active", 82 | time_stamp: a_kind_of(String), 83 | time_zone: a_kind_of(String), 84 | pause_reason: nil, 85 | forward_compatibility_map: nil, 86 | linked_agencies: { 87 | customer_info: [ 88 | { 89 | id: a_kind_of(String), 90 | name: a_kind_of(String), 91 | } 92 | ], 93 | }, 94 | sales_house_customer_id: nil, 95 | tax_information: "", 96 | back_up_payment_instrument_id: nil, 97 | billing_threshold_amount: nil, 98 | business_address: nil, 99 | auto_tag_type: "Inactive", 100 | sold_to_payment_instrument_id: nil 101 | } 102 | ) 103 | end 104 | end 105 | 106 | describe '#update_account' do 107 | let(:account) { get_account[:account] } 108 | subject do 109 | api.customer_management.update_account( 110 | account: { 111 | '@type' => 'AdvertiserAccount', 112 | id: account[:id], 113 | time_stamp: account[:time_stamp], 114 | name: "Test Account #{Time.now} - updated", 115 | } 116 | ) 117 | end 118 | 119 | it 'returns the last modified time' do 120 | is_expected.to include(last_modified_time: a_kind_of(String)) 121 | end 122 | end 123 | end 124 | 125 | describe 'Customer methods' do 126 | describe 'get_customer' do 127 | it 'returns customer data' do 128 | expect(get_customer).to include( 129 | customer: { 130 | customer_financial_status: "ClearFinancialStatus", 131 | id: a_kind_of(String), 132 | industry: "Entertainment", 133 | last_modified_by_user_id: a_kind_of(String), 134 | last_modified_time: a_kind_of(String), 135 | market_country: "US", 136 | forward_compatibility_map: a_kind_of(Hash), 137 | market_language: "English", 138 | name: a_string_starting_with("Test Customer"), 139 | service_level: "SelfServe", 140 | customer_life_cycle_status: "Active", 141 | time_stamp: a_kind_of(String), 142 | number: a_kind_of(String), 143 | customer_address: a_kind_of(Hash), 144 | } 145 | ) 146 | end 147 | end 148 | 149 | describe '#get_customers_info' do 150 | subject do 151 | api.customer_management.call(:get_customers_info, 152 | customer_name_filter: '', 153 | top_n: 1 154 | ) 155 | end 156 | 157 | it 'returns a list of simple customer information' do 158 | is_expected.to include( 159 | customers_info: { 160 | customer_info: a_collection_including( 161 | { 162 | id: a_kind_of(String), 163 | name: a_kind_of(String), 164 | } 165 | ) 166 | } 167 | ) 168 | end 169 | end 170 | 171 | describe '#update_customer' do 172 | let(:original_customer) { get_customer } 173 | 174 | subject do 175 | api.customer_management.call(:update_customer, { 176 | customer: { 177 | name: "Test Customer - #{Time.now}", 178 | id: Examples.customer_id, 179 | time_stamp: original_customer[:customer][:time_stamp], 180 | industry: original_customer[:customer][:industry] 181 | } 182 | }) 183 | end 184 | 185 | it 'returns the update timestamp' do 186 | is_expected.to include(last_modified_time: a_kind_of(String)) 187 | end 188 | end 189 | end 190 | end -------------------------------------------------------------------------------- /spec/examples/5_with_campaign/ad_extension_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../examples' 4 | 5 | RSpec.describe "AdExtension methods" do 6 | include_context "use api" 7 | 8 | def create_add_ad_extensions 9 | Examples.build_api.campaign_management.add_ad_extensions( 10 | account_id: Examples.account_id, 11 | ad_extensions: [ 12 | { 13 | call_ad_extension: { 14 | country_code: "NZ", 15 | is_call_only: false, 16 | phone_number: SecureRandom.random_number(999_999_999) 17 | } 18 | }, 19 | { 20 | callout_ad_extension: { 21 | text: Examples.random[0..11] 22 | } 23 | }, 24 | { 25 | sitelink_ad_extension: { 26 | description_1: "Description 1 - #{Examples.random}"[0..34], 27 | description_2: "Description 2 - #{Examples.random}"[0..34], 28 | display_text: "Display Text #{Examples.random}"[0..24], 29 | final_mobile_urls: [ { string: "http://mobile.example.com" } ], 30 | final_urls: [ { string: "http://www.example.com" } ], 31 | tracking_url_template: "{lpurl}" 32 | } 33 | } 34 | ] 35 | ) 36 | end 37 | 38 | def set_ad_extensions_associations(ad_extension_ids) 39 | Examples.build_api.campaign_management.set_ad_extensions_associations( 40 | account_id: Examples.account_id, 41 | ad_extension_id_to_entity_id_associations: ad_extension_ids.map do |id| 42 | { 43 | ad_extension_id_to_entity_id_association: { 44 | ad_extension_id: id, 45 | entity_id: Examples.campaign_id 46 | } 47 | } 48 | end, 49 | association_type: "Campaign" 50 | ) 51 | end 52 | 53 | def ad_extension_ids(creation_response) 54 | creation_response[:ad_extension_identities][:ad_extension_identity].map do |ext| 55 | ext[:id].to_i 56 | end 57 | end 58 | 59 | def get_ad_extensions_associations 60 | api.campaign_management.get_ad_extensions_associations( 61 | account_id: Examples.account_id, 62 | ad_extension_type: "CallAdExtension SitelinkAdExtension CalloutAdExtension", 63 | association_type: "Campaign", 64 | entity_ids: [{ long: Examples.campaign_id }] 65 | ) 66 | end 67 | 68 | context "with shared ad extensions" do 69 | before(:all) do 70 | # we use a global var to create ad extensions once, its enough 71 | $created_ad_extension_response = create_add_ad_extensions 72 | set_ad_extensions_associations(ad_extension_ids($created_ad_extension_response)) 73 | end 74 | 75 | describe "#add_ad_extensions" do 76 | it "returns AdExtension ids" do 77 | expect($created_ad_extension_response).to include( 78 | ad_extension_identities: { 79 | ad_extension_identity: [ 80 | { id: a_kind_of(String), version: "1" }, 81 | { id: a_kind_of(String), version: "1" }, 82 | { id: a_kind_of(String), version: "1" }, 83 | ], 84 | }, 85 | nested_partial_errors: "" 86 | ) 87 | end 88 | end 89 | 90 | describe "#get_ad_extensions_associations" do 91 | let(:call_ad_extension) do 92 | { 93 | ad_extension: { 94 | device_preference: nil, 95 | forward_compatibility_map: "", 96 | id: match(/[0-9]*/), 97 | scheduling: nil, 98 | status: a_kind_of(String), 99 | type: "CallAdExtension", 100 | version: match(/[0-9]*/), 101 | country_code: a_kind_of(String), 102 | is_call_only: "false", 103 | is_call_tracking_enabled: "false", 104 | phone_number: match(/[0-9]*/), 105 | require_toll_free_tracking_number: nil, 106 | }, 107 | association_type: "Campaign", 108 | editorial_status: a_kind_of(String), 109 | entity_id: Examples.campaign_id.to_s 110 | } 111 | end 112 | 113 | let(:callout_ad_extension) do 114 | { 115 | ad_extension: { 116 | device_preference: nil, 117 | forward_compatibility_map: "", 118 | id: match(/[0-9]*/), 119 | scheduling: nil, 120 | status: a_kind_of(String), 121 | type: "CalloutAdExtension", 122 | version: match(/[0-9]*/), 123 | text: a_kind_of(String) 124 | }, 125 | association_type: "Campaign", 126 | editorial_status: a_kind_of(String), 127 | entity_id: Examples.campaign_id.to_s 128 | } 129 | end 130 | 131 | let(:sitelink_ad_extension) do 132 | { 133 | ad_extension: { 134 | device_preference: nil, 135 | forward_compatibility_map: "", 136 | id: match(/[0-9]*/), 137 | status: a_kind_of(String), 138 | type: "SitelinkAdExtension", 139 | version: match(/[0-9]*/), 140 | description1: a_kind_of(String), 141 | description2: a_kind_of(String), 142 | destination_url: nil, 143 | display_text: a_kind_of(String), 144 | final_app_urls: nil, 145 | final_url_suffix: nil, 146 | final_mobile_urls: { 147 | string: a_kind_of(String) 148 | }, 149 | final_urls: { 150 | string: a_kind_of(String) 151 | }, 152 | scheduling: nil, 153 | tracking_url_template: a_kind_of(String), 154 | url_custom_parameters: nil 155 | }, 156 | association_type: "Campaign", 157 | editorial_status: "Active", 158 | entity_id: Examples.campaign_id.to_s 159 | } 160 | end 161 | 162 | def get_association(associations, type) 163 | associations.select { |record| record[:ad_extension][:type] == type }.first 164 | end 165 | 166 | it "returns a list of Associations" do 167 | fetched_associations = get_ad_extensions_associations 168 | 169 | # These are split apart to make it easier to figure out which one is missing 170 | expect(get_association(fetched_associations, "CallAdExtension")) 171 | .to include(call_ad_extension) 172 | 173 | expect(get_association(fetched_associations, "CalloutAdExtension")) 174 | .to include(callout_ad_extension) 175 | 176 | expect(get_association(fetched_associations, "SitelinkAdExtension")) 177 | .to include(sitelink_ad_extension) 178 | end 179 | end 180 | 181 | describe "#get_ad_extension_ids_by_account_id" do 182 | it "returns a list of IDs" do 183 | fetched_ad_extension_ids = api.campaign_management.get_ad_extension_ids_by_account_id( 184 | account_id: Examples.account_id, 185 | ad_extension_type: 'SitelinkAdExtension CallAdExtension CalloutAdExtension' 186 | ) 187 | ad_extension_ids($created_ad_extension_response).each do |id| 188 | expect(fetched_ad_extension_ids).to include id 189 | end 190 | end 191 | end 192 | 193 | describe "#get_ad_extensions_by_ids" do 194 | it "returns AdExtensions" do 195 | extensions = api.campaign_management.get_ad_extensions_by_ids( 196 | account_id: Examples.account_id, 197 | ad_extension_ids: ad_extension_ids($created_ad_extension_response).map { |id| { long: id } }, 198 | ad_extension_type: 'SitelinkAdExtension CallAdExtension CalloutAdExtension' 199 | ) 200 | expect(extensions).to be_an(Array) 201 | end 202 | end 203 | end 204 | 205 | describe "#delete_ad_extensions" do 206 | let(:response) { create_add_ad_extensions } 207 | 208 | it "returns no errors" do 209 | expect(api.campaign_management.call(:delete_ad_extensions, 210 | account_id: Examples.account_id, 211 | ad_extension_ids: [ { long: ad_extension_ids(response).first }] 212 | )).to eq(partial_errors: "") 213 | end 214 | end 215 | 216 | describe "#delete_ad_extensions_associations" do 217 | before do 218 | response = create_add_ad_extensions 219 | set_ad_extensions_associations(ad_extension_ids(response)) 220 | end 221 | let(:ad_extension_id) do 222 | get_ad_extensions_associations.first[:ad_extension][:id] 223 | end 224 | 225 | it "currently raises an error" do 226 | expect(api.campaign_management.call(:delete_ad_extensions_associations, 227 | account_id: Examples.account_id, 228 | ad_extension_id_to_entity_id_associations: [ 229 | ad_extension_id_to_entity_id_association: { 230 | ad_extension_id: ad_extension_id, 231 | entity_id: Examples.campaign_id, 232 | } 233 | ], 234 | association_type: "Campaign" 235 | )).to eq(partial_errors: "") 236 | end 237 | end 238 | end 239 | -------------------------------------------------------------------------------- /spec/bing_ads_ruby_sdk/services/campaign_management_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe BingAdsRubySdk::Services::CampaignManagement do 2 | 3 | let(:service_name) { described_class.service } 4 | let(:soap_client) { SpecHelpers.soap_client(service_name) } 5 | let(:expected_xml) { SpecHelpers.request_xml_for(service_name, action, filename) } 6 | let(:mocked_response) { SpecHelpers.response_xml_for(service_name, action, filename) } 7 | 8 | let(:service) { described_class.new(soap_client) } 9 | 10 | before do 11 | expect(BingAdsRubySdk::HttpClient).to receive(:post) do |req| 12 | expect(Nokogiri::XML(req.content).to_xml).to eq expected_xml.to_xml 13 | mocked_response 14 | end 15 | end 16 | 17 | describe "get_campaigns_by_account_id" do 18 | let(:action) { 'get_campaigns_by_account_id' } 19 | let(:filename) { 'standard' } 20 | 21 | it "returns expected result" do 22 | expect( 23 | service.get_campaigns_by_account_id(account_id: 150168726) 24 | ).to contain_exactly( 25 | a_hash_including(name: "20200015 - 20200015 - SN - B - Activité - Stations_Service - Geoloc - ETA"), 26 | a_hash_including(name: "20200015 - 20200015 - SN - E - Produits - Stations_Service - Geoloc - ETA"), 27 | a_hash_including(name: "20200015 - SN - X - Station Service #1 - Geozone_custom - 5KW - V3 - ETA") 28 | ) 29 | end 30 | end 31 | 32 | describe "get_budgets_by_ids" do 33 | let(:action) { 'get_budgets_by_ids' } 34 | let(:filename) { 'standard' } 35 | 36 | it "returns expected result" do 37 | expect( 38 | service.get_budgets_by_ids 39 | ).to contain_exactly( 40 | a_hash_including(name: "budget_DEFAULT"), 41 | ) 42 | end 43 | end 44 | 45 | describe "add_uet_tags" do 46 | let(:action) { 'add_uet_tags' } 47 | let(:filename) { 'standard' } 48 | 49 | it "returns expected result" do 50 | expect( 51 | service.add_uet_tags({ uet_tags: [ { uet_tag: { name: 'SDK-test', description: nil }}]}) 52 | ).to include({ 53 | uet_tags: a_hash_including({ 54 | uet_tag: a_collection_containing_exactly( 55 | a_hash_including(name: "SDK-test") 56 | ) 57 | }), 58 | partial_errors: "" 59 | }) 60 | end 61 | end 62 | 63 | describe "update_uet_tags" do 64 | let(:action) { 'update_uet_tags' } 65 | let(:filename) { 'standard' } 66 | 67 | it "returns expected result" do 68 | expect( 69 | service.update_uet_tags({ uet_tags: [ { uet_tag: { name: 'updated SDK-test', id: 96031109, description: nil}}]}) 70 | ).to eq({ 71 | partial_errors: "" 72 | }) 73 | end 74 | end 75 | 76 | describe "get_uet_tags_by_ids" do 77 | let(:action) { 'get_uet_tags_by_ids' } 78 | let(:filename) { 'standard' } 79 | 80 | it "returns expected result" do 81 | expect( 82 | service.get_uet_tags_by_ids(tag_ids: [{ long: 96031109 }]) 83 | ).to contain_exactly( 84 | a_hash_including(name: "updated SDK-test") 85 | ) 86 | end 87 | end 88 | 89 | describe "add_conversion_goals" do 90 | let(:action) { 'add_conversion_goals' } 91 | let(:filename) { 'standard' } 92 | 93 | it "returns expected result" do 94 | expect( 95 | service.add_conversion_goals(conversion_goals: [{ 96 | event_goal: { 97 | action_expression: 'contact_form', 98 | action_operator: 'Equals', 99 | conversion_window_in_minutes: 43200, 100 | count_type: "Unique", 101 | name: "sdk test", 102 | revenue: { "type": "NoValue" }, 103 | type: "Event", 104 | tag_id: 96031109 105 | } 106 | }])).to eq({ 107 | conversion_goal_ids: [46068449], 108 | partial_errors: "" 109 | }) 110 | end 111 | end 112 | 113 | describe "update_conversion_goals" do 114 | let(:action) { 'update_conversion_goals' } 115 | let(:filename) { 'standard' } 116 | 117 | it "returns expected result" do 118 | expect( 119 | service.update_conversion_goals(conversion_goals: [{ 120 | event_goal: { 121 | id: 46068449, 122 | action_expression: 'contact_form', 123 | action_operator: 'Equals', 124 | conversion_window_in_minutes: 43200, 125 | count_type: "Unique", 126 | name: "updated sdk test", 127 | revenue: { "type": "NoValue" }, 128 | tag_id: 96031109 129 | } 130 | }])).to eq({ 131 | partial_errors: "" 132 | }) 133 | end 134 | end 135 | 136 | describe "get_conversion_goals_by_ids" do 137 | let(:action) { 'get_conversion_goals_by_ids' } 138 | let(:filename) { 'standard' } 139 | 140 | it "returns expected result" do 141 | expect( 142 | service.get_conversion_goals_by_ids( 143 | conversion_goal_types: "Event", 144 | conversion_goal_ids: [{ long: 46068449 }, { long: 46068448 }] 145 | ) 146 | ).to contain_exactly( 147 | a_hash_including(name: "updated sdk test"), 148 | a_hash_including(name: "random") 149 | ) 150 | end 151 | end 152 | 153 | describe "add_ad_extensions" do 154 | let(:action) { 'add_ad_extensions' } 155 | let(:filename) { 'standard' } 156 | 157 | it "returns expected result" do 158 | expect( 159 | service.add_ad_extensions( 160 | account_id: 150168726, 161 | ad_extensions: [ 162 | { 163 | call_ad_extension: { 164 | scheduling: {}, 165 | country_code: "NZ", 166 | phone_number: "0123456699", 167 | } 168 | } 169 | ] 170 | )).to include({ 171 | ad_extension_identities: a_hash_including({ 172 | ad_extension_identity: a_collection_containing_exactly( 173 | a_hash_including(id: "8177660966625") 174 | ) 175 | }), 176 | nested_partial_errors: "" 177 | }) 178 | end 179 | end 180 | 181 | describe "get_ad_extension_ids_by_account_id" do 182 | let(:action) { 'get_ad_extension_ids_by_account_id' } 183 | let(:filename) { 'standard' } 184 | 185 | it "returns expected result" do 186 | expect( 187 | service.get_ad_extension_ids_by_account_id( 188 | account_id: 150168726, 189 | ad_extension_type: "CallAdExtension SitelinkAdExtension CalloutAdExtension" 190 | ) 191 | ).to eq([ 192 | 8177660966625 193 | ]) 194 | end 195 | end 196 | 197 | describe "set_ad_extensions_associations" do 198 | let(:action) { 'set_ad_extensions_associations' } 199 | let(:filename) { 'standard' } 200 | 201 | it "returns expected result" do 202 | expect( 203 | service.set_ad_extensions_associations( 204 | account_id: 150168726, 205 | ad_extension_id_to_entity_id_associations: [{ 206 | ad_extension_id_to_entity_id_association: { 207 | ad_extension_id: 8177660966942, 208 | entity_id: 349704437 209 | } 210 | }], 211 | association_type: "Campaign" 212 | )).to eq({ 213 | partial_errors: "" 214 | }) 215 | end 216 | end 217 | 218 | describe "get_ad_extensions_associations" do 219 | let(:action) { 'get_ad_extensions_associations' } 220 | let(:filename) { 'standard' } 221 | 222 | it "returns expected result" do 223 | expect( 224 | service.get_ad_extensions_associations( 225 | account_id: 150168726, 226 | association_type: "Campaign", 227 | ad_extension_type: "CalloutAdExtension", 228 | entity_ids: [ { long: 349704437 }] 229 | ) 230 | ).to contain_exactly( 231 | a_hash_including( 232 | ad_extension: a_hash_including(id: '8177650858590', text: "Informations Et Contact") 233 | ), 234 | a_hash_including( 235 | ad_extension: a_hash_including(id: '8177660966942', text: "CalloutText") 236 | ) 237 | ) 238 | end 239 | end 240 | 241 | describe "add_shared_entity" do 242 | let(:action) { 'add_shared_entity' } 243 | let(:filename) { 'standard' } 244 | 245 | it "returns expected result" do 246 | expect( 247 | service.add_shared_entity( 248 | negative_keyword_list: { 249 | name: 'sdk list' 250 | } 251 | )).to eq({ 252 | list_item_ids: "", 253 | partial_errors: "", 254 | shared_entity_id: "229798145242911" 255 | }) 256 | end 257 | end 258 | 259 | describe "get_shared_entities_by_account_id" do 260 | let(:action) { 'get_shared_entities_by_account_id' } 261 | let(:filename) { 'standard' } 262 | 263 | it "returns expected result" do 264 | expect( 265 | service.get_shared_entities_by_account_id( 266 | shared_entity_type: "NegativeKeywordList" 267 | )).to contain_exactly( 268 | a_hash_including(id: '229798145242911', name: "sdk list") 269 | ) 270 | end 271 | end 272 | 273 | describe "get_shared_entity_associations_by_entity_ids" do 274 | let(:action) { 'get_shared_entity_associations_by_entity_ids' } 275 | let(:filename) { 'standard' } 276 | 277 | it "returns expected result" do 278 | expect( 279 | service.get_shared_entity_associations_by_entity_ids({ 280 | entity_ids: [{ long: 349704435 }], 281 | entity_type: "Campaign", 282 | shared_entity_type: "NegativeKeywordList" 283 | })).to eq([ 284 | { entity_id: "349704435", entity_type: "Campaign", shared_entity_id: "223200992903993", shared_entity_type: "NegativeKeywordList" } 285 | ]) 286 | end 287 | end 288 | end 289 | -------------------------------------------------------------------------------- /spec/bing_ads_ruby_sdk/errors/error_handler_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe BingAdsRubySdk::Errors::ErrorHandler do 4 | let(:general_error) { BingAdsRubySdk::Errors::GeneralError } 5 | let(:subject) { described_class.new(api_response).call } 6 | 7 | def shared_expectations 8 | expect { subject }.to raise_error do |error| 9 | expect(error).to be_a(error_class) 10 | expect(error).to have_attributes(error_attributes) 11 | end 12 | end 13 | 14 | context 'when there is no fault' do 15 | let(:api_response) do 16 | { 17 | is_migrated: 'false', 18 | nested_partial_errors: '' 19 | } 20 | end 21 | 22 | it { is_expected.to be nil } 23 | end 24 | 25 | context 'when there is a fault' do 26 | let(:api_response) do 27 | { 28 | faultcode: 's:Server', 29 | faultstring: 30 | 'Invalid client data. Check the SOAP fault details', 31 | detail: detail 32 | } 33 | end 34 | 35 | let(:batch_error) do 36 | { 37 | batch_error: { 38 | code: '0000', 39 | details: 'Batch error details', 40 | error_code: 'ErrorCode', 41 | field_path: '{lpurl}', 42 | index: '0', 43 | message: 'Batch error message', 44 | type: 'reserved for internal use' 45 | } 46 | } 47 | end 48 | let(:batch_error_list) { [batch_error] } 49 | 50 | let(:editorial_error_list) do 51 | [ 52 | # Inherits from BatchError 53 | batch_error[:batch_error].merge( 54 | appealable: true, 55 | disapproved_text: 'The text that caused the entity to ...', 56 | location: 'ElementName', 57 | publisher_country: 'New Zealand', 58 | reason_code: 4 59 | ) 60 | ] 61 | end 62 | 63 | let(:operation_errors) do 64 | [{ 65 | operation_error: { 66 | code: '4503', 67 | details: 'Invalid API Campaign Criterion Type : 0 on API tier', 68 | error_code: 'TypeInvalid', 69 | message: "The campaign criterion ..." 70 | } 71 | }] 72 | end 73 | 74 | let(:error_message) do 75 | 'Bing Ads API error - Invalid client data. Check the SOAP fault details' 76 | end 77 | 78 | context 'of type ApiFaultDetail' do 79 | let(:detail) do 80 | { 81 | api_fault_detail: { 82 | tracking_id: '14f89175-e806-4822-8aa7-32b0c7734e11', 83 | batch_errors: '', 84 | operation_errors: operation_errors 85 | } 86 | } 87 | end 88 | 89 | let(:error_attributes) do 90 | { 91 | batch_errors: [], 92 | operation_errors: operation_errors, 93 | message: "TypeInvalid - The campaign criterion ..." 94 | } 95 | end 96 | 97 | let(:error_class) { BingAdsRubySdk::Errors::ApiFaultDetail } 98 | 99 | it('raises an error') { shared_expectations } 100 | end 101 | 102 | context 'of type AdApiFaultDetail' do 103 | let(:detail) do 104 | { 105 | ad_api_fault_detail: { 106 | tracking_id: '14f89175-e806-4822-8aa7-32b0c7734e11', 107 | errors: errors 108 | } 109 | } 110 | end 111 | 112 | let(:errors) do 113 | [{ 114 | ad_api_error: { 115 | code: '0000', 116 | detail: 'Details about error', 117 | error_code: 'ErrorCode', 118 | message: 'Fault message' 119 | } 120 | }] 121 | end 122 | 123 | let(:error_class) { BingAdsRubySdk::Errors::AdApiFaultDetail } 124 | let(:error_attributes) do 125 | { 126 | errors: errors, 127 | message: 'ErrorCode - Fault message' 128 | } 129 | end 130 | 131 | it('raises an error') { shared_expectations } 132 | end 133 | 134 | context 'of type EditorialApiFaultDetail' do 135 | let(:detail) do 136 | { 137 | editorial_api_fault_detail: { 138 | tracking_id: '14f89175-e806-4822-8aa7-32b0c7734e11', 139 | batch_errors: batch_error_list, 140 | editorial_errors: editorial_error_list, 141 | operation_errors: operation_errors 142 | } 143 | } 144 | end 145 | 146 | let(:error_class) { BingAdsRubySdk::Errors::EditorialApiFaultDetail } 147 | 148 | let(:error_attributes) do 149 | { 150 | batch_errors: batch_error_list, 151 | editorial_errors: editorial_error_list, 152 | operation_errors: operation_errors, 153 | message: error_message 154 | } 155 | end 156 | 157 | context 'when all the lists have errors' do 158 | let(:error_message) do 159 | 'API raised 3 errors, including: ErrorCode - Batch error message' 160 | end 161 | it('raises an error') { shared_expectations } 162 | end 163 | 164 | context 'when some of the lists are empty' do 165 | let(:batch_error_list) { [] } 166 | let(:editorial_error_list) { [] } 167 | let(:error_message) do 168 | "TypeInvalid - The campaign criterion ..." 169 | end 170 | 171 | it('raises an error') { shared_expectations } 172 | end 173 | end 174 | 175 | context 'of type ApiBatchFault' do 176 | let(:detail) do 177 | { 178 | api_batch_fault: { 179 | tracking_id: '14f89175-e806-4822-8aa7-32b0c7734e11', 180 | batch_errors: batch_error_list 181 | } 182 | } 183 | end 184 | 185 | let(:error_class) { BingAdsRubySdk::Errors::ApiBatchFault } 186 | let(:error_attributes) do 187 | { 188 | batch_errors: batch_error_list, 189 | message: 'ErrorCode - Batch error message' 190 | } 191 | end 192 | 193 | it('raises an error') { shared_expectations } 194 | end 195 | 196 | context 'of type ApiFault' do 197 | let(:detail) do 198 | { 199 | api_fault: { 200 | tracking_id: '14f89175-e806-4822-8aa7-32b0c7734e11', 201 | operation_errors: operation_errors 202 | } 203 | } 204 | end 205 | 206 | let(:error_attributes) do 207 | { 208 | operation_errors: operation_errors, 209 | message: "TypeInvalid - The campaign criterion ..." 210 | } 211 | end 212 | let(:error_class) { BingAdsRubySdk::Errors::ApiFault } 213 | 214 | it('raises an error') { shared_expectations } 215 | end 216 | 217 | context 'of type InvalidCredentials' do 218 | let(:api_response) do 219 | { 220 | code: '105', 221 | detail: nil, 222 | error_code: 'InvalidCredentials', 223 | message: 'Authentication failed. Either supplied ...' 224 | } 225 | end 226 | 227 | let(:error_class) { general_error } 228 | 229 | let(:error_attributes) do 230 | { 231 | raw_response: api_response, 232 | message: "InvalidCredentials - #{api_response[:message]}" 233 | } 234 | end 235 | 236 | it('raises an error') { shared_expectations } 237 | end 238 | 239 | context 'of an unknown type' do 240 | let(:detail) do 241 | { 242 | new_fault_unknown_to_sdk: { 243 | tracking_id: '14f89175-e806-4822-8aa7-32b0c7734e11', 244 | new_field: 'value' 245 | } 246 | } 247 | end 248 | 249 | let(:error_class) { general_error } 250 | 251 | let(:error_attributes) do 252 | { 253 | raw_response: api_response, 254 | message: error_message 255 | } 256 | end 257 | 258 | it('raises an error') { shared_expectations } 259 | end 260 | 261 | context 'when there are no details' do 262 | let(:detail) { nil } 263 | let(:error_class) { general_error } 264 | let(:error_attributes) do 265 | { 266 | raw_response: api_response, 267 | message: error_message 268 | } 269 | end 270 | 271 | it('raises an error') { shared_expectations } 272 | end 273 | 274 | context 'when there is no error_code' do 275 | let(:detail) do 276 | { 277 | api_fault: { 278 | tracking_id: '14f89175-e806-4822-8aa7-32b0c7734e11', 279 | batch_errors: '', 280 | operation_errors: { 281 | operation_error: { 282 | code: '1001', 283 | details: '', 284 | message: 'The user is not authorized to perform this action.' 285 | } 286 | } 287 | } 288 | } 289 | end 290 | let(:error_class) { BingAdsRubySdk::Errors::ApiFault } 291 | let(:error_attributes) do 292 | { 293 | raw_response: api_response, 294 | message: 'The user is not authorized to perform this action.' 295 | } 296 | end 297 | 298 | it('raises an error') { shared_expectations } 299 | end 300 | end 301 | 302 | context 'when there is a deserialization error' do 303 | # rubocop:disable Metrics/LineLength 304 | let(:api_response) do 305 | { 306 | faultcode: 'a:DeserializationFailed', 307 | faultstring: "The formatter threw an exception while trying to deserialize the message: There was an error while trying to deserialize parameter https://bingads.microsoft.com/CampaignManagement/v11:CampaignId. The InnerException message was 'There was an error deserializing the object of type System.Int64. The value '' cannot be parsed as the type 'Int64'.'. Please see InnerException for more details.", 308 | detail: { 309 | exception_detail: { 310 | help_link: nil, 311 | inner_exception: { 312 | help_link: nil, 313 | inner_exception: { 314 | help_link: nil, 315 | inner_exception: { 316 | help_link: nil, 317 | inner_exception: nil, 318 | message: 'Input string was not in a correct format.', 319 | stack_trace: " at System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal)\r\n at System.Number.ParseInt64(String value, NumberStyles options, NumberFormatInfo numfmt)\r\n at System.Xml.XmlConverter.ToInt64(String value)", 320 | type: 'System.FormatException' 321 | }, 322 | message: "The value '' cannot be parsed as the type 'Int64'.", 323 | stack_trace: " at System.Xml.XmlConverter.ToInt64(String value)\r\n at System.Xml.XmlDictionaryReader.ReadElementContentAsLong()\r\n at System.Runtime.Serialization.LongDataContract.ReadXmlValue(XmlReaderDelegator reader, XmlObjectSerializerReadContext context)\r\n at System.Runtime.Serialization.XmlObjectSerializer.ReadObjectHandleExceptions(XmlReaderDelegator reader, Boolean verifyObjectName, DataContractResolver dataContractResolver)", 324 | type: 'System.Xml.XmlException' 325 | }, 326 | message: "There was an error deserializing the object of type System.Int64. The value '' cannot be parsed as the type 'Int64'.", 327 | stack_trace: " at System.Runtime.Serialization.XmlObjectSerializer.ReadObjectHandleExceptions(XmlReaderDelegator reader, Boolean verifyObjectName, DataContractResolver dataContractResolver)\r\n at System.Runtime.Serialization.DataContractSerializer.ReadObject(XmlDictionaryReader reader, Boolean verifyObjectName)\r\n at System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.PartInfo.ReadObject(XmlDictionaryReader reader, XmlObjectSerializer serializer)\r\n at System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.DeserializeParameterPart(XmlDictionaryReader reader, PartInfo part, Boolean isRequest)", 328 | type: 'System.Runtime.Serialization.SerializationException' 329 | }, 330 | message: "The formatter threw an exception while trying to deserialize the message: There was an error while trying to deserialize parameter https://bingads.microsoft.com/CampaignManagement/v11:CampaignId. The InnerException message was 'There was an error deserializing the object of type System.Int64. The value '' cannot be parsed as the type 'Int64'.'. Please see InnerException for more details.", 331 | stack_trace: " at System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.DeserializeParameterPart(XmlDictionaryReader reader, PartInfo part, Boolean isRequest)\r\n at System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.DeserializeParameters(XmlDictionaryReader reader, PartInfo[] parts, Object[] parameters, Boolean isRequest)\r\n at System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.DeserializeBody(XmlDictionaryReader reader, MessageVersion version, String action, MessageDescription messageDescription, Object[] parameters, Boolean isRequest)\r\n at System.ServiceModel.Dispatcher.OperationFormatter.DeserializeBodyContents(Message message, Object[] parameters, Boolean isRequest)\r\n at System.ServiceModel.Dispatcher.OperationFormatter.DeserializeRequest(Message message, Object[] parameters)\r\n at System.ServiceModel.Dispatcher.DispatchOperationRuntime.DeserializeInputs(MessageRpc& rpc)\r\n at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc)\r\n at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc& rpc)\r\n at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage11(MessageRpc& rpc)\r\n at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)", 332 | type: 'System.ServiceModel.Dispatcher.NetDispatcherFaultException' 333 | } 334 | } 335 | } 336 | end 337 | # rubocop:enable Metrics/LineLength 338 | 339 | let(:error_class) { BingAdsRubySdk::Errors::GeneralError } 340 | let(:error_attributes) do 341 | { 342 | raw_response: api_response, 343 | message: "Bing Ads API error - The formatter threw an exception while trying to deserialize the message: There was an error while trying to deserialize parameter https://bingads.microsoft.com/CampaignManagement/v11:CampaignId. The InnerException message was 'There was an error deserializing the object of type System.Int64. The value '' cannot be parsed as the type 'Int64'.'. Please see InnerException for more details." 344 | } 345 | end 346 | 347 | it('raises an error') { shared_expectations } 348 | end 349 | 350 | context 'when there are nested partial errors' do 351 | let(:api_response) do 352 | { 353 | campaign_criterion_ids: [], 354 | is_migrated: 'false', 355 | nested_partial_errors: { 356 | batch_error_collection: [ 357 | { 358 | batch_errors: nil, 359 | code: '1043', 360 | details: 'Criterion already exists', 361 | error_code: 'AlreadyExists', 362 | field_path: nil, 363 | forward_compatibility_map: nil, 364 | index: '0', 365 | message: 'The specified entity already exists.', 366 | type: 'BatchErrorCollection' 367 | } 368 | ] 369 | } 370 | } 371 | end 372 | 373 | context 'when the default behaviour is used' do 374 | let(:error_class) { BingAdsRubySdk::Errors::NestedPartialError } 375 | let(:error_attributes) do 376 | { 377 | raw_response: api_response, 378 | message: 'AlreadyExists - The specified entity already exists.' 379 | } 380 | end 381 | 382 | it('raises an error') { shared_expectations } 383 | end 384 | 385 | context 'when the ignore partial errors switch is on' do 386 | it 'should return the NestedPartialError as a Hash' 387 | end 388 | end 389 | 390 | context 'when there are partial errors - multiple batch_errors' do 391 | let(:api_response) do 392 | { 393 | campaign_ids: [], 394 | partial_errors: { 395 | batch_error: [ 396 | { 397 | code: '4701', 398 | details: nil, 399 | error_code: 'UnsupportedBiddingScheme', 400 | field_path: nil, 401 | forward_compatibility_map: nil, 402 | index: '0', 403 | message: 'The bidding...', 404 | type: 'BatchError' 405 | }, 406 | { 407 | code: '4701', 408 | details: nil, 409 | error_code: 'UnsupportedBiddingScheme', 410 | field_path: nil, 411 | forward_compatibility_map: nil, 412 | index: '1', 413 | message: 'The bidding...', 414 | type: 'BatchError' 415 | } 416 | ] 417 | } 418 | } 419 | end 420 | 421 | context 'when the default behaviour is used' do 422 | let(:error_class) { BingAdsRubySdk::Errors::PartialError } 423 | 424 | let(:error_attributes) do 425 | { 426 | raw_response: api_response, 427 | message: 'API raised 2 errors, including: UnsupportedBiddingScheme - The bidding...' 428 | } 429 | end 430 | 431 | it('raises an error') { shared_expectations } 432 | end 433 | 434 | context 'when the ignore partial errors switch is on' do 435 | it 'should return the PartialError as a Hash' 436 | end 437 | end 438 | 439 | context 'when there are partial errors - one batch_error' do 440 | let(:api_response) do 441 | { 442 | campaign_ids: [], 443 | partial_errors: { 444 | batch_error: { 445 | code: '4701', 446 | details: nil, 447 | error_code: 'UnsupportedBiddingScheme', 448 | field_path: nil, 449 | forward_compatibility_map: nil, 450 | index: '0', 451 | message: 'The bidding...', 452 | type: 'BatchError' 453 | }, 454 | } 455 | } 456 | end 457 | 458 | context 'when the default behaviour is used' do 459 | let(:error_class) { BingAdsRubySdk::Errors::PartialError } 460 | 461 | let(:error_attributes) do 462 | { 463 | raw_response: api_response, 464 | message: 'UnsupportedBiddingScheme - The bidding...' 465 | } 466 | end 467 | 468 | it('raises an error') { shared_expectations } 469 | 470 | it "contains a collection of batch_error" do 471 | expect { subject }.to raise_error do |error| 472 | expect(error).to respond_to(:batch_error) 473 | expect(error.batch_error).to be_a(Array) 474 | expect(error.batch_error.size).to eq(1) 475 | end 476 | end 477 | end 478 | end 479 | end 480 | --------------------------------------------------------------------------------