├── 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 =~ /^
27 | end
28 |
29 | def apply_filter(document)
30 | return document unless document.errors.empty?
31 |
32 | filters.each do |filter|
33 | apply_filter! document, filter
34 | end
35 |
36 | document
37 | end
38 |
39 | def apply_filter!(document, filter)
40 | document.xpath("//*[local-name()='#{filter}']").each do |node|
41 | node.content = FILTERED
42 | end
43 | end
44 |
45 | def nokogiri_options
46 | pretty_print ? { indent: 2 } : { save_with: Nokogiri::XML::Node::SaveOptions::AS_XML }
47 | end
48 |
49 | def pretty_print
50 | BingAdsRubySdk.config.pretty_print_xml
51 | end
52 |
53 | def filters
54 | BingAdsRubySdk.config.filters
55 | end
56 | end
57 | end
--------------------------------------------------------------------------------
/lib/bing_ads_ruby_sdk/header.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module BingAdsRubySdk
4 | # Contains the SOAP Request header informations
5 | class Header
6 | # @param developer_token
7 | # @param client_id
8 | # @param store instance of a store
9 | def initialize(developer_token:, client_id:, store:)
10 | @developer_token = developer_token
11 | @client_id = client_id
12 | @oauth_store = store
13 | @customer = {}
14 | end
15 |
16 | # @return [Hash] Authorization and identification data that will be added to the SOAP header
17 | def content
18 | {
19 | "AuthenticationToken" => 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 |
--------------------------------------------------------------------------------