├── .github ├── CODEOWNERS └── workflows │ ├── CODEOWNERS │ ├── main.yml │ └── publish.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .ruby-version ├── .yardopts ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── zendesk_api.rb └── zendesk_api │ ├── actions.rb │ ├── association.rb │ ├── associations.rb │ ├── client.rb │ ├── collection.rb │ ├── configuration.rb │ ├── core_ext │ └── inflection.rb │ ├── delegator.rb │ ├── error.rb │ ├── helpers.rb │ ├── lru_cache.rb │ ├── middleware │ ├── request │ │ ├── encode_json.rb │ │ ├── etag_cache.rb │ │ ├── raise_rate_limited.rb │ │ ├── retry.rb │ │ ├── upload.rb │ │ └── url_based_access_token.rb │ └── response │ │ ├── callback.rb │ │ ├── deflate.rb │ │ ├── gzip.rb │ │ ├── logger.rb │ │ ├── parse_iso_dates.rb │ │ ├── parse_json.rb │ │ ├── raise_error.rb │ │ └── sanitize_response.rb │ ├── pagination.rb │ ├── resource.rb │ ├── resources.rb │ ├── search.rb │ ├── sideloading.rb │ ├── silent_mash.rb │ ├── track_changes.rb │ ├── trackie.rb │ ├── verbs.rb │ └── version.rb ├── spec ├── core │ ├── association_spec.rb │ ├── bulk_actions_spec.rb │ ├── client_spec.rb │ ├── collection_spec.rb │ ├── configuration_spec.rb │ ├── create_resource_spec.rb │ ├── data_namespace_spec.rb │ ├── data_resource_spec.rb │ ├── data_spec.rb │ ├── error_spec.rb │ ├── helpers_spec.rb │ ├── inflection_spec.rb │ ├── lru_cache_spec.rb │ ├── middleware │ │ ├── request │ │ │ ├── encode_json_spec.rb │ │ │ ├── etag_cache_spec.rb │ │ │ ├── raise_rate_limited_spec.rb │ │ │ ├── retry_spec.rb │ │ │ ├── test.jpg │ │ │ └── upload_spec.rb │ │ └── response │ │ │ ├── callback_spec.rb │ │ │ ├── deflate_spec.rb │ │ │ ├── gzip_spec.rb │ │ │ ├── parse_iso_dates_spec.rb │ │ │ ├── parse_json_spec.rb │ │ │ ├── raise_error_spec.rb │ │ │ └── sanitize_response_spec.rb │ ├── read_resource_spec.rb │ ├── resource_spec.rb │ ├── resources │ │ ├── automation_spec.rb │ │ ├── cbp_spec_helper.rb │ │ ├── macro_spec.rb │ │ ├── trigger_spec.rb │ │ └── view_spec.rb │ ├── search_export_spec.rb │ ├── search_spec.rb │ ├── spec_helper.rb │ └── trackie_spec.rb ├── fixtures │ ├── Argentina.gif │ ├── Argentina2.gif │ ├── credentials.yml.example │ ├── sample_app.zip │ ├── test_resources.rb │ └── zendesk.rb ├── live │ ├── Readme.md │ ├── app_installation_spec.rb │ ├── app_spec.rb │ ├── article_spec.rb │ ├── audit_spec.rb │ ├── automation_spec.rb │ ├── bookmark_spec.rb │ ├── brand_spec.rb │ ├── category_spec.rb │ ├── cbp_support_spec.rb │ ├── collection_spec.rb │ ├── custom_role_spec.rb │ ├── custom_status_spec.rb │ ├── dynamic_content │ │ ├── item_spec.rb │ │ └── variant_spec.rb │ ├── group_membership_spec.rb │ ├── group_spec.rb │ ├── identity_spec.rb │ ├── locale_spec.rb │ ├── macro_spec.rb │ ├── organization_field_spec.rb │ ├── organization_membership_spec.rb │ ├── organization_spec.rb │ ├── organization_subscription_spec.rb │ ├── push_notification_device_spec.rb │ ├── request_spec.rb │ ├── satisfaction_rating_spec.rb │ ├── schedule_spec.rb │ ├── section_spec.rb │ ├── setting_spec.rb │ ├── suspended_ticket_spec.rb │ ├── tag_spec.rb │ ├── target_spec.rb │ ├── ticket_field_spec.rb │ ├── ticket_form_spec.rb │ ├── ticket_metrics_spec.rb │ ├── ticket_spec.rb │ ├── topic_spec.rb │ ├── topic_subscription_spec.rb │ ├── trigger_category_spec.rb │ ├── trigger_spec.rb │ ├── upload_spec.rb │ ├── user_field_spec.rb │ ├── user_spec.rb │ ├── user_view_spec.rb │ ├── view_spec.rb │ ├── voice │ │ └── phone_number_spec.rb │ └── webhook_spec.rb └── macros │ └── resource_macros.rb ├── util ├── resource_handler.rb └── verb_handler.rb └── zendesk_api.gemspec /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @zendesk/redback 2 | -------------------------------------------------------------------------------- /.github/workflows/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @zendesk/redback 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: 8 | - master 9 | - spec_live 10 | 11 | jobs: 12 | test-and-lint: 13 | name: Test and lint 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | ruby: 18 | - "2.7" 19 | - "3.0" 20 | - "3.1" 21 | - "3.2" 22 | - "3.3" 23 | - "3.4" 24 | - "jruby-9.4" 25 | steps: 26 | - name: Checkout code 27 | uses: zendesk/checkout@v2 28 | - name: Set up Ruby 29 | uses: zendesk/setup-ruby@v1 30 | with: 31 | bundler-cache: true 32 | ruby-version: ${{ matrix.ruby }} 33 | - name: Test and Lint 34 | run: | 35 | bundle install 36 | bundle exec rake 37 | bundle exec rubocop 38 | spec-live: 39 | name: Spec live 40 | if: github.repository == 'zendesk/zendesk_api_client_rb' 41 | env: 42 | SPEC_LIVE_USERNAME: ${{ secrets.SPEC_LIVE_USERNAME }} 43 | SPEC_LIVE_PASSWORD: ${{ secrets.SPEC_LIVE_PASSWORD }} 44 | SPEC_LIVE_ZENDESK_HOST: ${{ secrets.SPEC_LIVE_ZENDESK_HOST }} 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Checkout code 48 | uses: zendesk/checkout@v2 49 | - name: Set up Ruby 50 | uses: zendesk/setup-ruby@v1 51 | with: 52 | bundler-cache: true 53 | ruby-version: 3.1 54 | - name: spec:live 55 | run: | 56 | bundle install 57 | bundle exec rake clean_live set_ci_credentials spec:live || 58 | bundle exec rake clean_live && 59 | bundle exec rspec spec/live --only-failures 60 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to RubyGems.org 2 | 3 | on: 4 | push: 5 | branches: main 6 | paths: lib/zendesk_api/version.rb 7 | workflow_dispatch: 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | environment: rubygems-publish 13 | if: github.repository_owner == 'zendesk' 14 | permissions: 15 | id-token: write 16 | contents: write 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | bundler-cache: false 23 | 24 | - name: Install dependencies 25 | run: bundle install 26 | - uses: rubygems/release-gem@v1 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/** 2 | spec/examples.txt 3 | spec/fixtures/cassettes 4 | spec/fixtures/credentials.yml 5 | coverage/** 6 | .yardoc/** 7 | Gemfile.lock 8 | .ruby-version 9 | vendor/bundle/** 10 | pkg/** 11 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | AllCops: 4 | TargetRubyVersion: 2.7 5 | DisplayCopNames: true 6 | Exclude: 7 | - .git/**/* 8 | - spec/core/middleware/response/sanitize_response_spec.rb 9 | - vendor/**/* 10 | 11 | # Prevents Ruby 3.1 incompatibility error. You can enable this cop when Ruby 2.7 support is dropped. 12 | # See https://github.com/rubocop/rubocop/issues/10258 13 | Layout/BlockAlignment: 14 | Enabled: false 15 | 16 | # Align ends correctly. 17 | Layout/EndAlignment: 18 | EnforcedStyleAlignWith: variable 19 | 20 | # Align the elements of a hash literal if they span more than one line. 21 | Layout/HashAlignment: 22 | EnforcedLastArgumentHashStyle: ignore_implicit 23 | 24 | # Align the parameters of a method call if they span more than one line. 25 | Layout/ParameterAlignment: 26 | EnforcedStyle: with_fixed_indentation 27 | 28 | # Checks the indentation of hanging closing parentheses. 29 | Layout/ClosingParenthesisIndentation: 30 | Enabled: false 31 | 32 | # Checks the indentation of the first parameter in a method call. 33 | Layout/FirstParameterIndentation: 34 | EnforcedStyle: consistent 35 | 36 | # Checks indentation of method calls with the dot operator that span more than one line. 37 | Layout/MultilineMethodCallIndentation: 38 | EnforcedStyle: indented 39 | 40 | # Checks indentation of binary operations that span more than one line. 41 | Layout/MultilineOperationIndentation: 42 | EnforcedStyle: indented 43 | 44 | Metrics: 45 | Enabled: false 46 | 47 | Style/DoubleNegation: 48 | Enabled: false 49 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.6 2 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --tag internal 2 | --hide-tag internal 3 | --no-private 4 | --protected 5 | --embed-mixins 6 | --load ./util/resource_handler.rb 7 | --load ./util/verb_handler.rb 8 | -r Readme.md 9 | lib/**/*.rb 10 | - 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "jruby-openssl", platforms: :jruby 4 | gem "mini_mime" 5 | gem "rake" 6 | gem "addressable", ">= 2.8.0" 7 | gem "yard" 8 | gem "json", ">= 2.3.0", platforms: :ruby 9 | gem "scrub_rb" 10 | 11 | gem "rubocop", "~> 0.79.0", require: false # Handling of Ruby 2.7 syntax 12 | 13 | group :test do 14 | gem "webmock" 15 | gem "vcr", "~> 6.0" 16 | 17 | # Hardcoding these gems as the newer version makes the tests fail in Ruby 3 18 | # See https://github.com/zendesk/zendesk_api_client_rb/runs/5013748785?check_suite_focus=true#step:4:59 19 | # NOTE: This affects previous build re-runs because we don't store Gemfile.lock 20 | gem "rspec-support", "3.10.3" 21 | gem "rspec-core", "3.10.1" 22 | gem "rspec-expectations", "3.10.2" 23 | gem "rspec-mocks", "3.10.2" 24 | gem "rspec", "3.10.0" 25 | 26 | # only used for uploads testing 27 | gem "actionpack", ">= 5.2.4.6" 28 | end 29 | 30 | group :dev do 31 | gem "bump" 32 | end 33 | 34 | group :dev, :test do 35 | gem "byebug", platforms: :ruby 36 | end 37 | 38 | gemspec 39 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'rake/testtask' 3 | require 'bundler/gem_tasks' 4 | require 'bump/tasks' 5 | require 'rubocop/rake_task' 6 | 7 | begin 8 | require 'rspec/core/rake_task' 9 | rescue LoadError 10 | puts "WARN: #{$ERROR_INFO.message} Continuing..." 11 | end 12 | 13 | if defined?(RSpec) 14 | desc "Run specs" 15 | RSpec::Core::RakeTask.new("spec") do |t| 16 | t.pattern = "spec/core/**/*_spec.rb" 17 | end 18 | 19 | desc "Run live specs" 20 | RSpec::Core::RakeTask.new("spec:live") do |t| 21 | t.pattern = "spec/live/**/*_spec.rb" 22 | end 23 | 24 | task :clean_live do 25 | sh "rm -rf spec/fixtures/cassettes" 26 | end 27 | 28 | task :set_ci_credentials do 29 | File.open("spec/fixtures/credentials.yml", "w") do |f| 30 | f.write( 31 | File.read("spec/fixtures/credentials.yml.example") 32 | .sub("your_username", ENV.fetch("SPEC_LIVE_USERNAME")) 33 | .sub("your_password", ENV.fetch("SPEC_LIVE_PASSWORD")) 34 | .sub("your_zendesk_host", ENV.fetch("SPEC_LIVE_ZENDESK_HOST")) 35 | ) 36 | end 37 | end 38 | 39 | desc 'Default: run specs.' 40 | task :default => "spec" 41 | end 42 | 43 | # extracted from https://github.com/grosser/project_template 44 | rule(/^version:bump:.*/) do |t| 45 | sh "git status | grep 'nothing to commit'" # ensure we are not dirty 46 | index = %w(major minor patch).index(t.name.split(':').last) 47 | file = 'lib/zendesk_api/version.rb' 48 | 49 | version_file = File.read(file) 50 | old_version, *version_parts = version_file.match(/(\d+)\.(\d+)\.(\d+)/).to_a 51 | version_parts[index] = version_parts[index].to_i + 1 52 | version_parts[2] = 0 if index < 2 # remove patch for minor 53 | version_parts[1] = 0 if index < 1 # remove minor for major 54 | new_version = version_parts * '.' 55 | File.open(file, 'w') { |f| f.write(version_file.sub(old_version, new_version)) } 56 | 57 | sh "bundle && git add #{file} Gemfile.lock && git commit -m 'bump version to #{new_version}'" 58 | end 59 | -------------------------------------------------------------------------------- /lib/zendesk_api.rb: -------------------------------------------------------------------------------- 1 | module ZendeskAPI; end 2 | 3 | require 'faraday' 4 | require 'faraday/multipart' 5 | 6 | require 'zendesk_api/helpers' 7 | require 'zendesk_api/core_ext/inflection' 8 | require 'zendesk_api/client' 9 | -------------------------------------------------------------------------------- /lib/zendesk_api/association.rb: -------------------------------------------------------------------------------- 1 | require 'zendesk_api/helpers' 2 | 3 | module ZendeskAPI 4 | # Represents an association between two resources 5 | # @private 6 | class Association 7 | class << self 8 | def namespaces 9 | [ZendeskAPI] + ZendeskAPI::DataNamespace.descendants 10 | end 11 | 12 | def class_from_namespace(klass_as_string) 13 | namespaces.each do |ns| 14 | if module_defines_class?(ns, klass_as_string) 15 | return ns.const_get(klass_as_string) 16 | end 17 | end 18 | 19 | nil 20 | end 21 | 22 | def module_defines_class?(mod, klass_as_string) 23 | mod.const_defined?(klass_as_string, false) 24 | end 25 | end 26 | 27 | # @return [Hash] Options passed into the association 28 | attr_reader :options 29 | 30 | # Options to pass in 31 | # * class - Required 32 | # * parent - Parent instance 33 | # * path - Optional path instead of resource name 34 | def initialize(options = {}) 35 | @options = SilentMash.new(options) 36 | end 37 | 38 | # Generate a path to the resource. 39 | # id and _id attributes will be deleted from passed in options hash if they are used in the built path. 40 | # Arguments that can be passed in: 41 | # An instance, any resource instance 42 | # Hash Options: 43 | # * with_parent - Include the parent path (false by default) 44 | # * with_id - Include the instance id, if possible (true) 45 | def generate_path(*args) 46 | options = SilentMash.new(:with_id => true) 47 | if args.last.is_a?(Hash) 48 | original_options = args.pop 49 | options.merge!(original_options) 50 | end 51 | 52 | instance = args.first 53 | 54 | namespace = @options[:class].to_s.split("::") 55 | namespace[-1] = @options[:class].resource_path 56 | # Remove components without path information 57 | ignorable_namespace_strings.each { |ns| namespace.delete(ns) } 58 | has_parent = namespace.size > 1 || (options[:with_parent] && @options.parent) 59 | 60 | if has_parent 61 | parent_class = @options.parent ? @options.parent.class : Association.class_from_namespace(ZendeskAPI::Helpers.modulize_string(namespace[0])) 62 | parent_namespace = build_parent_namespace(parent_class, instance, options, original_options) 63 | namespace[1..1] = parent_namespace if parent_namespace 64 | namespace[0] = parent_class.resource_path 65 | else 66 | namespace[0] = @options.path || @options[:class].resource_path 67 | end 68 | 69 | if id = extract_id(instance, options, original_options) 70 | namespace << id 71 | end 72 | 73 | namespace.join("/") 74 | end 75 | 76 | # Tries to place side loads onto given resources. 77 | def side_load(resources, side_loads) 78 | key = "#{options.name}_id" 79 | plural_key = "#{Inflection.singular options.name.to_s}_ids" 80 | 81 | resources.each do |resource| 82 | if resource.key?(plural_key) # Grab associations from child_ids field on resource 83 | side_load_from_child_ids(resource, side_loads, plural_key) 84 | elsif resource.key?(key) || options.singular 85 | side_load_from_child_or_parent_id(resource, side_loads, key) 86 | else # Grab associations from parent_id field from multiple child resources 87 | side_load_from_parent_id(resource, side_loads, key) 88 | end 89 | end 90 | end 91 | 92 | private 93 | 94 | # @return [Array] ['ZendeskAPI', 'Voice', etc.. ] 95 | def ignorable_namespace_strings 96 | ZendeskAPI::DataNamespace.descendants.map { |klass| klass.to_s.split('::') }.flatten.uniq 97 | end 98 | 99 | def _side_load(resource, side_loads) 100 | side_loads.map! do |side_load| 101 | resource.send(:wrap_resource, side_load, options) 102 | end 103 | 104 | ZendeskAPI::Collection.new(resource.client, options[:class]).tap do |collection| 105 | collection.replace(side_loads) 106 | end 107 | end 108 | 109 | def side_load_from_parent_id(resource, side_loads, key) 110 | key = "#{resource.class.singular_resource_name}_id" 111 | 112 | resource.send("#{options.name}=", _side_load(resource, side_loads.select {|side_load| 113 | side_load[key] == resource.id 114 | })) 115 | end 116 | 117 | def side_load_from_child_ids(resource, side_loads, plural_key) 118 | ids = resource.send(plural_key) 119 | 120 | resource.send("#{options.name}=", _side_load(resource, side_loads.select {|side_load| 121 | ids.include?(side_load[options.include_key]) 122 | })) 123 | end 124 | 125 | def side_load_from_child_or_parent_id(resource, side_loads, key) 126 | # Either grab association from child_id field on resource or parent_id on child resource 127 | if resource.key?(key) 128 | id = resource.send(key) 129 | include_key = options.include_key 130 | else 131 | id = resource.id 132 | include_key = "#{resource.class.singular_resource_name}_id" 133 | end 134 | 135 | return unless id 136 | 137 | side_load = side_loads.detect do |side_load| 138 | id == side_load[include_key] 139 | end 140 | 141 | resource.send("#{options.name}=", side_load) if side_load 142 | end 143 | 144 | def build_parent_namespace(parent_class, instance, options, original_options) 145 | path = @options.path 146 | 147 | association_on_parent = parent_class.associations.detect { |a| a[:name] == @options[:name] } 148 | association_on_parent ||= parent_class.associations.detect do |a| 149 | !a[:inline] && a[:class] == @options[:class] 150 | end 151 | 152 | if association_on_parent 153 | path ||= association_on_parent[:path] 154 | path ||= association_on_parent[:name].to_s 155 | end 156 | 157 | path ||= @options[:class].resource_path 158 | 159 | [ 160 | extract_parent_id(parent_class, instance, options, original_options), 161 | path 162 | ] 163 | end 164 | 165 | def extract_parent_id(parent_class, instance, options, original_options) 166 | parent_id_column = "#{parent_class.singular_resource_name}_id" 167 | 168 | if @options.parent 169 | @options.parent.id 170 | elsif instance 171 | instance.send(parent_id_column) 172 | elsif options[parent_id_column] 173 | original_options.delete(parent_id_column) || original_options.delete(parent_id_column.to_sym) 174 | else 175 | raise ArgumentError.new("#{@options[:class].resource_name} requires #{parent_id_column} or parent") 176 | end 177 | end 178 | 179 | def extract_id(instance, options, original_options) 180 | if options[:with_id] && !@options[:class].ancestors.include?(SingularResource) 181 | if instance && instance.id 182 | instance.id 183 | elsif options[:id] 184 | original_options.delete(:id) || original_options.delete("id") 185 | end 186 | end 187 | end 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /lib/zendesk_api/associations.rb: -------------------------------------------------------------------------------- 1 | require 'zendesk_api/helpers' 2 | 3 | module ZendeskAPI 4 | # This module holds association method for resources. 5 | # Associations can be loaded in three ways: 6 | # * Commonly used resources are automatically side-loaded server side and sent along with their parent object. 7 | # * Associated resource ids are sent and are then loaded one-by-one into the parent collection. 8 | # * The association is represented with Rails' nested association urls (such as tickets/:id/groups) and are loaded that way. 9 | # 10 | # @private 11 | module Associations 12 | def self.included(base) 13 | base.extend ClassMethods 14 | end 15 | 16 | def wrap_resource(resource, class_level_association, options = {}) 17 | instance_association = Association.new(class_level_association.merge(:parent => self)) 18 | klass = class_level_association[:class] 19 | 20 | case resource 21 | when Hash 22 | klass.new(@client, resource.merge(:association => instance_association)) 23 | when String, Integer 24 | klass.new(@client, (options[:include_key] || :id) => resource, :association => instance_association) 25 | else 26 | resource.association = instance_association 27 | resource 28 | end 29 | end 30 | 31 | # @private 32 | module ClassMethods 33 | def self.extended(klass) 34 | klass.extend Has 35 | klass.extend HasMany 36 | end 37 | 38 | def associations 39 | @associations ||= [] 40 | end 41 | 42 | def associated_with(name) 43 | associations.inject([]) do |associated_with, association| 44 | if association[:include] == name.to_s 45 | associated_with.push(Association.new(association)) 46 | end 47 | 48 | associated_with 49 | end 50 | end 51 | 52 | private 53 | 54 | def build_association(klass, resource_name, options) 55 | { 56 | :class => klass, 57 | :name => resource_name, 58 | :inline => options.delete(:inline), 59 | :path => options.delete(:path), 60 | :include => (options.delete(:include) || klass.resource_name).to_s, 61 | :include_key => (options.delete(:include_key) || :id).to_s, 62 | :singular => options.delete(:singular), 63 | :extensions => Array(options.delete(:extend)) 64 | } 65 | end 66 | 67 | def define_used(association) 68 | define_method "#{association[:name]}_used?" do 69 | !!instance_variable_get("@#{association[:name]}") 70 | end 71 | end 72 | 73 | module Has 74 | # Represents a parent-to-child association between resources. Options to pass in are: class, path. 75 | # @param [Symbol] resource_name_or_class The underlying resource name or a class to get it from 76 | # @param [Hash] class_level_options The options to pass to the method definition. 77 | def has(resource_name_or_class, class_level_options = {}) 78 | if klass = class_level_options.delete(:class) 79 | resource_name = resource_name_or_class 80 | else 81 | klass = resource_name_or_class 82 | resource_name = klass.singular_resource_name 83 | end 84 | 85 | class_level_association = build_association(klass, resource_name, class_level_options) 86 | class_level_association.merge!(:singular => true, :id_column => "#{resource_name}_id") 87 | 88 | associations << class_level_association 89 | 90 | define_used(class_level_association) 91 | define_has_getter(class_level_association) 92 | define_has_setter(class_level_association) 93 | end 94 | 95 | private 96 | 97 | def define_has_getter(association) 98 | klass = association[:class] # shorthand 99 | 100 | define_method association[:name] do |*args| 101 | instance_options = args.last.is_a?(Hash) ? args.pop : {} 102 | 103 | # return if cached 104 | cached = instance_variable_get("@#{association[:name]}") 105 | return cached if cached && !instance_options[:reload] 106 | 107 | # find and cache association 108 | instance_association = Association.new(association.merge(:parent => self)) 109 | resource = if klass.respond_to?(:find) && resource_id = method_missing(association[:id_column]) 110 | klass.find(@client, :id => resource_id, :association => instance_association) 111 | elsif found = method_missing(association[:name].to_sym) 112 | wrap_resource(found, association, :include_key => association[:include_key]) 113 | elsif klass.superclass == DataResource && !association[:inline] 114 | response = @client.connection.get(instance_association.generate_path(:with_parent => true)) 115 | klass.new(@client, response.body[klass.singular_resource_name].merge(:association => instance_association)) 116 | end 117 | 118 | send("#{association[:id_column]}=", resource.id) if resource && has_key?(association[:id_column]) 119 | instance_variable_set("@#{association[:name]}", resource) 120 | end 121 | end 122 | 123 | def define_has_setter(association) 124 | define_method "#{association[:name]}=" do |resource| 125 | resource = wrap_resource(resource, association) 126 | send("#{association[:id_column]}=", resource.id) if has_key?(association[:id_column]) 127 | instance_variable_set("@#{association[:name]}", resource) 128 | end 129 | end 130 | end 131 | 132 | module HasMany 133 | # Represents a parent-to-children association between resources. Options to pass in are: class, path. 134 | # @param [Symbol] resource_name_or_class The underlying resource name or class to get it from 135 | # @param [Hash] class_level_options The options to pass to the method definition. 136 | def has_many(resource_name_or_class, class_level_options = {}) 137 | if klass = class_level_options.delete(:class) 138 | resource_name = resource_name_or_class 139 | else 140 | klass = resource_name_or_class 141 | resource_name = klass.resource_name 142 | end 143 | 144 | class_level_association = build_association(klass, resource_name, class_level_options) 145 | class_level_association.merge!(:singular => false, :id_column => "#{resource_name}_ids") 146 | 147 | associations << class_level_association 148 | 149 | define_used(class_level_association) 150 | define_has_many_getter(class_level_association) 151 | define_has_many_setter(class_level_association) 152 | end 153 | 154 | private 155 | 156 | def define_has_many_getter(association) 157 | klass = association[:class] 158 | 159 | define_method association[:name] do |*args| 160 | instance_opts = args.last.is_a?(Hash) ? args.pop : {} 161 | 162 | # return if cached 163 | cached = instance_variable_get("@#{association[:name]}") 164 | return cached if cached && !instance_opts[:reload] 165 | 166 | # find and cache association 167 | instance_association = Association.new(association.merge(:parent => self)) 168 | singular_resource_name = Inflection.singular(association[:name].to_s) 169 | 170 | resources = if (ids = method_missing("#{singular_resource_name}_ids")) && ids.any? 171 | ids.map do |id| 172 | klass.find(@client, :id => id, :association => instance_association) 173 | end.compact 174 | elsif (resources = method_missing(association[:name].to_sym)) && resources.any? 175 | resources.map { |res| wrap_resource(res, association) } 176 | else 177 | [] 178 | end 179 | 180 | collection = ZendeskAPI::Collection.new(@client, klass, instance_opts.merge(:association => instance_association)) 181 | 182 | if association[:extensions].any? 183 | collection.extend(*association[:extensions]) 184 | end 185 | 186 | if resources.any? 187 | collection.replace(resources) 188 | end 189 | 190 | send("#{association[:id_column]}=", resources.map(&:id)) if has_key?(association[:id_column]) 191 | instance_variable_set("@#{association[:name]}", collection) 192 | end 193 | end 194 | 195 | def define_has_many_setter(association) 196 | define_method "#{association[:name]}=" do |resources| 197 | if resources.is_a?(Array) 198 | wrapped = resources.map { |attr| wrap_resource(attr, association) } 199 | send(association[:name]).replace(wrapped) 200 | else 201 | resources.association = Association.new(association.merge(:parent => self)) 202 | instance_variable_set("@#{association[:name]}", resources) 203 | end 204 | 205 | send("#{association[:id_column]}=", resources.map(&:id)) if resources && has_key?(association[:id_column]) 206 | resource 207 | end 208 | end 209 | end 210 | end 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /lib/zendesk_api/configuration.rb: -------------------------------------------------------------------------------- 1 | module ZendeskAPI 2 | # Holds the configuration options for the client and connection 3 | class Configuration 4 | # @return [String] The basic auth username. 5 | attr_accessor :username 6 | 7 | # @return [String] The basic auth password. 8 | attr_accessor :password 9 | 10 | # @return [String] The basic auth token. 11 | attr_accessor :token 12 | 13 | # @return [String] The API url. Must be https unless {#allow_http} is set. 14 | attr_accessor :url 15 | 16 | # @return [Boolean] Whether to attempt to retry when rate-limited (http status: 429). 17 | attr_accessor :retry 18 | 19 | # @return [Boolean] Whether to raise error when rate-limited (http status: 429). 20 | attr_accessor :raise_error_when_rate_limited 21 | 22 | # @return [Logger] Logger to use when logging requests. 23 | attr_accessor :logger 24 | 25 | # @return [Hash] Client configurations (eg ssh config) to pass to Faraday 26 | attr_accessor :client_options 27 | 28 | # @return [Symbol] Faraday adapter 29 | attr_accessor :adapter 30 | 31 | # @return [Proc] Faraday adapter proc 32 | attr_accessor :adapter_proc 33 | 34 | # @return [Boolean] Whether to allow non-HTTPS connections for development purposes. 35 | attr_accessor :allow_http 36 | 37 | # @return [String] OAuth2 access_token 38 | attr_accessor :access_token 39 | 40 | attr_accessor :url_based_access_token 41 | 42 | # Use this cache instead of default ZendeskAPI::LRUCache.new 43 | # - must respond to read/write/fetch e.g. ActiveSupport::Cache::MemoryStore.new) 44 | # - pass false to disable caching 45 | # @return [ZendeskAPI::LRUCache] 46 | attr_accessor :cache 47 | 48 | # @return [Boolean] Whether to use resource_cache or not 49 | attr_accessor :use_resource_cache 50 | 51 | # specify the server error codes in which you want a retry to be attempted 52 | attr_accessor :retry_codes 53 | 54 | # specify if you want a (network layer) exception to elicit a retry 55 | attr_accessor :retry_on_exception 56 | 57 | def initialize 58 | @client_options = {} 59 | @use_resource_cache = true 60 | 61 | self.cache = ZendeskAPI::LRUCache.new(1000) 62 | end 63 | 64 | # Sets accept and user_agent headers, and url. 65 | # 66 | # @return [Hash] Faraday-formatted hash of options. 67 | def options 68 | { 69 | :headers => { 70 | :accept => 'application/json', 71 | :accept_encoding => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 72 | :user_agent => "ZendeskAPI Ruby #{ZendeskAPI::VERSION}" 73 | }, 74 | :request => { 75 | :open_timeout => 10, 76 | :timeout => 60 77 | }, 78 | :url => @url 79 | }.merge(client_options) 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/zendesk_api/core_ext/inflection.rb: -------------------------------------------------------------------------------- 1 | require 'inflection' 2 | 3 | Inflection.plural_rule 'forum', 'forums' 4 | -------------------------------------------------------------------------------- /lib/zendesk_api/delegator.rb: -------------------------------------------------------------------------------- 1 | require 'delegate' 2 | 3 | module ZendeskAPI 4 | class Delegator < SimpleDelegator; end 5 | end 6 | -------------------------------------------------------------------------------- /lib/zendesk_api/error.rb: -------------------------------------------------------------------------------- 1 | # tested via spec/core/middleware/response/raise_error_spec.rb 2 | module ZendeskAPI 3 | module Error 4 | class ClientError < Faraday::ClientError 5 | attr_reader :wrapped_exception 6 | 7 | def to_s 8 | if response 9 | "#{super} -- #{response.method} #{response.url}" 10 | else 11 | super 12 | end 13 | end 14 | end 15 | 16 | class RecordInvalid < ClientError 17 | attr_accessor :errors 18 | 19 | def initialize(*) 20 | super 21 | 22 | if response[:body].is_a?(Hash) 23 | @errors = response[:body]["details"] || generate_error_msg(response[:body]) 24 | end 25 | 26 | @errors ||= {} 27 | end 28 | 29 | def to_s 30 | "#{self.class.name}: #{@errors}" 31 | end 32 | 33 | private 34 | 35 | def generate_error_msg(response_body) 36 | response_body.values_at("description", "message", "error", "errors").compact.join(" - ") 37 | end 38 | end 39 | 40 | class NetworkError < ClientError; end 41 | class RecordNotFound < ClientError; end 42 | class RateLimited < ClientError; end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/zendesk_api/helpers.rb: -------------------------------------------------------------------------------- 1 | module ZendeskAPI 2 | # @private 3 | module Helpers 4 | def self.present?(value) 5 | ![nil, false, "", " ", [], {}].include?(value) 6 | end 7 | 8 | # From https://github.com/rubyworks/facets/blob/master/lib/core/facets/string/modulize.rb 9 | # Converts a string to module name representation. 10 | # 11 | # This is essentially #camelcase, but it also converts 12 | # '/' to '::' which is useful for converting paths to 13 | # namespaces. 14 | # 15 | # Examples 16 | # 17 | # "method_name".modulize #=> "MethodName" 18 | # "method/name".modulize #=> "Method::Name" 19 | # 20 | # @param string [string] input, `module/class_name` 21 | # @return [string] a string that can become a class, `Module::ClassName` 22 | def self.modulize_string(string) 23 | # gsub('__','/'). # why was this ever here? 24 | string.gsub(/__(.?)/) { "::#{$1.upcase}" }. 25 | gsub(/\/(.?)/) { "::#{$1.upcase}" }. 26 | gsub(/(?:_+|-+)([a-z])/) { $1.upcase }. 27 | gsub(/(\A|\s)([a-z])/) { $1 + $2.upcase } 28 | end 29 | 30 | # From https://github.com/rubyworks/facets/blob/master/lib/core/facets/string/snakecase.rb 31 | # Underscore a string such that camelcase, dashes and spaces are 32 | # replaced by underscores. This is the reverse of {#camelcase}, 33 | # albeit not an exact inverse. 34 | # 35 | # "SnakeCase".snakecase #=> "snake_case" 36 | # "Snake-Case".snakecase #=> "snake_case" 37 | # "Snake Case".snakecase #=> "snake_case" 38 | # "Snake - Case".snakecase #=> "snake_case" 39 | def self.snakecase_string(string) 40 | # gsub(/::/, '/'). 41 | string.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2'). 42 | gsub(/([a-z\d])([A-Z])/, '\1_\2'). 43 | tr('-', '_'). 44 | gsub(/\s/, '_'). 45 | gsub(/__+/, '_'). 46 | downcase 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/zendesk_api/lru_cache.rb: -------------------------------------------------------------------------------- 1 | module ZendeskAPI 2 | # http://codesnippets.joyent.com/posts/show/12329 3 | # @private 4 | class ZendeskAPI::LRUCache 5 | attr_accessor :size 6 | 7 | def initialize(size = 10) 8 | @size = size 9 | @store = {} 10 | @lru = [] 11 | end 12 | 13 | def write(key, value) 14 | @store[key] = value 15 | set_lru(key) 16 | @store.delete(@lru.pop) if @lru.size > @size 17 | value 18 | end 19 | 20 | def read(key) 21 | set_lru(key) 22 | @store[key] 23 | end 24 | 25 | def fetch(key) 26 | if @store.has_key? key 27 | read key 28 | else 29 | write key, yield 30 | end 31 | end 32 | 33 | private 34 | 35 | def set_lru(key) 36 | @lru.unshift(@lru.delete(key) || key) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/zendesk_api/middleware/request/encode_json.rb: -------------------------------------------------------------------------------- 1 | module ZendeskAPI 2 | # @private 3 | module Middleware 4 | # @private 5 | module Request 6 | class EncodeJson < Faraday::Middleware 7 | CONTENT_TYPE = 'Content-Type'.freeze 8 | MIME_TYPE = 'application/json'.freeze 9 | 10 | def call(env) 11 | type = env[:request_headers][CONTENT_TYPE].to_s 12 | type = type.split(';', 2).first if type.index(';') 13 | type 14 | 15 | if env[:body] && !(env[:body].respond_to?(:to_str) && env[:body].empty?) && (type.empty? || type == MIME_TYPE) 16 | env[:body] = JSON.dump(env[:body]) 17 | env[:request_headers][CONTENT_TYPE] ||= MIME_TYPE 18 | end 19 | 20 | @app.call(env) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/zendesk_api/middleware/request/etag_cache.rb: -------------------------------------------------------------------------------- 1 | require "faraday/middleware" 2 | 3 | module ZendeskAPI 4 | module Middleware 5 | module Request 6 | # Request middleware that caches responses based on etags 7 | # can be removed once this is merged: https://github.com/pengwynn/faraday_middleware/pull/42 8 | # @private 9 | class EtagCache < Faraday::Middleware 10 | def initialize(app, options = {}) 11 | @app = app 12 | @cache = options[:cache] || 13 | raise("need :cache option e.g. ActiveSupport::Cache::MemoryStore.new") 14 | @cache_key_prefix = options.fetch(:cache_key_prefix, :faraday_etags) 15 | end 16 | 17 | def cache_key(env) 18 | [@cache_key_prefix, env[:url].to_s] 19 | end 20 | 21 | def call(environment) 22 | return @app.call(environment) unless [:get, :head].include?(environment[:method]) 23 | 24 | # send known etag 25 | cached = @cache.read(cache_key(environment)) 26 | 27 | if cached 28 | environment[:request_headers]["If-None-Match"] ||= cached[:response_headers]["Etag"] 29 | end 30 | 31 | @app.call(environment).on_complete do |env| 32 | if cached && env[:status] == 304 # not modified 33 | # Handle differences in serialized env keys in Faraday < 1.0 and 1.0 34 | # See https://github.com/lostisland/faraday/pull/847 35 | env[:body] = cached[:body] 36 | env[:response_body] = cached[:response_body] 37 | 38 | env[:response_headers].merge!( 39 | :etag => cached[:response_headers][:etag], 40 | :content_type => cached[:response_headers][:content_type], 41 | :content_length => cached[:response_headers][:content_length], 42 | :content_encoding => cached[:response_headers][:content_encoding] 43 | ) 44 | elsif env[:status] == 200 && env[:response_headers]["Etag"] # modified and cacheable 45 | @cache.write(cache_key(env), env.to_hash) 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/zendesk_api/middleware/request/raise_rate_limited.rb: -------------------------------------------------------------------------------- 1 | require 'faraday/middleware' 2 | require 'zendesk_api/error' 3 | 4 | module ZendeskAPI 5 | module Middleware 6 | # @private 7 | module Request 8 | # Faraday middleware to handle HTTP Status 429 (rate limiting) / 503 (maintenance) 9 | # @private 10 | class RaiseRateLimited < Faraday::Middleware 11 | ERROR_CODES = [429, 503].freeze 12 | 13 | def initialize(app, options = {}) 14 | super(app) 15 | @logger = options[:logger] 16 | end 17 | 18 | def call(env) 19 | response = @app.call(env) 20 | 21 | if ERROR_CODES.include?(response.env[:status]) 22 | @logger&.warn 'You have been rate limited. Raising error...' 23 | raise Error::RateLimited, env 24 | else 25 | response 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/zendesk_api/middleware/request/retry.rb: -------------------------------------------------------------------------------- 1 | require "faraday/middleware" 2 | module ZendeskAPI 3 | module Middleware 4 | # @private 5 | module Request 6 | # Faraday middleware to handle HTTP Status 429 (rate limiting) / 503 (maintenance) 7 | # @private 8 | class Retry < Faraday::Middleware 9 | DEFAULT_RETRY_AFTER = 10 10 | DEFAULT_ERROR_CODES = [429, 503] 11 | 12 | def initialize(app, options = {}) 13 | super(app) 14 | @logger = options[:logger] 15 | @error_codes = options.key?(:retry_codes) && options[:retry_codes] ? options[:retry_codes] : DEFAULT_ERROR_CODES 16 | @retry_on_exception = options.key?(:retry_on_exception) && options[:retry_on_exception] ? options[:retry_on_exception] : false 17 | end 18 | 19 | def call(env) 20 | original_env = env.dup 21 | exception_happened = false 22 | if @retry_on_exception 23 | begin 24 | response = @app.call(env) 25 | rescue StandardError => e 26 | exception_happened = true 27 | end 28 | else 29 | response = @app.call(env) 30 | end 31 | 32 | if exception_happened || @error_codes.include?(response.env[:status]) 33 | 34 | if exception_happened 35 | seconds_left = DEFAULT_RETRY_AFTER.to_i 36 | @logger.warn "An exception happened, waiting #{seconds_left} seconds... #{e}" if @logger 37 | else 38 | seconds_left = (response.env[:response_headers][:retry_after] || DEFAULT_RETRY_AFTER).to_i 39 | end 40 | 41 | @logger.warn "You have been rate limited. Retrying in #{seconds_left} seconds..." if @logger 42 | 43 | seconds_left.times do |i| 44 | sleep 1 45 | time_left = seconds_left - i 46 | @logger.warn "#{time_left}..." if time_left > 0 && time_left % 5 == 0 && @logger 47 | end 48 | 49 | @logger.warn "" if @logger 50 | 51 | @app.call(original_env) 52 | else 53 | response 54 | end 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/zendesk_api/middleware/request/upload.rb: -------------------------------------------------------------------------------- 1 | require "faraday/middleware" 2 | require "mini_mime" 3 | require 'tempfile' 4 | 5 | module ZendeskAPI 6 | module Middleware 7 | module Request 8 | # @private 9 | class Upload < Faraday::Middleware 10 | def call(env) 11 | if env[:body] 12 | set_file(env[:body], :file, true) 13 | traverse_hash(env[:body]) 14 | end 15 | 16 | @app.call(env) 17 | end 18 | 19 | private 20 | 21 | # Sets the proper file parameters :uploaded_data and :filename 22 | # If top_level, then it removes key and and sets the parameters directly on hash, 23 | # otherwise it adds the parameters to hash[key] 24 | def set_file(hash, key, top_level) 25 | return unless hash.key?(key) 26 | 27 | file = if hash[key].is_a?(Hash) && hash[key].key?(:file) 28 | hash[key].delete(:file) 29 | else 30 | hash.delete(key) 31 | end 32 | 33 | case file 34 | when File, Tempfile 35 | path = file.path 36 | when String 37 | path = file 38 | else 39 | if defined?(ActionDispatch) && file.is_a?(ActionDispatch::Http::UploadedFile) 40 | path = file.tempfile.path 41 | mime_type = file.content_type 42 | else 43 | warn "WARNING: Passed invalid filename #{file} of type #{file.class} to upload" 44 | end 45 | end 46 | 47 | if path 48 | if !top_level 49 | hash[key] ||= {} 50 | hash = hash[key] 51 | end 52 | 53 | unless defined?(mime_type) && !mime_type.nil? 54 | mime_type = MiniMime.lookup_by_filename(path) 55 | mime_type = mime_type ? mime_type.content_type : "application/octet-stream" 56 | end 57 | 58 | hash[:filename] ||= if file.respond_to?(:original_filename) 59 | file.original_filename 60 | else 61 | File.basename(path) 62 | end 63 | 64 | hash[:uploaded_data] = Faraday::UploadIO.new(path, mime_type, hash[:filename]) 65 | end 66 | end 67 | 68 | # Calls #set_file on File instances or Hashes 69 | # of the format { :file => File (, :filename => ...) } 70 | def traverse_hash(hash) 71 | hash.keys.each do |key| 72 | if hash[key].is_a?(File) 73 | set_file(hash, key, false) 74 | elsif hash[key].is_a?(Hash) 75 | if hash[key].key?(:file) 76 | set_file(hash, key, false) 77 | else 78 | traverse_hash(hash[key]) 79 | end 80 | end 81 | end 82 | end 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/zendesk_api/middleware/request/url_based_access_token.rb: -------------------------------------------------------------------------------- 1 | module ZendeskAPI 2 | # @private 3 | module Middleware 4 | # @private 5 | module Request 6 | class UrlBasedAccessToken < Faraday::Middleware 7 | def initialize(app, token) 8 | super(app) 9 | @token = token 10 | end 11 | 12 | def call(env) 13 | if env[:url].query 14 | env[:url].query += '&' 15 | else 16 | env[:url].query = '' 17 | end 18 | 19 | env[:url].query += "access_token=#{@token}" 20 | 21 | @app.call(env) 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/zendesk_api/middleware/response/callback.rb: -------------------------------------------------------------------------------- 1 | require "faraday/response" 2 | 3 | module ZendeskAPI 4 | module Middleware 5 | module Response 6 | # @private 7 | class Callback < Faraday::Middleware 8 | def initialize(app, client) 9 | super(app) 10 | @client = client 11 | end 12 | 13 | def call(env) 14 | @app.call(env).on_complete do |env| 15 | @client.callbacks.each { |c| c.call(env) } 16 | end 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/zendesk_api/middleware/response/deflate.rb: -------------------------------------------------------------------------------- 1 | module ZendeskAPI 2 | # @private 3 | module Middleware 4 | # @private 5 | module Response 6 | # Faraday middleware to handle content-encoding = inflate 7 | # @private 8 | class Deflate < Faraday::Middleware 9 | def on_complete(env) 10 | return if env[:response_headers]['content-encoding'] != "deflate" 11 | return if env.body.strip.empty? 12 | 13 | env.body = Zlib::Inflate.inflate(env.body) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/zendesk_api/middleware/response/gzip.rb: -------------------------------------------------------------------------------- 1 | require 'zlib' 2 | require 'stringio' 3 | 4 | module ZendeskAPI 5 | # @private 6 | module Middleware 7 | # @private 8 | module Response 9 | # Faraday middleware to handle content-encoding = gzip 10 | class Gzip < Faraday::Middleware 11 | def on_complete(env) 12 | return if env[:response_headers]['content-encoding'] != "gzip" 13 | return if env[:body].force_encoding(Encoding::BINARY).strip.empty? 14 | 15 | env[:body] = Zlib::GzipReader.new(StringIO.new(env[:body])).read 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/zendesk_api/middleware/response/logger.rb: -------------------------------------------------------------------------------- 1 | module ZendeskAPI 2 | module Middleware 3 | module Response 4 | # Faraday middleware to handle logging 5 | # @private 6 | class Logger < Faraday::Middleware 7 | LOG_LENGTH = 1000 8 | 9 | def initialize(app, logger = nil) 10 | super(app) 11 | 12 | @logger = logger || begin 13 | require 'logger' 14 | ::Logger.new(STDOUT) 15 | end 16 | end 17 | 18 | def call(env) 19 | @logger.info "#{env[:method]} #{env[:url].to_s}" 20 | @logger.debug dump_debug(env, :request_headers) 21 | 22 | @app.call(env).on_complete do |env| 23 | info = "Status #{env[:status]}" 24 | info.concat(" #{env[:body].to_s[0, LOG_LENGTH]}") if (400..499).cover?(env[:status].to_i) 25 | 26 | @logger.info info 27 | @logger.debug dump_debug(env, :response_headers) 28 | end 29 | end 30 | 31 | private 32 | 33 | def dump_debug(env, headers_key) 34 | info = env[headers_key].map { |k, v| " #{k}: #{v.inspect}" }.join("\n") 35 | unless env[:body].nil? 36 | info.concat("\n") 37 | info.concat(env[:body].inspect) 38 | end 39 | info 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/zendesk_api/middleware/response/parse_iso_dates.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | require "faraday/response" 3 | 4 | module ZendeskAPI 5 | module Middleware 6 | module Response 7 | # Parse ISO dates from response body 8 | # @private 9 | class ParseIsoDates < Faraday::Middleware 10 | def call(env) 11 | @app.call(env).on_complete do |env| 12 | parse_dates!(env[:body]) 13 | end 14 | end 15 | 16 | private 17 | 18 | def parse_dates!(value) 19 | case value 20 | when Hash then value.each { |key, element| value[key] = parse_dates!(element) } 21 | when Array then value.each_with_index { |element, index| value[index] = parse_dates!(element) } 22 | when /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\Z/m then Time.parse(value) 23 | else 24 | value 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/zendesk_api/middleware/response/parse_json.rb: -------------------------------------------------------------------------------- 1 | module ZendeskAPI 2 | # @private 3 | module Middleware 4 | # @private 5 | module Response 6 | class ParseJson < Faraday::Middleware 7 | CONTENT_TYPE = 'Content-Type'.freeze 8 | 9 | def on_complete(env) 10 | type = env[:response_headers][CONTENT_TYPE].to_s 11 | type = type.split(';', 2).first if type.index(';') 12 | 13 | return unless type == 'application/json' 14 | 15 | unless env[:body].strip.empty? 16 | env[:body] = JSON.parse(env[:body]) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/zendesk_api/middleware/response/raise_error.rb: -------------------------------------------------------------------------------- 1 | require 'zendesk_api/error' 2 | 3 | module ZendeskAPI 4 | module Middleware 5 | module Response 6 | class RaiseError < Faraday::Response::RaiseError 7 | def call(env) 8 | super 9 | rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e 10 | raise Error::NetworkError.new(e, env) 11 | end 12 | 13 | def on_complete(env) 14 | case env[:status] 15 | when 404 16 | raise Error::RecordNotFound.new(env) 17 | when 422, 413 18 | raise Error::RecordInvalid.new(env) 19 | when 100..199, 400..599, 300..303, 305..399 20 | raise Error::NetworkError.new(env) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/zendesk_api/middleware/response/sanitize_response.rb: -------------------------------------------------------------------------------- 1 | module ZendeskAPI 2 | module Middleware 3 | module Response 4 | class SanitizeResponse < Faraday::Middleware 5 | def on_complete(env) 6 | env[:body].scrub!('') 7 | end 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/zendesk_api/pagination.rb: -------------------------------------------------------------------------------- 1 | module ZendeskAPI 2 | class Collection 3 | # Contains all methods related to pagination in an attempt to slim down collection.rb 4 | module Pagination 5 | DEFAULT_PAGE_SIZE = 100 6 | def more_results?(response) 7 | Helpers.present?(response["meta"]) && response["meta"]["has_more"] 8 | end 9 | alias has_more_results? more_results? # For backward compatibility with 1.33.0 and 1.34.0 10 | 11 | # Changes the per_page option. Returns self, so it can be chained. No execution. 12 | # @return [Collection] self 13 | def per_page(count) 14 | clear_cache if count 15 | @options["per_page"] = count 16 | self 17 | end 18 | 19 | # Changes the page option. Returns self, so it can be chained. No execution. 20 | # @return [Collection] self 21 | def page(number) 22 | clear_cache if number 23 | @options["page"] = number 24 | self 25 | end 26 | 27 | def first_page? 28 | !@prev_page 29 | end 30 | 31 | def last_page? 32 | !@next_page || @next_page == @query 33 | end 34 | 35 | private 36 | 37 | def page_links(body) 38 | if body["meta"] && body["links"] 39 | [body["links"]["next"], body["links"]["prev"]] 40 | else 41 | [body["next_page"], body["previous_page"]] 42 | end 43 | end 44 | 45 | def cbp_response?(body) 46 | !!(body["meta"] && body["links"]) 47 | end 48 | 49 | def set_cbp_options 50 | @options_per_page_was = @options.delete("per_page") 51 | # Default to CBP by using the page param as a map 52 | @options.page = { size: (@options_per_page_was || DEFAULT_PAGE_SIZE) } 53 | end 54 | 55 | # CBP requests look like: `/resources?page[size]=100` 56 | # OBP requests look like: `/resources?page=2` 57 | def cbp_request? 58 | @options["page"].is_a?(Hash) 59 | end 60 | 61 | def intentional_obp_request? 62 | Helpers.present?(@options["page"]) && !cbp_request? 63 | end 64 | 65 | def supports_cbp? 66 | @resource_class.cbp_path_regexes.any? { |supported_path_regex| path.match?(supported_path_regex) } 67 | end 68 | 69 | def first_cbp_request? 70 | # @next_page will be nil when making the first cbp request 71 | @next_page.nil? 72 | end 73 | 74 | def set_page_and_count(body) 75 | @count = (body["count"] || @resources.size).to_i 76 | @next_page, @prev_page = page_links(body) 77 | 78 | if cbp_response?(body) 79 | set_cbp_response_options(body) 80 | elsif @next_page =~ /page=(\d+)/ 81 | @options["page"] = Regexp.last_match(1).to_i - 1 82 | elsif @prev_page =~ /page=(\d+)/ 83 | @options["page"] = Regexp.last_match(1).to_i + 1 84 | end 85 | end 86 | 87 | def set_cbp_response_options(body) 88 | @options.page = {} unless cbp_request? 89 | # the line above means an intentional CBP request where page[size] is passed on the query 90 | # this is to cater for CBP responses where we don't specify page[size] but the endpoint 91 | # responds CBP by default. i.e `client.trigger_categories.fetch` 92 | @options.page.merge!( 93 | before: body["meta"]["before_cursor"], 94 | after: body["meta"]["after_cursor"] 95 | ) 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/zendesk_api/resource.rb: -------------------------------------------------------------------------------- 1 | require 'zendesk_api/helpers' 2 | require 'zendesk_api/trackie' 3 | require 'zendesk_api/actions' 4 | require 'zendesk_api/association' 5 | require 'zendesk_api/associations' 6 | require 'zendesk_api/verbs' 7 | 8 | # See docs: https://developer.zendesk.com/api-reference/ 9 | module ZendeskAPI 10 | # Represents an abstract resource that only holds data. 11 | class Data 12 | include Associations 13 | 14 | class << self 15 | def inherited(klass) 16 | subclasses.push(klass) 17 | end 18 | 19 | def subclasses 20 | @subclasses ||= [] 21 | end 22 | 23 | # The singular resource name taken from the class name (e.g. ZendeskAPI::Ticket -> ticket) 24 | def singular_resource_name 25 | @singular_resource_name ||= ZendeskAPI::Helpers.snakecase_string(to_s.split("::").last) 26 | end 27 | 28 | # The resource name taken from the class name (e.g. ZendeskAPI::Ticket -> tickets) 29 | def resource_name 30 | @resource_name ||= Inflection.plural(singular_resource_name) 31 | end 32 | 33 | def resource_path 34 | [@namespace, resource_name].compact.join("/") 35 | end 36 | 37 | alias :model_key :resource_name 38 | 39 | def namespace(namespace) 40 | @namespace = namespace 41 | end 42 | 43 | def new_from_response(client, response, includes = nil) 44 | new(client).tap do |resource| 45 | resource.handle_response(response) 46 | resource.set_includes(resource, includes, response.body) if includes 47 | resource.attributes.clear_changes 48 | end 49 | end 50 | end 51 | 52 | # @return [Hash] The resource's attributes 53 | attr_reader :attributes 54 | # @return [ZendeskAPI::Association] The association 55 | attr_accessor :association 56 | # @return [Array] The last received errors 57 | attr_accessor :errors 58 | # Place to dump the last response 59 | attr_accessor :response 60 | 61 | # Create a new resource instance. 62 | # @param [Client] client The client to use 63 | # @param [Hash] attributes The optional attributes that describe the resource 64 | def initialize(client, attributes = {}) 65 | raise "Expected a Hash for attributes, got #{attributes.inspect}" unless attributes.is_a?(Hash) 66 | 67 | @association = attributes.delete(:association) || Association.new(:class => self.class) 68 | @global_params = attributes.delete(:global) || {} 69 | @client = client 70 | @attributes = ZendeskAPI::Trackie.new(attributes) 71 | 72 | if self.class.associations.none? { |a| a[:name] == self.class.singular_resource_name } 73 | ZendeskAPI::Client.check_deprecated_namespace_usage @attributes, self.class.singular_resource_name 74 | end 75 | 76 | @attributes.clear_changes unless new_record? 77 | end 78 | 79 | def self.new_from_response(client, response, includes = nil) 80 | new(client).tap do |resource| 81 | resource.handle_response(response) 82 | resource.set_includes(resource, includes, response.body) if includes 83 | resource.attributes.clear_changes 84 | end 85 | end 86 | 87 | # Passes the method onto the attributes hash. 88 | # If the attributes are nested (e.g. { :tickets => { :id => 1 } }), passes the method onto the nested hash. 89 | def method_missing(*args, &block) 90 | raise NoMethodError, ":save is not defined" if args.first.to_sym == :save 91 | @attributes.send(*args, &block) 92 | end 93 | 94 | def respond_to_missing?(method, include_private = false) 95 | @attributes.respond_to?(method) || super 96 | end 97 | 98 | # Returns the resource id of the object or nil 99 | def id 100 | key?(:id) ? method_missing(:id) : nil 101 | end 102 | 103 | # Has this been object been created server-side? Does this by checking for an id. 104 | def new_record? 105 | id.nil? 106 | end 107 | 108 | # @private 109 | def loaded_associations 110 | self.class.associations.select do |association| 111 | loaded = @attributes.method_missing(association[:name]) 112 | loaded && !(loaded.respond_to?(:empty?) && loaded.empty?) 113 | end 114 | end 115 | 116 | # Returns the path to the resource 117 | def path(options = {}) 118 | @association.generate_path(self, options) 119 | end 120 | 121 | # Passes #to_json to the underlying attributes hash 122 | def to_json(*args) 123 | method_missing(:to_json, *args) 124 | end 125 | 126 | # @private 127 | def to_s 128 | "#{self.class.singular_resource_name}: #{attributes.inspect}" 129 | end 130 | alias :inspect :to_s 131 | 132 | # Compares resources by class and id. If id is nil, then by object_id 133 | def ==(other) 134 | return false unless other 135 | 136 | return true if other.object_id == object_id 137 | 138 | return other.id && (other.id == id) if other.is_a?(Data) 139 | 140 | return id == other if other.is_a?(Integer) 141 | 142 | warn "Trying to compare #{other.class} to a Resource 143 | from #{caller.first}" 144 | end 145 | alias :eql :== 146 | 147 | # @private 148 | def inspect 149 | "#<#{self.class.name} #{@attributes.to_hash.inspect}>" 150 | end 151 | 152 | alias :to_param :attributes 153 | 154 | def attributes_for_save 155 | { self.class.singular_resource_name.to_sym => attribute_changes } 156 | end 157 | 158 | private 159 | 160 | # Send only the changes, for example, if the "status" attriubte 161 | # goes from "new" to "new", we don't need to send anything 162 | def attribute_changes 163 | attributes.changes 164 | end 165 | end 166 | 167 | # Indexable resource 168 | class DataResource < Data 169 | attr_accessor :error, :error_message 170 | extend Verbs 171 | 172 | def self.cbp_path_regexes 173 | [] 174 | end 175 | end 176 | 177 | # Represents a resource that can only GET 178 | class ReadResource < DataResource 179 | include Read 180 | end 181 | 182 | # Represents a resource that can only POST 183 | class CreateResource < DataResource 184 | include Create 185 | end 186 | 187 | # Represents a resource that can only PUT 188 | class UpdateResource < DataResource 189 | include Update 190 | end 191 | 192 | # Represents a resource that can only DELETE 193 | class DeleteResource < DataResource 194 | include Destroy 195 | end 196 | 197 | # Represents an abstract resource that can CRUD (create, read, update, delete). 198 | class Resource < DataResource 199 | include Read 200 | include Create 201 | 202 | include Update 203 | include Destroy 204 | end 205 | 206 | class SingularResource < Resource 207 | def attributes_for_save 208 | { self.class.resource_name.to_sym => attribute_changes } 209 | end 210 | end 211 | 212 | # Namespace parent class for Data/Resource classes 213 | module DataNamespace 214 | class << self 215 | def included(base) 216 | @descendants ||= [] 217 | @descendants << base 218 | end 219 | 220 | def descendants 221 | @descendants || [] 222 | end 223 | end 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /lib/zendesk_api/search.rb: -------------------------------------------------------------------------------- 1 | # `zendesk_api` gem root 2 | module ZendeskAPI 3 | # A rich factory that returns a class for your searches 4 | class Search 5 | # Creates a search collection 6 | def self.search(client, options = {}) 7 | if (options.keys.map(&:to_s) & %w[query external_id]).empty? 8 | warn "you have not specified a query for this search" 9 | end 10 | 11 | ZendeskAPI::Collection.new(client, self, options) 12 | end 13 | 14 | # Quack like a Resource 15 | # Creates the correct resource class from `attributes[:result_type]` 16 | def self.new(client, attributes) 17 | present_result_type = (attributes[:result_type] || attributes["result_type"]).to_s 18 | result_type = ZendeskAPI::Helpers.modulize_string(present_result_type) 19 | klass = begin 20 | ZendeskAPI.const_get(result_type) 21 | rescue NameError 22 | Result 23 | end 24 | 25 | (klass || Result).new(client, attributes) 26 | end 27 | 28 | def self.cbp_path_regexes 29 | [] 30 | end 31 | 32 | class Result < Data; end 33 | 34 | class << self 35 | def resource_name 36 | "search" 37 | end 38 | alias resource_path resource_name 39 | 40 | def model_key 41 | "results" 42 | end 43 | end 44 | end 45 | 46 | # This will use cursor pagination by default 47 | class SearchExport < Search 48 | class << self 49 | def resource_name 50 | "search/export" 51 | end 52 | alias resource_path resource_name 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/zendesk_api/sideloading.rb: -------------------------------------------------------------------------------- 1 | module ZendeskAPI 2 | # @private 3 | module Sideloading 4 | def self.included(klass) 5 | klass.send(:attr_reader, :included) 6 | end 7 | 8 | def set_includes(resource_or_resources, includes, body) 9 | @included = {} 10 | 11 | includes.each do |side_load| 12 | unless body.key?(side_load.to_s) 13 | @client.config.logger.warn "Missing #{side_load} key in response -- cannot side load" 14 | end 15 | end 16 | 17 | resources = to_array(resource_or_resources) 18 | resource_class = resources.first.class 19 | 20 | return if resources.empty? 21 | 22 | body.keys.each do |name| 23 | @included[name] = body[name] 24 | _side_load(name, resource_class, resources) 25 | end 26 | end 27 | 28 | private 29 | 30 | # Traverses the resource looking for associations 31 | # then descends into those associations and checks for applicable 32 | # resources to side load 33 | def _side_load(name, klass, resources) 34 | associations = klass.associated_with(name) 35 | 36 | associations.each do |association| 37 | association.side_load(resources, @included[name]) 38 | end 39 | 40 | resources.each do |resource| 41 | loaded_associations = resource.loaded_associations 42 | loaded_associations.each do |association| 43 | loaded = resource.send(association[:name]) 44 | next unless loaded 45 | _side_load(name, association[:class], to_array(loaded)) 46 | end 47 | end 48 | end 49 | 50 | def to_array(item) 51 | if item.is_a?(Collection) 52 | item 53 | else 54 | [item].flatten.compact 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/zendesk_api/silent_mash.rb: -------------------------------------------------------------------------------- 1 | require 'hashie' 2 | 3 | module ZendeskAPI 4 | # @private 5 | class SilentMash < Hashie::Mash 6 | disable_warnings 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/zendesk_api/track_changes.rb: -------------------------------------------------------------------------------- 1 | module ZendeskAPI 2 | # Shamelessly stolen and modified from https://github.com/archan937/dirty_hashy 3 | # @private 4 | module TrackChanges 5 | def self.included(base) 6 | base.method_defined?(:regular_writer).tap do |defined| 7 | base.send :include, InstanceMethods 8 | unless defined 9 | base.send :alias_method, :_store, :store 10 | base.send :alias_method, :store, :regular_writer 11 | base.send :alias_method, :[]=, :store 12 | base.send :define_method, :update do |other| 13 | other.each { |key, value| store key, value } 14 | end 15 | base.send :alias_method, :merge!, :update 16 | end 17 | end 18 | end 19 | 20 | # @private 21 | module InstanceMethods 22 | def clear_changes 23 | each do |k, v| 24 | if v.respond_to?(:clear_changes) 25 | v.clear_changes 26 | elsif v.is_a?(Array) 27 | v.each do |val| 28 | if val.respond_to?(:clear_changes) 29 | val.clear_changes 30 | end 31 | end 32 | end 33 | end 34 | 35 | changes.clear 36 | end 37 | 38 | def replace(other) 39 | clear 40 | merge! other 41 | end 42 | 43 | def clear 44 | keys.each { |key| delete key } 45 | end 46 | 47 | def regular_writer(key, value) 48 | if has_key?(key) && self[key] == value 49 | value 50 | else 51 | changes[key] = value 52 | defined?(_store) ? _store(key, value) : super(key, value) 53 | end 54 | end 55 | 56 | def delete(key) 57 | super.tap do |value| 58 | changes[key] = nil 59 | end 60 | end 61 | 62 | def changes 63 | (@changes ||= self.class.superclass.new).tap do |changes| 64 | each do |k, v| 65 | if v.respond_to?(:changed?) && v.changed? 66 | changes[k] = v.changes 67 | elsif v.is_a?(Array) && v.any? { |val| val.respond_to?(:changed?) && val.changed? } 68 | changes[k] = v 69 | end 70 | end 71 | end 72 | end 73 | 74 | def changed?(key = nil) 75 | if key.nil? 76 | !changes.empty? || any? { |_, v| v.respond_to?(:changed?) && v.changed? } 77 | else 78 | changes.key?(key) 79 | end 80 | end 81 | 82 | alias :dirty? :changed? 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/zendesk_api/trackie.rb: -------------------------------------------------------------------------------- 1 | require 'zendesk_api/track_changes' 2 | require 'zendesk_api/silent_mash' 3 | 4 | module ZendeskAPI 5 | # @private 6 | class Trackie < SilentMash 7 | include ZendeskAPI::TrackChanges 8 | 9 | def size 10 | self['size'] 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/zendesk_api/verbs.rb: -------------------------------------------------------------------------------- 1 | module ZendeskAPI 2 | # Creates put, post, delete class methods for custom resource methods. 3 | module Verbs 4 | class << self 5 | private 6 | 7 | # @macro [attach] container.create_verb 8 | # @method $1(method) 9 | # Executes a $1 using the passed in method as a path. 10 | # Reloads the resource's attributes if any are in the response body. 11 | # 12 | # Created method takes an optional options hash. Valid options to be passed in to the created method: reload (for caching, default: false) 13 | def create_verb(method_verb) 14 | define_method method_verb do |method| 15 | define_method "#{method}!" do |*method_args| 16 | opts = method_args.last.is_a?(Hash) ? method_args.pop : {} 17 | 18 | if method_verb == :any 19 | verb = opts.delete(:verb) 20 | raise(ArgumentError, ":verb required for method defined as :any") unless verb 21 | else 22 | verb = method_verb 23 | end 24 | 25 | @response = @client.connection.send(verb, "#{path}/#{method}") do |req| 26 | req.body = opts 27 | end 28 | 29 | return false unless @response.success? 30 | return false unless @response.body 31 | 32 | resource = nil 33 | 34 | if @response.body.is_a?(Hash) 35 | resource = @response.body[self.class.singular_resource_name] 36 | resource ||= @response.body.fetch(self.class.resource_name, []).detect { |res| res["id"] == id } 37 | end 38 | 39 | @attributes.replace @attributes.deep_merge(resource || {}) 40 | @attributes.clear_changes 41 | clear_associations 42 | 43 | true 44 | end 45 | 46 | define_method method do |*method_args| 47 | send("#{method}!", *method_args) 48 | rescue ZendeskAPI::Error::RecordInvalid => e 49 | @errors = e.errors 50 | false 51 | rescue ZendeskAPI::Error::ClientError 52 | false 53 | end 54 | end 55 | end 56 | end 57 | 58 | create_verb :put 59 | create_verb :post 60 | create_verb :delete 61 | create_verb :any 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/zendesk_api/version.rb: -------------------------------------------------------------------------------- 1 | module ZendeskAPI 2 | VERSION = "3.1.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/core/bulk_actions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::DestroyMany do 4 | subject { ZendeskAPI::BulkTestResource } 5 | 6 | context "destroy_many!" do 7 | before(:each) do 8 | stub_json_request(:delete, %r{bulk_test_resources/destroy_many}, json(:job_status => { :id => 'abc' })) 9 | @response = subject.destroy_many!(client, [1, 2, 3]) 10 | end 11 | 12 | it 'calls the destroy_many endpoint' do 13 | assert_requested(:delete, %r{bulk_test_resources/destroy_many\?ids=1,2,3$}) 14 | end 15 | 16 | it 'returns a JobStatus' do 17 | expect(@response).to be_instance_of(ZendeskAPI::JobStatus) 18 | expect(@response.id).to eq('abc') 19 | end 20 | end 21 | end 22 | 23 | describe ZendeskAPI::CreateMany do 24 | subject { ZendeskAPI::BulkTestResource } 25 | 26 | context "create_many!" do 27 | let(:attributes) { [{ :name => 'A' }, { :name => 'B' }] } 28 | 29 | before(:each) do 30 | stub_json_request(:post, %r{bulk_test_resources/create_many}, json(:job_status => { :id => 'def' })) 31 | @response = subject.create_many!(client, attributes) 32 | end 33 | 34 | it 'calls the create_many endpoint' do 35 | assert_requested(:post, %r{bulk_test_resources/create_many}, 36 | :body => json(:bulk_test_resources => attributes) 37 | ) 38 | end 39 | 40 | it 'returns a JobStatus' do 41 | expect(@response).to be_instance_of(ZendeskAPI::JobStatus) 42 | expect(@response.id).to eq('def') 43 | end 44 | end 45 | 46 | describe ZendeskAPI::UpdateMany do 47 | subject { ZendeskAPI::BulkTestResource } 48 | 49 | context "update_many!" do 50 | context "updating a list of ids" do 51 | let(:attributes) { { :name => 'A', :age => 25 } } 52 | 53 | before(:each) do 54 | stub_json_request(:put, %r{bulk_test_resources/update_many}, json(:job_status => { :id => 'ghi' })) 55 | @response = subject.update_many!(client, [1, 2, 3], attributes) 56 | end 57 | 58 | it 'calls the update_many endpoint' do 59 | assert_requested(:put, %r{bulk_test_resources/update_many\?ids=1,2,3$}, 60 | :body => json(:bulk_test_resource => attributes) 61 | ) 62 | end 63 | 64 | it 'returns a JobStatus' do 65 | expect(@response).to be_instance_of(ZendeskAPI::JobStatus) 66 | expect(@response.id).to eq('ghi') 67 | end 68 | end 69 | 70 | context "updating with multiple attribute hashes" do 71 | let(:attributes) { [{ :id => 1, :name => 'A' }, { :id => 2, :name => 'B' }] } 72 | 73 | before(:each) do 74 | stub_json_request(:put, %r{bulk_test_resources/update_many}, json(:job_status => { :id => 'jkl' })) 75 | @response = subject.update_many!(client, attributes) 76 | end 77 | 78 | it 'calls the update_many endpoint' do 79 | assert_requested(:put, %r{bulk_test_resources/update_many$}, 80 | :body => json(:bulk_test_resources => attributes) 81 | ) 82 | end 83 | 84 | it 'returns a JobStatus' do 85 | expect(@response).to be_instance_of(ZendeskAPI::JobStatus) 86 | expect(@response.id).to eq('jkl') 87 | end 88 | end 89 | end 90 | end 91 | end 92 | 93 | RSpec.describe ZendeskAPI::CreateOrUpdateMany do 94 | subject(:resource) { ZendeskAPI::BulkTestResource } 95 | 96 | context "create_or_update_many!" do 97 | context "creating or updating with multiple attribute hashes" do 98 | let(:attributes) { [{ :id => 1, :name => 'A' }, { :id => 2, :name => 'B' }] } 99 | 100 | subject { resource.create_or_update_many!(client, attributes) } 101 | 102 | before do 103 | stub_json_request(:post, %r{bulk_test_resources/create_or_update_many}, json(:job_status => { :id => 'jkl' })) 104 | end 105 | 106 | it 'calls the create_or_update_many endpoint' do 107 | subject 108 | 109 | assert_requested(:post, %r{bulk_test_resources/create_or_update_many$}, 110 | :body => json(:bulk_test_resources => attributes) 111 | ) 112 | end 113 | 114 | it 'returns a JobStatus' do 115 | expect(subject).to be_a(ZendeskAPI::JobStatus) 116 | expect(subject.id).to eq('jkl') 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /spec/core/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Configuration do 4 | subject { ZendeskAPI::Configuration.new } 5 | 6 | it "should properly merge options" do 7 | url = "test.host" 8 | subject.url = url 9 | expect(subject.options[:url]).to eq(url) 10 | end 11 | 12 | it "should set accept header properly" do 13 | expect(subject.options[:headers][:accept]).to eq('application/json') 14 | end 15 | 16 | it "should set user agent header properly" do 17 | expect(subject.options[:headers][:user_agent]).to match(/ZendeskAPI Ruby/) 18 | end 19 | 20 | it "should set a default open_timeout" do 21 | expect(subject.options[:request][:open_timeout]).to eq(10) 22 | end 23 | 24 | it "should set a default timeout" do 25 | expect(subject.options[:request][:timeout]).to eq(60) 26 | end 27 | 28 | it "should merge options with client_options" do 29 | subject.client_options = { :ssl => { :verify => false } } 30 | expect(subject.options[:ssl][:verify]).to eq(false) 31 | end 32 | 33 | it "sets a default for use_resource_cache" do 34 | expect(subject.use_resource_cache).to be true 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/core/create_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::CreateResource do 4 | context "create" do 5 | let(:attr) { { :test_field => "blah" } } 6 | subject { ZendeskAPI::TestResource } 7 | 8 | before(:each) do 9 | stub_request(:post, %r{test_resources}).to_return(:body => json) 10 | end 11 | 12 | it "should return instance of resource" do 13 | expect(subject.create(client, attr)).to be_instance_of(subject) 14 | end 15 | 16 | context "with client error" do 17 | before(:each) do 18 | stub_request(:post, %r{test_resources}).to_return(:status => 500) 19 | end 20 | 21 | it "should handle it properly" do 22 | expect { silence_logger { expect(subject.create(client, attr)).to be_nil } }.to_not raise_error 23 | end 24 | end 25 | end 26 | 27 | context "create!" do 28 | subject { ZendeskAPI::TestResource } 29 | 30 | before(:each) do 31 | stub_request(:post, %r{test_resources}).to_return(:status => 500) 32 | end 33 | 34 | it "should raise if save fails" do 35 | expect { subject.create!(client, :test_field => "blah") }.to raise_error(ZendeskAPI::Error::NetworkError) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/core/data_namespace_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | class ZendeskAPI::DataNamespaceTest; end 4 | 5 | describe ZendeskAPI::DataNamespace do 6 | describe "descendants" do 7 | let(:target_klass) { ZendeskAPI::DataNamespaceTest } 8 | it "adds class to its descendants list when included" do 9 | expect(ZendeskAPI::DataNamespace.descendants).not_to include(target_klass) 10 | expect { target_klass.send(:include, ZendeskAPI::DataNamespace) }. 11 | to change { ZendeskAPI::DataNamespace.descendants.count }.by(1) 12 | expect(ZendeskAPI::DataNamespace.descendants).to include(target_klass) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/core/data_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::DataResource do 4 | specify "singular resource name" do 5 | expect(ZendeskAPI::Ticket.singular_resource_name).to eq("ticket") 6 | expect(ZendeskAPI::TicketField.singular_resource_name).to eq("ticket_field") 7 | end 8 | 9 | specify "resource name" do 10 | expect(ZendeskAPI::Ticket.resource_name).to eq("tickets") 11 | expect(ZendeskAPI::TicketField.resource_name).to eq("ticket_fields") 12 | expect(ZendeskAPI::Category.resource_name).to eq("categories") 13 | end 14 | 15 | context "association" do 16 | subject { ZendeskAPI::TestResource.new(client, :id => 1) } 17 | let(:options) { {} } 18 | 19 | before(:each) do 20 | ZendeskAPI::TestResource.has :nil, options.merge(:class => ZendeskAPI::NilDataResource) 21 | end 22 | 23 | it "should try and find non-existent object" do 24 | stub_json_request(:get, %r{test_resources/1/nil}, json(:nil_data_resource => {})) 25 | 26 | expect(subject.nil).to be_instance_of(ZendeskAPI::NilDataResource) 27 | end 28 | 29 | context "inline => true" do 30 | let(:options) { { :inline => true } } 31 | 32 | it "should not try and find non-existent object" do 33 | subject.nil 34 | end 35 | end 36 | end 37 | 38 | context "user" do 39 | context "with first order attributes" do 40 | subject { ZendeskAPI::TestResource.new(client) } 41 | before(:each) { subject.attributes[:priority] = "normal" } 42 | 43 | it "should be able to access underlying attributes" do 44 | expect(subject.priority).to eq("normal") 45 | end 46 | 47 | it "should be able to change underlying attributes" do 48 | expect { subject.priority = "urgent" }.to_not raise_error 49 | end 50 | 51 | it "should be able to iterate over underlying attributes" do 52 | expect do 53 | subject.map do |k, v| 54 | [k.to_sym, v] 55 | end 56 | end.to_not raise_error 57 | end 58 | end 59 | 60 | context "with second order attributes" do 61 | subject { ZendeskAPI::TestResource.new(client) } 62 | before(:each) { subject.priority = "normal" } 63 | 64 | it "should be able to change underlying attributes" do 65 | expect(subject.priority).to eq("normal") 66 | end 67 | 68 | it "should be able to change underlying attributes" do 69 | expect { subject.priority = "urgent" }.to_not raise_error 70 | end 71 | 72 | it "should be able to iterate over underlying attributes" do 73 | expect do 74 | subject.map do |k, v| 75 | [k.to_sym, v] 76 | end 77 | end.to_not raise_error 78 | end 79 | end 80 | end 81 | 82 | context "has" do 83 | before(:each) { ZendeskAPI::TestResource.has ZendeskAPI::TestResource } 84 | 85 | context "class methods" do 86 | subject { ZendeskAPI::TestResource } 87 | 88 | it "should define a method with the same name" do 89 | expect(subject.instance_methods.map(&:to_s)).to include("test_resource") 90 | end 91 | 92 | context "with explicit class name" do 93 | before(:all) { ZendeskAPI::TestResource.has :baz, :class => ZendeskAPI::TestResource } 94 | 95 | it "should define a method with the same name" do 96 | expect(subject.instance_methods.map(&:to_s)).to include("baz") 97 | end 98 | end 99 | end 100 | 101 | context "instance method" do 102 | context "with no side-loading" do 103 | subject { ZendeskAPI::TestResource.new(client, :id => 1, :test_resource_id => 1) } 104 | before(:each) { stub_json_request(:get, %r{test_resources/\d+}, json(:test_resource => {})) } 105 | 106 | it "should attempt to grab the resource from the host" do 107 | expect(subject.test_resource).to be_instance_of(ZendeskAPI::TestResource) 108 | end 109 | 110 | it "should pass the path on to the resource" do 111 | expect(subject.test_resource.path).to eq("test_resources") 112 | end 113 | 114 | context "with a client error" do 115 | before(:each) { stub_request(:get, %r{test_resources/\d+}).to_return(:status => 500) } 116 | 117 | it "should handle it properly" do 118 | expect { silence_logger { expect(subject.test_resource).to be_nil } }.to_not raise_error 119 | end 120 | end 121 | 122 | context "with an explicit path set" do 123 | before(:each) do 124 | ZendeskAPI::TestResource.has ZendeskAPI::TestResource, :path => "blergh" 125 | stub_json_request(:get, %r{blergh/\d+}, json(:test_resource => {})) 126 | end 127 | 128 | it "should call the right path" do 129 | expect(subject.test_resource).to be_instance_of(ZendeskAPI::TestResource) 130 | end 131 | end 132 | end 133 | 134 | context "with side-loading of resource" do 135 | let(:test_resource) { { :message => "FOO_OBJ" } } 136 | subject { ZendeskAPI::TestResource.new(client, :test_resource => test_resource).test_resource } 137 | 138 | it "should load the correct instance" do 139 | expect(subject).to be_instance_of(ZendeskAPI::TestResource) 140 | end 141 | 142 | it "should load foo from the hash" do 143 | expect(subject.message).to eq("FOO_OBJ") 144 | end 145 | end 146 | 147 | context "with side-loading of id" do 148 | subject { ZendeskAPI::TestResource.new(client, :test_resource_id => 1) } 149 | before(:each) do 150 | stub_json_request(:get, %r{test_resources/1}, json("test_resource" => {})) 151 | end 152 | 153 | it "should find foo_id and load it from the api" do 154 | subject.test_resource 155 | end 156 | 157 | it "should handle nil response from find api" do 158 | expect(ZendeskAPI::TestResource).to receive(:find).twice.and_return(nil) 159 | expect(subject.test_resource).to be_nil 160 | subject.test_resource 161 | end 162 | end 163 | end 164 | end 165 | 166 | context "has_many" do 167 | before(:each) { ZendeskAPI::TestResource.has_many ZendeskAPI::TestResource } 168 | 169 | context "class methods" do 170 | subject { ZendeskAPI::TestResource } 171 | 172 | it "should define a method with the same name" do 173 | expect(subject.instance_methods.map(&:to_s)).to include("test_resources") 174 | end 175 | 176 | context "with explicit class name" do 177 | before(:each) { ZendeskAPI::TestResource.has_many :cats, :class => ZendeskAPI::TestResource } 178 | 179 | it "should define a method with the same name" do 180 | expect(subject.instance_methods.map(&:to_s)).to include("cats") 181 | end 182 | end 183 | end 184 | 185 | context "instance method" do 186 | context "with no side-loading" do 187 | subject { ZendeskAPI::TestResource.new(client, :id => 1) } 188 | 189 | it "should not attempt to grab the resource from the host" do 190 | expect(subject.test_resources).to be_instance_of(ZendeskAPI::Collection) 191 | end 192 | 193 | it "should pass the path on to the resource" do 194 | expect(subject.test_resources.path).to eq("test_resources/1/test_resources") 195 | end 196 | 197 | context "with an explicit path set" do 198 | before(:each) do 199 | ZendeskAPI::TestResource.has_many ZendeskAPI::TestResource, :path => "blargh" 200 | end 201 | 202 | it "should call the right path" do 203 | expect(subject.test_resources.path).to eq("test_resources/1/blargh") 204 | end 205 | end 206 | end 207 | 208 | context "with side-loading of resource" do 209 | let(:test_resources) { [{ :message => "FOO_OBJ" }] } 210 | subject { ZendeskAPI::TestResource.new(client, :test_resources => test_resources).test_resources.first } 211 | 212 | it "should properly create instance" do 213 | expect(subject.message).to eq("FOO_OBJ") 214 | end 215 | 216 | it "should map bars onto TestResource class" do 217 | expect(subject).to be_instance_of(ZendeskAPI::TestResource) 218 | end 219 | end 220 | 221 | context "with side-loading of id" do 222 | let(:test_resource_ids) { [1, 2, 3] } 223 | subject { ZendeskAPI::TestResource.new(client, :test_resource_ids => test_resource_ids) } 224 | 225 | it "should find foo_id and load it from the api" do 226 | expect(ZendeskAPI::TestResource).to receive(:find).with(client, kind_of(Hash)).exactly(test_resource_ids.length).times 227 | subject.test_resources 228 | end 229 | 230 | it "should handle nil response from find api" do 231 | expect(ZendeskAPI::TestResource).to receive(:find).with(client, kind_of(Hash)).exactly(test_resource_ids.length).times.and_return(nil) 232 | expect(subject.test_resources).to be_empty 233 | subject.test_resources # Test expectations 234 | end 235 | end 236 | end 237 | end 238 | end 239 | -------------------------------------------------------------------------------- /spec/core/data_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | RSpec.describe ZendeskAPI::Data do 4 | describe ".new_from_response" do 5 | let(:response) { double(:response) } 6 | 7 | it "returns an instance with the response" do 8 | expect(described_class.new_from_response(client, response)) 9 | .to be_instance_of(described_class) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/core/error_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Error do 4 | describe ZendeskAPI::Error::ClientError do 5 | it "works without a response" do 6 | expect(ZendeskAPI::Error::ClientError.new("foo").message).to eq "foo" 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/core/helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | RSpec.describe ZendeskAPI::Helpers do 4 | describe "#present?" do 5 | it "is false when nil, or empty" do 6 | expect(described_class.present?(nil)).to be(false) 7 | expect(described_class.present?("")).to be(false) 8 | expect(described_class.present?(" ")).to be(false) 9 | expect(described_class.present?([])).to be(false) 10 | expect(described_class.present?({})).to be(false) 11 | end 12 | 13 | it "is true when there's something" do 14 | expect(described_class.present?(1)).to be(true) 15 | expect(described_class.present?(:a)).to be(true) 16 | expect(described_class.present?("b")).to be(true) 17 | expect(described_class.present?([:a])).to be(true) 18 | expect(described_class.present?(c: 3)).to be(true) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/core/inflection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe String do 4 | specify "the plural of forum if forums" do 5 | expect(Inflection.plural("forum")).to eq("forums") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/core/lru_cache_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::LRUCache do 4 | let(:cache) { ZendeskAPI::LRUCache.new(2) } 5 | 6 | it "writes and reads" do 7 | expect(cache.write("x", 1)).to eq(1) 8 | expect(cache.read("x")).to eq(1) 9 | end 10 | 11 | it "drops" do 12 | cache.write("x", 1) 13 | cache.write("y", 1) 14 | cache.write("x", 1) 15 | cache.write("z", 1) 16 | expect(cache.read("z")).to eq(1) 17 | expect(cache.read("x")).to eq(1) 18 | expect(cache.read("y")).to eq(nil) 19 | end 20 | 21 | it "fetches" do 22 | expect(cache.fetch("x") { 1 }).to eq(1) 23 | expect(cache.read("x")).to eq(1) 24 | expect(cache.fetch("x") { 2 }).to eq(1) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/core/middleware/request/encode_json_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Middleware::Request::EncodeJson do 4 | let(:app) do 5 | ZendeskAPI::Middleware::Request::EncodeJson.new(lambda { |x| x }) 6 | end 7 | 8 | let(:response) { app.call({ :request_headers => {} }.merge(env)) } 9 | 10 | context 'with a nil body' do 11 | let(:env) { { :body => nil } } 12 | 13 | it 'should not return json' do 14 | expect(response[:body]).to be_nil 15 | end 16 | end 17 | 18 | context 'with an empty body' do 19 | let(:env) { { :body => '' } } 20 | 21 | it 'should not return json' do 22 | expect(response[:body]).to eq('') 23 | end 24 | end 25 | 26 | context 'with a proper mime type' do 27 | context 'empty' do 28 | let(:env) { { :body => { :a => :b } } } 29 | 30 | it 'encodes json' do 31 | expect(response[:body]).to eq(JSON.dump(:a => :b)) 32 | end 33 | 34 | it 'sets the content type' do 35 | expect(response[:request_headers]['Content-Type']).to eq('application/json') 36 | end 37 | end 38 | 39 | context 'application/json' do 40 | let(:env) { 41 | { 42 | :body => { :a => :b }, 43 | :request_headers => { 44 | 'Content-Type' => 'application/json' 45 | } 46 | } 47 | } 48 | 49 | it 'encodes json' do 50 | expect(response[:body]).to eq(JSON.dump(:a => :b)) 51 | end 52 | 53 | it 'keeps the content type' do 54 | expect(response[:request_headers]['Content-Type']).to eq('application/json') 55 | end 56 | end 57 | 58 | context 'application/json; encoding=utf-8' do 59 | let(:env) { 60 | { 61 | :body => { :a => :b }, 62 | :request_headers => { 63 | 'Content-Type' => 'application/json; encoding=utf-8' 64 | } 65 | } 66 | } 67 | 68 | it 'encodes json' do 69 | expect(response[:body]).to eq(JSON.dump(:a => :b)) 70 | end 71 | 72 | it 'keeps the content type' do 73 | expect(response[:request_headers]['Content-Type']).to eq('application/json; encoding=utf-8') 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/core/middleware/request/etag_cache_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Middleware::Request::EtagCache do 4 | it "caches" do 5 | client.config.cache.size = 1 6 | 7 | stub_json_request(:get, %r{blergh}, '{"x":1}', :headers => { "Etag" => "x" }) 8 | first_response = client.connection.get("blergh") 9 | expect(first_response.status).to eq(200) 10 | expect(first_response.body).to eq({ "x" => 1 }) 11 | 12 | stub_request(:get, %r{blergh}).to_return(:status => 304, :headers => { "Etag" => "x" }) 13 | response = client.connection.get("blergh") 14 | expect(response.status).to eq(304) 15 | expect(response.body).to eq({ "x" => 1 }) 16 | 17 | %w{content_encoding content_type content_length etag}.each do |header| 18 | expect(response.headers[header]).to eq(first_response.headers[header]) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/core/middleware/request/raise_rate_limited_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Middleware::Request::RaiseRateLimited do 4 | before do 5 | client.config.retry = false 6 | client.config.raise_error_when_rate_limited = true 7 | 8 | stub_request(:get, /blergh/).to_return(status: 429) 9 | end 10 | 11 | it 'should raise RateLimited' do 12 | expect do 13 | client.connection.get('blergh') 14 | end.to raise_error(ZendeskAPI::Error::RateLimited) 15 | end 16 | 17 | it 'should print to logger' do 18 | expect(client.config.logger).to receive(:warn) 19 | client.connection.get('blergh') rescue ZendeskAPI::Error::RateLimited # rubocop:disable Style/RescueModifier 20 | end 21 | 22 | it 'should not fail without a logger', :prevent_logger_changes do 23 | client.config.logger = false 24 | client.connection.get('blergh') rescue ZendeskAPI::Error::RateLimited # rubocop:disable Style/RescueModifier 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/core/middleware/request/retry_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Middleware::Request::Retry do 4 | def runtime 5 | start = Time.now.to_f 6 | yield 7 | Time.now.to_f - start 8 | end 9 | 10 | [429, 503].each do |error_code| 11 | it "should wait requisite seconds and then retry request on #{error_code}" do 12 | stub_request(:get, %r{blergh}). 13 | to_return(:status => 429, :headers => { :retry_after => 1 }). 14 | to_return(:status => 200) 15 | 16 | seconds = runtime { 17 | expect(client.connection.get("blergh").status).to eq(200) 18 | } 19 | 20 | expect(seconds).to be_within(0.2).of(1) 21 | end 22 | end 23 | 24 | context "with a failed connection and not retrying" do 25 | context "connection failed" do 26 | before(:each) do 27 | stub_request(:any, /.*/).to_raise(Faraday::ConnectionFailed) 28 | end 29 | 30 | it "should raise NetworkError, but never retry" do 31 | expect_any_instance_of(ZendeskAPI::Middleware::Request::Retry).to receive(:sleep).exactly(10).never 32 | expect { client.connection.get "/non_existent" }.to raise_error(ZendeskAPI::Error::NetworkError) 33 | end 34 | end 35 | end 36 | 37 | context "with a failed connection, explicit retry true on exception, and retrying" do 38 | context "connection failed" do 39 | before(:each) do 40 | client.config.retry_on_exception = true 41 | stub_request(:any, /.*/).to_raise(Faraday::ConnectionFailed).to_return(:status => 200) 42 | end 43 | 44 | it "should raise NetworkError, but then actually retry" do 45 | expect_any_instance_of(ZendeskAPI::Middleware::Request::Retry).to receive(:sleep).exactly(10).times.with(1) 46 | expect(client.connection.get("blergh").status).to eq(200) 47 | end 48 | end 49 | end 50 | 51 | context "with a failed connection, explicit retry false on exception, and retrying" do 52 | context "connection failed" do 53 | before(:each) do 54 | client.config.retry_on_exception = false 55 | stub_request(:any, /.*/).to_raise(Faraday::ConnectionFailed).to_return(:status => 200) 56 | end 57 | 58 | it "should raise NetworkError, but never retry" do 59 | expect_any_instance_of(ZendeskAPI::Middleware::Request::Retry).to receive(:sleep).exactly(10).never 60 | expect { client.connection.get "/non_existent" }.to raise_error(ZendeskAPI::Error::NetworkError) 61 | end 62 | end 63 | end 64 | 65 | [503].each do |error_code| 66 | context "with failing request because server is not ready with default error code #{error_code}", :prevent_logger_changes do 67 | before do 68 | stub_request(:get, %r{blergh}). 69 | to_return(:status => error_code). 70 | to_return(:status => 200) 71 | 72 | expect_any_instance_of(ZendeskAPI::Middleware::Request::Retry).to receive(:sleep).exactly(10).times.with(1) 73 | end 74 | 75 | it "should wait default timeout seconds and then retry request on error" do 76 | expect(runtime { 77 | expect(client.connection.get("blergh").status).to eq(200) 78 | }).to be <= 0.5 79 | end 80 | 81 | it "should print to logger" do 82 | expect(client.config.logger).to receive(:warn).exactly(4) 83 | client.connection.get("blergh") 84 | end 85 | 86 | it "should not fail without a logger" do 87 | client.config.logger = false 88 | client.connection.get("blergh") 89 | end 90 | end 91 | end 92 | 93 | [501, 503].each do |error_code| 94 | context "with failing request because server is not ready with default error code #{error_code}", :prevent_logger_changes do 95 | before do 96 | client.config.retry_codes = [501, 503] 97 | stub_request(:get, %r{blergh}). 98 | to_return(:status => error_code). 99 | to_return(:status => 200) 100 | 101 | expect_any_instance_of(ZendeskAPI::Middleware::Request::Retry).to receive(:sleep).exactly(10).times.with(1) 102 | end 103 | 104 | it "should wait default timeout seconds and then retry request on error" do 105 | expect(runtime { 106 | expect(client.connection.get("blergh").status).to eq(200) 107 | }).to be <= 0.5 108 | end 109 | 110 | it "should print to logger" do 111 | expect(client.config.logger).to receive(:warn).exactly(4) 112 | client.connection.get("blergh") 113 | end 114 | 115 | it "should not fail without a logger" do 116 | client.config.logger = false 117 | client.connection.get("blergh") 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /spec/core/middleware/request/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/zendesk_api_client_rb/0693480ae6cd0069e7b195ad1ea2de664cf3a3fa/spec/core/middleware/request/test.jpg -------------------------------------------------------------------------------- /spec/core/middleware/request/upload_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | require 'tempfile' 3 | require 'action_dispatch' 4 | 5 | describe ZendeskAPI::Middleware::Request::Upload do 6 | subject { ZendeskAPI::Middleware::Request::Upload.new(lambda { |env| env }) } 7 | let(:filename) { File.join(File.dirname(__FILE__), "test.jpg") } 8 | 9 | it "should handle no body" do 10 | expect(subject.call({})).to eq({}) 11 | end 12 | 13 | it "should handle body with no file" do 14 | expect(subject.call(:body => {})[:body]).to eq({}) 15 | end 16 | 17 | it "should handle invalid types" do 18 | expect(subject).to receive(:warn) 19 | expect(subject.call(:body => { :file => :invalid })[:body]).to eq({}) 20 | end 21 | 22 | context "with file string" do 23 | before(:each) do 24 | @env = subject.call(:body => { :file => filename }) 25 | end 26 | 27 | it "should convert file string to UploadIO" do 28 | expect(@env[:body][:uploaded_data]).to be_instance_of(Faraday::UploadIO) 29 | end 30 | 31 | it "should remove file string" do 32 | expect(@env[:body][:file]).to be_nil 33 | end 34 | 35 | it "should add filename if none exist" do 36 | expect(@env[:body][:filename]).to eq("test.jpg") 37 | end 38 | 39 | context "with filename" do 40 | before(:each) do 41 | @env = subject.call(:body => { :file => filename, :filename => "test" }) 42 | end 43 | 44 | it "should not change filename" do 45 | expect(@env[:body][:filename]).to_not eq("test.jpg") 46 | end 47 | end 48 | end 49 | 50 | context "with an ActionDispatch::Http::UploadedFile" do 51 | before(:each) do 52 | @upload = ActionDispatch::Http::UploadedFile.new(:filename => "hello.jpg", :tempfile => Tempfile.new(['hello', '.jpg'])) 53 | @env = subject.call(:body => { :file => @upload }) 54 | end 55 | 56 | it "should convert file string to UploadIO" do 57 | expect(@env[:body][:uploaded_data]).to be_instance_of(Faraday::UploadIO) 58 | end 59 | 60 | it "should remove file string" do 61 | expect(@env[:body][:file]).to be_nil 62 | end 63 | 64 | it "should add filename if none exist" do 65 | expect(@env[:body][:filename]).to eq("hello.jpg") 66 | end 67 | 68 | it "should use the content type of the tempfile" do 69 | expect(@env[:body][:uploaded_data].content_type).to eq("image/jpeg") 70 | end 71 | 72 | context "when path does not resolve a mime_type" do 73 | it "should pass correct filename to Faraday::UploadIO" do 74 | expect(@env[:body][:filename]).to eq("hello.jpg") 75 | expect(@env[:body][:uploaded_data].original_filename).to eq(@env[:body][:filename]) 76 | end 77 | 78 | it "should use the content_type of ActionDispatch::Http::UploadedFile " do 79 | @upload.content_type = 'application/random' 80 | 81 | env = subject.call(:body => { :file => @upload }) 82 | expect(env[:body][:uploaded_data].content_type).to eq('application/random') 83 | end 84 | end 85 | end 86 | 87 | context "with a Tempfile" do 88 | before(:each) do 89 | @tempfile = Tempfile.new(File.basename(filename)) 90 | @env = subject.call(:body => { :file => @tempfile }) 91 | end 92 | 93 | it "should convert file string to UploadIO" do 94 | expect(@env[:body][:uploaded_data]).to be_instance_of(Faraday::UploadIO) 95 | end 96 | 97 | it "should remove file string" do 98 | expect(@env[:body][:file]).to be_nil 99 | end 100 | 101 | it "should add filename if none exist" do 102 | expect(@env[:body][:filename]).to eq(File.basename(@tempfile.path)) 103 | end 104 | end 105 | 106 | context "with file instance" do 107 | context "top-level" do 108 | before(:each) do 109 | @env = subject.call(:body => { :file => File.new(filename) }) 110 | end 111 | 112 | it "should convert file string to UploadIO" do 113 | expect(@env[:body][:uploaded_data]).to be_instance_of(Faraday::UploadIO) 114 | end 115 | 116 | it "should remove file string" do 117 | expect(@env[:body][:file]).to be_nil 118 | end 119 | 120 | it "should add filename if none exist" do 121 | expect(@env[:body][:filename]).to eq("test.jpg") 122 | end 123 | 124 | context "with filename" do 125 | before(:each) do 126 | @env = subject.call(:body => { :file => File.new(filename), :filename => "test" }) 127 | end 128 | 129 | it "should not change filename" do 130 | expect(@env[:body][:filename]).to_not eq("test.jpg") 131 | end 132 | end 133 | end 134 | 135 | context "underneath a key" do 136 | context "only a file" do 137 | before(:each) do 138 | @env = subject.call(:body => { :user => { :photo => File.new(filename) } }) 139 | end 140 | 141 | it "should convert file string to UploadIO" do 142 | expect(@env[:body][:user][:photo][:uploaded_data]).to be_instance_of(Faraday::UploadIO) 143 | end 144 | 145 | it "should add filename if none exist" do 146 | expect(@env[:body][:user][:photo][:filename]).to eq("test.jpg") 147 | end 148 | end 149 | 150 | context "with filename" do 151 | before(:each) do 152 | @env = subject.call(:body => { :user => { :photo => { :file => File.new(filename), :filename => "test" } } }) 153 | end 154 | 155 | it "should convert file string to UploadIO" do 156 | expect(@env[:body][:user][:photo][:uploaded_data]).to be_instance_of(Faraday::UploadIO) 157 | end 158 | 159 | it "should not change filename" do 160 | expect(@env[:body][:user][:photo][:filename]).to_not eq("test.jpg") 161 | end 162 | end 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /spec/core/middleware/response/callback_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Middleware::Response::Callback do 4 | let(:response) { "TEST" } 5 | 6 | before(:each) do 7 | client.insert_callback do |env| 8 | env[:body] = response 9 | end 10 | 11 | stub_request(:get, %r{test_endpoint}).to_return(:body => JSON.dump({ "ABC" => "DEF" })) 12 | end 13 | 14 | it "should call callbacks " do 15 | expect(client.connection.get("test_endpoint").body).to eq(response) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/core/middleware/response/deflate_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Middleware::Response::Deflate do 4 | context "with content-encoding = 'deflate'" do 5 | subject { '{ "TESTDATA": true }' } 6 | 7 | before(:each) do 8 | stub_request(:get, %r{blergh}).to_return( 9 | :headers => { 10 | :content_encoding => "deflate", 11 | :content_type => "application/json" 12 | }, 13 | :body => Zlib::Deflate.deflate(subject) 14 | ) 15 | end 16 | 17 | it "should inflate returned body" do 18 | expect(client.connection.get("blergh").body['TESTDATA']).to be(true) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/core/middleware/response/gzip_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Middleware::Response::Gzip do 4 | context "with content-encoding = 'gzip'" do 5 | subject { '{ "TESTDATA": true }' } 6 | before(:each) do 7 | encoded_data = StringIO.new 8 | gz = Zlib::GzipWriter.new(encoded_data) 9 | gz.write(subject) 10 | gz.close 11 | 12 | stub_request(:get, %r{blergh}).to_return( 13 | :headers => { 14 | :content_encoding => "gzip", 15 | :content_type => "application/json" 16 | }, 17 | :body => encoded_data.string 18 | ) 19 | end 20 | 21 | it "should inflate returned body" do 22 | expect(client.connection.get("blergh").body['TESTDATA']).to be(true) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/core/middleware/response/parse_iso_dates_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Middleware::Response::ParseIsoDates do 4 | def fake_response(data) 5 | stub_json_request(:get, %r{blergh}, data) 6 | response = client.connection.get("blergh") 7 | expect(response.status).to eq(200) 8 | response 9 | end 10 | 11 | let(:parsed){ 12 | if RUBY_VERSION > "1.9" 13 | "2012-02-01 13:14:15 UTC" 14 | else 15 | "Wed Feb 01 13:14:15 UTC 2012" 16 | end 17 | } 18 | 19 | it "should parse dates" do 20 | expect(fake_response('{"x":"2012-02-01T13:14:15Z"}').body["x"].to_s).to eq(parsed) 21 | end 22 | 23 | it "should parse nested dates in hash" do 24 | expect(fake_response('{"x":{"y":"2012-02-01T13:14:15Z"}}').body["x"]["y"].to_s).to eq(parsed) 25 | end 26 | 27 | it "should parse nested dates in arrays" do 28 | expect(fake_response('{"x":[{"y":"2012-02-01T13:14:15Z"}]}').body["x"][0]["y"].to_s).to eq(parsed) 29 | end 30 | 31 | it "should not blow up on empty body" do 32 | expect(fake_response('').body).to eq('') 33 | end 34 | 35 | it "should leave arrays with ids alone" do 36 | expect(fake_response('{"x":[1,2,3]}').body).to eq({ "x" => [1, 2, 3] }) 37 | end 38 | 39 | it "should not parse date-like things" do 40 | expect(fake_response('{"x":"2012-02-01T13:14:15Z bla"}').body["x"].to_s).to eq("2012-02-01T13:14:15Z bla") 41 | expect(fake_response('{"x":"12012-02-01T13:14:15Z"}').body["x"].to_s).to eq("12012-02-01T13:14:15Z") 42 | expect(fake_response('{"x":"2012-02-01T13:14:15Z\\nfoo"}').body["x"].to_s).to eq("2012-02-01T13:14:15Z\nfoo") 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/core/middleware/response/parse_json_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Middleware::Response::ParseJson do 4 | context "with another content-type" do 5 | before(:each) do 6 | stub_request(:get, %r{blergh}).to_return( 7 | :headers => { 8 | :content_type => "application/xml" 9 | }, 10 | :body => '' 11 | ) 12 | end 13 | 14 | it "should not return nil body" do 15 | expect(client.connection.get("blergh").body).to eql('') 16 | end 17 | end 18 | 19 | context "with content-type = 'application/json'" do 20 | before(:each) do 21 | stub_request(:get, %r{blergh}).to_return( 22 | :headers => { 23 | :content_type => "application/json" 24 | }, 25 | :body => body 26 | ) 27 | end 28 | 29 | context "with a nil body" do 30 | let(:body) { nil } 31 | 32 | it "should return empty body" do 33 | expect(client.connection.get("blergh").body).to eql('') 34 | end 35 | end 36 | 37 | context "with a empty body" do 38 | let(:body) { '' } 39 | 40 | it "should return empty body" do 41 | expect(client.connection.get("blergh").body).to eql('') 42 | end 43 | end 44 | 45 | context 'proper json' do 46 | let(:body) { '{ "TESTDATA": true }' } 47 | 48 | it "should parse returned body" do 49 | expect(client.connection.get("blergh").body['TESTDATA']).to be(true) 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/core/middleware/response/raise_error_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Middleware::Response::RaiseError do 4 | context "with a failed connection" do 5 | context "connection failed" do 6 | before(:each) do 7 | stub_request(:any, /.*/).to_raise(Faraday::ConnectionFailed) 8 | end 9 | 10 | it "should raise NetworkError" do 11 | expect { client.connection.get "/non_existent" }.to raise_error(ZendeskAPI::Error::NetworkError) 12 | end 13 | end 14 | 15 | context "connection timeout" do 16 | before(:each) do 17 | stub_request(:any, /.*/).to_timeout 18 | end 19 | 20 | it "should raise NetworkError" do 21 | expect { client.connection.get "/non_existent" }.to raise_error(ZendeskAPI::Error::NetworkError) 22 | end 23 | end 24 | end 25 | 26 | context "status errors" do 27 | let(:body) { "" } 28 | 29 | before(:each) do 30 | stub_request(:any, /.*/).to_return(:status => status, :body => body, 31 | :headers => { :content_type => "application/json" }) 32 | end 33 | 34 | context "with status = 404" do 35 | let(:status) { 404 } 36 | 37 | it "should raise RecordNotFound when status is 404" do 38 | expect { client.connection.get "/non_existent" }.to raise_error(ZendeskAPI::Error::RecordNotFound) 39 | end 40 | end 41 | 42 | context "with status in 400...600" do 43 | let(:status) { 500 } 44 | 45 | it "should raise NetworkError" do 46 | expect { client.connection.get "/non_existent" }.to raise_error(ZendeskAPI::Error::NetworkError) 47 | end 48 | end 49 | 50 | context "with status in 1XX" do 51 | let(:status) { 100 } 52 | 53 | it "should raise NetworkError" do 54 | expect { client.connection.get "/non_existent" }.to raise_error(ZendeskAPI::Error::NetworkError) 55 | end 56 | end 57 | 58 | context "with status = 304" do 59 | let(:status) { 304 } 60 | 61 | it "should not raise" do 62 | client.connection.get "/abcdef" 63 | end 64 | end 65 | 66 | context "with status in 3XX" do 67 | let(:status) { 302 } 68 | 69 | it "raises NetworkError with the right message" do 70 | expect { client.connection.get "/non_existent" }.to raise_error( 71 | ZendeskAPI::Error::NetworkError, 72 | "the server responded with status 302 -- get https://#{client.connection.host}/non_existent" 73 | ) 74 | end 75 | end 76 | 77 | context "with status = 422" do 78 | let(:status) { 422 } 79 | 80 | it "should raise RecordInvalid" do 81 | expect { client.connection.get "/non_existent" }.to raise_error(ZendeskAPI::Error::RecordInvalid) 82 | end 83 | 84 | context "with a body" do 85 | let(:body) { JSON.dump(:details => "hello") } 86 | 87 | it "should return RecordInvalid with proper message" do 88 | client.connection.get "/non_existent" 89 | rescue ZendeskAPI::Error::RecordInvalid => e 90 | expect(e.errors).to eq("hello") 91 | expect(e.to_s).to eq("ZendeskAPI::Error::RecordInvalid: hello") 92 | else 93 | fail # didn't raise an error 94 | end 95 | 96 | { 97 | error: 'There was an error', 98 | errors: 'There were several errors' 99 | }.each do |key, message| 100 | context "with only an #{key} key" do 101 | let(:body) { JSON.dump(key => message) } 102 | 103 | it "should return RecordInvalid with proper message" do 104 | expect { client.connection.get "/non_existent" }.to raise_error do |error| 105 | expect(error).to be_a(ZendeskAPI::Error::RecordInvalid) 106 | expect(error.errors).to eq(message) 107 | end 108 | end 109 | end 110 | end 111 | end 112 | end 113 | 114 | context "with status = 413" do 115 | let(:status) { 413 } 116 | 117 | it "should raise RecordInvalid" do 118 | expect { client.connection.get "/non_existent" }.to raise_error(ZendeskAPI::Error::RecordInvalid) 119 | end 120 | 121 | context "with a body" do 122 | let(:body) { JSON.dump(:description => "big file is big", :message => "small file is small") } 123 | 124 | it "should return RecordInvalid with proper message" do 125 | client.connection.get "/non_existent" 126 | rescue ZendeskAPI::Error::RecordInvalid => e 127 | expect(e.errors).to eq("big file is big - small file is small") 128 | expect(e.to_s).to eq("ZendeskAPI::Error::RecordInvalid: big file is big - small file is small") 129 | else 130 | fail # didn't raise an error 131 | end 132 | 133 | { 134 | error: 'There was an error', 135 | errors: 'There were several errors' 136 | }.each do |key, message| 137 | context "with only an #{key} key" do 138 | let(:body) { JSON.dump(key => message) } 139 | 140 | it "should return RecordInvalid with proper message" do 141 | expect { client.connection.get "/non_existent" }.to raise_error do |error| 142 | expect(error).to be_a(ZendeskAPI::Error::RecordInvalid) 143 | expect(error.errors).to eq(message) 144 | end 145 | end 146 | end 147 | end 148 | end 149 | end 150 | 151 | context "with status = 200" do 152 | let(:status) { 200 } 153 | 154 | it "should not raise" do 155 | client.connection.get "/abcdef" 156 | end 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /spec/core/middleware/response/sanitize_response_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Middleware::Response::SanitizeResponse do 4 | def fake_response(data) 5 | stub_json_request(:get, %r{blergh}, data) 6 | response = client.connection.get('blergh') 7 | expect(response.status).to eq(200) 8 | response 9 | end 10 | 11 | describe 'with bad characters' do 12 | let(:response) { fake_response("{\"x\":\"2012-02-01T13:14:15Z\", \"y\":\"\u0315\u0316\u01333\u0270\u022712awesome!"+[0xd83d,0xdc4d].pack('U*')+"\"}") } 13 | 14 | it 'removes bad characters' do 15 | expect(response.body.to_s.valid_encoding?).to be(true) 16 | expect(response.body['y'].to_s).to eq("\u0315\u0316\u01333\u0270\u022712awesome!") 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/core/read_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::ReadResource do 4 | context "find" do 5 | let(:id) { 1 } 6 | subject { ZendeskAPI::TestResource } 7 | 8 | context "normal request" do 9 | before(:each) do 10 | stub_json_request(:get, %r{test_resources/#{id}}, json("test_resource" => {})) 11 | end 12 | 13 | it "should return instance of resource" do 14 | expect(subject.find(client, :id => id)).to be_instance_of(subject) 15 | end 16 | end 17 | 18 | it "should blow up without an id which would build an invalid url" do 19 | expect{ 20 | ZendeskAPI::User.find(client, :foo => :bar) 21 | }.to raise_error("No :id given") 22 | end 23 | 24 | context "with overriden handle_response" do 25 | subject do 26 | Class.new(ZendeskAPI::TestResource) do 27 | def self.singular_resource_name 28 | 'hello' 29 | end 30 | 31 | def handle_response(response) 32 | @attributes.replace(response.body) 33 | end 34 | end 35 | end 36 | 37 | before(:each) do 38 | stub_json_request(:get, %r{hellos/#{id}}, json(:testing => 1)) 39 | end 40 | 41 | it "should return instance of resource" do 42 | object = subject.find(client, :id => id) 43 | expect(object).to be_instance_of(subject) 44 | expect(object.testing).to eq(1) 45 | end 46 | end 47 | 48 | context "with side loads" do 49 | before(:each) do 50 | stub_json_request(:get, %r{test_resources/#{id}\?include=nil_resource}, json( 51 | "test_resource" => { :id => 1, :nil_resource_id => 2 }, 52 | "nil_resources" => [{ :id => 1, :name => :bye }, { :id => 2, :name => :hi }] 53 | )) 54 | 55 | subject.has ZendeskAPI::NilResource 56 | @resource = subject.find(client, :id => id, :include => :nil_resource) 57 | end 58 | 59 | it "should side load nil resource" do 60 | expect(@resource.nil_resource.name).to eq("hi") 61 | end 62 | end 63 | 64 | context "with client error" do 65 | it "should handle 500 properly" do 66 | stub_request(:get, %r{test_resources/#{id}}).to_return(:status => 500) 67 | expect(subject.find(client, :id => id)).to eq(nil) 68 | end 69 | 70 | it "should handle 404 properly" do 71 | stub_request(:get, %r{test_resources/#{id}}).to_return(:status => 404) 72 | expect(subject.find(client, :id => id)).to eq(nil) 73 | end 74 | end 75 | end 76 | 77 | context "#reload!" do 78 | let(:id) { 2 } 79 | 80 | subject { ZendeskAPI::TestResource.new(client, :id => id, :name => 'Old Name') } 81 | 82 | before(:each) do 83 | stub_json_request(:get, %r{test_resources/#{id}}, json("test_resource" => { :id => id, :name => "New Name" })) 84 | end 85 | 86 | it "reloads the data" do 87 | expect(subject.name).to eq('Old Name') 88 | assert_not_requested(:get, %r{test_resources/#{id}}) 89 | 90 | subject.reload! 91 | 92 | assert_requested(:get, %r{test_resources/#{id}}) 93 | expect(subject.name).to eq('New Name') 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/core/resources/automation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Automation do 4 | def valid_attributes 5 | { 6 | :title => "my test automation", 7 | :conditions => { 8 | :any => [{ :field => "assignee_id", :operator => "is", :value => 1 }], 9 | :all => [{ :field => "status", :operator => "is", :value => "open" }] 10 | }, 11 | :actions => [{ :field => "priority", :value => "urgent" }] 12 | } 13 | end 14 | 15 | subject do 16 | described_class.new(double, valid_attributes) 17 | end 18 | 19 | describe "#all_conditions=" do 20 | it "should assign new values to all conditions" do 21 | new_conditions = [ 22 | { "field" => "type", "operator" => "is", "value" => "question" }, 23 | { "field" => "status", "operator" => "less_than", "value" => "solved" } 24 | ] 25 | subject.all_conditions = new_conditions 26 | 27 | expect(subject.conditions[:all]).to eq(new_conditions) 28 | end 29 | end 30 | 31 | describe "#any_conditions=" do 32 | it "should assign new values to any conditions" do 33 | new_conditions = [ 34 | { "field" => "type", "operator" => "is", "value" => "question" }, 35 | { "field" => "status", "operator" => "less_than", "value" => "solved" } 36 | ] 37 | subject.any_conditions = new_conditions 38 | 39 | expect(subject.conditions[:any]).to eq(new_conditions) 40 | end 41 | end 42 | 43 | describe "#add_all_condition" do 44 | it "should add a condition to all condition" do 45 | new_condition = { :field => "type", :operator => "is", :value => "problem" } 46 | existing_conditions = subject.conditions[:all] 47 | 48 | expect(existing_conditions).not_to include(new_condition) 49 | 50 | subject.add_all_condition("type", "is", "problem") 51 | 52 | expect(subject.conditions[:all]).to eq(existing_conditions << new_condition) 53 | end 54 | end 55 | 56 | describe "#add_any_condition" do 57 | it "should add a condition to any condition" do 58 | new_condition = { :field => "type", :operator => "is", :value => "task" } 59 | existing_conditions = subject.conditions[:any] 60 | 61 | expect(existing_conditions).not_to include(new_condition) 62 | 63 | subject.add_any_condition("type", "is", "task") 64 | 65 | expect(subject.conditions[:any]).to eq(existing_conditions << new_condition) 66 | end 67 | end 68 | 69 | describe "#add_action" do 70 | it "should add an action to the current actions" do 71 | new_action = { :field => "status", :value => "solved" } 72 | existing_actions = subject.actions 73 | 74 | expect(existing_actions).not_to include(new_action) 75 | 76 | subject.add_action("status", "solved") 77 | 78 | expect(subject.actions).to eq(existing_actions << new_action) 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/core/resources/cbp_spec_helper.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'an endpoint that supports CBP' do 2 | let(:collection_fetched) do 3 | VCR.use_cassette("cbp_#{described_class}_collection_fetch") do 4 | collection.fetch 5 | collection 6 | end 7 | end 8 | 9 | let(:response_body) { collection_fetched.response.body } 10 | let(:collection_fetched_results) { collection_fetched.to_a } 11 | 12 | it 'returns a CBP response with all the correct keys' do 13 | expect(response_body).to have_key('meta') 14 | expect(response_body).to have_key('links') 15 | expect(response_body['meta'].keys).to match_array(%w[has_more after_cursor before_cursor]) 16 | expect(response_body['links'].keys).to match_array(%w[prev next]) 17 | end 18 | 19 | it "returns a list of #{described_class} objects" do 20 | expect(collection_fetched_results).to all(be_a(described_class)) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/core/resources/macro_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Macro do 4 | def valid_attributes 5 | { 6 | :title => "my test macro", 7 | :actions => [{ :field => "priority", :value => "urgent" }] 8 | } 9 | end 10 | 11 | subject do 12 | described_class.new(double, valid_attributes) 13 | end 14 | 15 | describe "#add_action" do 16 | it "should add an action to the current actions" do 17 | new_action = { :field => "status", :value => "solved" } 18 | existing_actions = subject.actions 19 | 20 | expect(existing_actions).not_to include(new_action) 21 | 22 | subject.add_action("status", "solved") 23 | 24 | expect(subject.actions).to eq(existing_actions << new_action) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/core/resources/trigger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Trigger do 4 | def valid_attributes 5 | { 6 | :title => "my test trigger", 7 | :conditions => { 8 | :any => [{ :field => "assignee_id", :operator => "is", :value => 1 }], 9 | :all => [{ :field => "status", :operator => "is", :value => "open" }] 10 | } 11 | } 12 | end 13 | 14 | subject do 15 | described_class.new(double, valid_attributes) 16 | end 17 | 18 | describe "#all_conditions=" do 19 | it "should assign new values to all conditions" do 20 | new_conditions = [ 21 | { "field" => "type", "operator" => "is", "value" => "question" }, 22 | { "field" => "status", "operator" => "less_than", "value" => "solved" } 23 | ] 24 | subject.all_conditions = new_conditions 25 | 26 | expect(subject.conditions[:all]).to eq(new_conditions) 27 | end 28 | end 29 | 30 | describe "#any_conditions=" do 31 | it "should assign new values to any conditions" do 32 | new_conditions = [ 33 | { "field" => "type", "operator" => "is", "value" => "question" }, 34 | { "field" => "status", "operator" => "less_than", "value" => "solved" } 35 | ] 36 | subject.any_conditions = new_conditions 37 | 38 | expect(subject.conditions[:any]).to eq(new_conditions) 39 | end 40 | end 41 | 42 | describe "#add_all_condition" do 43 | it "should add a condition to all condition" do 44 | new_condition = { :field => "type", :operator => "is", :value => "problem" } 45 | existing_conditions = subject.conditions[:all] 46 | 47 | expect(existing_conditions).not_to include(new_condition) 48 | 49 | subject.add_all_condition("type", "is", "problem") 50 | 51 | expect(subject.conditions[:all]).to eq(existing_conditions << new_condition) 52 | end 53 | end 54 | 55 | describe "#add_any_condition" do 56 | it "should add a condition to any condition" do 57 | new_condition = { :field => "type", :operator => "is", :value => "task" } 58 | existing_conditions = subject.conditions[:any] 59 | 60 | expect(existing_conditions).not_to include(new_condition) 61 | 62 | subject.add_any_condition("type", "is", "task") 63 | 64 | expect(subject.conditions[:any]).to eq(existing_conditions << new_condition) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/core/resources/view_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::View do 4 | def valid_attributes 5 | { 6 | :title => "my test view", 7 | :conditions => { 8 | :any => [{ :field => "assignee_id", :operator => "is", :value => 1 }], 9 | :all => [{ :field => "status", :operator => "is", :value => "open" }] 10 | }, 11 | :execution => { 12 | :columns => [:id => "status", :title => "Status"] 13 | } 14 | } 15 | end 16 | 17 | subject do 18 | described_class.new(double, valid_attributes) 19 | end 20 | 21 | describe "#columns=" do 22 | it "should add a single column" do 23 | new_column = ["priority"] 24 | subject.columns = new_column 25 | 26 | expect(subject.output["columns"]).to eq(new_column) 27 | end 28 | 29 | it "should set columns on output" do 30 | new_columns = %w(type priority) 31 | subject.columns = new_columns 32 | 33 | expect(subject.output["columns"]).to eq(new_columns) 34 | end 35 | end 36 | 37 | describe "#add_column" do 38 | it "should add a column to the existing columns" do 39 | existing_columns = subject.execution.columns.map { |c| c["id"] } 40 | expect(existing_columns.include?("type")).to eq(false) 41 | 42 | subject.add_column("type") 43 | 44 | expect(subject.output["columns"]).to eq(existing_columns << "type") 45 | end 46 | end 47 | 48 | describe "#all_conditions=" do 49 | it "should assign new values to all conditions" do 50 | new_conditions = [ 51 | { "field" => "type", "operator" => "is", "value" => "question" }, 52 | { "field" => "status", "operator" => "less_than", "value" => "solved" } 53 | ] 54 | subject.all_conditions = new_conditions 55 | 56 | expect(subject.conditions[:all]).to eq(new_conditions) 57 | end 58 | end 59 | 60 | describe "#any_conditions=" do 61 | it "should assign new values to any conditions" do 62 | new_conditions = [ 63 | { "field" => "type", "operator" => "is", "value" => "question" }, 64 | { "field" => "status", "operator" => "less_than", "value" => "solved" } 65 | ] 66 | subject.any_conditions = new_conditions 67 | 68 | expect(subject.conditions[:any]).to eq(new_conditions) 69 | end 70 | end 71 | 72 | describe "#add_all_condition" do 73 | it "should add a condition to all condition" do 74 | new_condition = { :field => "type", :operator => "is", :value => "problem" } 75 | existing_conditions = subject.conditions[:all] 76 | 77 | expect(existing_conditions).not_to include(new_condition) 78 | 79 | subject.add_all_condition("type", "is", "problem") 80 | 81 | expect(subject.conditions[:all]).to eq(existing_conditions << new_condition) 82 | end 83 | end 84 | 85 | describe "#add_any_condition" do 86 | it "should add a condition to any condition" do 87 | new_condition = { :field => "type", :operator => "is", :value => "task" } 88 | existing_conditions = subject.conditions[:any] 89 | 90 | expect(existing_conditions).not_to include(new_condition) 91 | 92 | subject.add_any_condition("type", "is", "task") 93 | 94 | expect(subject.conditions[:any]).to eq(existing_conditions << new_condition) 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /spec/core/search_export_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::SearchExport do 4 | describe ".new" do 5 | context "when given an existing class" do 6 | it "returns an instance of the specific class" do 7 | expect(ZendeskAPI::SearchExport.new(nil, "result_type" => "user")).to be_instance_of(ZendeskAPI::User) 8 | end 9 | end 10 | 11 | context "when given a nonexistent class" do 12 | it "returns an instance of the generic Search::Result" do 13 | expect(ZendeskAPI::SearchExport.new(nil, "result_type" => "blah")).to be_instance_of(ZendeskAPI::SearchExport::Result) 14 | end 15 | end 16 | 17 | context "when not given anything" do 18 | it "returns an instance of Search::Result by default" do 19 | expect(ZendeskAPI::SearchExport.new(nil, {})).to be_instance_of(ZendeskAPI::SearchExport::Result) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/core/search_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Search do 4 | context ".new" do 5 | context "when given an existing class" do 6 | it "should return the correct class" do 7 | expect(ZendeskAPI::Search.new(nil, { "result_type" => "user" })).to be_instance_of(ZendeskAPI::User) 8 | end 9 | end 10 | 11 | context "when given a nonexistent class" do 12 | it "should return an object of the type Search::Result" do 13 | expect(ZendeskAPI::Search.new(nil, { "result_type" => "blah" })).to be_instance_of(ZendeskAPI::Search::Result) 14 | end 15 | end 16 | 17 | context "when not given anything" do 18 | it "should return an object of the type Search::Result" do 19 | expect(ZendeskAPI::Search.new(nil, {})).to be_instance_of(ZendeskAPI::Search::Result) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/core/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.join(File.dirname(__FILE__), "macros")) 2 | 3 | ENV['TZ'] = 'CET' # something that is not local and not utc so we find all the bugs 4 | 5 | require 'zendesk_api' 6 | require 'vcr' 7 | require 'logger' 8 | require 'stringio' 9 | 10 | begin 11 | require 'byebug' 12 | rescue LoadError 13 | puts "WARN: #{$ERROR_INFO.message} Continuing..." 14 | end 15 | 16 | class String 17 | def encoding_aware? 18 | false 19 | end 20 | end 21 | 22 | require File.join(File.dirname(__FILE__), '..', 'macros', 'resource_macros') 23 | require File.join(File.dirname(__FILE__), '..', 'fixtures', 'zendesk') 24 | require File.join(File.dirname(__FILE__), '..', 'fixtures', 'test_resources') 25 | 26 | $credentials_warning = false 27 | 28 | # tests fail when this is included in a Module (someone else also defines client) 29 | def client 30 | credentials = File.join(File.dirname(__FILE__), '..', 'fixtures', 'credentials.yml') 31 | @client ||= begin 32 | client = ZendeskAPI::Client.new do |config| 33 | if File.exist?(credentials) 34 | data = YAML.load(File.read(credentials)) 35 | config.username = data["username"] 36 | 37 | if data["token"] 38 | config.access_token = data["token"] 39 | config.url_based_access_token = true 40 | else 41 | config.password = data["password"] 42 | end 43 | 44 | if data["auth"] 45 | config.extend(Module.new do 46 | attr_accessor :authorization 47 | 48 | def options 49 | super.tap do |options| 50 | options[:headers].merge!( 51 | :authorization => "Basic #{Base64.urlsafe_encode64(authorization)}" 52 | ) 53 | end 54 | end 55 | end) 56 | config.authorization = data["auth"] 57 | end 58 | 59 | config.url = data["url"] 60 | 61 | if data["url"].start_with?("http://") 62 | config.allow_http = true 63 | end 64 | else 65 | unless $credentials_warning 66 | warn "using default credentials: live specs will fail." 67 | warn "add your credentials to spec/fixtures/credentials.yml (see: spec/fixtures/credentials.yml.example)" 68 | $credentials_warning = true 69 | end 70 | 71 | config.username = "please.change" 72 | config.password = "me" 73 | config.url = "https://my.zendesk.com/api/v2" 74 | end 75 | 76 | config.logger = Logger.new("/dev/null") 77 | 78 | config.retry = true 79 | end 80 | 81 | client.config.logger.level = (ENV["LOG"] ? Logger::DEBUG : Logger::WARN) 82 | client.config.cache.size = 0 83 | client.callbacks.clear 84 | 85 | client.insert_callback do |env| 86 | warning = env[:response_headers]["X-Zendesk-API-Warn"] 87 | 88 | if warning && warning !~ /\["access_token"\]/ && client.config.logger 89 | client.config.logger.warn "WARNING: #{warning}" 90 | end 91 | end 92 | 93 | client 94 | end 95 | end 96 | 97 | def random_string(length = 10) 98 | ('a'..'z').to_a.shuffle.take(length).join 99 | end 100 | 101 | module TestHelper 102 | def silence_logger 103 | old_level = client.config.logger.level 104 | client.config.logger.level = 6 105 | yield 106 | ensure 107 | client.config.logger.level = old_level 108 | end 109 | 110 | def silence_stderr 111 | $stderr = File.new('/dev/null', 'w') 112 | yield 113 | ensure 114 | $stderr = STDERR 115 | end 116 | 117 | def json(body = {}) 118 | JSON.dump(body) 119 | end 120 | 121 | def stub_json_request(verb, path_matcher, body = json, options = {}) 122 | stub_request(verb, path_matcher).to_return(Hashie::Mash.new( 123 | :body => body, :headers => { :content_type => "application/json", :content_length => body.size } 124 | ).deep_merge(options)) 125 | end 126 | end 127 | 128 | RSpec.configure do |c| 129 | c.before(:each) do 130 | ZendeskAPI::TestResource.associations.clear 131 | ZendeskAPI::TestResource.has_many :children, :class => ZendeskAPI::TestResource::TestChild 132 | end 133 | 134 | c.before(:each) do 135 | WebMock.reset! 136 | end 137 | 138 | c.around(:each, :silence_logger) do |example| 139 | silence_logger { example.call } 140 | end 141 | 142 | c.around(:each, :prevent_logger_changes) do |example| 143 | old_logger = client.config.logger 144 | example.call 145 | ensure 146 | client.config.logger = old_logger 147 | end 148 | 149 | # Used by `rspec spec/live --only-failures` in CI 150 | c.example_status_persistence_file_path = "spec/examples.txt" 151 | 152 | c.extend ResourceMacros 153 | c.extend ZendeskAPI::Fixtures 154 | c.include ZendeskAPI::Fixtures 155 | c.include TestHelper 156 | end 157 | 158 | VCR.configure do |c| 159 | c.cassette_library_dir = File.join(File.dirname(__FILE__), '..', 'fixtures', 'cassettes') 160 | c.default_cassette_options = { :record => :new_episodes, :decode_compressed_response => true, :serialize_with => :json, :preserve_exact_body_bytes => true } 161 | c.hook_into :webmock 162 | c.configure_rspec_metadata! 163 | 164 | # In CI this doesn't matter, since we start by deleting all the cassettes. 165 | # In development, this helps debugging. 166 | c.allow_http_connections_when_no_cassette = true 167 | end 168 | 169 | include WebMock::API 170 | -------------------------------------------------------------------------------- /spec/core/trackie_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Trackie do 4 | subject { ZendeskAPI::Trackie.new } 5 | before(:each) { subject.clear_changes } 6 | 7 | it "should not be changed" do 8 | expect(subject.changed?).to be(false) 9 | end 10 | 11 | context "adding keys" do 12 | before(:each) { subject[:key] = true } 13 | 14 | it "should include key in changes" do 15 | expect(subject.changes[:key]).to be(true) 16 | end 17 | 18 | specify "key should be changed" do 19 | expect(subject.changed?(:key)).to be(true) 20 | expect(subject.changed?).to be(true) 21 | end 22 | end 23 | 24 | context "deleting keys" do 25 | before(:each) do 26 | subject[:key] = true 27 | end 28 | 29 | it "returns key on deletion" do 30 | expect(subject.delete(:key)).to be(true) 31 | end 32 | 33 | context "after deletion" do 34 | before(:each) { subject.delete(:key) } 35 | 36 | it "keeps the changes" do 37 | expect(subject.changed?(:key)).to be(true) 38 | end 39 | end 40 | end 41 | 42 | context "adding identical keys" do 43 | before(:each) do 44 | subject[:key] = "foo" 45 | subject.clear_changes 46 | 47 | subject[:key] = "foo" 48 | end 49 | 50 | it "should not include key in changes" do 51 | expect(subject.changes[:key]).to be_falsey 52 | end 53 | 54 | specify "key should not be changed" do 55 | expect(subject.changed?(:key)).to be(false) 56 | expect(subject.changed?).to be(false) 57 | end 58 | end 59 | 60 | context "nested hashes" do 61 | before(:each) do 62 | subject[:key] = ZendeskAPI::Trackie.new 63 | subject.clear_changes 64 | subject[:key][:test] = true 65 | end 66 | 67 | it "should include changes from nested hash" do 68 | expect(subject.changes[:key][:test]).to be(true) 69 | end 70 | 71 | specify "subject should be changed" do 72 | expect(subject.changed?).to be(true) 73 | end 74 | end 75 | 76 | =begin TODO 77 | context "nested arrays" do 78 | before(:each) do 79 | subject[:key] = [] 80 | subject.clear_changes 81 | subject[:key] << :test 82 | end 83 | 84 | it "should include changes from nested array" do 85 | expect(subject.changes[:key]).to eq([:test]) 86 | end 87 | 88 | specify "subject should be changed" do 89 | expect(subject.changed?).to be(true) 90 | end 91 | end 92 | =end 93 | 94 | context "nested hashes in arrays" do 95 | before(:each) do 96 | subject[:key] = [ZendeskAPI::Trackie.new] 97 | subject.clear_changes 98 | subject[:key].first[:test] = true 99 | end 100 | 101 | it "should include changes from nested array" do 102 | expect(subject.changes[:key].first[:test]).to be(true) 103 | end 104 | 105 | specify "subject should be changed" do 106 | expect(subject.changed?).to be(true) 107 | end 108 | 109 | context "clearing" do 110 | before(:each) do 111 | subject.clear_changes 112 | end 113 | 114 | it "should not have any changes" do 115 | expect(subject.changes).to be_empty 116 | end 117 | end 118 | end 119 | 120 | describe "#size" do 121 | before do 122 | subject[:size] = 42 123 | end 124 | 125 | it "returns the value corresponding to the :size key" do 126 | expect(subject.size).to eq(42) 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /spec/fixtures/Argentina.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/zendesk_api_client_rb/0693480ae6cd0069e7b195ad1ea2de664cf3a3fa/spec/fixtures/Argentina.gif -------------------------------------------------------------------------------- /spec/fixtures/Argentina2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/zendesk_api_client_rb/0693480ae6cd0069e7b195ad1ea2de664cf3a3fa/spec/fixtures/Argentina2.gif -------------------------------------------------------------------------------- /spec/fixtures/credentials.yml.example: -------------------------------------------------------------------------------- 1 | username: your_username 2 | password: your_password 3 | url: https://your_zendesk_host/api/v2 4 | -------------------------------------------------------------------------------- /spec/fixtures/sample_app.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/zendesk_api_client_rb/0693480ae6cd0069e7b195ad1ea2de664cf3a3fa/spec/fixtures/sample_app.zip -------------------------------------------------------------------------------- /spec/fixtures/test_resources.rb: -------------------------------------------------------------------------------- 1 | class ZendeskAPI::TestResource < ZendeskAPI::Resource 2 | def self.test(client) 3 | "hi" 4 | end 5 | 6 | class TestChild < ZendeskAPI::Resource 7 | end 8 | end 9 | 10 | class ZendeskAPI::BulkTestResource < ZendeskAPI::DataResource 11 | extend ZendeskAPI::CreateMany 12 | extend ZendeskAPI::CreateOrUpdateMany 13 | extend ZendeskAPI::DestroyMany 14 | extend ZendeskAPI::UpdateMany 15 | end 16 | 17 | class ZendeskAPI::CreateOrUpdateTestResource < ZendeskAPI::DataResource 18 | extend ZendeskAPI::CreateOrUpdate 19 | end 20 | 21 | class ZendeskAPI::NilResource < ZendeskAPI::Data; end 22 | class ZendeskAPI::NilDataResource < ZendeskAPI::DataResource; end 23 | class ZendeskAPI::SingularTestResource < ZendeskAPI::SingularResource; end 24 | 25 | # `client.greetings` should ignore this class, as it's not in the right namespace 26 | class Greeting; end 27 | -------------------------------------------------------------------------------- /spec/fixtures/zendesk.rb: -------------------------------------------------------------------------------- 1 | module ZendeskAPI 2 | module Fixtures 3 | def user 4 | @user ||= find_or_create_user "end-user" 5 | end 6 | 7 | def current_user 8 | VCR.use_cassette('current_user') do 9 | @current_user ||= client.users.find(:id => 'me') 10 | end 11 | end 12 | 13 | def agent 14 | @agent ||= find_or_create_user "agent" 15 | end 16 | 17 | def find_or_create_user(role) 18 | VCR.use_cassette("valid_user_#{role}") do 19 | email = "zendesk-api-client-ruby-#{role}-#{client.config.username}" 20 | 21 | client.users.search(query: "email:#{email}").first || 22 | client.users.create( 23 | :name => "Test Valid with role #{role}", 24 | :verified => true, 25 | :email => email, 26 | :role => role 27 | ) 28 | end 29 | end 30 | 31 | def topic 32 | VCR.use_cassette('valid_topic') do 33 | @topic ||= client.topics.fetch.find { |t| t.name == "Test Topic" } 34 | @topic ||= client.topics.create( 35 | :name => "Test Topic", 36 | :description => "This is the body of a topic." 37 | ) 38 | end 39 | end 40 | 41 | def category 42 | VCR.use_cassette('valid_category') do 43 | @category ||= client.categories.first 44 | @category ||= client.categories.create(:name => "Test Category") 45 | end 46 | end 47 | 48 | def section 49 | VCR.use_cassette('valid_section') do 50 | @section ||= client.sections.first 51 | end 52 | end 53 | 54 | def article 55 | VCR.use_cassette('valid_article') do 56 | @article ||= client.articles.first 57 | end 58 | end 59 | 60 | def ticket 61 | VCR.use_cassette('valid_ticket') do 62 | @ticket ||= client.tickets.detect { |t| t.status != 'closed' } 63 | @ticket ||= client.tickets.create( 64 | :subject => "Test Ticket", 65 | :description => "This is a test of the emergency alert system.", 66 | :requester_id => user.id 67 | ) 68 | end 69 | end 70 | 71 | def suspended_ticket 72 | VCR.use_cassette('valid_suspended_ticket') do 73 | @suspended_ticket ||= client.suspended_tickets.first 74 | @suspended_ticket ||= begin 75 | client.anonymous_requests.create( 76 | :subject => "Test Ticket", 77 | :comment => { :value => "Help! I need somebody." }, 78 | :requester => { :email => "zendesk-api-client-ruby-anonymous-#{client.config.username}", :name => 'Anonymous User' } 79 | ) 80 | client.suspended_tickets(:reload => true).first 81 | end 82 | end 83 | end 84 | 85 | def group 86 | VCR.use_cassette('valid_group') do 87 | @ticket ||= client.groups.detect { |g| !g.default } 88 | @ticket ||= client.groups.create(:name => "Test Group") 89 | end 90 | end 91 | 92 | def organization 93 | VCR.use_cassette('valid_organization') do 94 | current_user.organization 95 | # ... or, if you can't work it out locally: 96 | # @organization ||= current_user.organization || client.organizations.fetch!.last 97 | end 98 | end 99 | 100 | def brand 101 | VCR.use_cassette('valid_brand') do 102 | @brand ||= client.brands.detect do |brand| 103 | client.config.url.start_with?(brand.brand_url) 104 | end 105 | end 106 | end 107 | 108 | def dynamic_content_item 109 | VCR.use_cassette('valid_dynamic_content') do 110 | @item ||= client.dynamic_content.items.first 111 | @item ||= client.dynamic_content.items.create!(:name => 'Test Item', :content => 'Testing', :default_locale_id => 1) 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /spec/live/Readme.md: -------------------------------------------------------------------------------- 1 | - Make an empty account (company name Z3N, email @zendesk.com, so it can be filtered) 2 | - Create an Admin user (ownwer cannot verify user identities) and ensure this new user has an organization. Then, add the new user credentials to `spec/fixtures/credentials.yml` 3 | - Mark 1 ticket as solved, change end-users email to on your can receive, copy ticket url, login as end-user (do not just assume identity), rate it 4 | - Create a new ticket and cc "zendesk-api-client-ruby-end-user-#{client.config.username}" (run tests once to create this user) 5 | - Suspend "zendesk-api-client-ruby-anonymous-#{client.config.username}" account, so tickets created by this account go to suspended 6 | - Ensure you allow admins to set up user password (or `POST /api/v2/users/{user_id}/password.json` will fail). You can check this in the admin centre > security > advanced 7 | - Go to Account > Localization and ensure you add `Spanish` to the list of additional languages. The `variant_spec` needs to create some items using spanish locale. 8 | - Ensure the authenticated agent is the assigned to the main fixture ticket at `spec/fixtures/zendesk.rb`, or the `activities_spec` won't get any update, and thus, will fail. 9 | -------------------------------------------------------------------------------- /spec/live/app_installation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::AppInstallation do 4 | it "should work" do 5 | upload = VCR.use_cassette("app_installations_upload_create") do 6 | ZendeskAPI::App::Upload.new(client, :id => "spec/fixtures/sample_app.zip").tap(&:save!) 7 | end 8 | 9 | attributes = { :upload_id => upload.id, :name => "My App", :short_description => "Testing" } 10 | 11 | app = ZendeskAPI::App.new(client, attributes) 12 | 13 | VCR.use_cassette("app_installations_create") { app.save! } 14 | 15 | body = {} 16 | 17 | VCR.use_cassette("app_installations_create_job_status") do 18 | until %w{failed completed}.include?(body["status"]) 19 | response = client.connection.get(app.response.headers["Location"]) 20 | body = response.body 21 | 22 | sleep(3) 23 | end 24 | end 25 | 26 | if body["status"] == "failed" 27 | fail "Could not create app: #{body.inspect}" 28 | end 29 | 30 | app.id = body["app_id"] 31 | 32 | attributes = { 33 | :app_id => app.id, 34 | :settings => { 35 | :name => "My App" 36 | } 37 | } 38 | 39 | install = ZendeskAPI::AppInstallation.new(client, attributes) 40 | 41 | VCR.use_cassette("app_install_create") { install.save! } 42 | 43 | installations = client.app.installations 44 | VCR.use_cassette("app_install_fetch") { installations.fetch! } 45 | VCR.use_cassette("app_install_find") { client.app.installations.find!(:id => install.id) } 46 | 47 | expect(installations).to include(install) 48 | 49 | install.settings.name = "My New Name" 50 | VCR.use_cassette("app_install_update") { install.save! } 51 | 52 | expect(install.settings.title).to eq("My New Name") 53 | 54 | VCR.use_cassette("app_install_destroy") { install.destroy! } 55 | 56 | VCR.use_cassette("app_installations_destroy") { app.destroy! } 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/live/app_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::App do 4 | it "should work" do 5 | upload = VCR.use_cassette("app_upload_create") do 6 | ZendeskAPI::App::Upload.new(client, :id => "spec/fixtures/sample_app.zip").tap(&:save!) 7 | end 8 | 9 | attributes = { :upload_id => upload.id, :name => "My App", :short_description => "Testing" } 10 | 11 | app = ZendeskAPI::App.new(client, attributes) 12 | 13 | VCR.use_cassette("app_create") { app.save! } 14 | 15 | body = check_job(app) 16 | 17 | app.id = body["app_id"] 18 | app.author_name = "Mr. Sprinkles" 19 | app.author_email = "sprinkle@example.com" 20 | 21 | VCR.use_cassette("app_save") { app.save! } 22 | 23 | expect(app.author_name).to eq("Mr. Sprinkles") 24 | 25 | VCR.use_cassette("app_find") { client.apps.find!(:id => app.id) } 26 | VCR.use_cassette("app_destroy") { app.destroy! } 27 | end 28 | 29 | it "should be able to handle the simplest creation api call" do 30 | VCR.use_cassette("app_simple_create") do 31 | app = ZendeskAPI::App.create!(client, { :name => "Testing App Creation", :upload => "spec/fixtures/sample_app.zip" }) 32 | 33 | body = check_job(app) 34 | 35 | app.id = body["app_id"] 36 | VCR.use_cassette("app_destroy") { app.destroy! } 37 | end 38 | end 39 | 40 | def check_job(app) 41 | body = {} 42 | 43 | VCR.use_cassette("app_create_job_status") do 44 | until %w{failed completed}.include?(body["status"]) 45 | response = client.connection.get(app.response.headers["Location"]) 46 | body = response.body 47 | 48 | sleep(3) 49 | end 50 | end 51 | 52 | if body["status"] == "failed" 53 | fail "Could not create app: #{body.inspect}" 54 | end 55 | 56 | body 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/live/article_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | RSpec.describe ZendeskAPI::Article, :delete_after do 4 | it "expects article to exist" do 5 | expect(article).not_to be_nil 6 | end 7 | 8 | it "can have translations", :vcr do 9 | article.translations.create(locale: "fr", title: "Traduction", body: "Bonjour") 10 | 11 | expect(article.translations.map(&:locale)).to include("fr") 12 | end 13 | 14 | it "can have labels", :vcr do 15 | label = article.labels.create!(name: "ruby-client-test-label") 16 | 17 | expect(article.labels.map(&:name)).to include(label.name) 18 | ensure # Cleanup 19 | label&.destroy 20 | end 21 | 22 | describe "creating articles within a section" do 23 | def valid_attributes 24 | { :name => "My Article", user_segment_id: nil, permission_group_id: 2801272, title: "My super article" } 25 | end 26 | 27 | let(:section_article) do 28 | VCR.use_cassette('create_article_within_section') do 29 | section.articles.create(valid_attributes) 30 | end 31 | end 32 | 33 | after do 34 | VCR.use_cassette('delete_article_within_section') do 35 | section_article.destroy 36 | end 37 | end 38 | 39 | it "can be created" do 40 | expect(section_article).not_to be_nil 41 | end 42 | 43 | describe "#search" do 44 | before { section_article } 45 | 46 | it "finds the article", :vcr do 47 | actual = client.articles.search(query: "What") 48 | 49 | expect(actual.count).to be > 0 50 | expect(actual.last.title).to eq("What are these sections and articles doing here?") 51 | expect(actual.last.votes.any?).to be(true) # Manually set in UI 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/live/audit_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Ticket::Audit do 4 | it_should_be_readable ticket, :audits 5 | 6 | describe ZendeskAPI::Ticket::Audit::Event, :vcr do 7 | it "should side-load events" do 8 | audit = ticket.audits(include: :users).first 9 | event = audit.events.first 10 | 11 | expect(event).to be_instance_of(ZendeskAPI::Ticket::Audit::Event) 12 | expect(event.author).to be_instance_of(ZendeskAPI::User) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/live/automation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Automation, :delete_after do 4 | def valid_attributes 5 | { 6 | :title => "my test automation_ruby_sdk_test", 7 | :conditions => { 8 | :all => [{ :field => "status", :operator => "is", :value => "open" }] 9 | }, 10 | :actions => [{ :field => "status", :value => "solved" }] 11 | } 12 | end 13 | 14 | it_should_be_readable :automations 15 | it_should_be_readable :automations, :active 16 | 17 | it_should_be_creatable 18 | it_should_be_updatable :conditions, { 19 | "any" => [], 20 | "all" => [{ "field" => "status", "operator" => "is", "value" => "pending" }] 21 | } 22 | it_should_be_deletable 23 | end 24 | -------------------------------------------------------------------------------- /spec/live/bookmark_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Bookmark, :not_findable, :delete_after do 4 | def valid_attributes 5 | { :ticket_id => ticket.id } 6 | end 7 | 8 | it_should_be_creatable 9 | it_should_be_deletable 10 | it_should_be_readable :bookmarks, :create => true 11 | end 12 | -------------------------------------------------------------------------------- /spec/live/brand_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Brand, :delete_after do 4 | def valid_attributes 5 | { :name => "awesomesauce_ruby_sdk_test", :subdomain => "zendeskapi#{SecureRandom.hex(3)}" } 6 | end 7 | 8 | it_should_be_creatable 9 | it_should_be_updatable :name, "awesomesauce_ruby_sdk_updated_name" 10 | it_should_be_readable :brands 11 | 12 | # Deleted brands are still findable by id, but in the index action 13 | it_should_be_deletable :find => nil 14 | end 15 | -------------------------------------------------------------------------------- /spec/live/category_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Category, :delete_after do 4 | def valid_attributes 5 | { :name => "My Category" } 6 | end 7 | 8 | it "can have translations", :vcr do 9 | category.translations.create(locale: "fr-ca", title: "Traduction", body: "Bon matin") 10 | 11 | expect(category.translations.map(&:locale)).to include("fr-ca") 12 | end 13 | 14 | it_should_be_creatable 15 | it_should_be_updatable :position, 2 16 | it_should_be_deletable 17 | it_should_be_readable :categories, :create => true 18 | end 19 | -------------------------------------------------------------------------------- /spec/live/cbp_support_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | require 'core/resources/cbp_spec_helper' 3 | 4 | describe 'Endpoints that support CBP' do 5 | describe ZendeskAPI::Group do 6 | describe '/groups' do 7 | it_behaves_like 'an endpoint that supports CBP' do 8 | let(:collection) { client.groups } 9 | end 10 | end 11 | 12 | describe '/groups/assignable' do 13 | it_behaves_like 'an endpoint that supports CBP' do 14 | let(:collection) { client.groups.assignable } 15 | end 16 | end 17 | end 18 | 19 | describe ZendeskAPI::GroupMembership do 20 | describe '/groups/:id/memberships' do 21 | let(:one_group) { VCR.use_cassette("cbp_group_memberships_all_groups") { client.groups.fetch.last } } 22 | it_behaves_like 'an endpoint that supports CBP' do 23 | let(:collection) { one_group.memberships } 24 | end 25 | end 26 | end 27 | 28 | describe ZendeskAPI::Organization do 29 | describe '/organizations' do 30 | it_behaves_like 'an endpoint that supports CBP' do 31 | let(:collection) { client.organizations } 32 | end 33 | end 34 | end 35 | 36 | describe ZendeskAPI::OrganizationMembership do 37 | describe '/organizations/:id/subscriptions' do 38 | let(:one_organization) { VCR.use_cassette("cbp_organization_subscriptions_all_organizations") { client.organizations.fetch.last } } 39 | it_behaves_like 'an endpoint that supports CBP' do 40 | let(:collection) { one_organization.subscriptions } 41 | end 42 | end 43 | end 44 | 45 | describe ZendeskAPI::Trigger do 46 | describe '/triggers' do 47 | it_behaves_like 'an endpoint that supports CBP' do 48 | let(:collection) { client.triggers } 49 | end 50 | end 51 | 52 | describe '/triggers/active' do 53 | it_behaves_like 'an endpoint that supports CBP' do 54 | let(:collection) { client.triggers.active } 55 | end 56 | end 57 | end 58 | 59 | describe ZendeskAPI::TicketField do 60 | describe '/ticket_fields' do 61 | it_behaves_like 'an endpoint that supports CBP' do 62 | let(:collection) { client.ticket_fields } 63 | end 64 | end 65 | end 66 | 67 | describe ZendeskAPI::Topic do 68 | describe '/community/topics' do 69 | let(:collection_fetched) do 70 | VCR.use_cassette("cbp_#{described_class}_collection_fetch") do 71 | client.topics.fetch 72 | client.topics 73 | end 74 | end 75 | 76 | let(:response_body) { collection_fetched.response.body } 77 | let(:collection_fetched_results) { collection_fetched.to_a } 78 | 79 | it 'returns a CBP response with all the correct keys' do 80 | expect(response_body).to have_key('meta') 81 | expect(response_body).to have_key('links') 82 | expect(response_body['meta'].keys).to match_array(%w[has_more after_cursor before_cursor]) 83 | # expect(response_body['links'].keys).to match_array(%w[prev next]) this implementation omits prev and next keys 84 | # instead of giving them a nil value 85 | end 86 | 87 | it "returns a list of #{described_class} objects" do 88 | expect(collection_fetched_results).to all(be_a(described_class)) 89 | end 90 | end 91 | end 92 | 93 | describe ZendeskAPI::View do 94 | describe '/views' do 95 | it_behaves_like 'an endpoint that supports CBP' do 96 | let(:collection) { client.views } 97 | end 98 | end 99 | end 100 | 101 | describe ZendeskAPI::Ticket do 102 | describe '/tickets' do 103 | it_behaves_like 'an endpoint that supports CBP' do 104 | let(:collection) { client.tickets } 105 | end 106 | end 107 | 108 | describe '/organizations/:id/tickets' do 109 | let(:organization) do 110 | VCR.use_cassette("cbp_#{described_class}_organization_fetch") do 111 | client.organizations.fetch.first 112 | end 113 | end 114 | 115 | it_behaves_like 'an endpoint that supports CBP' do 116 | let(:collection) { organization.tickets } 117 | end 118 | end 119 | 120 | describe '/users/:id/tickets/requested' do 121 | let(:user) do 122 | VCR.use_cassette("cbp_#{described_class}_user_fetch") do 123 | client.users.fetch.first 124 | end 125 | end 126 | 127 | it_behaves_like 'an endpoint that supports CBP' do 128 | let(:collection) { user.requested_tickets } 129 | end 130 | end 131 | end 132 | 133 | describe ZendeskAPI::Ticket::Audit do 134 | describe '/tickets/:id/audits' do 135 | let(:ticket) do 136 | VCR.use_cassette("cbp_#{described_class}_ticket_fetch") do 137 | client.tickets.fetch.first 138 | end 139 | end 140 | 141 | it_behaves_like 'an endpoint that supports CBP' do 142 | let(:collection) { ticket.audits } 143 | end 144 | end 145 | end 146 | 147 | describe ZendeskAPI::TicketMetric do 148 | describe '/ticket_metrics' do 149 | it_behaves_like 'an endpoint that supports CBP' do 150 | let(:collection) { client.ticket_metrics } 151 | end 152 | end 153 | end 154 | 155 | describe ZendeskAPI::Tag do 156 | describe '/tags' do 157 | it_behaves_like 'an endpoint that supports CBP' do 158 | let(:collection) { client.tags } 159 | end 160 | end 161 | end 162 | 163 | describe ZendeskAPI::SuspendedTicket do 164 | describe '/suspended_tickets' do 165 | it_behaves_like 'an endpoint that supports CBP' do 166 | let(:collection) { client.suspended_tickets } 167 | end 168 | end 169 | end 170 | 171 | describe ZendeskAPI::Activity do 172 | describe '/activities' do 173 | it_behaves_like 'an endpoint that supports CBP' do 174 | let(:collection) { client.activities } 175 | end 176 | end 177 | end 178 | 179 | describe ZendeskAPI::Automation do 180 | describe '/automations' do 181 | it_behaves_like 'an endpoint that supports CBP' do 182 | let(:collection) { client.automations } 183 | end 184 | end 185 | end 186 | 187 | describe ZendeskAPI::DeletedTicket do 188 | describe '/deleted_tickets' do 189 | it_behaves_like 'an endpoint that supports CBP' do 190 | let(:collection) { client.deleted_tickets } 191 | end 192 | end 193 | end 194 | 195 | describe ZendeskAPI::Macro do 196 | describe '/macros' do 197 | it_behaves_like 'an endpoint that supports CBP' do 198 | let(:collection) { client.macros } 199 | end 200 | end 201 | end 202 | 203 | describe ZendeskAPI::OauthClient do 204 | describe '/oauth/clients' do 205 | it_behaves_like 'an endpoint that supports CBP' do 206 | let(:collection) { client.oauth_clients } 207 | end 208 | end 209 | end 210 | 211 | describe ZendeskAPI::Brand do 212 | describe '/brands' do 213 | it_behaves_like 'an endpoint that supports CBP' do 214 | let(:collection) { client.brands } 215 | end 216 | end 217 | end 218 | 219 | describe ZendeskAPI::User do 220 | describe '/users' do 221 | it_behaves_like 'an endpoint that supports CBP' do 222 | let(:collection) { client.users } 223 | end 224 | end 225 | 226 | describe '/organizations/:id/users' do 227 | let(:organization) do 228 | VCR.use_cassette("cbp_#{described_class}_organization_fetch") do 229 | client.organizations.fetch.first 230 | end 231 | end 232 | it_behaves_like 'an endpoint that supports CBP' do 233 | let(:collection) { organization.users } 234 | end 235 | end 236 | end 237 | 238 | describe ZendeskAPI::AgentAvailability do 239 | describe '/agent_availabilities' do 240 | let(:collection_fetched) do 241 | VCR.use_cassette("cbp_#{described_class}_collection") do 242 | client.agent_availabilities.fetch 243 | client.agent_availabilities 244 | end 245 | end 246 | 247 | let(:response_body) { collection_fetched.response.body } 248 | let(:collection_fetched_results) { collection_fetched.to_a } 249 | it 'returns a CBP response with all the correct keys' do 250 | expect(response_body).to have_key('meta') 251 | expect(response_body).to have_key('links') 252 | expect(response_body['meta'].keys).to include('has_more') 253 | end 254 | 255 | it 'returns a list of AgentAvailability objects' do 256 | expect(collection_fetched_results).to all(be_a(described_class)) 257 | end 258 | end 259 | end 260 | end 261 | -------------------------------------------------------------------------------- /spec/live/collection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Collection do 4 | subject do 5 | ZendeskAPI::Collection.new(client, ZendeskAPI::TestResource) 6 | end 7 | 8 | context "with real data" do 9 | subject do 10 | ZendeskAPI::Collection.new(client, ZendeskAPI::User) 11 | end 12 | 13 | before(:each) do 14 | VCR.use_cassette('collection_fetch_users') do 15 | subject.per_page(1).page(2) 16 | subject.fetch(true) 17 | end 18 | end 19 | 20 | context "pagination with no options", :vcr do 21 | before(:each) { subject.per_page(nil).page(nil) } 22 | 23 | it "should find the next page by calling fetch" do 24 | current = subject.to_a.dup 25 | nxt = subject.next 26 | 27 | expect(nxt.size).to eq(1) 28 | expect(nxt).to_not eq(current) 29 | end 30 | 31 | it "should find the prev page by calling fetch" do 32 | current = subject.to_a.dup 33 | prev = subject.prev 34 | 35 | expect(prev.size).to eq(1) 36 | expect(prev).to_not eq(current) 37 | end 38 | end 39 | 40 | context "pagination with options", :vcr do 41 | before(:each) { subject.per_page(1).page(2) } 42 | 43 | it "should increase page option and not call fetch" do 44 | expect(subject.next).to eq(3) 45 | end 46 | 47 | it "should decrease page option and not call fetch" do 48 | expect(subject.prev).to eq(1) 49 | end 50 | 51 | context "with page == 1" do 52 | before do 53 | subject.page(1) 54 | subject.clear_cache 55 | expect(subject).to_not receive(:fetch) 56 | end 57 | 58 | it "should do nothing on #prev" do 59 | expect(subject.prev).to eq([]) 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/live/custom_role_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::CustomRole do 4 | it_should_be_readable :custom_roles 5 | end 6 | -------------------------------------------------------------------------------- /spec/live/custom_status_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | require 'securerandom' 3 | 4 | describe ZendeskAPI::CustomStatus, :delete_after do 5 | def valid_attributes 6 | { 7 | status_category: 'open', 8 | agent_label: "Agent Label #{SecureRandom.hex(6)}", 9 | end_user_label: "End User Label #{SecureRandom.hex(6)}", 10 | description: "Description #{SecureRandom.hex(6)}", 11 | end_user_description: "End User Description #{SecureRandom.hex(6)}", 12 | active: false 13 | } 14 | end 15 | 16 | it_should_be_creatable 17 | it_should_be_updatable :agent_label, "ruby_sdk_test_agent_label_updated" 18 | it_should_be_updatable :end_user_label, 'New End User Label' 19 | it_should_be_updatable :description, 'New Description' 20 | it_should_be_updatable :end_user_description, 'New End User Description' 21 | it_should_be_deletable find: [:active?, false] 22 | it_should_be_readable :custom_statuses 23 | end 24 | -------------------------------------------------------------------------------- /spec/live/dynamic_content/item_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::DynamicContent::Item, :delete_after do 4 | def valid_attributes 5 | { 6 | :name => "Dynamic Content Item name Ruby SDK test", 7 | :default_locale_id => 1, 8 | :content => "Ruby SDK test content" 9 | } 10 | end 11 | 12 | it_should_be_readable :dynamic_content, :items, :create => true 13 | it_should_be_creatable 14 | it_should_be_updatable :name, 'Updated Dynamic Content Item name Ruby SDK test' 15 | it_should_be_deletable 16 | end 17 | -------------------------------------------------------------------------------- /spec/live/dynamic_content/variant_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::DynamicContent::Item::Variant, :delete_after do 4 | def valid_attributes 5 | { 6 | :locale_id => 2, 7 | :active => true, 8 | :default => false, 9 | :content => 'Ruby SDK Test Variant Content' 10 | } 11 | end 12 | 13 | under dynamic_content_item do 14 | it_should_be_readable dynamic_content_item, :variants, :create => true 15 | it_should_be_creatable 16 | it_should_be_updatable :content 17 | it_should_be_deletable 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/live/group_membership_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::GroupMembership, :delete_after do 4 | before :all do 5 | VCR.use_cassette("delete_existing_group_memberships_create") do 6 | agent.group_memberships.each(&:destroy) 7 | end 8 | end 9 | 10 | def valid_attributes 11 | { :group_id => group.id, :user_id => agent.id } 12 | end 13 | 14 | it_should_be_creatable 15 | it_should_be_deletable 16 | it_should_be_readable :group_memberships 17 | it_should_be_readable agent, :group_memberships, :create => true 18 | end 19 | -------------------------------------------------------------------------------- /spec/live/group_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Group, :delete_after do 4 | def valid_attributes 5 | { :name => "My Group" } 6 | end 7 | 8 | it_should_be_creatable 9 | it_should_be_updatable :name 10 | it_should_be_deletable :find => [:deleted?, true] 11 | it_should_be_readable :groups 12 | it_should_be_readable :groups, :assignable 13 | 14 | context "with a membership" do 15 | before(:each) do 16 | VCR.use_cassette("read_ZendeskAPI::User_groups_create") do 17 | attrs = valid_attributes 18 | attrs.merge!(@default_options) if @default_options 19 | @object = described_class.create!(client, attrs) 20 | @membership = agent.group_memberships.create(:group_id => @object.id, :user_id => agent.id) 21 | end 22 | end 23 | 24 | after(:each) do 25 | VCR.use_cassette("read_ZendeskAPI::User_groups_delete") do 26 | @object.destroy 27 | end 28 | end 29 | 30 | it_should_be_readable agent, :groups 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/live/identity_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::User::Identity, :delete_after do 4 | def valid_attributes 5 | { :type => "email", :value => "ruby_sdk_test@example.com" } 6 | end 7 | 8 | under current_user do 9 | it_should_be_creatable 10 | it_should_be_updatable :verified, true 11 | it_should_be_deletable 12 | it_should_be_readable current_user, :identities, :create => true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/live/locale_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Locale, :vcr do 4 | specify "client#current_locale" do 5 | expect(client.current_locale).to be_instance_of(described_class) 6 | end 7 | 8 | it_should_be_readable :locales 9 | end 10 | -------------------------------------------------------------------------------- /spec/live/macro_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Macro, :delete_after do 4 | def valid_attributes 5 | { :title => "my test macro", :actions => [{ :field => "status", :value => "solved" }] } 6 | end 7 | 8 | it_should_be_readable :macros 9 | it_should_be_readable :macros, :active 10 | 11 | it_should_be_creatable 12 | it_should_be_updatable :actions, [{ "field" => "priority", "value" => "low" }] 13 | it_should_be_deletable 14 | 15 | describe "application", :vcr do 16 | subject { @object } 17 | 18 | before :all do 19 | VCR.use_cassette("#{described_class.to_s}_application_create") do 20 | @object = described_class.create(client, valid_attributes.merge(default_options)) 21 | end 22 | end 23 | 24 | after :all do 25 | VCR.use_cassette("#{described_class.to_s}_application_delete") do 26 | @object.destroy 27 | end 28 | end 29 | 30 | describe "to a ticket" do 31 | it "should return a hash" do 32 | result = subject.apply(ticket) 33 | expect(result.ticket).to_not be_nil 34 | end 35 | end 36 | 37 | it "should be appliable" do 38 | result = subject.apply 39 | expect(result.ticket).to_not be_nil 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/live/organization_field_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::OrganizationField, :delete_after do 4 | def valid_attributes 5 | { :type => "text", :title => "Age", :key => random_string(5) } 6 | end 7 | 8 | it_should_be_creatable 9 | it_should_be_updatable :title, "key" 10 | it_should_be_readable :organization_fields, :create => true 11 | it_should_be_deletable :marked_for_deletion => true 12 | end 13 | -------------------------------------------------------------------------------- /spec/live/organization_membership_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::OrganizationMembership, :delete_after do 4 | def valid_attributes 5 | { :organization_id => organization.id, :user_id => user.id } 6 | end 7 | 8 | it_should_be_creatable 9 | it_should_be_deletable 10 | it_should_be_readable :organization_memberships 11 | 12 | describe "create_or_update" do 13 | after do 14 | VCR.use_cassette("create_or_update_destroy_organization_membership") do 15 | organization_membership.destroy 16 | end 17 | end 18 | 19 | context "when the organization membership already exist" do 20 | let!(:organization) do 21 | VCR.use_cassette("create_or_update_create_organization_membership") do 22 | client.organization_memberships.create(name: "Existing", organization_id: organization.id, user_id: user.id) 23 | end 24 | end 25 | 26 | before do 27 | VCR.use_cassette("create_or_update_existing_organization_membership") do 28 | ZendeskAPI::OrganizationMembership.create_or_update!(client, organization_id: organization.id, user_id: user.id) 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/live/organization_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Organization, :delete_after do 4 | def valid_attributes 5 | { :name => 'organization_name_ruby_sdk_test' } 6 | end 7 | 8 | it_should_be_creatable 9 | it_should_be_updatable :name, 'organization_name_ruby_sdk_test_updated' 10 | it_should_be_deletable 11 | it_should_be_readable :organizations, :create => true 12 | 13 | describe "create_or_update" do 14 | after do 15 | VCR.use_cassette("create_or_update_destroy_organization") do 16 | organization.destroy 17 | end 18 | end 19 | 20 | context "when the organization already exist" do 21 | let!(:organization) do 22 | VCR.use_cassette("create_or_update_create_organization") do 23 | client.organizations.create(name: "Existing", external_id: "100") 24 | end 25 | end 26 | 27 | before do 28 | VCR.use_cassette("create_or_update_existing_organization") do 29 | ZendeskAPI::Organization.create_or_update!(client, name: "Updated!", external_id: "100") 30 | end 31 | end 32 | 33 | it "updates the existing organization" do 34 | VCR.use_cassette("create_or_update_find_existing_organization") do 35 | expect(client.organizations.find(id: organization.id).name).to eql "Updated!" 36 | end 37 | end 38 | end 39 | end 40 | 41 | describe "create_many and destroy_many" do 42 | let(:create_many_job) do 43 | VCR.use_cassette("create_many_organizations_job") do 44 | ZendeskAPI::Organization.create_many!( 45 | client, 46 | [ 47 | { name: "one", external_id: "101" }, 48 | { name: "two", external_id: "102" } 49 | ] 50 | ).tap do |job| 51 | job.reload! while job.status != "completed" 52 | end 53 | end 54 | end 55 | 56 | let(:destroy_many_job) do 57 | VCR.use_cassette("destroy_many_organizations_job") do 58 | ZendeskAPI::Organization.destroy_many!( 59 | client, 60 | create_many_job.results.map { |result| result["id"] } 61 | ).tap do |job| 62 | job.reload! while job.status != "completed" 63 | end 64 | end 65 | end 66 | 67 | # If fails, try deleting the orgs using the REPL 68 | it "creates many and then it can destroy many" do 69 | created_orgs = create_many_job.results.filter { |x| x["status"] == "Created" } 70 | 71 | expect(created_orgs.count).to be 2 72 | expect(destroy_many_job["total"]).to be 2 73 | end 74 | end 75 | 76 | describe "related" do 77 | let!(:organization) do 78 | VCR.use_cassette("organization_related_create_organization") do 79 | client.organizations.create(valid_attributes) 80 | end 81 | end 82 | 83 | after do 84 | VCR.use_cassette("organization_related_destroy_organization") do 85 | organization.destroy 86 | end 87 | end 88 | 89 | it "shows realated information" do 90 | VCR.use_cassette("organization_related_information") do 91 | expect(organization.related).to be_a ZendeskAPI::OrganizationRelated 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /spec/live/organization_subscription_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::OrganizationSubscription, :delete_after do 4 | before(:all) do 5 | VCR.use_cassette("enable_shared_tickets") do 6 | organization.update(shared_tickets: true) 7 | organization.save! 8 | end 9 | VCR.use_cassette("create_organization_membership") do 10 | @organization_membership = client.organization_memberships.create!(user_id: user.id, organization_id: organization.id) 11 | end 12 | end 13 | 14 | after(:all) do 15 | VCR.use_cassette("destroy_organization_membership") do 16 | @organization_membership.destroy 17 | end 18 | VCR.use_cassette("disable_shared_tickets") do 19 | organization.update(shared_tickets: false) 20 | organization.save! 21 | end 22 | end 23 | 24 | def valid_attributes 25 | { :organization_id => organization.id, :user_id => user.id } 26 | end 27 | 28 | it_should_be_creatable 29 | it_should_be_deletable 30 | end 31 | -------------------------------------------------------------------------------- /spec/live/push_notification_device_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::PushNotificationDevice do 4 | describe ".destroy_many" do 5 | describe "Existing push notification devices" do 6 | it "destroys the given push notification devices" do 7 | VCR.use_cassette("push_notification_devices_destroy_many") do 8 | ZendeskAPI::PushNotificationDevice.destroy_many(client, %w(foo bar)) 9 | end 10 | end 11 | end 12 | 13 | describe "Non-existing devices" do 14 | it "silently ignores the devices" do 15 | VCR.use_cassette("push_notification_devices_destroy_many") do 16 | ZendeskAPI::PushNotificationDevice.destroy_many(client, ["baz"]) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/live/request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Request do 4 | def valid_attributes 5 | { 6 | :subject => "This is a question!", 7 | :comment => { :value => "Haha, no." } 8 | } 9 | end 10 | 11 | it_should_be_creatable 12 | it_should_be_updatable :solved, true, { :comment => { :value => 'This is solved!' } } 13 | it_should_be_readable :requests 14 | it_should_be_readable user, :requests 15 | 16 | it "can upload while creating" do 17 | VCR.use_cassette("request_inline_uploads") do 18 | request = ZendeskAPI::Request.new(client, valid_attributes) 19 | request.comment.uploads << "spec/fixtures/Argentina.gif" 20 | request.comment.uploads << File.new("spec/fixtures/Argentina.gif") 21 | 22 | request.save! 23 | expect(request.changes).to eq({}) # uploads were set before save 24 | expect(request.comment.attributes[:uploads].map(&:class)).to eq([String, String]) # upload was sent as tokens 25 | end 26 | end 27 | 28 | it "can comment while creating" do 29 | VCR.use_cassette("request_inline_comments") do 30 | request = ZendeskAPI::Request.new(client, valid_attributes) 31 | request.comment = ZendeskAPI::Request::Comment.new(client, :value => "My comment") 32 | request.save! 33 | 34 | expect(request.changes).to eq({}) # comment was set before save 35 | expect(request.attributes[:comment]).to eq({ "value" => "My comment" }) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/live/satisfaction_rating_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::SatisfactionRating do 4 | it_should_be_readable :satisfaction_ratings 5 | it_should_be_readable :satisfaction_ratings, :received 6 | end 7 | -------------------------------------------------------------------------------- /spec/live/schedule_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | RSpec.describe ZendeskAPI::Schedule, :delete_after do 4 | def valid_attributes 5 | { 6 | name: "Brit Schedule", 7 | time_zone: "London" 8 | } 9 | end 10 | 11 | it_should_be_creatable 12 | end 13 | -------------------------------------------------------------------------------- /spec/live/section_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Section, :delete_after do 4 | it "expects section to exist" do 5 | expect(section).not_to be_nil 6 | end 7 | 8 | it "can have translations", :vcr do 9 | section.translations.create(locale: "es", title: "Traducción", body: "Hola") 10 | 11 | expect(section.translations.map(&:locale)).to include("es") 12 | end 13 | 14 | describe "creating sections withing categories" do 15 | def valid_attributes 16 | { :name => "My Section" } 17 | end 18 | 19 | let(:category_section) do 20 | VCR.use_cassette('create_section_within_category') do 21 | category.sections.create(valid_attributes) 22 | end 23 | end 24 | 25 | after do 26 | VCR.use_cassette('delete_section_within_category') do 27 | category_section.destroy 28 | end 29 | end 30 | 31 | it "can be created" do 32 | expect(category_section).not_to be_nil 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/live/setting_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Setting do 4 | it_should_be_readable :settings, :path => 'account/settings' 5 | 6 | under(user = ZendeskAPI::User.new(client, :id => 'me')) do 7 | it_should_be_readable user, :settings 8 | 9 | describe 'updating', :vcr do 10 | it 'should be updatable' do 11 | settings = user.settings 12 | lotus = settings.detect { |set| set.on == "lotus" } 13 | 14 | original_setting = lotus.keyboard_shortcuts_enabled 15 | lotus.keyboard_shortcuts_enabled = !original_setting 16 | 17 | settings.save! 18 | settings.fetch!(true) 19 | 20 | lotus = settings.detect { |set| set.on == "lotus" } 21 | expect(lotus.keyboard_shortcuts_enabled).to eq(!original_setting) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/live/suspended_ticket_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::SuspendedTicket do 4 | def valid_attributes 5 | { 6 | :subject => "Test Ticket", 7 | :comment => { :value => "Help! I need somebody." }, 8 | :requester => { 9 | :email => "zendesk-api-client-ruby-anonymous-#{client.config.username}", 10 | :name => 'Anonymous User' 11 | } 12 | } 13 | end 14 | 15 | # TODO: We can't create now suspended tickets automatically via API calls, 16 | # which makes this tests very complicated to perform 17 | # it_should_be_readable :suspended_tickets 18 | # it_should_be_deletable :object => suspended_ticket 19 | end 20 | -------------------------------------------------------------------------------- /spec/live/tag_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | RSpec.describe ZendeskAPI::Tag, :vcr, :not_findable do 4 | [organization, user, ticket].each do |object| 5 | raise "Your setup is invalid, see spec/live/Readme.md" unless object 6 | 7 | under object do 8 | before do 9 | parent.tags = %w{tag2 tag3} 10 | parent.tags.save! 11 | end 12 | 13 | it "can be set" do 14 | expect(tags).to eq(%w{tag2 tag3}) 15 | end 16 | 17 | it "should be removable" do 18 | parent.tags.destroy!(:id => "tag2") 19 | 20 | expect(tags).to eq(%w{tag3}) 21 | end 22 | 23 | it "shouldn't re-save destroyed tags" do 24 | parent.tags.first.destroy! 25 | parent.tags.save! 26 | 27 | expect(tags).to eq(%w{tag3}) 28 | end 29 | 30 | it "should be updatable" do 31 | parent.tags.update!(:id => "tag4") 32 | 33 | expect(tags).to eq(%w{tag2 tag3 tag4}) 34 | end 35 | 36 | it "should be savable" do 37 | parent.tags << "tag4" 38 | parent.tags.save! 39 | 40 | expect(tags).to eq(%w{tag2 tag3 tag4}) 41 | end 42 | 43 | it "should be modifiable" do 44 | parent.tags.delete(ZendeskAPI::Tag.new(nil, :id => "tag2")) 45 | parent.tags.save! 46 | 47 | expect(tags).to eq(%w{tag3}) 48 | 49 | parent.tags.delete_if { |tag| tag.id == "tag3" } 50 | parent.tags.save! 51 | 52 | expect(tags).to be_empty 53 | end 54 | end 55 | end 56 | 57 | def tags 58 | parent.tags.fetch!(:reload).map(&:id).sort 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/live/target_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Target, :delete_after do 4 | def valid_attributes 5 | { 6 | :type => "email_target", 7 | :title => "Test Email Target", 8 | :email => "hello@example.com", 9 | :subject => "Test Target" 10 | } 11 | end 12 | 13 | it_should_be_readable :targets, :create => :true 14 | it_should_be_creatable 15 | it_should_be_updatable :active, false 16 | it_should_be_deletable 17 | end 18 | -------------------------------------------------------------------------------- /spec/live/ticket_field_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::TicketField, :delete_after do 4 | def valid_attributes 5 | { :type => "text", :title => "Age" } 6 | end 7 | 8 | it_should_be_creatable 9 | it_should_be_updatable :title 10 | it_should_be_deletable 11 | it_should_be_readable :ticket_fields 12 | end 13 | -------------------------------------------------------------------------------- /spec/live/ticket_form_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::TicketForm, :delete_after do 4 | def valid_attributes 5 | { :name => "Ticket Form-o", :position => 9999, :active => false } 6 | end 7 | 8 | it_should_be_creatable 9 | it_should_be_updatable :name 10 | it_should_be_deletable :find => false # Deleted ticket forms are still returned from the show action 11 | it_should_be_readable :ticket_forms 12 | 13 | # TODO: clone 14 | end 15 | -------------------------------------------------------------------------------- /spec/live/ticket_metrics_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::TicketMetric do 4 | it_should_be_readable :ticket_metrics 5 | it_should_be_readable ticket, :metrics 6 | end 7 | -------------------------------------------------------------------------------- /spec/live/ticket_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | RSpec.describe ZendeskAPI::Ticket do 4 | def valid_attributes 5 | { 6 | :type => "question", 7 | :subject => "This is a question?", 8 | :comment => { :value => "Indeed it is!" }, 9 | :priority => "normal", 10 | :requester_id => user.id, 11 | :assignee_id => current_user.id, 12 | :submitter_id => user.id, 13 | :collaborator_ids => [agent.id], 14 | :tags => %w(awesome blossom), 15 | :email_ccs => [ 16 | { :user_id => agent.id, "action": "put" } 17 | ] 18 | } 19 | end 20 | 21 | it_should_be_creatable 22 | it_should_be_updatable :subject 23 | it_should_be_deletable 24 | it_should_be_readable :tickets 25 | it_should_be_readable user, :requested_tickets 26 | it_should_be_readable current_user, :assigned_tickets, create: true 27 | it_should_be_readable agent, :ccd_tickets, create: true 28 | it_should_be_readable organization, :tickets 29 | 30 | describe "#create" do 31 | context "when passing large objects as parameters" do 32 | let(:requester) { client.users.search(query: 'role:end-user').detect(&:photo) } 33 | let(:organization) { client.organizations.sample } 34 | let(:ticket_parameters) do 35 | { 36 | subject: 'live spec subject', 37 | description: 'live spec description', 38 | requester: requester, 39 | organization: organization 40 | } # We should always use requester/organiztion _id for existing records. This test should not be used as a guideline on how to use the sdk. 41 | end 42 | 43 | before do 44 | VCR.use_cassette("ticket_create_with_large_objects") do 45 | @ticket = ZendeskAPI::Ticket.create(client, ticket_parameters) 46 | end 47 | end 48 | 49 | it 'is creatable' do 50 | expect(requester).to_not be_nil 51 | 52 | expect(@ticket.id).to_not be_nil 53 | expect(@ticket.description).to eq(ticket_parameters[:description]) 54 | end 55 | end 56 | 57 | after do 58 | return unless @ticket 59 | VCR.use_cassette("ticket_destroy_with_large_objects") do 60 | @ticket.destroy! 61 | end 62 | end 63 | end 64 | 65 | describe "#show_many" do 66 | let(:how_many_tickets) { 10 } 67 | before do 68 | VCR.use_cassette("get_tickets_show_many") do 69 | @tickets = client.tickets.per_page(how_many_tickets).fetch 70 | end 71 | 72 | VCR.use_cassette("tickets_show_many_with_ids") do 73 | @tickets_from_show_many = client.tickets.show_many(ids: @tickets.map(&:id)).fetch 74 | end 75 | end 76 | 77 | it "returns the correct number of tickets" do 78 | expect(@tickets_from_show_many.count).to eq(how_many_tickets) 79 | expect(@tickets.map(&:id).sort).to eq(@tickets_from_show_many.map(&:id).sort) 80 | end 81 | end 82 | 83 | describe "#attributes_for_save" do 84 | let :ticket do 85 | described_class.new(instance_double(ZendeskAPI::Client), status: :new) 86 | end 87 | 88 | it "keeps all the comments", :vcr do 89 | ticket.update(comment: { private: true, body: "Private comment" }) 90 | expect(ticket.attributes_for_save).to eq(ticket: { 91 | "status" => :new, 92 | "comment" => { "private" => true, "body" => "Private comment" } 93 | }) 94 | 95 | ticket.update(comment: { private: true, body: "Private comment2" }) 96 | expect(ticket.attributes_for_save).to eq(ticket: { 97 | "status" => :new, 98 | "comment" => { "private" => true, "body" => "Private comment2" } 99 | }) 100 | end 101 | end 102 | 103 | context "recent tickets" do 104 | before(:all) do 105 | VCR.use_cassette("visit_recent_ticket") do 106 | client.tickets.find(id: 1) 107 | 108 | sleep(5) 109 | end 110 | end 111 | 112 | it_should_be_readable :tickets, :recent 113 | end 114 | 115 | describe ".incremental_export" do 116 | let(:results) { ZendeskAPI::Ticket.incremental_export(client, Time.at(1023059503)) } # ~ 10 years ago 117 | 118 | around do |example| 119 | # 1 request every 5 minutes allowed <-> you can only test 1 call ... 120 | VCR.use_cassette("incremental_export") do 121 | client.config.retry = false 122 | 123 | example.call 124 | 125 | client.config.retry = true 126 | end 127 | end 128 | 129 | it "finds tickets after a old date" do 130 | expect(results.to_a.first).to be_an_instance_of ZendeskAPI::Ticket 131 | end 132 | 133 | it "is able to do next" do 134 | first = results.to_a.first 135 | stub_json_request(:get, %r{/api/v2/incremental/tickets}, json(:results => [])) 136 | 137 | results.next 138 | expect(results.first).to_not eq(first) 139 | end 140 | end 141 | 142 | describe ".import" do 143 | it "can import" do 144 | VCR.use_cassette("ticket_import_can_import") do 145 | old = Time.now - 5 * 365 * 24 * 60 * 60 146 | ticket = ZendeskAPI::Ticket.import(client, valid_attributes.merge(:created_at => old.iso8601)) 147 | expect(ZendeskAPI::Ticket.find(client, :id => ticket.id).created_at.year).to eq(old.year) 148 | end 149 | end 150 | 151 | it "returns nothing if import fails" do 152 | VCR.use_cassette("ticket_import_cannot_import") do 153 | silence_logger { expect(ZendeskAPI::Ticket.import(client, {})).to eq(nil) } 154 | end 155 | end 156 | end 157 | 158 | it "can upload while creating" do 159 | VCR.use_cassette("ticket_inline_uploads") do 160 | ticket = ZendeskAPI::Ticket.new(client, valid_attributes) 161 | ticket.comment.uploads << "spec/fixtures/Argentina.gif" 162 | ticket.comment.uploads << File.new("spec/fixtures/Argentina.gif") 163 | 164 | ticket.save! 165 | expect(ticket.changes).to eq({}) # uploads were set before save 166 | expect(ticket.comment.attributes[:uploads].map(&:class)).to eq([String, String]) # upload was sent as tokens 167 | end 168 | end 169 | 170 | it "can comment while creating" do 171 | VCR.use_cassette("ticket_inline_comments") do 172 | ticket = ZendeskAPI::Ticket.new(client, valid_attributes) 173 | ticket.comment = ZendeskAPI::Ticket::Comment.new(client, :value => "My comment", :public => false) 174 | ticket.save! 175 | 176 | expect(ticket.changes).to eq({}) # comment was set before save 177 | expect(ticket.attributes[:comment]).to eq({ "value" => "My comment", "public" => false }) 178 | end 179 | end 180 | 181 | describe "import race condition" do 182 | let(:email) { "test+#{rand(100000)}@test.com" } 183 | 184 | it "should handle it" do 185 | VCR.use_cassette("ticket_import_race") do 186 | threads = [] 187 | 188 | 3.times do 189 | threads << Thread.new do 190 | client.insert_callback do |response| 191 | Thread.current[:response] = response 192 | end 193 | 194 | ZendeskAPI::Ticket.import(client, :requester => { :email => email, :name => "Hello" }, :subject => "Test", :description => "Test") 195 | end 196 | end 197 | 198 | threads.map! do |thread| 199 | thread.join(5) 200 | fail("could not get response in 5 seconds") unless thread[:response] 201 | thread[:response][:status] 202 | end 203 | 204 | user = client.users.detect { |user| user.email == email } 205 | expect(user).to_not be_nil 206 | 207 | user.requested_tickets.each(&:destroy) 208 | user.destroy 209 | 210 | expect(threads.all? { |st| [201, 422, 409].include?(st) }).to be(true) 211 | end 212 | end 213 | end 214 | end 215 | -------------------------------------------------------------------------------- /spec/live/topic_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | RSpec.describe ZendeskAPI::Topic do 4 | def valid_attributes 5 | { 6 | :name => "My Topic", 7 | :description => "The mayan calendar ends December 31st. Coincidence? I think not." 8 | } 9 | end 10 | 11 | it_should_be_creatable 12 | it_should_be_updatable :title 13 | it_should_be_deletable :create => true 14 | it_should_be_readable :topics 15 | 16 | it "can upload while creating" do 17 | VCR.use_cassette("topic_inline_uploads") do 18 | topic = ZendeskAPI::Topic.new(client, valid_attributes) 19 | topic.uploads << "spec/fixtures/Argentina.gif" 20 | topic.uploads << File.new("spec/fixtures/Argentina.gif") 21 | 22 | topic.save! 23 | expect(topic.changes).to eq({}) # uploads were set before save 24 | expect(topic.attributes[:uploads].map(&:class)).to eq([String, String]) # upload was sent as tokens 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/live/topic_subscription_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::TopicSubscription, :delete_after, :not_findable do 4 | let!(:subscription) do 5 | VCR.use_cassette("create_inline_topic_subscription") do 6 | topic.subscriptions.create!(user_id: user.id, topic_id: topic.id) 7 | end 8 | end 9 | 10 | it_should_be_readable topic, :subscriptions 11 | end 12 | -------------------------------------------------------------------------------- /spec/live/trigger_category_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::TriggerCategory, :delete_after do 4 | def valid_attributes 5 | { :name => "New category" } 6 | end 7 | 8 | it_should_be_readable :trigger_categories 9 | it_should_be_creatable 10 | it_should_be_updatable :name 11 | it_should_be_deletable 12 | end 13 | -------------------------------------------------------------------------------- /spec/live/trigger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Trigger, :delete_after do 4 | def valid_attributes 5 | { 6 | :category_id => "3", 7 | :title => "my test trigger", 8 | :conditions => { 9 | :all => [{ :field => "status", :operator => "is", :value => "open" }] 10 | }, 11 | :actions => [{ :field => "status", :value => "solved" }] 12 | } 13 | end 14 | 15 | it_should_be_readable :triggers 16 | it_should_be_readable :triggers, :active 17 | 18 | it_should_be_creatable 19 | it_should_be_updatable :conditions, { 20 | "any" => [], 21 | "all" => [{ "field" => "priority", "operator" => "is", "value" => "low" }] 22 | } 23 | it_should_be_deletable 24 | end 25 | -------------------------------------------------------------------------------- /spec/live/upload_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Upload, :not_findable do 4 | def valid_attributes 5 | { :file => "spec/fixtures/Argentina.gif" } 6 | end 7 | 8 | it_should_be_creatable 9 | it_should_be_deletable 10 | end 11 | -------------------------------------------------------------------------------- /spec/live/user_field_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::UserField, :delete_after do 4 | def valid_attributes 5 | { :type => "text", :title => random_string(20), :key => random_string(10) } 6 | end 7 | 8 | it_should_be_deletable :marked_for_deletion => true 9 | it_should_be_creatable 10 | it_should_be_updatable :title, random_string(22) 11 | it_should_be_readable :user_fields, :create => true 12 | end 13 | -------------------------------------------------------------------------------- /spec/live/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::User, :delete_after do 4 | def valid_attributes 5 | { name: "Test U.", email: "test+#{Time.now.to_i}@example.org" } 6 | end 7 | 8 | it_should_be_creatable 9 | it_should_be_updatable :name 10 | it_should_be_deletable :find => [:active?, false] 11 | it_should_be_readable :users 12 | it_should_be_readable organization, :users 13 | 14 | it "should be able to find by email" do 15 | VCR.use_cassette("user_find_by_email") do 16 | expect(client.users.search(:query => current_user.email).to_a).to eq([current_user]) 17 | end 18 | end 19 | 20 | describe "related" do 21 | it "shows realated users" do 22 | VCR.use_cassette("current_user_related_users") do 23 | client.users.search(:query => current_user.email).first 24 | expect(current_user.related).to be_a ZendeskAPI::UserRelated 25 | end 26 | end 27 | end 28 | 29 | context "passwords", :vcr do 30 | let(:password) { client.config.password || ENV['PASSWORD'] } 31 | 32 | it "sets the password" do 33 | agent.set_password!(:password => password) 34 | end 35 | 36 | it "changes the password" do 37 | current_user.change_password!(:previous_password => password, :password => password) 38 | end 39 | end 40 | 41 | context "side-loading" do 42 | context "no permission set" do 43 | subject do 44 | VCR.use_cassette("user_admin_role") { client.users.find(:id => 20014182, :include => :roles) } 45 | end 46 | 47 | it "should include role" do 48 | if subject 49 | expect(subject.changes.key?(:role_id)).to be(false) 50 | expect(subject.role).to_not be_nil 51 | expect(subject.role.id).to be_nil 52 | expect(subject.role.name).to eq("admin") 53 | expect(subject.role.configuration).to_not be_nil 54 | 55 | expect(subject.custom_role).to be_nil 56 | end 57 | end 58 | end 59 | 60 | context "create_or_update" do 61 | after do 62 | VCR.use_cassette("create_or_update_destroy_user") do 63 | user.destroy 64 | end 65 | end 66 | 67 | context "when the user already exist" do 68 | let!(:user) do 69 | VCR.use_cassette("create_or_update_create_user") do 70 | client.users.create(name: "Existing", email: "unkown@example.org") 71 | end 72 | end 73 | 74 | before do 75 | VCR.use_cassette("create_or_update_existing_user") do 76 | ZendeskAPI::User.create_or_update!(client, name: "Updated!", email: "unkown@example.org") 77 | end 78 | end 79 | 80 | it "updates the existing user" do 81 | VCR.use_cassette("create_or_update_find_existing_user") do 82 | expect(client.users.find(id: user.id).name).to eql "Updated!" 83 | end 84 | end 85 | end 86 | end 87 | 88 | describe "create_many, update_many and destroy_many" do 89 | let(:create_many_users_job) do 90 | VCR.use_cassette("create_many_users_job") do 91 | ZendeskAPI::User.create_many!( 92 | client, 93 | [ 94 | { name: "one", email: "1@example.org" }, 95 | { name: "two", email: "2@example.org" } 96 | ] 97 | ).tap do |job| 98 | job.reload! while job.status != "completed" 99 | end 100 | end 101 | end 102 | 103 | let(:destroy_many_users_job) do 104 | VCR.use_cassette("destroy_many_users_job") do 105 | ZendeskAPI::User.destroy_many!( 106 | client, 107 | created_user_ids 108 | ).tap do |job| 109 | job.reload! while job.status != "completed" 110 | end 111 | end 112 | end 113 | 114 | let(:created_user_ids) do 115 | create_many_users_job.results.filter do |item| 116 | item["status"] == "Created" 117 | end.map(&:id) 118 | end 119 | 120 | let(:created_user_objects) do 121 | VCR.use_cassette("created_users_objects") do 122 | created_user_ids.map do |user_id| 123 | client.users.find(id: user_id) 124 | end 125 | end 126 | end 127 | 128 | before do 129 | VCR.use_cassette("update_many_users") do 130 | ZendeskAPI::User.update_many!(client, created_user_ids, notes: "this is a note").tap do |job| 131 | job.reload! while job.status != "completed" 132 | end 133 | end 134 | end 135 | 136 | # If fails, try deleting the orgs using the REPL 137 | it "updates all the users, and then, it deletes them properly" do 138 | created_user_objects.each do |user| 139 | expect(user.notes).to eq "this is a note" 140 | end 141 | 142 | expect(destroy_many_users_job["total"]).to be 2 143 | end 144 | end 145 | 146 | context "permission set" do 147 | subject do 148 | VCR.use_cassette("user_permission_set") { client.users.find(:id => 20014327, :include => :roles) } 149 | end 150 | 151 | it "should include role" do 152 | if subject 153 | expect(subject.changes.key?(:role_id)).to be(false) 154 | expect(subject.role).to_not be_nil 155 | expect(subject.role.id).to be_nil 156 | expect(subject.role.name).to eq("agent") 157 | 158 | expect(subject.custom_role).to_not be_nil 159 | expect(subject.custom_role.id).to eq(3692) 160 | expect(subject.custom_role.name).to eq("Staff") 161 | expect(subject.custom_role.configuration).to_not be_nil 162 | end 163 | end 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /spec/live/user_view_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::UserView, :delete_after do 4 | def valid_attributes 5 | { 6 | :title => "Overseas gold member", 7 | :all => [ 8 | { field: "name", operator: "is", value: "abcd" } 9 | ] 10 | } 11 | end 12 | 13 | it_should_be_readable :user_views 14 | it_should_be_creatable 15 | it_should_be_deletable 16 | end 17 | -------------------------------------------------------------------------------- /spec/live/view_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::View, :delete_after do 4 | def valid_attributes 5 | { 6 | :title => "my test view", 7 | :conditions => { 8 | :all => [{ :field => "status", :operator => "is", :value => "open" }] 9 | } 10 | } 11 | end 12 | 13 | it_should_be_readable :views 14 | it_should_be_readable :views, :active 15 | 16 | it_should_be_creatable 17 | it_should_be_updatable :conditions, { 18 | "any" => [], 19 | "all" => [{ "field" => "status", "operator" => "is", "value" => "solved" }] 20 | } 21 | it_should_be_deletable 22 | end 23 | -------------------------------------------------------------------------------- /spec/live/voice/phone_number_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | describe ZendeskAPI::Voice::PhoneNumber, :delete_after do 4 | # We have to find a valid token before we create a phone number 5 | def available_phone_token 6 | @available_phone_token ||= begin 7 | VCR.use_cassette("find_valid_phone_number_token_for_creation") do 8 | client.voice.phone_numbers( 9 | path: "channels/voice/phone_numbers/search.json", country: "US" 10 | ).first.token 11 | end 12 | end 13 | end 14 | 15 | def valid_attributes 16 | { 17 | token: available_phone_token 18 | } 19 | end 20 | 21 | it_should_be_creatable 22 | 23 | # TODO: currently is a bit complicate to find / create the resource since 24 | # we need to prefetch an available token, which complicates how we can create to then 25 | # destroy a resource. 26 | # it_should_be_deletable 27 | end 28 | -------------------------------------------------------------------------------- /spec/live/webhook_spec.rb: -------------------------------------------------------------------------------- 1 | require 'core/spec_helper' 2 | 3 | RSpec.describe ZendeskAPI::Webhook, :delete_after do 4 | def valid_attributes 5 | { 6 | name: "Random Hook", 7 | endpoint: "https://lvh.me", 8 | status: :active, 9 | http_method: :get, 10 | request_format: :json 11 | } 12 | end 13 | 14 | it_should_be_creatable 15 | it_should_be_deletable 16 | end 17 | -------------------------------------------------------------------------------- /spec/macros/resource_macros.rb: -------------------------------------------------------------------------------- 1 | module ResourceMacros 2 | def self.extended(klass) 3 | klass.send(:define_method, :default_options) { {} } 4 | end 5 | 6 | def under(object, &blk) 7 | context "under a #{object.class.singular_resource_name}" do 8 | let(:parent) { object } 9 | 10 | define_method(:default_options) do 11 | { "#{object.class.singular_resource_name}_id" => object.id } 12 | end 13 | 14 | instance_eval(&blk) 15 | end 16 | end 17 | 18 | def it_should_be_creatable(options = {}) 19 | context "creation", :vcr do 20 | subject { described_class } 21 | 22 | before(:all) do 23 | VCR.use_cassette("#{described_class.to_s}_create") do 24 | @creatable_object = described_class.create!(client, valid_attributes.merge(default_options)) 25 | end 26 | end 27 | 28 | it "should have an id" do 29 | expect(@creatable_object).to_not be_nil 30 | expect(@creatable_object.send(:id)).to_not be_nil 31 | end 32 | 33 | it "should be findable", :unless => metadata[:not_findable] do 34 | options = default_options 35 | options.merge!(:id => @creatable_object.id) unless described_class.ancestors.include?(ZendeskAPI::SingularResource) 36 | expect(described_class.find(client, options)).to eq(@creatable_object) 37 | end 38 | 39 | after(:all) do 40 | return unless @creatable_object&.id 41 | 42 | VCR.use_cassette("#{described_class.to_s}_create_delete") do 43 | @creatable_object.destroy 44 | end 45 | end if metadata[:delete_after] 46 | end 47 | end 48 | 49 | def it_should_be_updatable(attribute, value = "TESTDATA", extra = {}) 50 | context "update", :vcr do 51 | before(:all) do 52 | VCR.use_cassette("#{described_class.to_s}_update_create") do 53 | @updatable_object = described_class.create!(client, valid_attributes.merge(default_options)) 54 | end 55 | end 56 | 57 | before(:each) do 58 | @updatable_object.public_send("#{attribute}=", value) 59 | extra.each { |k, v| @updatable_object.public_send("#{k}=", v) } 60 | end 61 | 62 | it "should be savable" do 63 | expect(@updatable_object.save).to be(true), "Expected object to save, but it failed with errors: #{@updatable_object.errors&.full_messages&.join(', ')}" 64 | end 65 | 66 | context "after save" do 67 | before(:each) do 68 | @updatable_object.save 69 | end 70 | 71 | it "should keep attributes" do 72 | expect(@updatable_object.send(attribute)).to eq(value) 73 | end 74 | 75 | it "should be findable", :unless => metadata[:not_findable] do 76 | options = default_options 77 | options.merge!(:id => @updatable_object.id) unless described_class.ancestors.include?(ZendeskAPI::SingularResource) 78 | expect(described_class.find(client, options)).to eq(@updatable_object) 79 | end 80 | end 81 | 82 | after(:all) do 83 | VCR.use_cassette("#{described_class.to_s}_update_delete") do 84 | @updatable_object&.destroy 85 | end 86 | end if metadata[:delete_after] 87 | end 88 | end 89 | 90 | def it_should_be_deletable(options = {}) 91 | context "deletion", :vcr do 92 | before(:all) do 93 | if options[:object] 94 | @deletable_object = options.delete(:object) 95 | else 96 | VCR.use_cassette("#{described_class.to_s}_delete_create") do 97 | @deletable_object = described_class.create!(client, valid_attributes.merge(default_options)) 98 | end 99 | end 100 | end 101 | 102 | it "should be destroyable" do |example| 103 | expect(@deletable_object.destroy).to be(true) 104 | expect(@deletable_object.destroyed?).to be(true) 105 | if (!options.key?(:find) || options[:find]) && !example.metadata[:not_findable] 106 | opts = default_options 107 | opts.merge!(:id => @deletable_object.id) unless described_class.ancestors.include?(ZendeskAPI::SingularResource) 108 | obj = described_class.find(client, opts) 109 | 110 | if options[:find] 111 | expect(obj.send(options[:find].first)).to eq(options[:find].last) 112 | else 113 | options[:marked_for_deletion] ? (expect(obj.active?).to be_falsey) : (expect(obj).to be_nil) 114 | end 115 | end 116 | end 117 | end 118 | end 119 | 120 | def it_should_be_readable(*args) 121 | options = args.last.is_a?(Hash) ? args.pop : {} 122 | create = !!options.delete(:create) 123 | klass = args.first.is_a?(ZendeskAPI::DataResource) ? args.shift : client 124 | context_name = "read_#{klass.class}_#{args.join('_')}" 125 | 126 | context context_name, :vcr do 127 | before(:all) do 128 | VCR.use_cassette("#{described_class.to_s}_#{context_name}_create") do 129 | @readable_object = described_class.create!(client, valid_attributes.merge(default_options)) 130 | end 131 | end if create 132 | 133 | after(:all) do 134 | VCR.use_cassette("#{described_class.to_s}_#{context_name}_delete") do 135 | @readable_object.destroy 136 | end 137 | end if create 138 | 139 | it "should be findable" do |example| 140 | result = klass 141 | args.each { |a| result = result.send(a, options) } 142 | 143 | if result.is_a?(ZendeskAPI::Collection) 144 | if create 145 | object = nil 146 | 147 | result.all do |o| 148 | object = o if @readable_object == o 149 | end 150 | 151 | expect(object).to_not be_nil 152 | else 153 | expect(result.fetch(true)).to_not be_empty 154 | object = result.first 155 | end 156 | else 157 | expect(result).to_not be_nil 158 | expect(result).to eq(@readable_object) if create 159 | object = result 160 | end 161 | 162 | if described_class.respond_to?(:find) && !example.metadata[:not_findable] 163 | options = default_options 164 | options.merge!(:id => object.id) unless described_class.ancestors.include?(ZendeskAPI::SingularResource) 165 | expect(described_class.find(client, options)).to_not be_nil 166 | end 167 | end 168 | end 169 | end 170 | end 171 | -------------------------------------------------------------------------------- /util/resource_handler.rb: -------------------------------------------------------------------------------- 1 | require 'zendesk_api' 2 | 3 | class ResourceHandler < YARD::Handlers::Ruby::Base 4 | handles method_call(:has), method_call(:has_many) 5 | 6 | def process 7 | many = statement.jump(:ident).source == "has_many" 8 | 9 | klass = get_klass(statement) 10 | 11 | if klass 12 | begin 13 | klass = klass.split("::").inject(ZendeskAPI) do |p, k| 14 | p.const_get(k) 15 | end 16 | rescue NameError 17 | parent = walk_namespace(namespace) 18 | klass = parent.const_get(klass) 19 | end 20 | 21 | name = statement.parameters.first.jump(:ident).source 22 | else 23 | klass = statement.parameters.first.source 24 | 25 | begin 26 | klass = ZendeskAPI.const_get(klass) 27 | rescue NameError 28 | parent = walk_namespace(namespace) 29 | klass = parent.const_get(klass) 30 | end 31 | 32 | name = many ? klass.resource_name : klass.singular_resource_name 33 | end 34 | 35 | reader = YARD::CodeObjects::MethodObject.new(namespace, name) 36 | register(reader) 37 | reader.dynamic = true 38 | reader.docstring.add_tag(YARD::Tags::Tag.new(:return, "The associated object", klass.name)) 39 | 40 | if many 41 | reader.signature = "def #{name}=(options = {})" 42 | reader.parameters = [['options', {}]] 43 | reader.docstring.add_tag(YARD::Tags::Tag.new(:param, "Options to pass to the collection object", "Hash", "options")) 44 | end 45 | 46 | writer = YARD::CodeObjects::MethodObject.new(namespace, "#{name}=") 47 | register(writer) 48 | writer.signature = "def #{name}=(value)" 49 | writer.parameters = [['value', nil]] 50 | writer.dynamic = true 51 | writer.docstring.add_tag(YARD::Tags::Tag.new(:return, "The associated object", klass.name)) 52 | writer.docstring.add_tag(YARD::Tags::Tag.new(:param, "The associated object or its attributes", "Hash or #{klass.name}", "value")) 53 | end 54 | 55 | def walk_namespace(namespace) 56 | namespace.to_s.split('::').inject(ZendeskAPI) do |klass, namespace| 57 | klass.const_get(namespace) 58 | end 59 | end 60 | 61 | def get_klass(statement) 62 | statement.traverse do |node| 63 | if node.type == :assoc && node.jump(:kw).source == "class" 64 | node.traverse do |value| 65 | if value.type == :const_path_ref || value.type == :var_ref 66 | return value.source 67 | end 68 | end 69 | end 70 | end 71 | 72 | nil 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /util/verb_handler.rb: -------------------------------------------------------------------------------- 1 | class VerbHandler < YARD::Handlers::Ruby::Base 2 | handles method_call(:put), method_call(:post), method_call(:get) 3 | 4 | def process 5 | name = statement.parameters.first.jump(:ident).source 6 | 7 | verb = YARD::CodeObjects::MethodObject.new(namespace, name) 8 | register(verb) 9 | verb.dynamic = true 10 | verb.docstring.add_tag(YARD::Tags::Tag.new(:return, "Success of this call", "Boolean")) 11 | 12 | verb.signature = "def #{name}=(options = {})" 13 | verb.parameters = [['options', {}]] 14 | verb.docstring.add_tag(YARD::Tags::Tag.new(:param, "Options to pass to the request", "Hash", "options")) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /zendesk_api.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib/', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | 4 | require 'zendesk_api/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "zendesk_api" 8 | s.version = ZendeskAPI::VERSION 9 | s.authors = ["Steven Davidovitz", "Michael Grosser"] 10 | s.email = ["support@zendesk.com"] 11 | s.homepage = "https://developer.zendesk.com" 12 | s.summary = 'Zendesk REST API Client' 13 | s.description = 'Ruby wrapper for the REST API at https://www.zendesk.com. Documentation at https://developer.zendesk.com.' 14 | s.license = 'Apache-2.0' 15 | 16 | s.metadata = { 17 | 'bug_tracker_uri' => 'https://github.com/zendesk/zendesk_api_client_rb/issues', 18 | 'changelog_uri' => "https://github.com/zendesk/zendesk_api_client_rb/blob/v#{s.version}/CHANGELOG.md", 19 | 'documentation_uri' => "https://www.rubydoc.info/gems/zendesk_api/#{s.version}", 20 | 'source_code_uri' => "https://github.com/zendesk/zendesk_api_client_rb/tree/v#{s.version}", 21 | 'wiki_uri' => 'https://github.com/zendesk/zendesk_api_client_rb/wiki' 22 | } 23 | 24 | s.files = Dir.glob('{lib,util}/**/*') << 'LICENSE' 25 | 26 | s.required_ruby_version = ">= 2.7" 27 | s.required_rubygems_version = ">= 1.3.6" 28 | 29 | s.add_runtime_dependency "faraday", "> 2.0.0" 30 | s.add_runtime_dependency "faraday-multipart" 31 | s.add_runtime_dependency "hashie", ">= 3.5.2", "< 6.0.0" 32 | s.add_runtime_dependency "inflection" 33 | s.add_runtime_dependency "multipart-post", "~> 2.0" 34 | s.add_runtime_dependency "mini_mime" 35 | end 36 | --------------------------------------------------------------------------------