chore: Split google-apis-core into a separate gem (#2134)

This commit is contained in:
Daniel Azuma authored and GitHub committed 2020-12-27 17:31:47 -08:00
1 parent 1bc1737dc1
commit a6a5b6071e
66 files changed
+763 -151

No files matched your search

+1 -8
View File
@@ -3,6 +3,7 @@ source 'https://rubygems.org'
# Specify your gem's dependencies in google-apis.gemspec
gemspec
gem 'google-apis-core', path: '../google-apis-core'
group :development do
gem 'bundler', '>= 1.7'
@@ -42,14 +43,6 @@ platforms :ruby do
end
end
platforms :mri_21, :mri_22, :mri_23 do
group :development do
gem 'derailed_benchmarks'
gem 'stackprof'
gem 'rack-test'
end
end
if ENV['RAILS_VERSION']
gem 'rails', ENV['RAILS_VERSION']
end
+59
View File
@@ -1,3 +1,62 @@
require "bundler/gem_tasks"
namespace :metrics do
task :lines do
lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
for file_name in FileList['lib/**/*.rb', 'bin/generate-api']
f = File.open(file_name)
while line = f.gets
lines += 1
next if line =~ /^\s*$/
next if line =~ /^\s*#/
codelines += 1
end
puts "L: #{sprintf('%4d', lines)}, " +
"LOC #{sprintf('%4d', codelines)} | #{file_name}"
total_lines += lines
total_codelines += codelines
lines, codelines = 0, 0
end
puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
end
end
require 'rubocop/rake_task'
desc 'Run RuboCop on the lib directory'
RuboCop::RakeTask.new(:rubocop) do |task|
task.patterns = ['lib/**/*.rb']
# only show the files with failures
task.formatters = ['progress']
# don't abort rake on failure
task.fail_on_error = false
end
require 'rake/clean'
CLOBBER.include('coverage')
require 'rspec/core/rake_task'
namespace :spec do
RSpec::Core::RakeTask.new(:all)
end
desc 'Alias to spec:all'
task 'spec' => 'spec:all'
begin
require 'yard'
require 'yard/rake/yardoc_task'
YARD::Rake::YardocTask.new do |t|
t.files = ['lib/**/*.rb', 'generated/**/*.rb']
t.options = ['--verbose', '--markup', 'markdown']
end
rescue LoadError
puts "YARD not available"
end
task :default => :spec
+7 -12
View File
@@ -13,24 +13,19 @@ Gem::Specification.new do |gem|
gem.metadata = {
"documentation_uri" => "https://googleapis.dev/ruby/google-api-client/v#{Google::Apis::VERSION}"
}
gem.files = `git ls-files -z`
.split("\x0")
.reject { |f| f.match(%r{^(spec|script)/|^api_names_out|synth\.metadata}) }
gem.files = Dir.glob("lib/**/*") +
Dir.glob("generated/**/*.rb") +
Dir.glob("docs/**/*") +
Dir.glob("bin/**/*") +
Dir.glob("*.md") +
[".yardopts", "LICENSE"]
gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
gem.require_paths = %w[lib generated]
gem.required_ruby_version = '>= 2.4'
gem.add_runtime_dependency 'representable', '~> 3.0'
gem.add_runtime_dependency 'retriable', '>= 2.0', '< 4.0'
gem.add_runtime_dependency 'addressable', '~> 2.5', '>= 2.5.1'
gem.add_runtime_dependency 'mini_mime', '~> 1.0'
gem.add_runtime_dependency 'signet', '~> 0.12'
gem.add_runtime_dependency 'googleauth', '~> 0.9'
gem.add_runtime_dependency 'httpclient', '>= 2.8.1', '< 3.0'
gem.add_runtime_dependency 'rexml'
gem.add_runtime_dependency 'google-apis-core', '~> 0.0'
gem.add_development_dependency 'thor', '~> 0.19'
gem.add_development_dependency 'activesupport', '>= 4.2', '< 5.1'
end
@@ -1,143 +0,0 @@
# Copyright 2010 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'webrick'
launchy_available =
begin
require 'launchy'
true
rescue LoadError
warn "Attempted to require google/api_client/auth/installed_app.rb when" \
" launchy is not available. The InstalledAppFlow class is disabled."
false
end
module Google
class APIClient
# Small helper for the sample apps for performing OAuth 2.0 flows from the command
# line or in any other installed app environment.
#
# This class is used in some sample apps and tests but is not really part
# of the client libraries, and probably does not belong here. As such, it
# is deprecated. If you do choose to use it, note that you must include the
# `launchy` gem in your bundle, as it is required by this class but not
# listed in the google-api-client gem's requirements.
#
# @example
#
# flow = Google::APIClient::InstalledAppFlow.new(
# :client_id => '691380668085.apps.googleusercontent.com',
# :client_secret => '...',
# :scope => 'https://www.googleapis.com/auth/drive'
# )
# authorization = flow.authorize
# Drive = Google::Apis::DriveV2
# drive = Drive::DriveService.new
# drive.authorization = authorization
#
# @deprecated Use google-auth-library-ruby instead
class InstalledAppFlow
RESPONSE_BODY = <<-HTML
<html>
<head>
<script>
function closeWindow() {
window.open('', '_self', '');
window.close();
}
setTimeout(closeWindow, 10);
</script>
</head>
<body>You may close this window.</body>
</html>
HTML
##
# Configure the flow
#
# @param [Hash] options The configuration parameters for the client.
# @option options [Fixnum] :port
# Port to run the embedded server on. Defaults to 9292
# @option options [String] :client_id
# A unique identifier issued to the client to identify itself to the
# authorization server.
# @option options [String] :client_secret
# A shared symmetric secret issued by the authorization server,
# which is used to authenticate the client.
# @option options [String] :scope
# The scope of the access request, expressed either as an Array
# or as a space-delimited String.
#
# @see Signet::OAuth2::Client
def initialize(options)
@port = options[:port] || 9292
@authorization = Signet::OAuth2::Client.new({
:authorization_uri => 'https://accounts.google.com/o/oauth2/auth',
:token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
:redirect_uri => "http://localhost:#{@port}/"}.update(options)
)
end
##
# Request authorization. Opens a browser and waits for response.
#
# @param [Google::APIClient::Storage] storage
# Optional object that responds to :write_credentials, used to serialize
# the OAuth 2 credentials after completing the flow.
#
# @return [Signet::OAuth2::Client]
# Authorization instance, nil if user cancelled.
def authorize(storage=nil, options={})
auth = @authorization
server = WEBrick::HTTPServer.new(
:Port => @port,
:BindAddress =>"localhost",
:Logger => WEBrick::Log.new(STDOUT, 0),
:AccessLog => []
)
begin
trap("INT") { server.shutdown }
server.mount_proc '/' do |req, res|
auth.code = req.query['code']
if auth.code
auth.fetch_access_token!
end
res.status = WEBrick::HTTPStatus::RC_ACCEPTED
res.body = RESPONSE_BODY
server.stop
end
Launchy.open(auth.authorization_uri(options).to_s)
server.start
ensure
server.shutdown
end
if @authorization.access_token
if storage.respond_to?(:write_credentials)
storage.write_credentials(@authorization)
end
return @authorization
else
return nil
end
end
end
end
end if launchy_available
@@ -1,94 +0,0 @@
# Copyright 2010 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
module Google
class APIClient
##
# Helper for loading keys from the PKCS12 files downloaded when
# setting up service accounts at the APIs Console.
#
# @deprecated Use google-auth-library-ruby instead
module KeyUtils
##
# Loads a key from PKCS12 file, assuming a single private key
# is present.
#
# @param [String] keyfile
# Path of the PKCS12 file to load. If not a path to an actual file,
# assumes the string is the content of the file itself.
# @param [String] passphrase
# Passphrase for unlocking the private key
#
# @return [OpenSSL::PKey] The private key for signing assertions.
def self.load_from_pkcs12(keyfile, passphrase)
load_key(keyfile, passphrase) do |content, pass_phrase|
OpenSSL::PKCS12.new(content, pass_phrase).key
end
end
##
# Loads a key from a PEM file.
#
# @param [String] keyfile
# Path of the PEM file to load. If not a path to an actual file,
# assumes the string is the content of the file itself.
# @param [String] passphrase
# Passphrase for unlocking the private key
#
# @return [OpenSSL::PKey] The private key for signing assertions.
#
def self.load_from_pem(keyfile, passphrase)
load_key(keyfile, passphrase) do | content, pass_phrase|
OpenSSL::PKey::RSA.new(content, pass_phrase)
end
end
private
##
# Helper for loading keys from file or memory. Accepts a block
# to handle the specific file format.
#
# @param [String] keyfile
# Path of thefile to load. If not a path to an actual file,
# assumes the string is the content of the file itself.
# @param [String] passphrase
# Passphrase for unlocking the private key
#
# @yield [String, String]
# Key file & passphrase to extract key from
# @yieldparam [String] keyfile
# Contents of the file
# @yieldparam [String] passphrase
# Passphrase to unlock key
# @yieldreturn [OpenSSL::PKey]
# Private key
#
# @return [OpenSSL::PKey] The private key for signing assertions.
def self.load_key(keyfile, passphrase, &block)
begin
begin
content = File.open(keyfile, 'rb') { |io| io.read }
rescue
content = keyfile
end
block.call(content, passphrase)
rescue OpenSSL::OpenSSLError
raise ArgumentError.new("Invalid keyfile or passphrase")
end
end
end
end
end
@@ -1,104 +0,0 @@
# Copyright 2013 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'signet/oauth_2/client'
module Google
class APIClient
##
# Represents cached OAuth 2 tokens stored on local disk in a
# JSON serialized file. Meant to resemble the serialized format
# http://google-api-python-client.googlecode.com/hg/docs/epy/oauth2client.file.Storage-class.html
#
# @deprecated Use google-auth-library-ruby instead
class Storage
AUTHORIZATION_URI = 'https://accounts.google.com/o/oauth2/auth'
TOKEN_CREDENTIAL_URI = 'https://accounts.google.com/o/oauth2/token'
# @return [Object] Storage object.
attr_accessor :store
# @return [Signet::OAuth2::Client]
attr_reader :authorization
##
# Initializes the Storage object.
#
# @param [Object] store
# Storage object
def initialize(store)
@store= store
@authorization = nil
end
##
# Write the credentials to the specified store.
#
# @param [Signet::OAuth2::Client] authorization
# Optional authorization instance. If not provided, the authorization
# already associated with this instance will be written.
def write_credentials(authorization=nil)
@authorization = authorization if authorization
if @authorization.respond_to?(:refresh_token) && @authorization.refresh_token
store.write_credentials(credentials_hash)
end
end
##
# Loads credentials and authorizes an client.
# @return [Object] Signet::OAuth2::Client or NIL
def authorize
@authorization = nil
cached_credentials = load_credentials
if cached_credentials && cached_credentials.size > 0
@authorization = Signet::OAuth2::Client.new(cached_credentials)
@authorization.issued_at = Time.at(cached_credentials['issued_at'].to_i)
self.refresh_authorization if @authorization.expired?
end
return @authorization
end
##
# refresh credentials and save them to store
def refresh_authorization
authorization.refresh!
self.write_credentials
end
private
##
# Attempt to read in credentials from the specified store.
def load_credentials
store.load_credentials
end
##
# @return [Hash] with credentials
def credentials_hash
{
:access_token => authorization.access_token,
:authorization_uri => AUTHORIZATION_URI,
:client_id => authorization.client_id,
:client_secret => authorization.client_secret,
:expires_in => authorization.expires_in,
:refresh_token => authorization.refresh_token,
:token_credential_uri => TOKEN_CREDENTIAL_URI,
:issued_at => authorization.issued_at.to_i
}
end
end
end
end
@@ -1,57 +0,0 @@
# Copyright 2013 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'json'
module Google
class APIClient
##
# Represents cached OAuth 2 tokens stored on local disk in a
# JSON serialized file. Meant to resemble the serialized format
# http://google-api-python-client.googlecode.com/hg/docs/epy/oauth2client.file.Storage-class.html
#
# @deprecated Use google-auth-library-ruby instead
class FileStore
attr_accessor :path
##
# Initializes the FileStorage object.
#
# @param [String] path
# Path to the credentials file.
def initialize(path)
@path= path
end
##
# Attempt to read in credentials from the specified file.
def load_credentials
open(path, 'r') { |f| JSON.parse(f.read) }
rescue
nil
end
##
# Write the credentials to the specified file.
#
# @param [Hash] credentials_hash
def write_credentials(credentials_hash)
open(self.path, 'w+') do |f|
f.write(credentials_hash.to_json)
end
end
end
end
end
@@ -1,59 +0,0 @@
# Copyright 2013 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'json'
module Google
class APIClient
# @deprecated Use google-auth-library-ruby instead
class RedisStore
DEFAULT_REDIS_CREDENTIALS_KEY = "google_api_credentials"
attr_accessor :redis
##
# Initializes the RedisStore object.
#
# @param [Object] redis
# Redis instance
# @param [Object] key
# Optional key to store credentials under. Defaults to 'google_api_credentials'
def initialize(redis, key = nil)
@redis= redis
@redis_credentials_key = key
end
##
# Attempt to read in credentials from redis.
# @return [Hash]
def load_credentials
credentials = redis.get redis_credentials_key
JSON.parse(credentials) if credentials
end
def redis_credentials_key
@redis_credentials_key || DEFAULT_REDIS_CREDENTIALS_KEY
end
##
# Write the credentials to redis.
#
# @param [Hash] credentials_hash
def write_credentials(credentials_hash)
redis.set(redis_credentials_key, credentials_hash.to_json)
end
end
end
end
@@ -1,176 +0,0 @@
# Copyright 2010 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'json'
require 'googleauth'
module Google
class APIClient
##
# Manages the persistence of client configuration data and secrets. Format
# inspired by the Google API Python client.
#
# @see https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
# @deprecated Use google-auth-library-ruby instead
# @example
# {
# "web": {
# "client_id": "asdfjasdljfasdkjf",
# "client_secret": "1912308409123890",
# "redirect_uris": ["https://www.example.com/oauth2callback"],
# "auth_uri": "https://accounts.google.com/o/oauth2/auth",
# "token_uri": "https://accounts.google.com/o/oauth2/token"
# }
# }
#
# @example
# {
# "installed": {
# "client_id": "837647042410-75ifg...usercontent.com",
# "client_secret":"asdlkfjaskd",
# "redirect_uris": ["http://localhost", "urn:ietf:oauth:2.0:oob"],
# "auth_uri": "https://accounts.google.com/o/oauth2/auth",
# "token_uri": "https://accounts.google.com/o/oauth2/token"
# }
# }
class ClientSecrets
##
# Reads client configuration from a file
#
# @param [String] filename
# Path to file to load
#
# @return [Google::APIClient::ClientSecrets]
# OAuth client settings
def self.load(filename=nil)
if filename && File.directory?(filename)
search_path = File.expand_path(filename)
filename = nil
end
while filename == nil
search_path ||= File.expand_path('.')
if File.exists?(File.join(search_path, 'client_secrets.json'))
filename = File.join(search_path, 'client_secrets.json')
elsif search_path == File.expand_path('..', search_path)
raise ArgumentError,
'No client_secrets.json filename supplied ' +
'and/or could not be found in search path.'
else
search_path = File.expand_path(File.join(search_path, '..'))
end
end
data = File.open(filename, 'r') { |file| JSON.load(file.read) }
return self.new(data)
end
##
# Initialize OAuth client settings.
#
# @param [Hash] options
# Parsed client secrets files
def initialize(options={})
# Client auth configuration
@flow = options[:flow] || options.keys.first.to_s || 'web'
fdata = options[@flow.to_sym] || options[@flow]
@client_id = fdata[:client_id] || fdata["client_id"]
@client_secret = fdata[:client_secret] || fdata["client_secret"]
@redirect_uris = fdata[:redirect_uris] || fdata["redirect_uris"]
@redirect_uris ||= [fdata[:redirect_uri] || fdata["redirect_uri"]].compact
@javascript_origins = (
fdata[:javascript_origins] ||
fdata["javascript_origins"]
)
@javascript_origins ||= [fdata[:javascript_origin] || fdata["javascript_origin"]].compact
@authorization_uri = fdata[:auth_uri] || fdata["auth_uri"]
@authorization_uri ||= fdata[:authorization_uri]
@token_credential_uri = fdata[:token_uri] || fdata["token_uri"]
@token_credential_uri ||= fdata[:token_credential_uri]
# Associated token info
@access_token = fdata[:access_token] || fdata["access_token"]
@refresh_token = fdata[:refresh_token] || fdata["refresh_token"]
@id_token = fdata[:id_token] || fdata["id_token"]
@expires_in = fdata[:expires_in] || fdata["expires_in"]
@expires_at = fdata[:expires_at] || fdata["expires_at"]
@issued_at = fdata[:issued_at] || fdata["issued_at"]
end
attr_reader(
:flow, :client_id, :client_secret, :redirect_uris, :javascript_origins,
:authorization_uri, :token_credential_uri, :access_token,
:refresh_token, :id_token, :expires_in, :expires_at, :issued_at
)
##
# Serialize back to the original JSON form
#
# @return [String]
# JSON
def to_json
return Json.dump(to_hash)
end
def to_hash
{
self.flow => ({
'client_id' => self.client_id,
'client_secret' => self.client_secret,
'redirect_uris' => self.redirect_uris,
'javascript_origins' => self.javascript_origins,
'auth_uri' => self.authorization_uri,
'token_uri' => self.token_credential_uri,
'access_token' => self.access_token,
'refresh_token' => self.refresh_token,
'id_token' => self.id_token,
'expires_in' => self.expires_in,
'expires_at' => self.expires_at,
'issued_at' => self.issued_at
}).inject({}) do |accu, (k, v)|
# Prunes empty values from JSON output.
unless v == nil || (v.respond_to?(:empty?) && v.empty?)
accu[k] = v
end
accu
end
}
end
def to_authorization
# NOTE: Do not rely on this default value, as it may change
new_authorization = Signet::OAuth2::Client.new
new_authorization.client_id = self.client_id
new_authorization.client_secret = self.client_secret
new_authorization.authorization_uri = (
self.authorization_uri ||
'https://accounts.google.com/o/oauth2/auth'
)
new_authorization.token_credential_uri = (
self.token_credential_uri ||
'https://accounts.google.com/o/oauth2/token'
)
new_authorization.redirect_uri = self.redirect_uris.first
# These are supported, but unlikely.
new_authorization.access_token = self.access_token
new_authorization.refresh_token = self.refresh_token
new_authorization.id_token = self.id_token
new_authorization.expires_in = self.expires_in
new_authorization.issued_at = self.issued_at if self.issued_at
new_authorization.expires_at = self.expires_at if self.expires_at
return new_authorization
end
end
end
end
-58
View File
@@ -1,58 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'google/apis/version'
require 'logger'
module Google
module Apis
ROOT = File.expand_path('..', File.dirname(__dir__))
# @!attribute [rw] logger
# @return [Logger] The logger.
def self.logger
@logger ||= rails_logger || default_logger
end
class << self
attr_writer :logger
end
private
# Create and configure a logger
# @return [Logger]
def self.default_logger
logger = Logger.new($stdout)
logger.level = Logger::WARN
logger
end
# Check to see if client is being used in a Rails environment and get the logger if present.
# Setting the ENV variable 'GOOGLE_API_USE_RAILS_LOGGER' to false will force the client
# to use its own logger.
#
# @return [Logger]
def self.rails_logger
if 'true' == ENV.fetch('GOOGLE_API_USE_RAILS_LOGGER', 'true') &&
defined?(::Rails) &&
::Rails.respond_to?(:logger) &&
!::Rails.logger.nil?
::Rails.logger
else
nil
end
end
end
end
@@ -1,209 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'addressable/uri'
require 'addressable/template'
require 'google/apis/core/http_command'
require 'google/apis/errors'
require 'json'
require 'retriable'
module Google
module Apis
module Core
# Command for executing most basic API request with JSON requests/responses
class ApiCommand < HttpCommand
JSON_CONTENT_TYPE = 'application/json'
FIELDS_PARAM = 'fields'
ERROR_REASON_MAPPING = {
'rateLimitExceeded' => Google::Apis::RateLimitError,
'userRateLimitExceeded' => Google::Apis::RateLimitError,
'projectNotLinked' => Google::Apis::ProjectNotLinkedError
}
# JSON serializer for request objects
# @return [Google::Apis::Core::JsonRepresentation]
attr_accessor :request_representation
# Request body to serialize
# @return [Object]
attr_accessor :request_object
# JSON serializer for response objects
# @return [Google::Apis::Core::JsonRepresentation]
attr_accessor :response_representation
# Class to instantiate when de-serializing responses
# @return [Object]
attr_accessor :response_class
# Serialize the request body
#
# @return [void]
def prepare!
set_api_client_header
set_user_project_header
if options&.api_format_version
header['X-Goog-Api-Format-Version'] = options.api_format_version.to_s
end
query[FIELDS_PARAM] = normalize_fields_param(query[FIELDS_PARAM]) if query.key?(FIELDS_PARAM)
if request_representation && request_object
header['Content-Type'] ||= JSON_CONTENT_TYPE
if options && options.skip_serialization
self.body = request_object
else
self.body = request_representation.new(request_object).to_json(user_options: { skip_undefined: true })
end
end
super
end
# Deserialize the response body if present
#
# @param [String] content_type
# Content type of body
# @param [String, #read] body
# Response body
# @return [Object]
# Response object
# noinspection RubyUnusedLocalVariable
def decode_response_body(content_type, body)
return super unless response_representation
return super if options && options.skip_deserialization
return super if content_type.nil?
return nil unless content_type.start_with?(JSON_CONTENT_TYPE)
body = "{}" if body.empty?
instance = response_class.new
response_representation.new(instance).from_json(body, unwrap: response_class)
instance
end
# Check the response and raise error if needed
#
# @param [Fixnum] status
# HTTP status code of response
# @param [Hash] header
# HTTP response headers
# @param [String] body
# HTTP response body
# @param [String] message
# Error message text
# @return [void]
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required
def check_status(status, header = nil, body = nil, message = nil)
case status
when 400, 402...500
reason, message = parse_error(body)
if reason
message = sprintf('%s: %s', reason, message)
raise ERROR_REASON_MAPPING[reason].new(
message,
status_code: status,
header: header,
body: body
) if ERROR_REASON_MAPPING.key?(reason)
end
super(status, header, body, message)
else
super(status, header, body, message)
end
end
def allow_form_encoding?
request_representation.nil? && super
end
private
def set_api_client_header
old_xgac = header
.find_all { |k, v| k.downcase == 'x-goog-api-client' }
.map { |(a, b)| b }
.join(' ')
.split
.find_all { |s| s !~ %r{^gl-ruby/|^gdcl/} }
.join(' ')
xgac = "gl-ruby/#{RUBY_VERSION} gdcl/#{Google::Apis::VERSION}"
xgac = old_xgac.empty? ? xgac : "#{old_xgac} #{xgac}"
header.delete_if { |k, v| k.downcase == 'x-goog-api-client' }
header['X-Goog-Api-Client'] = xgac
end
def set_user_project_header
quota_project_id = options.quota_project
if !quota_project_id && options&.authorization.respond_to?(:quota_project_id)
quota_project_id = options.authorization.quota_project_id
end
header['X-Goog-User-Project'] = quota_project_id if quota_project_id
end
# Attempt to parse a JSON error message
# @param [String] body
# HTTP response body
# @return [Array<(String, String)>]
# Error reason and message
def parse_error(body)
obj = JSON.load(body)
error = obj['error']
if error['details']
return extract_v2_error_details(error)
elsif error['errors']
return extract_v1_error_details(error)
else
fail 'Can not parse error message. No "details" or "errors" detected'
end
rescue
return [nil, nil]
end
# Extracts details from a v1 error message
# @param [Hash] error
# Parsed JSON
# @return [Array<(String, String)>]
# Error reason and message
def extract_v1_error_details(error)
reason = error['errors'].first['reason']
message = error['message']
return [reason, message]
end
# Extracts details from a v2error message
# @param [Hash] error
# Parsed JSON
# @return [Array<(String, String)>]
# Error reason and message
def extract_v2_error_details(error)
reason = error['status']
message = error['message']
return [reason, message]
end
# Convert field names from ruby conventions to original names in JSON
#
# @param [String] fields
# Value of 'fields' param
# @return [String]
# Updated header value
def normalize_fields_param(fields)
# TODO: Generate map of parameter names during code gen. Small possibility that camelization fails
fields.gsub(/:/, '').gsub(/\w+/) do |str|
str.gsub(/(?:^|_)([a-z])/){ Regexp.last_match.begin(0) == 0 ? $1 : $1.upcase }
end
end
end
end
end
end
@@ -1,447 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'addressable/uri'
require 'addressable/template'
require 'google/apis/version'
require 'google/apis/core/api_command'
require 'google/apis/core/batch'
require 'google/apis/core/upload'
require 'google/apis/core/download'
require 'google/apis/options'
require 'googleauth'
require 'httpclient'
module Google
module Apis
module Core
# Helper class for enumerating over a result set requiring multiple fetches
class PagedResults
include Enumerable
attr_reader :last_result
# @param [BaseService] service
# Current service instance
# @param [Fixnum] max
# Maximum number of items to iterate over. Nil if no limit
# @param [Boolean] cache
# True (default) if results should be cached so multiple iterations can be used.
# @param [Symbol] items
# Name of the field in the result containing the items. Defaults to :items
def initialize(service, max: nil, items: :items, cache: true, response_page_token: :next_page_token, &block)
@service = service
@block = block
@max = max
@items_field = items
@response_page_token_field = response_page_token
if cache
@result_cache = Hash.new do |h, k|
h[k] = @block.call(k, @service)
end
@fetch_proc = Proc.new { |token| @result_cache[token] }
else
@fetch_proc = Proc.new { |token| @block.call(token, @service) }
end
end
# Iterates over result set, fetching additional pages as needed
def each
page_token = nil
item_count = 0
loop do
@last_result = @fetch_proc.call(page_token)
items = @last_result.send(@items_field)
if items.kind_of?(Array)
for item in items
item_count = item_count + 1
break if @max && item_count > @max
yield item
end
elsif items.kind_of?(Hash)
items.each do |key, val|
item_count = item_count + 1
break if @max && item_count > @max
yield key, val
end
elsif items
# yield singular non-nil items (for genomics API)
yield items
end
break if @max && item_count >= @max
next_page_token = @last_result.send(@response_page_token_field)
break if next_page_token.nil? || next_page_token == page_token
page_token = next_page_token
end
end
end
# Base service for all APIs. Not to be used directly.
#
class BaseService
include Logging
# Root URL (host/port) for the API
# @return [Addressable::URI]
attr_accessor :root_url
# Additional path prefix for all API methods
# @return [Addressable::URI]
attr_accessor :base_path
# Alternate path prefix for media uploads
# @return [Addressable::URI]
attr_accessor :upload_path
# Alternate path prefix for all batch methods
# @return [Addressable::URI]
attr_accessor :batch_path
# HTTP client
# @return [HTTPClient]
attr_accessor :client
# General settings
# @return [Google::Apis::ClientOptions]
attr_accessor :client_options
# Default options for all requests
# @return [Google::Apis::RequestOptions]
attr_accessor :request_options
# @param [String,Addressable::URI] root_url
# Root URL for the API
# @param [String,Addressable::URI] base_path
# Additional path prefix for all API methods
# @api private
def initialize(root_url, base_path)
self.root_url = root_url
self.base_path = base_path
self.upload_path = "upload/#{base_path}"
self.batch_path = 'batch'
self.client_options = Google::Apis::ClientOptions.default.dup
self.request_options = Google::Apis::RequestOptions.default.dup
end
# @!attribute [rw] authorization
# @return [Signet::OAuth2::Client]
# OAuth2 credentials
def authorization=(authorization)
request_options.authorization = authorization
end
def authorization
request_options.authorization
end
# TODO: with(options) method
# Perform a batch request. Calls made within the block are sent in a single network
# request to the server.
#
# @example
# service.batch do |s|
# s.get_item(id1) do |res, err|
# # process response for 1st call
# end
# # ...
# s.get_item(idN) do |res, err|
# # process response for Nth call
# end
# end
#
# @param [Hash, Google::Apis::RequestOptions] options
# Request-specific options
# @yield [self]
# @return [void]
def batch(options = nil)
batch_command = BatchCommand.new(:post, Addressable::URI.parse(root_url + batch_path))
batch_command.options = request_options.merge(options)
apply_command_defaults(batch_command)
begin
start_batch(batch_command)
yield self
ensure
end_batch
end
batch_command.execute(client)
end
# Perform a batch upload request. Calls made within the block are sent in a single network
# request to the server. Batch uploads are useful for uploading multiple small files. For larger
# files, use single requests which use a resumable upload protocol.
#
# @example
# service.batch do |s|
# s.insert_item(upload_source: 'file1.txt') do |res, err|
# # process response for 1st call
# end
# # ...
# s.insert_item(upload_source: 'fileN.txt') do |res, err|
# # process response for Nth call
# end
# end
#
# @param [Hash, Google::Apis::RequestOptions] options
# Request-specific options
# @yield [self]
# @return [void]
def batch_upload(options = nil)
batch_command = BatchUploadCommand.new(:put, Addressable::URI.parse(root_url + upload_path))
batch_command.options = request_options.merge(options)
apply_command_defaults(batch_command)
begin
start_batch(batch_command)
yield self
ensure
end_batch
end
batch_command.execute(client)
end
# Get the current HTTP client
# @return [HTTPClient]
def client
@client ||= new_client
end
# Simple escape hatch for making API requests directly to a given
# URL. This is not intended to be used as a generic HTTP client
# and should be used only in cases where no service method exists
# (e.g. fetching an export link for a Google Drive file.)
#
# @param [Symbol] method
# HTTP method as symbol (e.g. :get, :post, :put, ...)
# @param [String] url
# URL to call
# @param [Hash<String,String>] params
# Optional hash of query parameters
# @param [#read] body
# Optional body for POST/PUT
# @param [IO, String] download_dest
# IO stream or filename to receive content download
# @param [Google::Apis::RequestOptions] options
# Request-specific options
#
# @yield [result, err] Result & error if block supplied
# @yieldparam result [String] HTTP response body
# @yieldparam err [StandardError] error object if request failed
#
# @return [String] HTTP response body
def http(method, url, params: nil, body: nil, download_dest: nil, options: nil, &block)
if download_dest
command = DownloadCommand.new(method, url, body: body)
else
command = HttpCommand.new(method, url, body: body)
end
command.options = request_options.merge(options)
apply_command_defaults(command)
command.query.merge(Hash(params))
execute_or_queue_command(command, &block)
end
# Executes a given query with paging, automatically retrieving
# additional pages as necessary. Requires a block that returns the
# result set of a page. The current page token is supplied as an argument
# to the block.
#
# Note: The returned enumerable also contains a `last_result` field
# containing the full result of the last query executed.
#
# @param [Fixnum] max
# Maximum number of items to iterate over. Defaults to nil -- no upper bound.
# @param [Symbol] items
# Name of the field in the result containing the items. Defaults to :items
# @param [Boolean] cache
# True (default) if results should be cached so multiple iterations can be used.
# @return [Enumerble]
# @yield [token, service]
# Current page token & service instance
# @yieldparam [String] token
# Current page token to be used in the query
# @yieldparam [service]
# Current service instance
# @since 0.9.4
#
# @example Retrieve all files,
# file_list = service.fetch_all { |token, s| s.list_files(page_token: token) }
# file_list.each { |f| ... }
def fetch_all(max: nil, items: :items, cache: true, response_page_token: :next_page_token, &block)
fail "fetch_all may not be used inside a batch" if batch?
return PagedResults.new(self, max: max, items: items, cache: cache, response_page_token: response_page_token, &block)
end
protected
# Create a new upload command.
#
# @param [symbol] method
# HTTP method for uploading (typically :put or :post)
# @param [String] path
# Additional path to upload endpoint, appended to API base path
# @param [Hash, Google::Apis::RequestOptions] options
# Request-specific options
# @return [Google::Apis::Core::UploadCommand]
def make_upload_command(method, path, options)
template = Addressable::Template.new(root_url + upload_path + path)
if batch?
command = MultipartUploadCommand.new(method, template)
else
command = ResumableUploadCommand.new(method, template)
end
command.options = request_options.merge(options)
apply_command_defaults(command)
command
end
# Create a new download command.
#
# @param [symbol] method
# HTTP method for uploading (typically :get)
# @param [String] path
# Additional path to download endpoint, appended to API base path
# @param [Hash, Google::Apis::RequestOptions] options
# Request-specific options
# @return [Google::Apis::Core::DownloadCommand]
def make_download_command(method, path, options)
template = Addressable::Template.new(root_url + base_path + path)
command = DownloadCommand.new(method, template)
command.options = request_options.merge(options)
command.query['alt'] = 'media'
apply_command_defaults(command)
command
end
# Create a new command.
#
# @param [symbol] method
# HTTP method (:get, :post, :delete, etc...)
# @param [String] path
# Additional path, appended to API base path
# @param [Hash, Google::Apis::RequestOptions] options
# Request-specific options
# @return [Google::Apis::Core::DownloadCommand]
def make_simple_command(method, path, options)
full_path =
if path.start_with? "/"
path[1..-1]
else
base_path + path
end
template = Addressable::Template.new(root_url + full_path)
command = ApiCommand.new(method, template)
command.options = request_options.merge(options)
apply_command_defaults(command)
command
end
# Execute the request. If a batch is in progress, the request is added to the batch instead.
#
# @param [Google::Apis::Core::HttpCommand] command
# Command to execute
# @return [Object] response object if command executed and no callback supplied
# @yield [result, err] Result & error if block supplied
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required
def execute_or_queue_command(command, &callback)
batch_command = current_batch
if batch_command
fail "Can not combine services in a batch" if Thread.current[:google_api_batch_service] != self
batch_command.add(command, &callback)
nil
else
command.execute(client, &callback)
end
end
# Update commands with service-specific options. To be implemented by subclasses
# @param [Google::Apis::Core::HttpCommand] _command
def apply_command_defaults(_command)
end
private
# Get the current batch context
#
# @return [Google:Apis::Core::BatchRequest]
def current_batch
Thread.current[:google_api_batch]
end
# Check if a batch is in progress
# @return [Boolean]
def batch?
!current_batch.nil?
end
# Start a new thread-local batch context
# @param [Google::Apis::Core::BatchCommand] cmd
def start_batch(cmd)
fail "Batch already in progress" if batch?
Thread.current[:google_api_batch] = cmd
Thread.current[:google_api_batch_service] = self
end
# Clear thread-local batch context
def end_batch
Thread.current[:google_api_batch] = nil
Thread.current[:google_api_batch_service] = nil
end
# Create a new HTTP client
# @return [HTTPClient]
def new_client
client = ::HTTPClient.new
if client_options.transparent_gzip_decompression
client.transparent_gzip_decompression = client_options.transparent_gzip_decompression
end
client.proxy = client_options.proxy_url if client_options.proxy_url
if client_options.open_timeout_sec
client.connect_timeout = client_options.open_timeout_sec
end
if client_options.read_timeout_sec
client.receive_timeout = client_options.read_timeout_sec
end
if client_options.send_timeout_sec
client.send_timeout = client_options.send_timeout_sec
end
client.follow_redirect_count = 5
client.default_header = { 'User-Agent' => user_agent }
client.debug_dev = logger if client_options.log_http_requests
client
end
# Build the user agent header
# @return [String]
def user_agent
sprintf('%s/%s google-api-ruby-client/%s %s (gzip)',
client_options.application_name,
client_options.application_version,
Google::Apis::VERSION,
Google::Apis::OS_VERSION)
end
end
end
end
end
@@ -1,236 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'google/apis/core/multipart'
require 'google/apis/core/http_command'
require 'google/apis/core/upload'
require 'google/apis/core/download'
require 'google/apis/core/composite_io'
require 'addressable/uri'
require 'securerandom'
module Google
module Apis
module Core
# Wrapper request for batching multiple calls in a single server request
class BatchCommand < HttpCommand
MULTIPART_MIXED = 'multipart/mixed'
# @param [symbol] method
# HTTP method
# @param [String,Addressable::URI, Addressable::Template] url
# HTTP URL or template
def initialize(method, url)
super(method, url)
@calls = []
@base_id = SecureRandom.uuid
end
##
# Add a new call to the batch request.
#
# @param [Google::Apis::Core::HttpCommand] call API Request to add
# @yield [result, err] Result & error when response available
# @return [Google::Apis::Core::BatchCommand] self
def add(call, &block)
ensure_valid_command(call)
@calls << [call, block]
self
end
protected
##
# Deconstruct the batch response and process the individual results
#
# @param [String] content_type
# Content type of body
# @param [String, #read] body
# Response body
# @return [Object]
# Response object
def decode_response_body(content_type, body)
m = /.*boundary=(.+)/.match(content_type)
if m
parts = split_parts(body, m[1])
deserializer = CallDeserializer.new
parts.each_index do |index|
response = deserializer.to_http_response(parts[index])
outer_header = response.shift
call_id = header_to_id(outer_header['Content-ID'].first) || index
call, callback = @calls[call_id]
begin
result = call.process_response(*response) unless call.nil?
success(result, &callback)
rescue => e
error(e, &callback)
end
end
end
nil
end
def split_parts(body, boundary)
parts = body.split(/\r?\n?--#{Regexp.escape(boundary)}/)
parts[1...-1]
end
# Encode the batch request
# @return [void]
# @raise [Google::Apis::BatchError] if batch is empty
def prepare!
fail BatchError, 'Cannot make an empty batch request' if @calls.empty?
serializer = CallSerializer.new
multipart = Multipart.new(content_type: MULTIPART_MIXED)
@calls.each_index do |index|
call, _ = @calls[index]
content_id = id_to_header(index)
io = serializer.to_part(call)
multipart.add_upload(io, content_type: 'application/http', content_id: content_id)
end
self.body = multipart.assemble
header['Content-Type'] = multipart.content_type
super
end
def ensure_valid_command(command)
if command.is_a?(Google::Apis::Core::BaseUploadCommand) || command.is_a?(Google::Apis::Core::DownloadCommand)
fail Google::Apis::ClientError, 'Can not include media requests in batch'
end
fail Google::Apis::ClientError, 'Invalid command object' unless command.is_a?(HttpCommand)
end
def id_to_header(call_id)
return sprintf('<%s+%i>', @base_id, call_id)
end
def header_to_id(content_id)
match = /<response-.*\+(\d+)>/.match(content_id)
return match[1].to_i if match
return nil
end
end
# Wrapper request for batching multiple uploads in a single server request
class BatchUploadCommand < BatchCommand
def ensure_valid_command(command)
fail Google::Apis::ClientError, 'Can only include upload commands in batch' \
unless command.is_a?(Google::Apis::Core::BaseUploadCommand)
end
def prepare!
header['X-Goog-Upload-Protocol'] = 'batch'
super
end
end
# Serializes a command for embedding in a multipart batch request
# @private
class CallSerializer
##
# Serialize a single batched call for assembling the multipart message
#
# @param [Google::Apis::Core::HttpCommand] call
# the call to serialize.
# @return [IO]
# the serialized request
def to_part(call)
call.prepare!
# This will add the Authorization header if needed.
call.apply_request_options(call.header)
parts = []
parts << build_head(call)
parts << build_body(call) unless call.body.nil?
length = parts.inject(0) { |a, e| a + e.length }
Google::Apis::Core::CompositeIO.new(*parts)
end
protected
def build_head(call)
request_head = "#{call.method.to_s.upcase} #{Addressable::URI.parse(call.url).request_uri} HTTP/1.1"
call.header.each do |key, value|
request_head << sprintf("\r\n%s: %s", key, value)
end
request_head << sprintf("\r\nHost: %s", call.url.host)
request_head << "\r\n\r\n"
StringIO.new(request_head)
end
def build_body(call)
return nil if call.body.nil?
return call.body if call.body.respond_to?(:read)
StringIO.new(call.body)
end
end
# Deconstructs a raw HTTP response part
# @private
class CallDeserializer
# Parse a batched response.
#
# @param [String] call_response
# the response to parse.
# @return [Array<(Fixnum, Hash, String)>]
# Status, header, and response body.
def to_http_response(call_response)
outer_header, outer_body = split_header_and_body(call_response)
status_line, payload = outer_body.split(/\n/, 2)
_, status = status_line.split(' ', 3)
header, body = split_header_and_body(payload)
[outer_header, status.to_i, header, body]
end
protected
# Auxiliary method to split the header from the body in an HTTP response.
#
# @param [String] response
# the response to parse.
# @return [Array<(HTTP::Message::Headers, String)>]
# the header and the body, separately.
def split_header_and_body(response)
header = HTTP::Message::Headers.new
payload = response.lstrip
while payload
line, payload = payload.split(/\n/, 2)
line.sub!(/\s+\z/, '')
break if line.empty?
match = /\A([^:]+):\s*/.match(line)
fail BatchError, sprintf('Invalid header line in response: %s', line) if match.nil?
header[match[1]] = match.post_match
end
[header, payload]
end
end
end
end
end
@@ -1,97 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'google/apis/core/http_command'
require 'google/apis/core/upload'
require 'google/apis/core/download'
require 'addressable/uri'
require 'securerandom'
module Google
module Apis
module Core
class CompositeIO
def initialize(*ios)
@ios = ios.flatten
@pos = 0
@index = 0
@sizes = @ios.map(&:size)
end
def read(length = nil, buf = nil)
buf = buf ? buf.replace('') : ''
begin
io = @ios[@index]
break if io.nil?
result = io.read(length)
if result
buf << result
if length
length -= result.length
break if length == 0
end
end
@index += 1
end while @index < @ios.length
buf.length > 0 ? buf : nil
end
def size
@sizes.reduce(:+)
end
alias_method :length, :size
def pos
@pos
end
def pos=(pos)
fail ArgumentError, "Position can not be negative" if pos < 0
@pos = pos
new_index = nil
@ios.each_with_index do |io,idx|
size = io.size
if pos <= size
new_index ||= idx
io.pos = pos
pos = 0
else
io.pos = size
pos -= size
end
end
@index = new_index unless new_index.nil?
end
def rewind
self.pos = 0
end
end
end
end
end
@@ -1,118 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'google/apis/core/api_command'
require 'google/apis/errors'
require 'addressable/uri'
module Google
module Apis
module Core
# Streaming/resumable media download support
class DownloadCommand < ApiCommand
RANGE_HEADER = 'Range'
OK_STATUS = [200, 201, 206]
# File or IO to write content to
# @return [String, File, #write]
attr_accessor :download_dest
# Ensure the download destination is a writable stream.
#
# @return [void]
def prepare!
@state = :start
@download_url = nil
@offset = 0
if download_dest.respond_to?(:write)
@download_io = download_dest
@close_io_on_finish = false
elsif download_dest.is_a?(String)
@download_io = File.open(download_dest, 'wb')
@close_io_on_finish = true
else
@download_io = StringIO.new('', 'wb')
@close_io_on_finish = false
end
super
end
# Close IO stream when command done. Only closes the stream if it was opened by the command.
def release!
@download_io.close if @close_io_on_finish
end
# Execute the upload request once. Overrides the default implementation to handle streaming/chunking
# of file content.
#
# @private
# @param [HTTPClient] client
# HTTP client
# @yield [result, err] Result or error if block supplied
# @return [Object]
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required
def execute_once(client, &block)
request_header = header.dup
apply_request_options(request_header)
download_offset = nil
if @offset > 0
logger.debug { sprintf('Resuming download from offset %d', @offset) }
request_header[RANGE_HEADER] = sprintf('bytes=%d-', @offset)
end
http_res = client.get(url.to_s,
query: query,
header: request_header,
follow_redirect: true) do |res, chunk|
status = res.http_header.status_code.to_i
next unless OK_STATUS.include?(status)
download_offset ||= (status == 206 ? @offset : 0)
download_offset += chunk.bytesize
if download_offset - chunk.bytesize == @offset
next_chunk = chunk
else
# Oh no! Requested a chunk, but received the entire content
chunk_index = @offset - (download_offset - chunk.bytesize)
next_chunk = chunk.byteslice(chunk_index..-1)
next if next_chunk.nil?
end
# logger.debug { sprintf('Writing chunk (%d bytes, %d total)', chunk.length, bytes_read) }
@download_io.write(next_chunk)
@offset += next_chunk.bytesize
end
@download_io.flush if @download_io.respond_to?(:flush)
if @close_io_on_finish
result = nil
else
result = @download_io
end
check_status(http_res.status.to_i, http_res.header, http_res.body)
success(result, &block)
rescue => e
@download_io.flush if @download_io.respond_to?(:flush)
error(e, rethrow: true, &block)
end
end
end
end
end
@@ -1,44 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
module Google
module Apis
module Core
# Adds to_hash to objects
module Hashable
# Convert object to hash representation
#
# @return [Hash]
def to_h
Hash[instance_variables.map { |k| [k[1..-1].to_sym, Hashable.process_value(instance_variable_get(k))] }]
end
# Recursively serialize an object
#
# @param [Object] val
# @return [Hash]
def self.process_value(val)
case val
when Hash
Hash[val.map {|k, v| [k.to_sym, Hashable.process_value(v)] }]
when Array
val.map{ |v| Hashable.process_value(v) }
else
val.respond_to?(:to_h) ? val.to_h : val
end
end
end
end
end
end
@@ -1,447 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'addressable/uri'
require 'addressable/template'
require 'google/apis/options'
require 'google/apis/errors'
require 'retriable'
require 'google/apis/core/logging'
require 'pp'
module Google
module Apis
module Core
# Command for HTTP request/response.
class HttpCommand
include Logging
RETRIABLE_ERRORS = [Google::Apis::ServerError, Google::Apis::RateLimitError, Google::Apis::TransmissionError]
begin
require 'opencensus'
OPENCENSUS_AVAILABLE = true
rescue LoadError
OPENCENSUS_AVAILABLE = false
end
# Request options
# @return [Google::Apis::RequestOptions]
attr_accessor :options
# HTTP request URL
# @return [String, Addressable::URI]
attr_accessor :url
# HTTP headers
# @return [Hash]
attr_accessor :header
# Request body
# @return [#read]
attr_accessor :body
# HTTP method
# @return [symbol]
attr_accessor :method
# HTTP Client
# @return [HTTPClient]
attr_accessor :connection
# Query params
# @return [Hash]
attr_accessor :query
# Path params for URL Template
# @return [Hash]
attr_accessor :params
# @param [symbol] method
# HTTP method
# @param [String,Addressable::URI, Addressable::Template] url
# HTTP URL or template
# @param [String, #read] body
# Request body
def initialize(method, url, body: nil)
self.options = Google::Apis::RequestOptions.default.dup
self.url = url
self.url = Addressable::Template.new(url) if url.is_a?(String)
self.method = method
self.header = Hash.new
self.body = body
self.query = {}
self.params = {}
@opencensus_span = nil
end
# Execute the command, retrying as necessary
#
# @param [HTTPClient] client
# HTTP client
# @yield [result, err] Result or error if block supplied
# @return [Object]
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required
def execute(client)
prepare!
opencensus_begin_span
begin
Retriable.retriable tries: options.retries + 1,
base_interval: 1,
multiplier: 2,
on: RETRIABLE_ERRORS do |try|
# This 2nd level retriable only catches auth errors, and supports 1 retry, which allows
# auth to be re-attempted without having to retry all sorts of other failures like
# NotFound, etc
auth_tries = (try == 1 && authorization_refreshable? ? 2 : 1)
Retriable.retriable tries: auth_tries,
on: [Google::Apis::AuthorizationError, Signet::AuthorizationError, Signet::RemoteServerError, Signet::UnexpectedStatusError],
on_retry: proc { |*| refresh_authorization } do
execute_once(client).tap do |result|
if block_given?
yield result, nil
end
end
end
end
rescue => e
if block_given?
yield nil, e
else
raise e
end
end
ensure
opencensus_end_span
@http_res = nil
release!
end
# Refresh the authorization authorization after a 401 error
#
# @private
# @return [void]
def refresh_authorization
# Handled implicitly by auth lib, here in case need to override
logger.debug('Retrying after authentication failure')
end
# Check if attached credentials can be automatically refreshed
# @return [Boolean]
def authorization_refreshable?
options.authorization.respond_to?(:apply!)
end
# Prepare the request (e.g. calculate headers, add query params, serialize data, etc) before sending
#
# @private
# @return [void]
def prepare!
normalize_unicode = true
if options
header.update(options.header) if options.header
query.update(options.query) if options.query
normalize_unicode = options.normalize_unicode
end
self.url = url.expand(params, nil, normalize_unicode) if url.is_a?(Addressable::Template)
url.query_values = normalize_query_values(query).merge(url.query_values || {})
if allow_form_encoding?
@form_encoded = true
self.body = Addressable::URI.form_encode(url.query_values(Array))
self.header['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
self.url.query_values = {}
else
@form_encoded = false
end
self.body = '' unless self.body
end
# Release any resources used by this command
# @private
# @return [void]
def release!
end
# Check the response and either decode body or raise error
#
# @param [Fixnum] status
# HTTP status code of response
# @param [Hash] header
# Response headers
# @param [String, #read] body
# Response body
# @return [Object]
# Response object
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required
def process_response(status, header, body)
check_status(status, header, body)
decode_response_body(header['Content-Type'].first, body)
end
# Check the response and raise error if needed
#
# @param [Fixnum] status
# HTTP status code of response
# @param [Hash] header
# HTTP response headers
# @param [String] body
# HTTP response body
# @param [String] message
# Error message text
# @return [void]
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required
def check_status(status, header = nil, body = nil, message = nil)
# TODO: 304 Not Modified depends on context...
case status
when 200...300
nil
when 301, 302, 303, 307
message ||= sprintf('Redirect to %s', header['Location'])
raise Google::Apis::RedirectError.new(message, status_code: status, header: header, body: body)
when 401
message ||= 'Unauthorized'
raise Google::Apis::AuthorizationError.new(message, status_code: status, header: header, body: body)
when 429
message ||= 'Rate limit exceeded'
raise Google::Apis::RateLimitError.new(message, status_code: status, header: header, body: body)
when 304, 400, 402...500
message ||= 'Invalid request'
raise Google::Apis::ClientError.new(message, status_code: status, header: header, body: body)
when 500...600
message ||= 'Server error'
raise Google::Apis::ServerError.new(message, status_code: status, header: header, body: body)
else
logger.warn(sprintf('Encountered unexpected status code %s', status))
message ||= 'Unknown error'
raise Google::Apis::TransmissionError.new(message, status_code: status, header: header, body: body)
end
end
# Process the actual response body. Intended to be overridden by subclasses
#
# @param [String] _content_type
# Content type of body
# @param [String, #read] body
# Response body
# @return [Object]
def decode_response_body(_content_type, body)
body
end
# Process a success response
# @param [Object] result
# Result object
# @return [Object] result if no block given
# @yield [result, nil] if block given
def success(result, &block)
logger.debug { sprintf('Success - %s', safe_object_representation(result)) }
block.call(result, nil) if block_given?
result
end
# Process an error response
# @param [StandardError] err
# Error object
# @param [Boolean] rethrow
# True if error should be raised again after handling
# @return [void]
# @yield [nil, err] if block given
# @raise [StandardError] if no block
def error(err, rethrow: false, &block)
logger.debug { sprintf('Error - %s', PP.pp(err, '')) }
if err.is_a?(HTTPClient::BadResponseError)
begin
res = err.res
raise Google::Apis::TransmissionError.new(err) if res.nil?
check_status(res.status.to_i, res.header, res.body)
rescue Google::Apis::Error => e
err = e
end
elsif err.is_a?(HTTPClient::TimeoutError) || err.is_a?(SocketError)
err = Google::Apis::TransmissionError.new(err)
end
block.call(nil, err) if block_given?
fail err if rethrow || block.nil?
end
# Execute the command once.
#
# @private
# @param [HTTPClient] client
# HTTP client
# @return [Object]
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required
def execute_once(client)
body.rewind if body.respond_to?(:rewind)
begin
logger.debug { sprintf('Sending HTTP %s %s', method, url) }
request_header = header.dup
apply_request_options(request_header)
@http_res = client.request(method.to_s.upcase,
url.to_s,
query: nil,
body: body,
header: request_header,
follow_redirect: true)
logger.debug { @http_res.status }
logger.debug { safe_response_representation @http_res }
response = process_response(@http_res.status.to_i, @http_res.header, @http_res.body)
success(response)
rescue => e
logger.debug { sprintf('Caught error %s', e) }
error(e, rethrow: true)
end
end
# Update the request with any specified options.
# @param [Hash] req_header
# HTTP headers
# @return [void]
def apply_request_options(req_header)
if options.authorization.respond_to?(:apply!)
options.authorization.apply!(req_header)
elsif options.authorization.is_a?(String)
req_header['Authorization'] = sprintf('Bearer %s', options.authorization)
end
req_header.update(header)
end
def allow_form_encoding?
[:post, :put].include?(method) && body.nil?
end
private
UNSAFE_CLASS_NAMES = [
"Google::Apis::CloudkmsV1::DecryptResponse"
]
def safe_object_representation obj
name = obj.class.name
if UNSAFE_CLASS_NAMES.include? name
"#<#{name} (fields redacted)>"
else
PP.pp(obj, "")
end
end
def safe_response_representation http_res
if respond_to?(:response_class) && response_class.is_a?(Class) &&
UNSAFE_CLASS_NAMES.include?(response_class.name)
return "#<#{http_res.class.name} (fields redacted)>"
end
http_res.inspect
end
def opencensus_begin_span
return unless OPENCENSUS_AVAILABLE && options.use_opencensus
return if @opencensus_span
return unless OpenCensus::Trace.span_context
@opencensus_span = OpenCensus::Trace.start_span url.path.to_s
@opencensus_span.kind = OpenCensus::Trace::SpanBuilder::CLIENT
@opencensus_span.put_attribute "http.host", url.host.to_s
@opencensus_span.put_attribute "http.method", method.to_s.upcase
@opencensus_span.put_attribute "http.path", url.path.to_s
if body.respond_to? :bytesize
@opencensus_span.put_message_event \
OpenCensus::Trace::SpanBuilder::SENT, 1, body.bytesize
end
formatter = OpenCensus::Trace.config.http_formatter
if formatter.respond_to? :header_name
header[formatter.header_name] = formatter.serialize @opencensus_span.context.trace_context
end
rescue StandardError => e
# Log exceptions and continue, so opencensus failures don't cause
# the entire request to fail.
logger.debug { sprintf('Error opening OpenCensus span: %s', e) }
end
def opencensus_end_span
return unless OPENCENSUS_AVAILABLE
return unless @opencensus_span
return unless OpenCensus::Trace.span_context
if @http_res
if @http_res.body.respond_to? :bytesize
@opencensus_span.put_message_event \
OpenCensus::Trace::SpanBuilder::RECEIVED, 1, @http_res.body.bytesize
end
status = @http_res.status.to_i
if status > 0
@opencensus_span.set_status map_http_status status
@opencensus_span.put_attribute "http.status_code", status
end
end
OpenCensus::Trace.end_span @opencensus_span
@opencensus_span = nil
rescue StandardError => e
# Log exceptions and continue, so failures don't cause leaks by
# aborting cleanup.
logger.debug { sprintf('Error finishing OpenCensus span: %s', e) }
end
def form_encoded?
@form_encoded
end
def map_http_status http_status
case http_status
when 200..399 then 0 # OK
when 400 then 3 # INVALID_ARGUMENT
when 401 then 16 # UNAUTHENTICATED
when 403 then 7 # PERMISSION_DENIED
when 404 then 5 # NOT_FOUND
when 429 then 8 # RESOURCE_EXHAUSTED
when 501 then 12 # UNIMPLEMENTED
when 503 then 14 # UNAVAILABLE
when 504 then 4 # DEADLINE_EXCEEDED
else 2 # UNKNOWN
end
end
def normalize_query_values(input)
input.inject({}) do |h, (k, v)|
h[k] = normalize_query_value(v)
h
end
end
def normalize_query_value(v)
case v
when Array
v.map { |v2| normalize_query_value(v2) }
when nil
nil
else
v.to_s
end
end
end
end
end
end
@@ -1,153 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'representable/json'
require 'representable/json/hash'
require 'base64'
require 'date'
module Google
module Apis
module Core
# Support for serializing hashes + property value/nil/unset tracking
# To be included in representers as a feature.
# @private
module JsonRepresentationSupport
def self.included(base)
base.extend(JsonSupport)
end
# @private
module JsonSupport
# Returns a customized getter function for Representable. Allows
# indifferent hash/attribute access.
#
# @param [String] name Property name
# @return [Proc]
def getter_fn(name)
ivar_name = "@#{name}".to_sym
lambda do |_|
if respond_to?(:fetch)
fetch(name, instance_variable_get(ivar_name))
else
instance_variable_get(ivar_name)
end
end
end
# Returns a customized function for Representable that checks whether or not
# an attribute should be serialized. Allows proper patch semantics by distinguishing
# between nil & unset values
#
# @param [String] name Property name
# @return [Proc]
def if_fn(name)
ivar_name = "@#{name}".to_sym
lambda do |opts|
if opts[:user_options] && opts[:user_options][:skip_undefined]
if respond_to?(:key?)
self.key?(name) || instance_variable_defined?(ivar_name)
else
instance_variable_defined?(ivar_name)
end
else
true
end
end
end
def set_default_options(name, options)
if options[:base64]
options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : Base64.urlsafe_encode64(value) }
options[:parse_filter] = ->(fragment, _doc, *_args) { Base64.urlsafe_decode64(fragment) }
end
if options[:numeric_string]
options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : value.to_s}
options[:parse_filter] = ->(fragment, _doc, *_args) { fragment.to_i }
end
if options[:type] == DateTime
options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : value.is_a?(DateTime) ? value.rfc3339(3) : value.to_s }
options[:parse_filter] = ->(fragment, _doc, *_args) { DateTime.parse(fragment) }
end
if options[:type] == Date
options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : value.to_s}
options[:parse_filter] = ->(fragment, _doc, *_args) { Date.parse(fragment) }
end
options[:render_nil] = true
options[:getter] = getter_fn(name)
options[:if] = if_fn(name)
end
# Define a single value property
#
# @param [String] name
# Property name
# @param [Hash] options
def property(name, options = {})
set_default_options(name, options)
super(name, options)
end
# Define a collection property
#
# @param [String] name
# Property name
# @param [Hash] options
def collection(name, options = {})
set_default_options(name, options)
super(name, options)
end
# Define a hash property
#
# @param [String] name
# Property name
# @param [Hash] options
def hash(name = nil, options = nil)
return super() unless name # Allow Object.hash
set_default_options(name, options)
super(name, options)
end
end
end
# Base decorator for JSON representers
#
# @see https://github.com/apotonick/representable
class JsonRepresentation < Representable::Decorator
include Representable::JSON
feature JsonRepresentationSupport
end
module JsonObjectSupport
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def from_json(json)
representation = self.const_get(:Representation)
representation.new(self.new).from_json(json, unwrap: self)
end
end
def to_json(*a)
representation = self.class.const_get(:Representation)
representation.new(self).to_hash(user_options: { skip_undefined: true }).to_json(*a)
end
end
end
end
end
@@ -1,30 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'google/apis'
module Google
module Apis
module Core
# Logging support
module Logging
# Get the logger instance
# @return [Logger]
def logger
Google::Apis.logger
end
end
end
end
end
@@ -1,135 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
module Google
module Apis
module Core
# Part of a multipart request for holding JSON data
#
# @private
class JsonPart
# @param [String] value
# JSON content
# @param [Hash] header
# Additional headers
def initialize(value, header = {})
@value = value
@header = header
end
def to_io(boundary)
part = ''
part << "--#{boundary}\r\n"
part << "Content-Type: application/json\r\n"
@header.each do |(k, v)|
part << "#{k}: #{v}\r\n"
end
part << "\r\n"
part << "#{@value}\r\n"
StringIO.new(part)
end
end
# Part of a multipart request for holding arbitrary content.
#
# @private
class FilePart
# @param [IO] io
# IO stream
# @param [Hash] header
# Additional headers
def initialize(io, header = {})
@io = io
@header = header
@length = io.respond_to?(:size) ? io.size : nil
end
def to_io(boundary)
head = ''
head << "--#{boundary}\r\n"
@header.each do |(k, v)|
head << "#{k}: #{v}\r\n"
end
head << "Content-Length: #{@length}\r\n" unless @length.nil?
head << "Content-Transfer-Encoding: binary\r\n"
head << "\r\n"
Google::Apis::Core::CompositeIO.new(StringIO.new(head), @io, StringIO.new("\r\n"))
end
end
# Helper for building multipart requests
class Multipart
MULTIPART_RELATED = 'multipart/related'
# @return [String]
# Content type header
attr_reader :content_type
# @param [String] content_type
# Content type for the multipart request
# @param [String] boundary
# Part delimiter
def initialize(content_type: MULTIPART_RELATED, boundary: nil)
@parts = []
@boundary = boundary || Digest::SHA1.hexdigest(SecureRandom.random_bytes(8))
@content_type = "#{content_type}; boundary=#{@boundary}"
end
# Append JSON data part
#
# @param [String] body
# JSON text
# @param [String] content_id
# Optional unique ID of this part
# @return [self]
def add_json(body, content_id: nil)
header = {}
header['Content-ID'] = content_id unless content_id.nil?
@parts << Google::Apis::Core::JsonPart.new(body, header).to_io(@boundary)
self
end
# Append arbitrary data as a part
#
# @param [IO] upload_io
# IO stream
# @param [String] content_id
# Optional unique ID of this part
# @return [self]
def add_upload(upload_io, content_type: nil, content_id: nil)
header = {
'Content-Type' => content_type || 'application/octet-stream'
}
header['Content-Id'] = content_id unless content_id.nil?
@parts << Google::Apis::Core::FilePart.new(upload_io,
header).to_io(@boundary)
self
end
# Assemble the multipart requests
#
# @return [IO]
# IO stream
def assemble
@parts << StringIO.new("--#{@boundary}--\r\n\r\n")
Google::Apis::Core::CompositeIO.new(*@parts)
end
end
end
end
end
@@ -1,273 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'google/apis/core/multipart'
require 'google/apis/core/http_command'
require 'google/apis/core/api_command'
require 'google/apis/errors'
require 'addressable/uri'
require 'tempfile'
require 'mini_mime'
module Google
module Apis
module Core
# Base upload command. Not intended to be used directly
# @private
class BaseUploadCommand < ApiCommand
UPLOAD_PROTOCOL_HEADER = 'X-Goog-Upload-Protocol'
UPLOAD_CONTENT_TYPE_HEADER = 'X-Goog-Upload-Header-Content-Type'
UPLOAD_CONTENT_LENGTH = 'X-Goog-Upload-Header-Content-Length'
CONTENT_TYPE_HEADER = 'Content-Type'
# File name or IO containing the content to upload
# @return [String, File, #read]
attr_accessor :upload_source
# Content type of the upload material
# @return [String]
attr_accessor :upload_content_type
# Content, as UploadIO
# @return [Google::Apis::Core::UploadIO]
attr_accessor :upload_io
# Ensure the content is readable and wrapped in an IO instance.
#
# @return [void]
# @raise [Google::Apis::ClientError] if upload source is invalid
def prepare!
super
if streamable?(upload_source)
self.upload_io = upload_source
@close_io_on_finish = false
elsif self.upload_source.is_a?(String)
self.upload_io = File.new(upload_source, 'r')
if self.upload_content_type.nil?
type = MiniMime.lookup_by_filename(upload_source)
self.upload_content_type = type && type.content_type
end
@close_io_on_finish = true
else
fail Google::Apis::ClientError, 'Invalid upload source'
end
if self.upload_content_type.nil? || self.upload_content_type.empty?
self.upload_content_type = 'application/octet-stream'
end
end
# Close IO stream when command done. Only closes the stream if it was opened by the command.
def release!
upload_io.close if @close_io_on_finish
end
private
def streamable?(upload_source)
upload_source.is_a?(IO) || upload_source.is_a?(StringIO) || upload_source.is_a?(Tempfile)
end
end
# Implementation of the raw upload protocol
class RawUploadCommand < BaseUploadCommand
RAW_PROTOCOL = 'raw'
# Ensure the content is readable and wrapped in an {{Google::Apis::Core::UploadIO}} instance.
#
# @return [void]
# @raise [Google::Apis::ClientError] if upload source is invalid
def prepare!
super
self.body = upload_io
header[UPLOAD_PROTOCOL_HEADER] = RAW_PROTOCOL
header[UPLOAD_CONTENT_TYPE_HEADER] = upload_content_type
end
end
# Implementation of the multipart upload protocol
class MultipartUploadCommand < BaseUploadCommand
MULTIPART_PROTOCOL = 'multipart'
MULTIPART_RELATED = 'multipart/related'
# Encode the multipart request
#
# @return [void]
# @raise [Google::Apis::ClientError] if upload source is invalid
def prepare!
super
multipart = Multipart.new
multipart.add_json(body)
multipart.add_upload(upload_io, content_type: upload_content_type)
self.body = multipart.assemble
header['Content-Type'] = multipart.content_type
header[UPLOAD_PROTOCOL_HEADER] = MULTIPART_PROTOCOL
end
end
# Implementation of the resumable upload protocol
class ResumableUploadCommand < BaseUploadCommand
UPLOAD_COMMAND_HEADER = 'X-Goog-Upload-Command'
UPLOAD_OFFSET_HEADER = 'X-Goog-Upload-Offset'
BYTES_RECEIVED_HEADER = 'X-Goog-Upload-Size-Received'
UPLOAD_URL_HEADER = 'X-Goog-Upload-URL'
UPLOAD_STATUS_HEADER = 'X-Goog-Upload-Status'
STATUS_ACTIVE = 'active'
STATUS_FINAL = 'final'
STATUS_CANCELLED = 'cancelled'
RESUMABLE = 'resumable'
START_COMMAND = 'start'
QUERY_COMMAND = 'query'
UPLOAD_COMMAND = 'upload, finalize'
# Reset upload to initial state.
#
# @return [void]
# @raise [Google::Apis::ClientError] if upload source is invalid
def prepare!
@state = :start
@upload_url = nil
@offset = 0
# Prevent the command from populating the body with form encoding, by
# asserting that it already has a body. Form encoding is never used
# by upload requests.
self.body = '' unless self.body
super
end
# Check the to see if the upload is complete or needs to be resumed.
#
# @param [Fixnum] status
# HTTP status code of response
# @param [HTTP::Message::Headers] header
# Response headers
# @param [String, #read] body
# Response body
# @return [Object]
# Response object
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required
def process_response(status, header, body)
@offset = Integer(header[BYTES_RECEIVED_HEADER].first) unless header[BYTES_RECEIVED_HEADER].empty?
@upload_url = header[UPLOAD_URL_HEADER].first unless header[UPLOAD_URL_HEADER].empty?
upload_status = header[UPLOAD_STATUS_HEADER].first
logger.debug { sprintf('Upload status %s', upload_status) }
if upload_status == STATUS_ACTIVE
@state = :active
elsif upload_status == STATUS_FINAL
@state = :final
elsif upload_status == STATUS_CANCELLED
@state = :cancelled
fail Google::Apis::ClientError, body
end
super(status, header, body)
end
def send_start_command(client)
logger.debug { sprintf('Sending upload start command to %s', url) }
request_header = header.dup
apply_request_options(request_header)
request_header[UPLOAD_PROTOCOL_HEADER] = RESUMABLE
request_header[UPLOAD_COMMAND_HEADER] = START_COMMAND
request_header[UPLOAD_CONTENT_LENGTH] = upload_io.size.to_s
request_header[UPLOAD_CONTENT_TYPE_HEADER] = upload_content_type
client.request(method.to_s.upcase,
url.to_s, query: nil,
body: body,
header: request_header,
follow_redirect: true)
rescue => e
raise Google::Apis::ServerError, e.message
end
# Query for the status of an incomplete upload
#
# @param [HTTPClient] client
# HTTP client
# @return [HTTP::Message]
# @raise [Google::Apis::ServerError] Unable to send the request
def send_query_command(client)
logger.debug { sprintf('Sending upload query command to %s', @upload_url) }
request_header = header.dup
apply_request_options(request_header)
request_header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND
client.post(@upload_url, body: '', header: request_header, follow_redirect: true)
end
# Send the actual content
#
# @param [HTTPClient] client
# HTTP client
# @return [HTTP::Message]
# @raise [Google::Apis::ServerError] Unable to send the request
def send_upload_command(client)
logger.debug { sprintf('Sending upload command to %s', @upload_url) }
content = upload_io
content.pos = @offset
request_header = header.dup
apply_request_options(request_header)
request_header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND
request_header[UPLOAD_COMMAND_HEADER] = UPLOAD_COMMAND
request_header[UPLOAD_OFFSET_HEADER] = @offset.to_s
request_header[CONTENT_TYPE_HEADER] = upload_content_type
client.post(@upload_url, body: content, header: request_header, follow_redirect: true)
end
# Execute the upload request once. This will typically perform two HTTP requests -- one to initiate or query
# for the status of the upload, the second to send the (remaining) content.
#
# @private
# @param [HTTPClient] client
# HTTP client
# @yield [result, err] Result or error if block supplied
# @return [Object]
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required
def execute_once(client, &block)
case @state
when :start
response = send_start_command(client)
result = process_response(response.status_code, response.header, response.body)
when :active
response = send_query_command(client)
result = process_response(response.status_code, response.header, response.body)
when :cancelled, :final
error(@last_error, rethrow: true, &block)
end
if @state == :active
response = send_upload_command(client)
result = process_response(response.status_code, response.header, response.body)
end
success(result, &block) if @state == :final
rescue => e
# Some APIs like Youtube generate non-retriable 401 errors and mark
# the upload as finalized. Save the error just in case we get
# retried.
@last_error = e
error(e, rethrow: true, &block)
end
end
end
end
end
@@ -1,89 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
module Google
module Apis
# Base error, capable of wrapping another
class Error < StandardError
attr_reader :status_code
attr_reader :header
attr_reader :body
def initialize(err, status_code: nil, header: nil, body: nil)
@cause = nil
if err.respond_to?(:backtrace)
super(err.message)
@cause = err
else
super(err.to_s)
end
@status_code = status_code
@header = header.dup unless header.nil?
@body = body
end
def backtrace
if @cause
@cause.backtrace
else
super
end
end
def inspect
extra = ""
extra << " status_code: #{status_code.inspect}" unless status_code.nil?
extra << " header: #{header.inspect}" unless header.nil?
extra << " body: #{body.inspect}" unless body.nil?
"#<#{self.class.name}: #{message}#{extra}>"
end
end
# An error which is raised when there is an unexpected response or other
# transport error that prevents an operation from succeeding.
class TransmissionError < Error
end
# An exception that is raised if a redirect is required
#
class RedirectError < Error
end
# A 4xx class HTTP error occurred.
class ClientError < Error
end
# A 4xx class HTTP error occurred.
class RateLimitError < Error
end
# A 403 HTTP error occurred.
class ProjectNotLinkedError < Error
end
# A 401 HTTP error occurred.
class AuthorizationError < Error
end
# A 5xx class HTTP error occurred.
class ServerError < Error
end
# Error class for problems in batch requests.
class BatchError < Error
end
end
end
@@ -1,116 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
module Google
module Apis
# General options for API requests
ClientOptions = Struct.new(
:application_name,
:application_version,
:proxy_url,
:open_timeout_sec,
:read_timeout_sec,
:send_timeout_sec,
:log_http_requests,
:transparent_gzip_decompression)
RequestOptions = Struct.new(
:authorization,
:retries,
:header,
:normalize_unicode,
:skip_serialization,
:skip_deserialization,
:api_format_version,
:use_opencensus,
:quota_project,
:query)
# General client options
class ClientOptions
# @!attribute [rw] application_name
# @return [String] Name of the application, for identification in the User-Agent header
# @!attribute [rw] application_version
# @return [String] Version of the application, for identification in the User-Agent header
# @!attribute [rw] proxy_url
# @return [String] URL of a proxy server
# @!attribute [rw] log_http_requests
# @return [Boolean] True if raw HTTP requests should be logged
# @!attribute [rw] open_timeout_sec
# @return [Fixnum] How long, in seconds, before failed connections time out
# @!attribute [rw] read_timeout_sec
# @return [Fixnum] How long, in seconds, before requests time out
# @!attribute [rw] transparent_gzip_decompression
# @return [Boolean] True if gzip compression needs to be enabled
# Get the default options
# @return [Google::Apis::ClientOptions]
def self.default
@options ||= ClientOptions.new
end
end
# Request options
class RequestOptions
# @!attribute [rw] authorization
# @return [Signet::OAuth2::Client, #apply(Hash)] OAuth2 credentials.
# @!attribute [rw] retries
# @return [Fixnum] Number of times to retry requests on server error.
# @!attribute [rw] header
# @return [Hash<String,String>] Additional HTTP headers to include in requests.
# @!attribute [rw] normalize_unicode
# @return [Boolean] True if unicode strings should be normalized in path parameters.
# @!attribute [rw] skip_serialization
# @return [Boolean] True if body object should be treated as raw text instead of an object.
# @!attribute [rw] skip_deserialization
# @return [Boolean] True if response should be returned in raw form instead of deserialized.
# @!attribute [rw] api_format_version
# @return [Fixnum] Version of the error format to request/expect.
# @!attribute [rw] use_opencensus
# @return [Boolean] Whether OpenCensus spans should be generated for requests. Default is true.
# @!attribute [rw] quota_project
# @return [String] Project ID to charge quota, or `nil` to default to the credentials-specified project.
# @!attribute [rw] query
# @return [Hash<String,String>] Additional HTTP URL query parameters to include in requests.
# Get the default options
# @return [Google::Apis::RequestOptions]
def self.default
@options ||= RequestOptions.new
end
def merge(options)
return self if options.nil?
new_options = dup
members.each do |opt|
opt = opt.to_sym
new_options[opt] = options[opt] unless options[opt].nil?
end
new_options
end
end
ClientOptions.default.log_http_requests = false
ClientOptions.default.application_name = 'unknown'
ClientOptions.default.application_version = '0.0.0'
ClientOptions.default.transparent_gzip_decompression = true
RequestOptions.default.retries = 0
RequestOptions.default.normalize_unicode = false
RequestOptions.default.skip_serialization = false
RequestOptions.default.skip_deserialization = false
RequestOptions.default.api_format_version = nil
RequestOptions.default.use_opencensus = true
RequestOptions.default.quota_project = nil
end
end
@@ -18,27 +18,5 @@ module Google
module Apis
# Client library version
VERSION = "0.52.0".freeze
# Current operating system
# @private
OS_VERSION = begin
if RUBY_PLATFORM =~ /mswin|win32|mingw|bccwin|cygwin/
output, _ = Open3.capture2('ver')
output.sub(/\s*\[Version\s*/, '/').sub(']', '')
elsif RUBY_PLATFORM =~ /darwin/i
output, _ = Open3.capture2('sw_vers', '-productVersion')
"Mac OS X/#{output}"
elsif RUBY_PLATFORM == 'java'
require 'java'
name = java.lang.System.getProperty('os.name')
version = java.lang.System.getProperty('os.version')
"#{name}/#{version}"
else
output, _ = Open3.capture2('uname', '-sr')
output.sub(' ', '/')
end.strip
rescue
RUBY_PLATFORM
end
end
end
-22
View File
@@ -1,22 +0,0 @@
namespace :metrics do
task :lines do
lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
for file_name in FileList['lib/**/*.rb', 'bin/generate-api']
f = File.open(file_name)
while line = f.gets
lines += 1
next if line =~ /^\s*$/
next if line =~ /^\s*#/
codelines += 1
end
puts "L: #{sprintf('%4d', lines)}, " +
"LOC #{sprintf('%4d', codelines)} | #{file_name}"
total_lines += lines
total_codelines += codelines
lines, codelines = 0, 0
end
puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
end
end
-10
View File
@@ -1,10 +0,0 @@
require 'rubocop/rake_task'
desc 'Run RuboCop on the lib directory'
RuboCop::RakeTask.new(:rubocop) do |task|
task.patterns = ['lib/**/*.rb']
# only show the files with failures
task.formatters = ['progress']
# don't abort rake on failure
task.fail_on_error = false
end
-11
View File
@@ -1,11 +0,0 @@
require 'rake/clean'
require 'rspec/core/rake_task'
CLOBBER.include('coverage')
namespace :spec do
RSpec::Core::RakeTask.new(:all)
end
desc 'Alias to spec:all'
task 'spec' => 'spec:all'
-11
View File
@@ -1,11 +0,0 @@
begin
require 'yard'
require 'yard/rake/yardoc_task'
YARD::Rake::YardocTask.new do |t|
t.files = ['lib/**/*.rb', 'generated/**/*.rb']
t.options = ['--verbose', '--markup', 'markdown']
end
rescue LoadError
puts "YARD not available"
end
@@ -1,8 +0,0 @@
{ "access_token":"access_token_123456789",
"authorization_uri":"https://accounts.google.com/o/oauth2/auth",
"client_id":"123456789p.apps.googleusercontent.com",
"client_secret":"very_secret",
"expires_in":3600,
"refresh_token":"refresh_token_12345679",
"token_credential_uri":"https://accounts.google.com/o/oauth2/token",
"issued_at":1386053761}
Whitespace-only changes.
@@ -1 +0,0 @@
{"installed":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","client_secret":"i8YaXdGgiQ4_KrTVNGsB7QP1","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"","client_x509_cert_url":"","client_id":"898243283568.apps.googleusercontent.com","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}}
-1
View File
@@ -1 +0,0 @@
{"invalid"}
-1
View File
@@ -1 +0,0 @@
Hello world
@@ -1,41 +0,0 @@
require 'spec_helper'
require 'google/api_client/auth/installed_app'
describe Google::APIClient::InstalledAppFlow do
let(:secrets) do
file = File.join(FIXTURES_DIR, 'files', 'client_secrets.json')
Google::APIClient::ClientSecrets.load(file)
end
subject(:flow) { Google::APIClient::InstalledAppFlow.new(
:client_id => secrets.client_id,
:client_secret => secrets.client_secret,
:scope => 'https://www.googleapis.com/auth/drive.metadata.readonly',
:port => 5000)}
context 'initialization' do
it 'should be valid instance' do
expect(flow).to be_a Google::APIClient::InstalledAppFlow
end
it 'should have valid port' do
expect(flow.instance_variable_get("@port")).to eq 5000
end
it 'should have default port' do
another_flow = Google::APIClient::InstalledAppFlow.new(
:client_id => secrets.client_id,
:client_secret => secrets.client_secret,
:scope => 'https://www.googleapis.com/auth/drive.metadata.readonly')
expect(another_flow.instance_variable_get("@port")).to eq 9292
end
it 'should have valid client_id' do
auth = flow.instance_variable_get "@authorization"
expect(auth.client_id).to eq "898243283568.apps.googleusercontent.com"
end
end
end
@@ -1,128 +0,0 @@
require 'spec_helper'
require 'google/api_client/auth/storage'
describe Google::APIClient::Storage do
let(:root_path) { ROOT_DIR }
let(:json_file) { File.expand_path(File.join(FIXTURES_DIR, 'files', 'auth_stored_credentials.json')) }
let(:store) { double }
let(:client_stub) { double }
subject { Google::APIClient::Storage.new(store) }
describe 'constants' do
it "should have valid authorization and token_credential_uri" do
expect(Google::APIClient::Storage::AUTHORIZATION_URI).to eq "https://accounts.google.com/o/oauth2/auth"
expect(Google::APIClient::Storage::TOKEN_CREDENTIAL_URI).to eq "https://accounts.google.com/o/oauth2/token"
end
end
describe 'authorize' do
it 'should authorize' do
expect(subject).to respond_to(:authorization)
expect(subject.authorization).to be_nil
expect(subject.store).to be == store
end
end
describe 'authorize' do
describe 'with credentials' do
it 'should initialize a new OAuth Client' do
expect(subject).to receive(:load_credentials).and_return({:first => 'a dummy'})
expect(client_stub).to receive(:issued_at=)
expect(client_stub).to receive(:expired?).and_return(false)
expect(Signet::OAuth2::Client).to receive(:new).and_return(client_stub)
expect(subject).not_to receive(:refresh_authorization)
subject.authorize
end
it 'should refresh authorization' do
expect(subject).to receive(:load_credentials).and_return({:first => 'a dummy'})
expect(client_stub).to receive(:issued_at=)
expect(client_stub).to receive(:expired?).and_return(true)
expect(Signet::OAuth2::Client).to receive(:new).and_return(client_stub)
expect(subject).to receive(:refresh_authorization)
auth = subject.authorize
expect(auth).to be == subject.authorization
expect(auth).not_to be_nil
end
end
describe 'without credentials' do
it 'should return nil' do
expect(subject.authorization).to be_nil
expect(subject).to receive(:load_credentials).and_return({})
expect(subject.authorize).to be_nil
expect(subject.authorization).to be_nil
end
end
end
describe 'write_credentials' do
it 'should call store to write credentials' do
authorization_stub = double
expect(authorization_stub).to receive(:refresh_token).and_return(true)
expect(subject).to receive(:credentials_hash)
expect(subject.store).to receive(:write_credentials)
subject.write_credentials(authorization_stub)
expect(subject.authorization).to be == authorization_stub
end
it 'should not call store to write credentials' do
expect(subject).not_to receive(:credentials_hash)
expect(subject.store).not_to receive(:write_credentials)
expect {
subject.write_credentials()
}.not_to raise_error
end
it 'should not call store to write credentials' do
expect(subject).not_to receive(:credentials_hash)
expect(subject.store).not_to receive(:write_credentials)
expect {
subject.write_credentials('something')
}.not_to raise_error
end
end
describe 'refresh_authorization' do
it 'should call refresh and write credentials' do
expect(subject).to receive(:write_credentials)
authorization_stub = double
expect(subject).to receive(:authorization).and_return(authorization_stub)
expect(authorization_stub).to receive(:refresh!).and_return(true)
subject.refresh_authorization
end
end
describe 'load_credentials' do
it 'should call store to load credentials' do
expect(subject.store).to receive(:load_credentials)
subject.send(:load_credentials)
end
end
describe 'credentials_hash' do
it 'should return an hash' do
authorization_stub = double
expect(authorization_stub).to receive(:access_token)
expect(authorization_stub).to receive(:client_id)
expect(authorization_stub).to receive(:client_secret)
expect(authorization_stub).to receive(:expires_in)
expect(authorization_stub).to receive(:refresh_token)
expect(authorization_stub).to receive(:issued_at).and_return('100')
allow(subject).to receive(:authorization).and_return(authorization_stub)
credentials = subject.send(:credentials_hash)
expect(credentials).to include(:access_token)
expect(credentials).to include(:authorization_uri)
expect(credentials).to include(:client_id)
expect(credentials).to include(:client_secret)
expect(credentials).to include(:expires_in)
expect(credentials).to include(:refresh_token)
expect(credentials).to include(:token_credential_uri)
expect(credentials).to include(:issued_at)
end
end
end
@@ -1,48 +0,0 @@
require 'spec_helper'
require 'google/api_client/auth/storages/file_store'
describe Google::APIClient::FileStore do
let(:json_file) { File.join(FIXTURES_DIR, 'files', 'auth_stored_credentials.json') }
let(:credentials_hash) {{
"access_token"=>"my_access_token",
"authorization_uri"=>"https://accounts.google.com/o/oauth2/auth",
"client_id"=>"123456_test_client_id@.apps.googleusercontent.com",
"client_secret"=>"123456_client_secret",
"expires_in"=>3600,
"refresh_token"=>"my_refresh_token",
"token_credential_uri"=>"https://accounts.google.com/o/oauth2/token",
"issued_at"=>1384440275
}}
subject{Google::APIClient::FileStore.new('a file path')}
it 'should have a path' do
expect(subject.path).to be == 'a file path'
subject.path = 'an other file path'
expect(subject.path).to be == 'an other file path'
end
it 'should load credentials' do
subject.path = json_file
credentials = subject.load_credentials
expect(credentials).to include(
'access_token', 'authorization_uri', 'refresh_token', 'client_id',
'client_secret', 'expires_in', 'token_credential_uri', 'issued_at')
end
it 'should write credentials' do
io_stub = StringIO.new
expect(subject).to receive(:open).and_return(io_stub)
subject.write_credentials(credentials_hash)
end
it 'should not load credentials' do
file = StringIO.new
file.write "{invalid_hash}"
invalid_subject = Google::APIClient::FileStore.new(file)
expect(invalid_subject.load_credentials).to be_nil
end
end
@@ -1,68 +0,0 @@
require 'spec_helper'
require 'google/api_client/auth/storages/redis_store'
describe Google::APIClient::RedisStore do
let(:root_path) { File.expand_path(File.join(__FILE__, '..', '..', '..', '..', '..')) }
let(:json_file) { File.expand_path(File.join(root_path, 'fixtures', 'files', 'auth_stored_credentials.json')) }
let(:redis) {double}
let(:credentials_hash) { {
"access_token" => "my_access_token",
"authorization_uri" => "https://accounts.google.com/o/oauth2/auth",
"client_id" => "123456_test_client_id@.apps.googleusercontent.com",
"client_secret" => "123456_client_secret",
"expires_in" => 3600,
"refresh_token" => "my_refresh_token",
"token_credential_uri" => "https://accounts.google.com/o/oauth2/token",
"issued_at" => 1384440275
} }
subject { Google::APIClient::RedisStore.new('a redis instance') }
it 'should have a redis instance' do
expect(subject.redis).to be == 'a redis instance'
subject.redis = 'an other redis instance'
expect(subject.redis).to be == 'an other redis instance'
end
describe 'load_credentials' do
it 'should load credentials' do
subject.redis= redis
expect(redis).to receive(:get).and_return(credentials_hash.to_json)
expect(subject.load_credentials).to be == credentials_hash
end
it 'should return nil' do
subject.redis= redis
expect(redis).to receive(:get).and_return(nil)
expect(subject.load_credentials).to be_nil
end
end
describe 'redis_credentials_key' do
context 'without given key' do
it 'should return default key' do
expect(subject.redis_credentials_key).to be == "google_api_credentials"
end
end
context 'with given key' do
let(:redis_store) { Google::APIClient::RedisStore.new('a redis instance', 'another_google_api_credentials') }
it 'should use given key' do
expect(redis_store.redis_credentials_key).to be == "another_google_api_credentials"
end
end
end
describe 'write credentials' do
it 'should write credentials' do
subject.redis= redis
expect(redis).to receive(:set).and_return('ok')
expect(subject.write_credentials(credentials_hash)).to be_truthy
end
end
end
@@ -1,387 +0,0 @@
# Copyright 2013 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'spec_helper'
require 'google/api_client/client_secrets'
FIXTURES_PATH = File.expand_path('../../../fixtures', __FILE__)
RSpec.describe Google::APIClient::ClientSecrets do
describe '::new' do
let(:filename) { File.join(FIXTURES_PATH, 'files', 'client_secrets.json') }
let(:data) { File.open(filename, 'r') { |file| JSON.load(file.read) } }
context 'without options' do
subject { Google::APIClient::ClientSecrets.new(data) }
describe 'existence of instance variables' do
it '@access_token' do
expect(subject).to respond_to(:access_token)
end
it '@authorization_uri' do
expect(subject).to respond_to(:authorization_uri)
end
it '@client_id' do
expect(subject).to respond_to(:client_id)
end
it '@client_secret' do
expect(subject).to respond_to(:client_secret)
end
it '@expires_at' do
expect(subject).to respond_to(:expires_at)
end
it '@expires_in' do
expect(subject).to respond_to(:expires_in)
end
it '@id_token' do
expect(subject).to respond_to(:id_token)
end
it '@issued_at' do
expect(subject).to respond_to(:issued_at)
end
it '@javascript_origins' do
expect(subject).to respond_to(:javascript_origins)
end
it '@redirect_uris' do
expect(subject).to respond_to(:redirect_uris)
end
it '@refresh_token' do
expect(subject).to respond_to(:refresh_token)
end
it '@token_credential_uri' do
expect(subject).to respond_to(:token_credential_uri)
end
end
describe 'value of instance variables' do
it 'should set @access_token' do
expect(subject.access_token).to be_nil
end
it 'should set @authorization_uri' do
expect(subject.authorization_uri).to eq 'https://accounts.google.com/o/oauth2/auth'
end
it 'should set @client_id' do
expect(subject.client_id).to eq '898243283568.apps.googleusercontent.com'
end
it 'should set @client_secret' do
expect(subject.client_secret).to eq 'i8YaXdGgiQ4_KrTVNGsB7QP1'
end
it 'should set @expires_at' do
expect(subject.expires_at).to be_nil
end
it 'should set @expires_in' do
expect(subject.expires_in).to be_nil
end
it 'should set @flow' do
expect(subject.flow).to eq 'installed'
end
it 'should set @id_token' do
expect(subject.id_token).to be_nil
end
it 'should set @issued_at' do
expect(subject.issued_at).to be_nil
end
it 'should set @javascript_origins' do
expect(subject.javascript_origins).to eq []
end
it 'should set @redirect_uris' do
expect(subject.redirect_uris).to eq []
end
it 'should set @refresh_token' do
expect(subject.refresh_token).to be_nil
end
it 'should set @token_credential_uri' do
expect(subject.token_credential_uri).to eq 'https://accounts.google.com/o/oauth2/token'
end
end
end
context 'with options' do
context 'option keys are string' do
let(:string_options) do
{ 'samples' =>
{
'access_token' => 'sample_access_token',
'auth_uri' => 'sample_auth_uri',
'authorization_uri' => 'sample_authorization_uri',
'client_id' => 'sample_client_id',
'client_secret' => 'sample_client_secret',
'expires_at' => 'sample_expires_at',
'expires_in' => 'sample_expires_in',
'id_token' => 'sample_id_token',
'issued_at' => 'sample_issued_at',
'javascript_origin' => 'sample_javascript_origin',
'javascript_origins' => 'sample_javascript_origins',
'redirect_uris' => 'sample_redirect_uris',
'refresh_token' => 'sample_refresh_token',
'token_credential_uri' => 'sample_token_credential_uri',
'token_uri' => 'sample_token_uri' } }
end
subject { Google::APIClient::ClientSecrets.new(string_options) }
describe 'existence of instance variables' do
it '@access_token' do
expect(subject).to respond_to(:access_token)
end
it '@authorization_uri' do
expect(subject).to respond_to(:authorization_uri)
end
it '@client_id' do
expect(subject).to respond_to(:client_id)
end
it '@client_secret' do
expect(subject).to respond_to(:client_secret)
end
it '@expires_at' do
expect(subject).to respond_to(:expires_at)
end
it '@expires_in' do
expect(subject).to respond_to(:expires_in)
end
it '@id_token' do
expect(subject).to respond_to(:id_token)
end
it '@issued_at' do
expect(subject).to respond_to(:issued_at)
end
it '@javascript_origins' do
expect(subject).to respond_to(:javascript_origins)
end
it '@redirect_uris' do
expect(subject).to respond_to(:redirect_uris)
end
it '@refresh_token' do
expect(subject).to respond_to(:refresh_token)
end
it '@token_credential_uri' do
expect(subject).to respond_to(:token_credential_uri)
end
end
describe 'value of instance variables' do
it 'should set @access_token' do
expect(subject.access_token).to eq 'sample_access_token'
end
it 'should set @authorization_uri' do
expect(subject.authorization_uri).to eq 'sample_auth_uri'
end
it 'should set @client_id' do
expect(subject.client_id).to eq 'sample_client_id'
end
it 'should set @client_secret' do
expect(subject.client_secret).to eq 'sample_client_secret'
end
it 'should set @expires_at' do
expect(subject.expires_at).to eq 'sample_expires_at'
end
it 'should set @expires_in' do
expect(subject.expires_in).to eq 'sample_expires_in'
end
it 'should set @flow' do
expect(subject.flow).to eq 'samples'
end
it 'should set @id_token' do
expect(subject.id_token).to eq 'sample_id_token'
end
it 'should set @issued_at' do
expect(subject.issued_at).to eq 'sample_issued_at'
end
it 'should set @javascript_origins' do
expect(subject.javascript_origins).to eq 'sample_javascript_origins'
end
it 'should set @redirect_uris' do
expect(subject.redirect_uris).to eq 'sample_redirect_uris'
end
it 'should set @refresh_token' do
expect(subject.refresh_token).to eq 'sample_refresh_token'
end
it 'should set @token_credential_uri' do
expect(subject.token_credential_uri).to eq 'sample_token_uri'
end
end
end
context 'option keys are symbol' do
let(:symbol_options) do
{ samples:
{
access_token: 'sample_access_token',
auth_uri: 'sample_auth_uri',
authorization_uri: 'sample_authorization_uri',
client_id: 'sample_client_id',
client_secret: 'sample_client_secret',
expires_at: 'sample_expires_at',
expires_in: 'sample_expires_in',
id_token: 'sample_id_token',
issued_at: 'sample_issued_at',
javascript_origin: 'sample_javascript_origin',
javascript_origins: 'sample_javascript_origins',
redirect_uris: 'sample_redirect_uris',
refresh_token: 'sample_refresh_token',
token_credential_uri: 'sample_token_credential_uri',
token_uri: 'sample_token_uri' } }
end
subject { Google::APIClient::ClientSecrets.new(symbol_options) }
describe 'existence of instance variables' do
it '@access_token' do
expect(subject).to respond_to(:access_token)
end
it '@authorization_uri' do
expect(subject).to respond_to(:authorization_uri)
end
it '@client_id' do
expect(subject).to respond_to(:client_id)
end
it '@client_secret' do
expect(subject).to respond_to(:client_secret)
end
it '@expires_at' do
expect(subject).to respond_to(:expires_at)
end
it '@expires_in' do
expect(subject).to respond_to(:expires_in)
end
it '@id_token' do
expect(subject).to respond_to(:id_token)
end
it '@issued_at' do
expect(subject).to respond_to(:issued_at)
end
it '@javascript_origins' do
expect(subject).to respond_to(:javascript_origins)
end
it '@redirect_uris' do
expect(subject).to respond_to(:redirect_uris)
end
it '@refresh_token' do
expect(subject).to respond_to(:refresh_token)
end
it '@token_credential_uri' do
expect(subject).to respond_to(:token_credential_uri)
end
end
describe 'value of instance variables' do
it 'should set @access_token' do
expect(subject.access_token).to eq 'sample_access_token'
end
it 'should set @authorization_uri' do
expect(subject.authorization_uri).to eq 'sample_auth_uri'
end
it 'should set @client_id' do
expect(subject.client_id).to eq 'sample_client_id'
end
it 'should set @client_secret' do
expect(subject.client_secret).to eq 'sample_client_secret'
end
it 'should set @expires_at' do
expect(subject.expires_at).to eq 'sample_expires_at'
end
it 'should set @expires_in' do
expect(subject.expires_in).to eq 'sample_expires_in'
end
it 'should set @flow' do
expect(subject.flow).to eq 'samples'
end
it 'should set @id_token' do
expect(subject.id_token).to eq 'sample_id_token'
end
it 'should set @issued_at' do
expect(subject.issued_at).to eq 'sample_issued_at'
end
it 'should set @javascript_origins' do
expect(subject.javascript_origins).to eq 'sample_javascript_origins'
end
it 'should set @redirect_uris' do
expect(subject.redirect_uris).to eq 'sample_redirect_uris'
end
it 'should set @refresh_token' do
expect(subject.refresh_token).to eq 'sample_refresh_token'
end
it 'should set @token_credential_uri' do
expect(subject.token_credential_uri).to eq 'sample_token_uri'
end
end
end
end
end
context 'with JSON file' do
let(:file) { File.join(FIXTURES_PATH, 'files', 'client_secrets.json') }
subject(:secrets) { Google::APIClient::ClientSecrets.load(file)}
it 'should load the correct client ID' do
expect(secrets.client_id).to be == '898243283568.apps.googleusercontent.com'
end
it 'should load the correct client secret' do
expect(secrets.client_secret).to be == 'i8YaXdGgiQ4_KrTVNGsB7QP1'
end
context 'serialzed to hash' do
subject(:hash) { secrets.to_hash }
it 'should contain the flow as the first key' do
expect(hash).to have_key "installed"
end
it 'should contain the client ID' do
expect(hash["installed"]["client_id"]).to be == '898243283568.apps.googleusercontent.com'
end
it 'should contain the client secret' do
expect(hash["installed"]["client_secret"]).to be == 'i8YaXdGgiQ4_KrTVNGsB7QP1'
end
it 'should remove empty value' do
expect(hash["installed"]["redirect_uris"]).to be nil
end
it 'should remove nil values' do
expect(hash["installed"]["issued_at"]).to be nil
end
end
end
context 'with no existing JSON file' do
it 'should raise exception' do
file = File.join(FIXTURES_PATH, 'files', 'no_file.json')
expect { Google::APIClient::ClientSecrets.load(file) }.to raise_exception(Errno::ENOENT)
end
end
context 'with invalid JSON file' do
it 'should raise exception' do
file = File.join(FIXTURES_PATH, 'files', 'invalid.json')
expect { Google::APIClient::ClientSecrets.load(file) }.to raise_exception(JSON::ParserError)
end
end
context 'with folder name, which have json file in parents' do
it 'should load the correct client id' do
folder = File.join(FIXTURES_PATH, 'files', 'child')
secrets = Google::APIClient::ClientSecrets.load(folder)
expect(secrets.client_id).to be == '898243283568.apps.googleusercontent.com'
end
end
context 'with folder wihout client_secrets.json' do
it "should raise exception", fakefs: true do
FileUtils.mkdir("/tmp")
expect { Google::APIClient::ClientSecrets.load('/tmp') }.to raise_exception(ArgumentError)
end
end
end
@@ -1,384 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'spec_helper'
require 'google/apis/core/api_command'
require 'google/apis/core/json_representation'
RSpec.describe Google::Apis::Core::ApiCommand do
include TestHelpers
include_context 'HTTP client'
let(:model_class) do
Class.new do
attr_accessor :value
end
end
let(:representer_class) do
Class.new(Google::Apis::Core::JsonRepresentation) do
property :value
end
end
let(:x_goog_api_client_value) { "gl-ruby/#{RUBY_VERSION} gdcl/#{Google::Apis::VERSION}" }
context('with preparation') do
let(:command) do
Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
end
it 'should set X-Goog-Api-Client header if none is set' do
command.prepare!
expect(command.header['X-Goog-Api-Client']).to eql x_goog_api_client_value
end
it 'should append to x-goog-api-client header with case difference' do
command.header['x-goog-api-client'] = "foo/1.2.3"
command.prepare!
expect(command.header['X-Goog-Api-Client']).to eql "foo/1.2.3 #{x_goog_api_client_value}"
end
it 'should append to multiple x-goog-api-client headers' do
command.header['x-goog-api-client'] = "foo/1.2.3"
command.header['X-Goog-Api-Client'] = "bar/4.5.6"
command.prepare!
expect(command.header['X-Goog-Api-Client']).to eql "foo/1.2.3 bar/4.5.6 #{x_goog_api_client_value}"
expect(command.header['x-goog-api-client']).to be nil
end
it 'should override existing gl-ruby/ and gdcl/ clauses' do
command.header['x-goog-api-client'] = "gl-ruby/0.0 foo/1.2.3"
command.header['X-Goog-Api-Client'] = "bar/4.5.6 gdcl/0.0"
command.prepare!
expect(command.header['X-Goog-Api-Client']).to eql "foo/1.2.3 bar/4.5.6 #{x_goog_api_client_value}"
expect(command.header['x-goog-api-client']).to be nil
end
it 'should not set the X-Goog-User-Project header if there is no quota_project' do
command.prepare!
expect(command.header['X-Goog-User-Project']).to be_nil
end
it 'should set the X-Goog-User-Project to the credentials quota_project' do
command.options.authorization = OpenStruct.new quota_project_id: "b_project_id"
command.prepare!
expect(command.header['X-Goog-User-Project']).to eql "b_project_id"
end
it 'should set the X-Goog-User-Project to a custom quota_project in preference to credentials' do
command.options.authorization = OpenStruct.new quota_project_id: "b_project_id"
command.options.quota_project = "c_project_id"
command.prepare!
expect(command.header['X-Goog-User-Project']).to eql "c_project_id"
end
end
context('with a request body') do
let(:command) do
request = model_class.new
request.value = 'hello'
command = Google::Apis::Core::ApiCommand.new(:post, 'https://www.googleapis.com/zoo/animals')
command.request_representation = representer_class
command.request_object = request
command.query['a'] = 'b'
command
end
before(:example) do
stub_request(:post, 'https://www.googleapis.com/zoo/animals?a=b')
.to_return(headers: { 'Content-Type' => 'application/json' }, body: %({}))
end
it 'should serialize the request object' do
command.execute(client)
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals?a=b').with do |req|
be_json_eql(%({"value":"hello"})).matches?(req.body)
end).to have_been_made
end
it 'should not form encode query parameters when body expected but nil' do
command.query['a'] = 'b'
command.request_object = nil
command.execute(client)
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals?a=b').with(body: nil)).to have_been_made
end
end
context('with a raw request body') do
let(:command) do
request = model_class.new
command = Google::Apis::Core::ApiCommand.new(:post, 'https://www.googleapis.com/zoo/animals')
command.request_representation = representer_class
command.request_object = %({"value": "hello"})
command.options.skip_serialization = true
command
end
before(:example) do
stub_request(:post, 'https://www.googleapis.com/zoo/animals')
.to_return(headers: { 'Content-Type' => 'application/json' }, body: %({}))
end
it 'should allow raw JSON if skip_serialization = true' do
command.execute(client)
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals').with do |req|
be_json_eql(%({"value":"hello"})).matches?(req.body)
end).to have_been_made
end
end
context('with a JSON response') do
let(:command) do
command = Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
command.response_representation = representer_class
command.response_class = model_class
command
end
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(headers: { 'Content-Type' => 'application/json' }, body: %({"value" : "hello"}))
end
it 'should return a model instance' do
result = command.execute(client)
expect(result).to be_kind_of(model_class)
end
it 'should return a populated object' do
result = command.execute(client)
expect(result.value).to eql 'hello'
end
it 'should return a raw JSON if skip_deserialization true' do
command.options.skip_deserialization = true
result = command.execute(client)
expect(result).to eql %({"value" : "hello"})
end
end
context('with an invalid content-type response') do
let(:command) do
command = Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
command.response_representation = representer_class
command.response_class = model_class
command
end
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(headers: { 'Content-Type' => 'text/plain' }, body: %(Ignore me))
end
it 'should return nil' do
result = command.execute(client)
expect(result).to be_nil
end
end
context('with a field parameter') do
let(:command) do
command = Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
command.query['fields'] = ':items(:id, :long_name, :a_really_long_name), shouldBeLeftAlone '
command
end
before(:example) do
stub_request(:get, /.*/)
.to_return(headers: { 'Content-Type' => 'application/json' }, body: %({}))
end
it 'should normalize fields params' do
command.execute(client)
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
.with(query: { 'fields' => 'items(id, longName, aReallyLongName), shouldBeLeftAlone ' })) .to have_been_made
end
end
context('with a rate limit response') do
let(:command) do
Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
end
before(:example) do
json = <<EOF
{
"error": {
"errors": [
{
"domain": "global",
"reason": "rateLimitExceeded",
"message": "Rate limit exceeded"
}
],
"code": 403,
"message": "Rate limit exceeded"
}
}
EOF
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(status: [403, 'Rate Limit Exceeded'], headers: { 'Content-Type' => 'application/json' }, body: json)
.to_return(headers: { 'Content-Type' => 'application/json' }, body: %({}))
end
it 'should retry' do
command.execute(client)
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')).to have_been_made.times(2)
end
end
context('with a project not linked response') do
let(:command) do
Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
end
before(:example) do
json = <<EOF
{
"error": {
"errors": [
{
"domain": "global",
"reason": "projectNotLinked",
"message": "The project id used to call the Google Play Developer API has not been linked in the Google Play Developer Console."
}
],
"code": 403,
"message": "The project id used to call the Google Play Developer API has not been linked in the Google Play Developer Console."
}
}
EOF
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(status: [403, 'The project id used to call the Google Play Developer API has not been linked in the Google Play Developer Console.'], headers: {
'Content-Type' => 'application/json'
}, body: json)
.to_return(headers: { 'Content-Type' => 'application/json' }, body: %({}))
end
it 'should raise project not linked error' do
expect { command.execute(client) }.to raise_error(Google::Apis::ProjectNotLinkedError)
end
it 'should raise an error with the reason and message' do
expect { command.execute(client) }.to raise_error(
/projectNotLinked: The project id used to call the Google Play Developer API has not been linked in the Google Play Developer Console./)
end
end
context('with a client error response') do
let(:command) do
Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
end
before(:example) do
json = <<EOF
{
"error": {
"errors": [
{
"domain": "global",
"reason": "timeRangeEmpty",
"message": "The specified time range is empty."
}
],
"code": 400,
"message": "The specified time range is empty."
}
}
EOF
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(status: [400, 'Bad Request'], headers: { 'Content-Type' => 'application/json' }, body: json)
end
it 'should raise client error' do
expect { command.execute(client) }.to raise_error(Google::Apis::ClientError)
end
it 'should raise an error with the reason and message' do
expect { command.execute(client) }.to raise_error(
/timeRangeEmpty: The specified time range is empty/)
end
end
context('with an empty error body') do
let(:command) do
Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
end
before(:example) do
json = %({})
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(status: [403, 'Rate Limit Exceeded'], headers: { 'Content-Type' => 'application/json' }, body: json)
end
it 'should raise client error' do
expect { command.execute(client) }.to raise_error(Google::Apis::ClientError)
end
it 'should use the default error message' do
expect { command.execute(client) }.to raise_error(/Invalid request/)
end
end
context('with v2 error messages') do
let(:command) do
cmd = Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
cmd.options.api_format_version = 2
cmd
end
before(:example) do
json = <<EOF
{
"error": {
"code": 400,
"message": "Illegal character ':' in log name",
"status": "INVALID_ARGUMENT",
"details": [
{
"@type": "type.googleapis.com/google.logging.v2.WriteLogEntriesPartialErrors",
"logEntryErrors": {
"0": {
"code": 3,
"message": "Illegal character ':' in log name"
},
"1": {
"code": 7,
"message": "User not authorized."
}
}
}
]
}
}
EOF
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.with(headers: {'X-Goog-Api-Format-Version' => '2'})
.to_return(status: [400, 'Invalid Argument'], headers: { 'Content-Type' => 'application/json' }, body: json)
end
it 'should raise client error' do
expect { command.execute(client) }.to raise_error(Google::Apis::ClientError)
end
it 'should raise an error with the reason and message' do
expect { command.execute(client) }.to raise_error(
/INVALID_ARGUMENT: Illegal character ':' in log name/)
end
end
end
@@ -1,195 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'spec_helper'
require 'google/apis/core/batch'
require 'google/apis/core/json_representation'
RSpec.describe Google::Apis::Core::BatchCommand do
include TestHelpers
include_context 'HTTP client'
let(:command) do
command = Google::Apis::Core::BatchCommand.new(:post, 'https://www.googleapis.com/batch')
end
let(:get_command) { Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals/1') }
let(:post_with_string_command) do
command = Google::Apis::Core::HttpCommand.new(:post, 'https://www.googleapis.com/zoo/animals/2')
command.body = 'Hello world'
command.header['Content-Type'] = 'text/plain'
command
end
let(:post_with_io_command) do
command = Google::Apis::Core::HttpCommand.new(:post, 'https://www.googleapis.com/zoo/animals/3')
command.body = StringIO.new('Goodbye!')
command.header['Content-Type'] = 'text/plain'
command
end
before(:example) do
allow(SecureRandom).to receive(:uuid).and_return('ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f')
allow(Digest::SHA1).to receive(:hexdigest).and_return('123abc')
response = <<EOF
--batch123
Content-Type: application/http
Content-ID: <response-ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+0>
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Hello
--batch123
Content-Type: application/http
Content-ID: <response-ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+2>
HTTP/1.1 500 Server Error
Content-Type: text/plain; charset=UTF-8
Error!
--batch123
Content-Type: application/http
Content-ID: <response-ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+1>
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
world
--batch123--
EOF
stub_request(:post, 'https://www.googleapis.com/batch')
.to_return(headers: { 'Content-Type' => 'multipart/mixed; boundary=batch123' }, body: response)
end
it 'should send content' do
b = ->(_res, _err) {}
command.add(get_command, &b)
command.add(post_with_string_command, &b)
command.add(post_with_io_command, &b)
command.execute(client)
expected_body = <<EOF.gsub(/\n/, "\r\n")
--123abc
Content-Type: application/http
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+0>
Content-Length: 58
Content-Transfer-Encoding: binary
GET /zoo/animals/1? HTTP/1.1
Host: www.googleapis.com
--123abc
Content-Type: application/http
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+1>
Content-Length: 96
Content-Transfer-Encoding: binary
POST /zoo/animals/2? HTTP/1.1
Content-Type: text/plain
Host: www.googleapis.com
Hello world
--123abc
Content-Type: application/http
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+2>
Content-Length: 93
Content-Transfer-Encoding: binary
POST /zoo/animals/3? HTTP/1.1
Content-Type: text/plain
Host: www.googleapis.com
Goodbye!
--123abc--
EOF
expect(a_request(:post, 'https://www.googleapis.com/batch').with(body: expected_body)).to have_been_made
end
it 'should send content with authorization header when set' do
b = ->(_res, _err) {}
signet_auth = object_double(Signet::OAuth2::Client.new).tap do |a|
allow(a).to receive(:apply!) do |header|
header['Authorization'] = 'Bearer a_token_value'
end
end
command.add(get_command.tap { |c| c.options.authorization = 'oauthtoken' }, &b)
command.add(post_with_string_command.tap { |c| c.options.authorization = signet_auth }, &b)
command.add(post_with_io_command, &b)
command.execute(client)
expected_body = <<EOF.gsub(/\n/, "\r\n")
--123abc
Content-Type: application/http
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+0>
Content-Length: 92
Content-Transfer-Encoding: binary
GET /zoo/animals/1? HTTP/1.1
Authorization: Bearer oauthtoken
Host: www.googleapis.com
--123abc
Content-Type: application/http
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+1>
Content-Length: 133
Content-Transfer-Encoding: binary
POST /zoo/animals/2? HTTP/1.1
Content-Type: text/plain
Authorization: Bearer a_token_value
Host: www.googleapis.com
Hello world
--123abc
Content-Type: application/http
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+2>
Content-Length: 93
Content-Transfer-Encoding: binary
POST /zoo/animals/3? HTTP/1.1
Content-Type: text/plain
Host: www.googleapis.com
Goodbye!
--123abc--
EOF
expect(a_request(:post, 'https://www.googleapis.com/batch').with(body: expected_body)).to have_been_made
end
it 'should send decode responses' do
expect do |b|
command.add(get_command) do |res, err|
b.to_proc.call(1, res, err)
end
command.add(post_with_string_command) do |res, err|
b.to_proc.call(2, res, err)
end
command.add(post_with_io_command) do |res, err|
b.to_proc.call(3, res, err)
end
command.execute(client)
end.to yield_successive_args([1, 'Hello', nil], [3, nil, an_instance_of(Google::Apis::ServerError)], [2, 'world', nil],)
end
it 'should raise error if batch is empty' do
expect { command.execute(client) }.to raise_error(Google::Apis::BatchError)
end
end
@@ -1,76 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'spec_helper'
require 'google/apis/core/composite_io'
RSpec.describe Google::Apis::Core::CompositeIO do
shared_examples 'should act like IO' do
it 'should read from all IOs' do
expect(io.read).to eq 'Hello Cruel World'
end
it 'should respond to size' do
expect(io.size).to eq 17
end
it 'should respond to pos=' do
io.pos = 6
expect(io.read).to eq('Cruel World')
end
it 'should reject negative positions' do
expect { io.pos = -1 }.to raise_error(ArgumentError)
end
it 'should return nil if position beyond size' do
io.pos = 20
expect(io.read).to be_nil
end
it 'should be readable after rewinding' do
expect(io.read).to eq 'Hello Cruel World'
expect(io.read).to be_nil
io.rewind
expect(io.read).to eq 'Hello Cruel World'
end
end
context 'with StringIOs' do
let(:io) do
Google::Apis::Core::CompositeIO.new(
StringIO.new("Hello "),
StringIO.new("Cruel "),
StringIO.new("World"))
end
include_examples 'should act like IO'
end
context 'with Files' do
let(:io) do
files = []
dir = Dir.mktmpdir
['Hello ', 'Cruel ', 'World'].each_with_index do |text, index|
name = File.join(dir, "f#{index}")
File.open(name, 'w') { |f| f.write(text) }
files << name
end
Google::Apis::Core::CompositeIO.new(files.map { |name| File.open(name, 'r') })
end
include_examples 'should act like IO'
end
end
@@ -1,138 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'spec_helper'
require 'google/apis/core/download'
require 'google/apis/core/json_representation'
require 'tempfile'
require 'tmpdir'
RSpec.describe Google::Apis::Core::DownloadCommand do
include TestHelpers
include_context 'HTTP client'
let(:command) do
command = Google::Apis::Core::DownloadCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
command.download_dest = dest
command
end
shared_examples 'should download' do
context 'with successful response' do
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(headers: { 'Content-Type' => 'application/json' }, body: %(Hello world))
end
it 'should not include a range header' do
command.execute(client)
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
.with { |req| !req.headers.key?('Range') }
).to have_been_made
end
it 'should receive content' do
expect(received).to eql 'Hello world'
end
end
context 'with error' do
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(status: [404, 'Not Found'], body: '')
end
it 'should raise error' do
expect {received}.to raise_error(Google::Apis::ClientError)
end
end
context 'with disconnects' do
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(body: ['Hello ', Timeout::Error])
.to_return(status: [206, 'Partial content'], body: 'world')
end
it 'should receive entire content' do
expect(received).to eql('Hello world')
end
end
context 'with disconnects and no partial response' do
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(body: ['Hello ', Timeout::Error])
.to_return(body: 'Hello world')
end
it 'should receive entire content' do
expect(received).to eql('Hello world')
end
end
context 'with redirect' do
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(status: [302, 'Content moved'], headers: { 'Location' => 'https://content.googleapis.com/files/12345' }, body: %(Content moved))
stub_request(:get, 'https://content.googleapis.com/files/12345')
.to_return(headers: { 'Content-Type' => 'application/json' }, body: %(Hello world))
end
it 'should receive content' do
expect(received).to eql 'Hello world'
end
end
end
context 'with filename destination' do
let(:dest) { File.join(Dir.mktmpdir, 'test.txt') }
let(:received) do
command.execute(client)
File.read(dest)
end
include_examples 'should download'
end
context 'with default destination' do
let(:dest) { nil }
let(:received) { command.execute(client).string }
include_examples 'should download'
end
context 'with IO destination' do
let(:dest) { Tempfile.new('test') }
let(:received) do
command.execute(client)
dest.rewind
dest.read
end
include_examples 'should download'
end
context 'with #write destination' do
let(:dest) { WritableIO.new(StringIO.new) }
let(:received) { command.execute(client).io.string }
include_examples 'should download'
WritableIO = Struct.new(:io) do
def write(data)
io.write(data)
end
end
end
end
@@ -1,60 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'spec_helper'
require 'google/apis/core/hashable'
RSpec.describe Google::Apis::Core::Hashable do
let(:child_class) do
Class.new do
include Google::Apis::Core::Hashable
attr_accessor :value
end
end
let(:model_class) do
Class.new do
include Google::Apis::Core::Hashable
attr_accessor :value
attr_accessor :value2
attr_accessor :children
end
end
let(:model) do
obj = model_class.new
obj.value = 'hello'
obj.value2 = {
a: 'a'
}
child = child_class.new
child.value = 'goodbye'
obj.children = [child]
obj
end
let(:hash) { model.to_h }
it 'should serialize attributes' do
expect(hash).to include(value: 'hello')
end
it 'should serialize collections' do
expect(hash).to include(children: [{ value: 'goodbye' }])
end
it 'should serialize hashes' do
expect(hash[:value2]).to include(a: 'a')
end
end
@@ -1,508 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'spec_helper'
require 'google/apis/core/http_command'
module Google
module Apis
module CloudkmsV1
class DecryptResponse
end
end
end
end
RSpec.describe Google::Apis::Core::HttpCommand do
include TestHelpers
include_context 'HTTP client'
context('with credentials') do
let(:command) do
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
command.options.authorization = Signet::OAuth2::Client.new({
:token_credential_uri => 'https://accounts.google.com/o/oauth2/token'
})
command
end
context('with one token error') do
before(:example) do
stub_request(:post, 'https://accounts.google.com/o/oauth2/token').to_return(
{ status: [500, 'Server error'] },
{ status: [200, ''], headers: { 'Content-Type' => 'application/json' }, body: '{"access_token": "foo"}' })
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world))
end
it 'should send credentials' do
result = command.execute(client)
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
.with { |req| req.headers['Authorization'] == 'Bearer foo' }).to have_been_made
expect(result).to eql 'Hello world'
end
end
context('with two token errors') do
before(:example) do
stub_request(:post, 'https://accounts.google.com/o/oauth2/token').to_return(
{ status: [500, 'Server error'] },
{ status: [401, 'Unauthorized'] })
end
it 'should raise error' do
expect { command.execute(client) }.to raise_error(Signet::AuthorizationError)
end
end
end
context('with credentials') do
let(:command) do
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
command.options.authorization = authorization
command
end
context('that are refreshable') do
let(:authorization) do
calls = 0
auth = object_double(Signet::OAuth2::Client.new)
allow(auth).to receive(:apply!) do |header|
header['Authorization'] = sprintf('Bearer a_token_value_%d', calls)
calls += 1
end
auth
end
it 'should send credentials' do
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world))
result = command.execute(client)
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
.with { |req| req.headers['Authorization'] == 'Bearer a_token_value_0' }).to have_been_made
end
context('with authorizaton error') do
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(status: [401, 'Unauthorized'])
.to_return(body: %(Hello world))
end
it 'should refresh if auth error received' do
result = command.execute(client)
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
.with { |req| req.headers['Authorization'] == 'Bearer a_token_value_1' }).to have_been_made
end
it 'should ignore retry count' do
command.options.retries = 0
result = command.execute(client)
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
.with { |req| req.headers['Authorization'] == 'Bearer a_token_value_1' }).to have_been_made
end
end
end
context('that are bare tokens`') do
let(:authorization) { 'a_token_value' }
it 'should send credentials' do
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world))
result = command.execute(client)
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
.with { |req| expect(req.headers['Authorization']).to eql 'Bearer a_token_value' }).to have_been_made
end
it 'should send not refresh' do
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [401, 'Unauthorized'])
expect { command.execute(client) }.to raise_error(Google::Apis::AuthorizationError)
end
end
end
context('with a successful response') do
let(:command) do
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
end
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world))
end
it 'should return the response body if block not present' do
result = command.execute(client)
expect(result).to eql 'Hello world'
end
it 'should call block if present' do
expect { |b| command.execute(client, &b) }.to yield_with_args('Hello world', nil)
end
end
context('with server errors') do
let(:command) do
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
end
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(status: [500, 'Server error']).times(2)
.to_return(body: %(Hello world))
end
it 'should return the response body' do
result = command.execute(client)
expect(result).to eql 'Hello world'
end
it 'should raise error if retries exceeded' do
command.options.retries = 1
expect { command.execute(client) }.to raise_error(Google::Apis::ServerError)
end
context('with retries exceeded') do
before(:example) do
command.options.retries = 1
end
let(:err) do
begin
command.execute(client)
rescue Google::Apis::Error => e
e
end
end
it 'should raise error with HTTP status code' do
expect(err.status_code).to eq 500
end
end
context('with callbacks') do
it 'should return the response body after retries' do
expect { |b| command.execute(client, &b) }.to yield_with_args('Hello world', nil)
end
end
end
context('with unknown errors') do
let(:command) do
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
end
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_raise(HTTPClient::BadResponseError) # empty error, no res value
end
it 'should raise transmission error' do
command.options.retries = 1
err = nil
begin
command.execute(client)
rescue Google::Apis::Error => e
err = e
end
expect(err).to be_a(Google::Apis::TransmissionError)
expect(err.cause).to be_a(HTTPClient::BadResponseError)
expect(err.cause.res).to be_nil # no res value
end
end
context('with options') do
let(:command) do
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
command.options.header = { 'X-Foo' => 'bar' }
command
end
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(body: %(Hello world))
end
it 'should send user headers' do
result = command.execute(client)
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
.with { |req| req.headers['X-Foo'] == 'bar' }).to have_been_made
end
end
context('with redirects') do
let(:command) do
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
end
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(status: [302, 'Redirect'], headers: { 'Location' => 'https://zoo.googleapis.com/animals' })
stub_request(:get, 'https://zoo.googleapis.com/animals')
.to_return(body: %(Hello world))
end
it 'should return the response body' do
result = command.execute(client)
expect(result).to eql 'Hello world'
end
end
context('with too many redirects') do
let(:command) do
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
end
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(status: [302, 'Redirect'], headers: { 'Location' => 'https://www.googleapis.com/zoo/animals' }).times(6)
end
it 'should raise error if retries exceeded' do
expect { command.execute(client) }.to raise_error(Google::Apis::RedirectError)
end
end
context('with no server response') do
let(:command) do
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
end
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_timeout
end
it 'should raise transmission error' do
expect { command.execute(client) }.to raise_error(Google::Apis::TransmissionError)
end
end
context('with invalid status code') do
let(:command) do
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
end
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(status: [0, 'Wat!?'])
end
it 'should raise transmission error' do
expect { command.execute(client) }.to raise_error(Google::Apis::TransmissionError)
end
end
context('with client errors') do
let(:command) do
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
end
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
.to_return(status: [400, 'Invalid request'])
end
it 'should raise error without retry' do
command.options.retries = 1
expect { command.execute(client) }.to raise_error(Google::Apis::ClientError)
end
it 'should call block if present' do
expect { |b| command.execute(client, &b) }.to yield_with_args(nil, an_instance_of(Google::Apis::ClientError))
end
it 'should not swallow errors raised in block' do
expect { command.execute(client) { raise "Potatoes detected in tailpipe" } }.to raise_error("Potatoes detected in tailpipe")
end
end
context('with opencensus integration') do
it 'should create an opencensus span for a successful call' do
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [200, ''], body: "Hello world")
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
OpenCensus::Trace.start_request_trace do |span_context|
command.execute(client)
spans = span_context.build_contained_spans
expect(spans.size).to eql 1
span = spans.first
expect(span.name.value).to eql '/zoo/animals'
expect(span.status.code).to eql 0
attrs = span.attributes
expect(attrs['http.host'].value).to eql 'www.googleapis.com'
expect(attrs['http.method'].value).to eql 'GET'
expect(attrs['http.path'].value).to eql '/zoo/animals'
expect(attrs['http.status_code']).to eql 200
events = span.time_events
expect(events.size).to eql 2
expect(events[0].type).to eql :SENT
expect(events[0].uncompressed_size).to eql 0
expect(events[1].type).to eql :RECEIVED
expect(events[1].uncompressed_size).to eql 11
end
end
it 'should create an opencensus span for a call failure' do
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [403, ''])
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
OpenCensus::Trace.start_request_trace do |span_context|
expect { command.execute(client) }.to raise_error(Google::Apis::ClientError)
spans = span_context.build_contained_spans
expect(spans.size).to eql 1
span = spans.first
expect(span.name.value).to eql '/zoo/animals'
expect(span.status.code).to eql 7
attrs = span.attributes
expect(attrs['http.host'].value).to eql 'www.googleapis.com'
expect(attrs['http.method'].value).to eql 'GET'
expect(attrs['http.path'].value).to eql '/zoo/animals'
expect(attrs['http.status_code']).to eql 403
end
end
it 'should propagate trace context header' do
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world))
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
OpenCensus::Trace.start_request_trace do |span_context|
result = command.execute(client)
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
.with { |req| !req.headers['Traceparent'].empty? }).to have_been_made
end
end
it 'should not create an opencensus span if disabled' do
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [200, ''])
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
command.options.use_opencensus = false
OpenCensus::Trace.start_request_trace do |span_context|
command.execute(client)
spans = span_context.build_contained_spans
expect(spans.size).to eql 0
end
end
end if Google::Apis::Core::HttpCommand::OPENCENSUS_AVAILABLE
it 'should send repeated query parameters' do
stub_request(:get, 'https://www.googleapis.com/zoo/animals?a=1&a=2&a=3')
.to_return(status: [200, ''])
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
command.query['a'] = [1,2,3]
command.execute(client)
end
it 'should not remove initial query parameters' do
stub_request(:get, 'https://www.googleapis.com/zoo/animals?a=1&a=2&a=3&foo=bar')
.to_return(status: [200, ''])
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals?foo=bar')
command.query['a'] = [1,2,3]
command.execute(client)
end
it 'should send falsey query parameters' do
stub_request(:get, 'https://www.googleapis.com/zoo/animals?a=0&b=false')
.to_return(status: [200, ''])
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
command.query['a'] = 0
command.query['b'] = false
command.execute(client)
end
it 'should send DateTime query parameters' do
stub_request(:get, 'https://www.googleapis.com/zoo/animals?a=2019-06-22T13:51:37-07:00&b=2019-06-22T13:51:37-07:00&b=2019-07-23T14:54:12-07:00')
.to_return(status: [200, ''])
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
date1 = DateTime.new(2019, 6, 22, 13, 51, 37, "-0700")
date2 = DateTime.new(2019, 7, 23, 14, 54, 12, "-0700")
command.query['a'] = date1
command.query['b'] = [date1, date2]
command.execute(client)
end
it 'should form encode parameters when method is POST and no body present' do
stub_request(:post, 'https://www.googleapis.com/zoo/animals')
.with(body: 'a=1&a=2&a=3&b=hello&c=&d=0')
.to_return(status: [200, ''])
command = Google::Apis::Core::HttpCommand.new(:post, 'https://www.googleapis.com/zoo/animals')
command.query['a'] = [1,2,3]
command.query['b'] = 'hello'
command.query['c'] = nil
command.query['d'] = 0
command.execute(client)
end
it 'should prepend user query parameters from options and not remove initial query parameters', :focus do
stub_request(:get, 'https://www.googleapis.com/zoo/animals?a=1&a=2&a=3&b=false&foo=bar')
.to_return(status: [200, ''])
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals?foo=bar')
command.options.query = {
'a' => [1,2,3],
'b' => false
}
command.execute(client)
end
it 'should raise transmission error instead of socket error' do
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_raise(SocketError)
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
command.options.retries = 0
expect { command.execute(client) }.to raise_error(Google::Apis::TransmissionError)
end
it 'should raise rate limit error for 429 status codes' do
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [429, ''])
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
command.options.retries = 0
expect { command.execute(client) }.to raise_error(Google::Apis::RateLimitError)
end
it 'should not normalize unicode values by default' do
stub_request(:get, 'https://www.googleapis.com/Cafe%CC%81').to_return(status: [200, ''])
template = Addressable::Template.new('https://www.googleapis.com/{path}')
command = Google::Apis::Core::HttpCommand.new(:get, template)
command.params[:path] = "Cafe\u0301"
command.options.retries = 0
command.execute(client)
end
it 'should normalize unicode values when requested' do
stub_request(:get, 'https://www.googleapis.com/Caf%C3%A9').to_return(status: [200, ''])
template = Addressable::Template.new('https://www.googleapis.com/{path}')
command = Google::Apis::Core::HttpCommand.new(:get, template)
command.params[:path] = "Cafe\u0301"
command.options.retries = 0
command.options.normalize_unicode = true
command.execute(client)
end
describe "#safe_object_representation" do
let(:command) do
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
end
it "should show fields in a normal object" do
obj = Object.new
obj.instance_variable_set(:@foobar, "hi")
str = command.send(:safe_object_representation, obj)
expect(str).to match /@foobar/
end
it "should not show fields in a restricted object" do
obj = Google::Apis::CloudkmsV1::DecryptResponse.new
obj.instance_variable_set(:@foobar, "hi")
str = command.send(:safe_object_representation, obj)
expect(str).not_to match /@foobar/
end
end
end
@@ -1,265 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'spec_helper'
require 'google/apis/core/json_representation'
RSpec.describe Google::Apis::Core::JsonRepresentation do
let(:child_class) do
Class.new do
attr_accessor :value
end
end
let(:model_class) do
rep_class = representer_class
Class.new do
attr_accessor :unset_value
attr_accessor :nil_value
attr_accessor :numeric_value
attr_accessor :string_value
attr_accessor :boolean_value_true
attr_accessor :boolean_value_false
attr_accessor :datetime_value
attr_accessor :nil_datetime_value
attr_accessor :date_value
attr_accessor :nil_date_value
attr_accessor :bytes_value
attr_accessor :big_value
attr_accessor :items
attr_accessor :child
attr_accessor :children
const_set(:Representation, rep_class)
include Google::Apis::Core::JsonObjectSupport
end
end
let(:representer_class) do
klass = child_class
Class.new(Google::Apis::Core::JsonRepresentation) do
property :unset_value, as: 'unsetValue'
property :nil_value, as: 'nilValue'
property :numeric_value, as: 'numericValue'
property :string_value, as: 'stringValue'
property :boolean_value_true, as: 'booleanValueTrue'
property :boolean_value_false, as: 'booleanValueFalse'
property :datetime_value, as: 'dateTimeValue', type: DateTime
property :nil_datetime_value, as: 'nullDateTimeValue', type: DateTime
property :date_value, as: 'dateValue', type: Date
property :nil_date_value, as: 'nullDateValue', type: Date
property :bytes_value, as: 'bytesValue', base64: true
property :big_value, as: 'bigValue', numeric_string: true
property :items
property :child, class: klass do
property :value
end
collection :children, class: klass do
property :value
end
end
end
shared_examples 'it serializes' do
it 'does not serialize unset values' do
expect(json).not_to have_json_path('unsetValue')
end
it 'serializes explicit nil values' do
expect(json).to be_json_eql(%(null)).at_path('nilValue')
end
it 'serializes numeric values' do
expect(json).to be_json_eql(%(123)).at_path('numericValue')
end
it 'serializes string values' do
expect(json).to be_json_eql(%("test")).at_path('stringValue')
end
it 'serializes boolean values' do
expect(json).to be_json_eql(%(true)).at_path('booleanValueTrue')
expect(json).to be_json_eql(%(false)).at_path('booleanValueFalse')
end
it 'serializes datetime values' do
expect(json).to be_json_eql(%("2015-05-01T12:00:00.000+00:00")).at_path('dateTimeValue')
end
it 'allows nil datetime values' do
expect(json).to be_json_eql(%(null)).at_path('nullDateTimeValue')
end
it 'serializes date values' do
expect(json).to be_json_eql(%("2015-05-01")).at_path('dateValue')
end
it 'allows nil date values' do
expect(json).to be_json_eql(%(null)).at_path('nullDateValue')
end
it 'serializes byte values to base64' do
expect(json).to be_json_eql(%("SGVsbG8gd29ybGQ=")).at_path('bytesValue')
end
it 'serializes basic collections' do
expect(json).to be_json_eql(%([1,2,3])).at_path('items')
end
it 'serializes nested objects' do
expect(json).to be_json_eql(%({"value" : "child"})).at_path('child')
end
it 'serializes object collections' do
expect(json).to be_json_eql(%([{"value" : "child"}])).at_path('children')
end
it 'serializes numeric strings' do
expect(json).to be_json_eql(%("1208925819614629174706176")).at_path('bigValue')
end
end
context 'with model object' do
let(:json) { representer_class.new(model).to_json(user_options: { skip_undefined: true }) }
let(:model) do
model = model_class.new
model.nil_value = nil
model.numeric_value = 123
model.string_value = 'test'
model.datetime_value = DateTime.new(2015, 5, 1, 12)
model.nil_datetime_value = nil
model.date_value = Date.new(2015, 5, 1)
model.nil_date_value = nil
model.boolean_value_true = true
model.boolean_value_false = false
model.bytes_value = 'Hello world'
model.items = [1, 2, 3]
model.child = child_class.new
model.child.value = 'child'
model.children = [model.child]
model.big_value = 1208925819614629174706176
model
end
include_examples 'it serializes'
it 'supports standard serialization from JSON gem' do
str = JSON.generate(model)
expect(str).to start_with("{\"")
end
it 'supports pretty serialization from JSON gem' do
str = JSON.pretty_generate(model)
expect(str).to start_with("{\n ")
end
end
context 'with hash' do
let(:json) { representer_class.new(model).to_json(user_options: { skip_undefined: true }) }
let(:model) do
{
nil_value: nil,
string_value: 'test',
numeric_value: 123,
datetime_value: DateTime.new(2015, 5, 1, 12),
nil_datetime_value: nil,
date_value: Date.new(2015, 5, 1),
nil_date_value: nil,
boolean_value_true: true,
boolean_value_false: false,
bytes_value: 'Hello world',
big_value: 1208925819614629174706176,
items: [1, 2, 3],
child: {
value: 'child'
},
children: [{ value: 'child' }]
}
end
include_examples 'it serializes'
end
context 'when de-serializing' do
let(:model) { representer_class.new(model_class.new).from_json(json) }
let(:json) do
json = <<EOF
{ "stringValue": "test",
"nilValue": null,
"booleanValueTrue": true,
"booleanValueFalse": false,
"numericValue": 123,
"dateTimeValue": "2015-05-01T12:00:00+00:00",
"nullDateTimeValue": null,
"dateValue": "2015-05-01",
"nullDateValue": null,
"bytesValue": "SGVsbG8gd29ybGQ=",
"bigValue": "1208925819614629174706176",
"items": [1,2,3],
"child": {"value" : "hello"},
"children": [{"value" : "hello"}]
}
EOF
end
it 'deserializes string values' do
expect(model.string_value).to eql 'test'
end
it 'deserializes null values' do
expect(model.nil_value).to be_nil
end
it 'deserializes numeric values' do
expect(model.numeric_value).to eql 123
end
it 'deserializes boolean values' do
expect(model.boolean_value_true).to be_truthy
expect(model.boolean_value_false).to be_falsey
end
it 'deserializes datetime values' do
expect(model.datetime_value).to eql DateTime.new(2015, 5, 1, 12)
end
it 'deserializes null datetime values' do
expect(model.nil_datetime_value).to be_nil
end
it 'deserializes date values' do
expect(model.date_value).to eql Date.new(2015, 5, 1)
end
it 'deserializes null date values' do
expect(model.nil_date_value).to be_nil
end
it 'deserializes basic collections' do
expect(model.items).to contain_exactly(1, 2, 3)
end
it 'deserializes nested objects' do
expect(model.child.value).to eql 'hello'
end
it 'serializes object collections' do
expect(model.children[0].value).to eql 'hello'
end
it 'deserializes numeric strings' do
expect(model.big_value).to eql 1208925819614629174706176
end
end
end
@@ -1,428 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'spec_helper'
require 'google/apis/options'
require 'google/apis/core/base_service'
require 'google/apis/core/json_representation'
RSpec.describe Google::Apis::Core::BaseService do
include TestHelpers
let(:service) { Google::Apis::Core::BaseService.new('https://www.googleapis.com/', '') }
let(:service_with_base_path) { Google::Apis::Core::BaseService.new('https://www.googleapis.com/', 'my_service/v1/') }
let(:x_goog_api_client_value) { "gl-ruby/#{RUBY_VERSION} gdcl/#{Google::Apis::VERSION}" }
before do
Google::Apis::ClientOptions.default.application_name = 'test'
Google::Apis::ClientOptions.default.application_version = '1.0'
end
it 'should inherit default options' do
expect(service.client_options.application_name).to eql 'test'
end
it 'should include application in user agent' do
agent = service.send(:user_agent)
expect(agent).to match /^test\/1.0/
end
it 'should include os version in user agent' do
agent = service.send(:user_agent)
expect(agent).to match Regexp.new(Regexp.escape(Google::Apis::OS_VERSION))
end
it 'should inherit authorization' do
Google::Apis::RequestOptions.default.authorization = 'a token'
expect(service.authorization).to eql 'a token'
end
it 'should allow overiding authorization' do
Google::Apis::RequestOptions.default.authorization = 'a token'
service.authorization = 'another token'
expect(service.authorization).to eql 'another token'
end
shared_examples 'with options' do
it 'should inherit service options' do
service.request_options.retries = 2
expect(command.options.retries).to eql 2
end
it 'should include per-request options' do
expect(command.options.authorization).to eql 'foo'
end
it 'should allow per-request options to override service options' do
service.request_options.authorization = 'bar'
expect(command.options.authorization).to eql 'foo'
end
end
context 'when making raw http requests' do
context 'with :get methods' do
before(:example) do
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: 'hello world')
end
it 'should return body for get' do
response = service.http(:get, 'https://www.googleapis.com/zoo/animals')
expect(response).to eq 'hello world'
end
it 'should return allow downloads' do
response = service.http(:get, 'https://www.googleapis.com/zoo/animals', download_dest: StringIO.new)
expect(response.string).to eq 'hello world'
end
end
context 'with :post methods' do
before(:example) do
stub_request(:post, 'https://www.googleapis.com/zoo/animals').to_return(body: '')
end
it 'should post body' do
service.http(:post, 'https://www.googleapis.com/zoo/animals', body: 'hello')
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
.with(body: 'hello')).to have_been_made
end
end
end
context 'with proxy' do
after(:example) do
Google::Apis::ClientOptions.default.proxy_url = nil
end
it 'should allow proxy URLs as strings' do
Google::Apis::ClientOptions.default.proxy_url = 'http://gateway.example.com:1234'
service.client
end
end
context 'when making simple commands' do
let(:command) { service.send(:make_simple_command, :get, 'zoo/animals', authorization: 'foo') }
it 'should return the correct command type' do
expect(command).to be_an_instance_of(Google::Apis::Core::ApiCommand)
end
it 'should build a correct URL' do
url = command.url.expand({}).to_s
expect(url).to eql 'https://www.googleapis.com/zoo/animals'
end
it 'should send the command with x-goog-api-client header' do
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: '')
service.send(:execute_or_queue_command, command)
expected_headers = {'X-Goog-Api-Client': x_goog_api_client_value}
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals').with(headers: expected_headers)).to have_been_made
end
include_examples 'with options'
end
context 'when making simple commands with a base path' do
it 'should return the correct URL for a relative path' do
command = service_with_base_path.send(:make_simple_command, :get, 'zoo/animals', authorization: 'foo')
url = command.url.expand({}).to_s
expect(url).to eql 'https://www.googleapis.com/my_service/v1/zoo/animals'
end
it 'should return the correct URL for an absolute path' do
command = service_with_base_path.send(:make_simple_command, :get, '/zoo/animals', authorization: 'foo')
url = command.url.expand({}).to_s
expect(url).to eql 'https://www.googleapis.com/zoo/animals'
end
end
context 'when making download commands' do
let(:command) { service.send(:make_download_command, :get, 'zoo/animals', authorization: 'foo') }
it 'should return the correct command type' do
expect(command).to be_an_instance_of(Google::Apis::Core::DownloadCommand)
end
it 'should build a correct URL' do
url = command.url.expand({}).to_s
expect(url).to eql 'https://www.googleapis.com/zoo/animals'
end
it 'should include alt=media in params' do
expect(command.query).to include('alt' => 'media')
end
include_examples 'with options'
end
context 'when making upload commands' do
let(:command) { service.send(:make_upload_command, :post, 'zoo/animals', authorization: 'foo') }
it 'should return the correct command type' do
expect(command).to be_an_instance_of(Google::Apis::Core::ResumableUploadCommand)
end
it 'should build a correct URL' do
url = command.url.expand({}).to_s
expect(url).to eql 'https://www.googleapis.com/upload/zoo/animals'
end
include_examples 'with options'
end
context 'with batch' do
before(:example) do
response = <<EOF.gsub(/\n/, "\r\n")
--batch123
Content-Type: application/http
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Hello
--batch123--
EOF
stub_request(:post, 'https://www.googleapis.com/batch')
.to_return(headers: { 'Content-Type' => 'multipart/mixed; boundary=batch123' }, body: response)
end
it 'should add commands to a batch' do
expect do |b|
service.batch do |service|
command = service.send(:make_simple_command, :get, 'zoo/animals', {})
service.send(:execute_or_queue_command, command, &b)
end
end.to yield_with_args('Hello', nil)
end
it 'should disallow uploads in batch' do
expect do |b|
service.batch do |service|
command = service.send(:make_upload_command, :post, 'zoo/animals', {})
service.send(:execute_or_queue_command, command, &b)
end
end.to raise_error(Google::Apis::ClientError)
end
it 'should disallow downloads in batch' do
expect do |b|
service.batch do |service|
command = service.send(:make_download_command, :get, 'zoo/animals', {})
service.send(:execute_or_queue_command, command, &b)
end
end.to raise_error(Google::Apis::ClientError)
end
it 'should prevent mixing services in batch' do
expect do |b|
service.batch do |service|
command = service.send(:make_simple_command, :get, 'zoo/animals', {})
service.send(:execute_or_queue_command, command, &b)
service2 = service.dup
command2 = service.send(:make_simple_command, :get, 'zoo/animals', {})
service2.send(:execute_or_queue_command, command2, &b)
end
end.to raise_error
end
end
context 'with batch uploads' do
before(:example) do
allow(SecureRandom).to receive(:uuid).and_return('b1981e17-f622-49af-b2eb-203308b1b17d')
allow(Digest::SHA1).to receive(:hexdigest).and_return('outer', 'inner')
response = <<EOF.gsub(/\n/, "\r\n")
--batch123
Content-Type: application/http
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Hello
--batch123--
EOF
stub_request(:put, 'https://www.googleapis.com/upload/')
.to_return(headers: { 'Content-Type' => 'multipart/mixed; boundary=batch123' }, body: response)
end
it 'should add upload to a batch' do
expect do |b|
service.batch_upload do |service|
command = service.send(:make_upload_command, :post, 'zoo/animals', {})
command.upload_source = StringIO.new('test')
command.upload_content_type = 'text/plain'
service.send(:execute_or_queue_command, command, &b)
end
end.to yield_with_args('Hello', nil)
end
it 'should use multipart upload' do
expect do |b|
service.batch_upload do |service|
command = service.send(:make_upload_command, :post, 'zoo/animals', {})
command.upload_source = StringIO.new('test')
command.upload_content_type = 'text/plain'
expect(command).to be_an_instance_of(Google::Apis::Core::MultipartUploadCommand)
service.send(:execute_or_queue_command, command, &b)
end
end.to yield_with_args('Hello', nil)
end
it 'should send nested multipart' do
Google::Apis::RequestOptions.default.authorization = 'a token'
service.batch_upload do |service|
command = service.send(:make_upload_command, :post, 'zoo/animals', {})
command.upload_source = StringIO.new('test')
command.upload_content_type = 'text/plain'
service.send(:execute_or_queue_command, command)
end
expected_body = <<EOF.gsub(/\n/, "\r\n")
--outer
Content-Type: application/http
Content-Id: <b1981e17-f622-49af-b2eb-203308b1b17d\\+0>
Content-Length: \\d+
Content-Transfer-Encoding: binary
POST /upload/zoo/animals\\? HTTP/1\\.1
X-Goog-Api-Client: #{Regexp.escape(x_goog_api_client_value)}
Content-Type: multipart/related; boundary=inner
X-Goog-Upload-Protocol: multipart
Authorization: Bearer a token
Host: www\\.googleapis\\.com
--inner
Content-Type: application/json
--inner
Content-Type: text/plain
Content-Length: 4
Content-Transfer-Encoding: binary
test
--inner--
--outer--
EOF
expect(a_request(:put, 'https://www.googleapis.com/upload/').with(body: Regexp.new(expected_body))).to have_been_made
end
it 'should disallow downloads in batch' do
expect do |b|
service.batch_upload do |service|
command = service.send(:make_download_command, :get, 'zoo/animals', {})
service.send(:execute_or_queue_command, command, &b)
end
end.to raise_error(Google::Apis::ClientError)
end
it 'should disallow simple commands in batch' do
expect do |b|
service.batch_upload do |service|
command = service.send(:make_simple_command, :get, 'zoo/animals', {})
service.send(:execute_or_queue_command, command, &b)
end
end.to raise_error(Google::Apis::ClientError)
end
context 'with fetch_all' do
let(:responses) do
data = {}
data[nil] = OpenStruct.new(
next_page_token: 'p1', alt_page_token: 'p2', items: ['a', 'b', 'c'], alt_items: [1, 2, 3], singular: 'foo', hash_: { 'foo' => 1, 'bar' => 2 })
data['p1'] = OpenStruct.new(
next_page_token: 'p2', items: ['d', 'e', 'f'], alt_items: [4, 5, 6], singular: 'bar', hash_: nil)
data['p2'] = OpenStruct.new(
next_page_token: nil, alt_page_token: nil, items: ['g', 'h', 'i'], alt_items: [7, 8, 9], singular: 'baz', hash_: { 'baz' => 3 })
data
end
let(:items) { service.fetch_all { |token| responses[token] } }
it 'should fetch pages until next page token is nil' do
expect(items).to contain_exactly('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i')
end
it 'should stop on repeated page token' do
responses['p2'].next_page_token = 'p2'
expect(items).to contain_exactly('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i')
end
it 'should allow selecting another field for response page token' do
expect(service.fetch_all(response_page_token: :alt_page_token) { |token| responses[token] }
).to contain_exactly('a', 'b', 'c', 'g', 'h', 'i')
end
it 'should ignore nil collections' do
responses['p1'].items = nil
expect(items).to contain_exactly('a', 'b', 'c', 'g', 'h', 'i')
end
it 'should allow selecting another field for items' do
expect(service.fetch_all(items: :alt_items) { |token| responses[token] } ).to contain_exactly(1, 2, 3, 4, 5, 6, 7, 8, 9)
end
it 'should allow iterating over singular items' do
expect(service.fetch_all(items: :singular) { |token| responses[token] } ).to contain_exactly('foo', 'bar', 'baz')
end
it 'should collate hash entries' do
expect(service.fetch_all(items: :hash_) { |token| responses[token] } ).to contain_exactly(['foo', 1], ['bar', 2], ['baz', 3])
end
it 'should allow limiting the number of items to fetch' do
expect(service.fetch_all(max: 5) { |token| responses[token] } ).to contain_exactly('a', 'b', 'c', 'd', 'e')
end
it 'should yield the next token' do
expect do |b|
service.fetch_all do |token|
b.to_proc.call(token)
responses[token]
end.count
end.to yield_successive_args(nil, 'p1', 'p2')
end
it 'should cache results' do
count = 0
items = service.fetch_all do |token|
count = count + 1
responses[token]
end
items.each{ |i| puts i }
items.each{ |i| puts i }
expect(count).to eq 3
end
it 'should allow disabling caching' do
count = 0
items = service.fetch_all(cache: false) do |token|
count = count + 1
responses[token]
end
items.each{ |i| puts i }
items.each{ |i| puts i }
expect(count).to eq 6
end
end
end
end
@@ -1,247 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'spec_helper'
require 'google/apis/core/upload'
require 'google/apis/core/json_representation'
RSpec.describe Google::Apis::Core::RawUploadCommand do
include TestHelpers
include_context 'HTTP client'
let(:command) do
command = Google::Apis::Core::RawUploadCommand.new(:post, 'https://www.googleapis.com/zoo/animals')
command.upload_source = file
command.upload_content_type = 'text/plain'
command
end
shared_examples 'should upload' do
before(:example) do
stub_request(:post, 'https://www.googleapis.com/zoo/animals').to_return(body: '')
end
it 'should send content' do
command.execute(client)
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
.with(body: "Hello world")).to have_been_made
end
it 'should send upload protocol' do
command.execute(client)
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
.with { |req| req.headers['X-Goog-Upload-Protocol'] == 'raw' }).to have_been_made
end
it 'should send content-type header' do
command.execute(client)
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
.with { |req| req.headers['X-Goog-Upload-Header-Content-Type'] == 'text/plain' }).to have_been_made
end
end
context('with StringIO input') do
let(:file) { StringIO.new("Hello world") }
include_examples 'should upload'
end
context('with IO input') do
let(:file) { File.open(File.join(FIXTURES_DIR, 'files', 'test.txt'), 'r') }
include_examples 'should upload'
it 'should not close stream' do
expect(file.closed?).to be false
end
end
context('with Tempfile input') do
let(:file) do
temp_file = Tempfile.new("tempfile")
temp_file.write("Hello world")
temp_file.rewind
temp_file
end
include_examples 'should upload'
it 'should not close stream' do
expect(file.closed?).to be false
end
end
context('with file path input') do
let(:file) { File.join(FIXTURES_DIR, 'files', 'test.txt') }
include_examples 'should upload'
end
context('with invalid input') do
let(:file) { -> {} }
it 'should raise client error' do
expect { command.execute(client) }.to raise_error(Google::Apis::ClientError)
end
end
end
RSpec.describe Google::Apis::Core::MultipartUploadCommand do
include TestHelpers
include_context 'HTTP client'
let(:command) do
command = Google::Apis::Core::MultipartUploadCommand.new(:post, 'https://www.googleapis.com/zoo/animals')
command.upload_source = StringIO.new('Hello world')
command.upload_content_type = 'text/plain'
command.body = 'metadata'
command
end
before(:example) do
stub_request(:post, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world))
allow(Digest::SHA1).to receive(:hexdigest).and_return('123abc')
end
it 'should send content' do
expected_body = <<EOF.gsub(/\n/, "\r\n")
--123abc
Content-Type: application/json
metadata
--123abc
Content-Type: text/plain
Content-Length: 11
Content-Transfer-Encoding: binary
Hello world
--123abc--
EOF
command.execute(client)
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
.with(body: expected_body)).to have_been_made
end
it 'should send upload protocol' do
command.execute(client)
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
.with { |req| req.headers['X-Goog-Upload-Protocol'] == 'multipart' }).to have_been_made
end
end
RSpec.describe Google::Apis::Core::ResumableUploadCommand do
include TestHelpers
include_context 'HTTP client'
let(:command) do
command = Google::Apis::Core::ResumableUploadCommand.new(:post, 'https://www.googleapis.com/zoo/animals')
command.upload_source = StringIO.new('Hello world')
command.upload_content_type = 'text/plain'
command
end
context 'with uninterrupted upload' do
before(:example) do
stub_request(:post, 'https://www.googleapis.com/zoo/animals')
.to_return(headers: { 'X-Goog-Upload-Status' => 'active', 'X-Goog-Upload-URL' => 'https://www.googleapis.com/zoo/animals' })
.to_return(headers: { 'X-Goog-Upload-Status' => 'final' }, body: %(OK))
end
it 'should send upload protocol' do
command.execute(client)
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
.with { |req| req.headers['X-Goog-Upload-Protocol'] == 'resumable' }).to have_been_made
end
it 'should send start command' do
command.execute(client)
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
.with { |req| req.headers['X-Goog-Upload-Command'] == 'start' }).to have_been_made
end
it 'should send upload command' do
command.execute(client)
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
.with { |req| req.headers['X-Goog-Upload-Command'].include?('upload') }).to have_been_made
end
it 'should send upload content' do
command.execute(client)
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
.with(body: 'Hello world')).to have_been_made
end
it 'should generate a proper opencensus span' do
OpenCensus::Trace.start_request_trace do |span_context|
command.execute(client)
spans = span_context.build_contained_spans
expect(spans.size).to eql 1
span = spans.first
expect(span.name.value).to eql '/zoo/animals'
end
end
end
context 'with retriable error on start' do
before(:example) do
stub_request(:post, 'https://www.googleapis.com/zoo/animals')
.to_timeout
.to_return(headers: { 'X-Goog-Upload-Status' => 'active', 'X-Goog-Upload-URL' => 'https://www.googleapis.com/zoo/animals' })
.to_return(headers: { 'X-Goog-Upload-Status' => 'active', 'X-Goog-Upload-Size-Received' => '6' })
.to_return(headers: { 'X-Goog-Upload-Status' => 'final' }, body: %(OK))
end
it 'should retry start command and continue' do
command.execute(client)
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
.with(body: 'Hello world')).to have_been_made
end
end
context 'with non-retriable authorization error on start' do
before(:example) do
stub_request(:post, 'https://www.googleapis.com/zoo/animals')
.to_return(status: [401, 'unauthorized'], headers: { 'X-Goog-Upload-Status' => 'final' }, body: %(unauthorized))
end
it 'should propagate the original error' do
expect { command.execute(client) }.to raise_error Google::Apis::AuthorizationError
end
end
context 'with interruption' do
before(:example) do
stub_request(:post, 'https://www.googleapis.com/zoo/animals')
.to_return(headers: { 'X-Goog-Upload-Status' => 'active', 'X-Goog-Upload-URL' => 'https://www.googleapis.com/zoo/animals' })
.to_return(status: [500, 'Server error'])
.to_return(headers: { 'X-Goog-Upload-Status' => 'active', 'X-Goog-Upload-Size-Received' => '6' })
.to_return(headers: { 'X-Goog-Upload-Status' => 'final' }, body: %(OK))
end
it 'should send remaining upload content after failure' do
command.execute(client)
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
.with(body: 'world')).to have_been_made
end
end
context 'with cancelled upload' do
before(:example) do
stub_request(:post, 'https://www.googleapis.com/zoo/animals')
.to_return(headers: { 'X-Goog-Upload-Status' => 'active', 'X-Goog-Upload-URL' => 'https://www.googleapis.com/zoo/animals' })
.to_return(status: [500, 'Server error'])
.to_return(headers: { 'X-Goog-Upload-Status' => 'cancelled' })
end
it 'should raise error' do
expect { command.execute(client) }.to raise_error Google::Apis::ClientError
end
end
end
@@ -1,93 +0,0 @@
# Copyright 2016 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'spec_helper'
RSpec.describe Google::Apis::Error do
describe 'constructor' do
subject { described_class.new(error) }
describe 'message' do
context 'first arg is a string which does not respond to :backtrace' do
let(:error) { 'an error occurred' }
it 'creates a instance of Error with message' do
expect(subject.message).to eq 'an error occurred'
end
end
context 'first arg responds to :backtrace' do
let(:error) { double }
it 'creates a instance of Error with message' do
allow(error).to receive(:respond_to?).with(:backtrace) { true }
allow(error).to receive(:message) { 'error message' }
expect(subject.message).to eq 'error message'
end
end
end
describe 'instance variables @body, @header, and @status_code' do
let(:error) { 'an error occurred' }
context 'params :body, :header, and :status_code are not given' do
it 'instance variables @body, @header, and @status_code will be nil' do
expect(subject.body).to be_nil
expect(subject.header).to be_nil
expect(subject.status_code).to be_nil
end
end
context 'params :body, :header, and :status_code are given' do
subject do
described_class.new(
error,
body: 'this is body',
header: 'this is header',
status_code: 200)
end
it 'instance variables @body, @header, and @status_code will not be nil' do
expect(subject.body).to eq 'this is body'
expect(subject.header).to eq 'this is header'
expect(subject.status_code).to eq 200
end
end
end
end
describe '#backtrace' do
subject { described_class.new('error') }
let(:cause) { instance_double('Error', backtrace: 'this is backtrace') }
context '@cause is truthy' do
it 'calls @cause.backtrace' do
subject.instance_variable_set(:@cause, cause)
expect(subject.backtrace).to eq 'this is backtrace'
end
end
context '@cause is falsy' do
before do
expect_any_instance_of(subject.class.superclass).to receive(:backtrace).and_return(
"super class's #backtrace called")
end
it "calls super class's #backtrace" do
subject.instance_variable_set(:@cause, false)
expect(subject.backtrace).to eq "super class's #backtrace called"
end
end
end
end
@@ -1,100 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'spec_helper'
require 'google/apis'
require 'google/apis/core/logging'
RSpec.describe Google::Apis do
it 'should have a default logger' do
expect(Google::Apis.logger).to be_an_instance_of(Logger)
end
context 'with service' do
let(:service) do
Class.new do
include Google::Apis::Core::Logging
end.new
end
it 'should have a logger' do
expect(service.logger).to be_an_instance_of(Logger)
end
it 'should use the default logger' do
expect(service.logger).to be Google::Apis.logger
end
it 'should allow custom loggers' do
Google::Apis.logger = Logger.new(STDERR)
expect(service.logger).to be Google::Apis.logger
end
end
context 'with Rails' do
before(:example) do
Google::Apis.logger = nil
Kernel.const_set('Rails', Module.new) unless defined?(::Rails)
end
let(:logger) { Logger.new(STDERR) }
let(:service) do
Class.new do
include Google::Apis::Core::Logging
end.new
end
context 'with logger present' do
before(:example) do
allow(::Rails).to receive(:logger).and_return(logger)
end
it 'should use the Rails logger' do
expect(service.logger).to be Rails.logger
end
end
context 'with ENV bypass' do
before(:example) do
allow(::Rails).to receive(:logger).and_return(logger)
allow(::ENV).to receive(:fetch).and_return('false')
end
it 'should use own logger' do
expect(service.logger).not_to be Rails.logger
end
it 'should have a logger' do
expect(service.logger).to be_an_instance_of(Logger)
end
end
context 'with logger not present' do
before(:example) do
allow(::Rails).to receive(:logger).and_return(nil)
end
it 'should use own logger' do
expect(service.logger).not_to be Rails.logger
end
it 'should have a logger' do
expect(service.logger).to be_an_instance_of(Logger)
end
end
end
end
@@ -1,40 +0,0 @@
# Copyright 2015 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'spec_helper'
require 'google/apis/options'
RSpec.describe Google::Apis::RequestOptions do
let(:options) { Google::Apis::RequestOptions.new }
it 'should not merge nil values' do
options.retries = 1
expect(options.merge(authorization: 'foo').retries).to eql 1
end
it 'should merge non-nil values' do
options.retries = 1
expect(options.merge(authorization: 'foo').authorization).to eql 'foo'
end
it 'should merge from options' do
opts = Google::Apis::RequestOptions.new
opts.authorization = 'foo'
expect(options.merge(opts).authorization).to eql 'foo'
end
it 'should allow nil in merge' do
expect(options.merge(nil)).to be_an_instance_of(Google::Apis::RequestOptions)
end
end
@@ -17,9 +17,4 @@ require 'google/apis/version'
RSpec.describe Google::Apis::VERSION do
it { is_expected.to be_a(String) }
describe Google::Apis::OS_VERSION do
it { is_expected.to be_a(String) }
it { is_expected.not_to match /\n/ }
end
end
-1
View File
@@ -16,7 +16,6 @@ SPEC_DIR = File.expand_path(File.dirname(__FILE__))
ROOT_DIR = File.expand_path(File.join(SPEC_DIR, '..'))
LIB_DIR = File.expand_path(File.join(ROOT_DIR, 'lib'))
GENERATED_DIR = File.expand_path(File.join(ROOT_DIR, 'generated'))
THIRD_PARTY_DIR = File.expand_path(File.join(ROOT_DIR, 'third_party'))
FIXTURES_DIR = File.expand_path(File.join(SPEC_DIR, 'fixtures'))
$LOAD_PATH.uniq!